pwning bin2json

So OJ posted this pwnable that had evidently survived the
BsidesCBR and Kiwicon CTFs and it got my interest.

I haven’t really been CTFing actively for years, but I still occasionally enjoy
pwning things, and I don’t remember if I’ve ever done one of OJ’s challenges.
So I grabbed a copy, and decided to see how lazy I could be while still shaking
a shell out of this thing.

His challenge talks some goofy binary protocol that I didn’t even attempt to
understand, but he helpfully included a sample script that emits a valid
message. I started building qemu for afl, and had a quick look at the
binary in binja. A quick patch to make it exit after a call to display_all
later to make fuzzing practical later, I started the fuzzer and got dinner.

Just to be clear, the process of setting up afl is really nothing special. All I did here was put a single testcase (The verbatim output of OJs sample) into an input directory and run it with:

afl-fuzz -Q -i input -o output -- ./bin2json

After about 20 mins I had a crash, and a quick inspection revealed this:

0x0000000000400ca3         mov        rax, qword [ds:rax+0x210]
0x0000000000400caa         test       rax, rax
0x0000000000400cad         je         0x400cc3

0x0000000000400caf         mov        rax, qword [ds:_IO_stdout]
0x0000000000400cb6         mov        rsi, rax                                  ; argument #2 for method fputc
0x0000000000400cb9         mov        edi, 0x2c                                 ; argument #1 for method fputc
0x0000000000400cbe         call       fputc

0x0000000000400cc3         mov        rax, qword [ss:rbp+var_338]               ; XREF=display_all+100
0x0000000000400cca         mov        eax, dword [ds:rax+0x200]
0x0000000000400cd0         test       eax, eax
0x0000000000400cd2         jne        0x400ce1

0x0000000000400cd4         mov        qword [ss:rbp+display_func], 0x400b9d
0x0000000000400cdf         jmp        0x400d1d

0x0000000000400ce1         mov        rax, qword [ss:rbp+var_338]               ; XREF=display_all+137
0x0000000000400ce8         mov        eax, dword [ds:rax+0x200]
0x0000000000400cee         cmp        eax, 0x2
0x0000000000400cf1         jne        0x400d00

0x0000000000400cf3         mov        qword [ss:rbp+display_func], 0x400bfa
0x0000000000400cfe         jmp        0x400d1d

0x0000000000400d00         mov        rax, qword [ss:rbp+var_338]               ; XREF=display_all+168
0x0000000000400d07         mov        eax, dword [ds:rax+0x200]
0x0000000000400d0d         cmp        eax, 0x1
0x0000000000400d10         jne        0x400d1d

0x0000000000400d12         mov        qword [ss:rbp+display_func], 0x400b4e

0x0000000000400d1d         mov        rax, qword [ss:rbp+display_func]          ; XREF=display_all+150, display_all+181, display_all+199
0x0000000000400d24         lea        rdx, qword [ss:rbp+var_320]
0x0000000000400d2b         mov        rdi, rdx
0x0000000000400d2e         call       rax

So, a classic uninitialized variable bug. var_320 is not initialized on
entry, so if var_338 is not 0, 1 or 2, then the value is left
unchanged, and subsequently called.

This is pretty simple to exploit, just find some other function called by main,
and corrupt that same slot on the stack, then arrange for display_all to be
called, and control pc.

As it happens, OJ thought of this and arranged the stack neatly in the only
other callable function (create_person) to avoid this. This is where I got
bored and wandered off, since this testcase gave me nothing to go on.

A few hours later, afl had found a second testcase that allowed PC control. I
never actually got around to reversing it to see how it worked, but quickly
looking at the crash I saw pc = 0xfcfcfcfcfcfcfcfc, and my testcase had a few
massive slabs of fc bytes in it.

A quick script to tamper with them in turn until I found which bytes were
corrupting that slot on the stack revealed that I had control of about 440
bytes of stack. I put a few quick gadgets together by hand to pop over the
garbage I needed on the stack, and then used ROPgadget to
generate a chain to get a shell. Their chain was gigantic, I dicked around for
a while trying to find a couple of neat gadgets to smuggle r10 into rax, which
was pretty close, and then remembered that I had full control just used a pop
rax
to populate the syscall number, which ROPgadget doesn’t do for reasons I
don’t understand.

Finally:

(yes, my dumb DO image I brought up quickly doesn’t have lldb in it’s repo for
some reason)

(gdb) r < /root/pwn/exploit_with_payload
Starting program: /root/pwn/bin2json < /root/pwn//exploit_with_payload
process 2099 is executing new program: /bin/dash

You can find the shitty script I used to patch my testcase here:

In the spirit of really, really avoiding investing effort in this, I’ve left
all my janky experiments in as comments.

It’s worth shouting out that voltron saved my ass again, the stack view is the best when you’re trying to map between a binary testcase and where things end up in memory.

And of course, thanks OJ for nerdsniping me!

About richo

Flying, coffee, computer stuff.
This entry was posted in ctf. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *