Goal
- Manipulate writeable address and with rop we can read any file.
Key points:
- Where to write? (e.g., The address should be writable and will not change during runtime to be a filename)
- What to write? (e.g., What filename do we desire? How to concatenate the strings?)
- How to make a ROP chain? (e.g., What gadget to find? In this challenge,
rdi
only consumes pointer, so how to pass the pointer tordi
?)
My first thought:
- Make a symlink that points to the
nonexistent -> flag.txt,
so we can get the flag easily (it will work just by calling the usefulFunction). But it turns out that it will only work locally, and if we want to solve the pwn challenge remotely, we stand no chance of changing the server’s environment.
Description
Challenge Hint
Read/Write
Hopefully, you’ve realised that ROP is just a form of arbitrary code execution and if we get creative, we can leverage it to do things like write to or read from memory. The question we need to answer is: what mechanism are we going to use to solve this problem? Is there any built-in functionality to do the writing or do we need to use gadgets? In this challenge, we won’t be using built-in functionality since that’s too similar to the previous challenges, instead we’ll be looking for gadgets that let us write a value to memory such as mov [reg], reg.
What/Where
Perhaps the most important thing to consider in this challenge is where we’re going to write our “flag.txt” string. Use rabin2 or readelf to check out the different sections of this binary and their permissions. Learn a little about ELF sections and their purpose. Consider how much space each section might give you to work with and whether corrupting the information stored at these locations will cause you problems later if you need some kind of stability from this program.
Decisions, decisions
Once you’ve figured out how to write your string into memory and where to write it, go ahead and call print_file() with its location as its only argument. You could consider wrapping your write gadgets in helper a function; if you can write a 4 or 8 byte value to a location in memory, you could craft a function (e.g. in Python using pwntools) that takes a string and a memory location and returns a ROP chain that will write that string to your chosen location. Crafting templates like this will make your life much easier in the long run. As ever, with the MIPS challenge, don’t forget about the branch delay slot.
Let’s download the binary!
unzip
1 | jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ unzip -l write4.zip |
We got three files from the write4.zip.
ldd
1 | jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ ldd write4 |
write4
is the primary binary.libwrite4.so
is the library write4.
(we can find it out with ldd
)flag.txt
is our target filename.
Static Analysis (with IDA Pro)
First take a look at main function:
1 | .text:0000000000400607 ; int __cdecl main(int argc, const char **argv, const char **envp) |
external function
1 | .plt:0000000000400500 ; __int64 __fastcall pwnme(__int64, __int64, __int64) |
We can see the main function is calling _pwnme,
which is an external function in libwrite4.so.
usefulFunction
1 | .text:0000000000400617 usefulFunction proc near |
The binary provides a usefulFunction
which moves "nonexistent"
to edi
and calls _print_file
. We know from calling convention that edi
will be the first argument for _print_file
that we can exploit. If we can change "nonexistent"
to "./flag.txt"
, we will have a chance to capture the flag!
usefulGadgets
1 | .text:0000000000400628 usefulGadgets: |
Although it doesn’t show in the list of functions in IDA Pro, we can find it under the usefulFuntion
in assembly mode. We will use this gadget to pass the pointer of the file name.
Dynamic Analysis (with GDB)
Purpose: To find the padding before the return address.
There are two ways to find the buffer size:
1. Use GDB
1 | // 1. Create payload |
Use cyclic to count the offset from rsp
(don’t need any arithmetic) and we can get the padding is 40.
2. Read pwnme() assembly
1 | public pwnme |
Look through the code from IDA Pro (which is more straightforward.)
- From line 32, we know that it puts
rax
(which is the buffer size) torsi.
Andrax
comes from line 30, which is[rbp+s]
ands
equals0x20
(from line 4). Thus we know the buffer size is0x20
bytes. - Before the return address, there lies a
rbp,
so we have to plus another 8 bytes to0x20
. Thus, we got40
bytes.
In both ways, we can conclude that to reach the return address we have to insert 40
bytes padding.
Once we control the rip
, we can make this binary execute anything we want!
Exploit
Exploit code
1 | from pwn import * |
I’ll provide some illustrations of finding the address from line 4 to line 9 in the exploit code.
1. 0x601028
1 | jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ readelf --section-headers -W ./write4 | grep data |
- The reason that we choose
.data
not.rodata
is because it’sWA
(writeable and accessible). Thus that’s how we get0x601028.
- 0x601030 is calculated by
0x601028 + 0x8
, since the./flag.txt
(10 bytes) is too long, and the gadget we get can only move qword (8 bytes) at a time, so we have to move the leftover after next 8 bytes.
3. Use the usefulGadgets which is mentioned above to get 0x400628 and 0x400690
1 | jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ ropper -f ./write4 | grep r14 |
- We use
pop r14; pop r15; ret;
to store the pointer and the filename respectively. Besides,mov qword ptr [r14], r15; ret;
it first dereferences r14 and stores the value from r15 to it.
4. 0x400693
1 | jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ ropper -f ./write4 | grep rdi |
- It’s the gadget to store the pointer to the filename
5. 0x400620
1 | .text:0000000000400617 usefulFunction proc near |
- Although usefulFunction starts at
0x400617
, it changes therdi
at0x40061B
, which destroys our plan. Thus we decide to jump from0x400620
and make it only call_print_file
without changingrdi.
Get the flag
1 | jeff14994@jeff14994-VirtualBox:~/Desktop/asu-hacking/03_03$ python exploit.py |
Happy ropping!