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:
000008aaint64_tpwnme()000008aa{000008aasetvbuf(*(uint64_t*)stdout,nullptr,2,0);000008daputs("write4 by ROP Emporium");000008e6puts("x86_64\n");000008fcvoidbuf;000008fcmemset(&buf,0,0x20);00000908puts("Go ahead and give me the input a…");00000919printf("> ");0000092fread(0,&buf,0x200);00000942returnputs("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:
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:
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: