Lessons Learned From My First ROP Exploit
I have been following corelanc0d3rs excellent exploit writing tutorials and I have finished the one about Return Oriented Programming. That was easier than I had thought.
But I wanted to test myself to see if I had really understood the technique and it turned out that I still had a lot to learn (surprise!).
The software I chose to work on was Millenium MP3 Studio which was the topic of a Corelan tutorial on SEH exploitation. I had exploited this one previously so I kind of knew what was going on.
I chose to go for a call to VirtualProtect as in the Corelan ROP tutorial and there was a lengthy list of bad characters (in the beginning I only knew about 0x00, 0x0a and 0x0d but more came up later), which narrowed down which addresses and other data could be expressed.
The Immunity Debugger is an excellent tool and I recommend it highly, and with the Mona.py command exploitation gets much easier. I collected the ROP gadgets found by Mona and started digging for useful ones.
I found a gadget, which could pivot the stack into my payload:
0x10019d3c ADD ESP, 0x41c # RETN
…thus, my chain could begin.
I needed a chain, which could write an arbitrary number into an arbitrary address, and this was not really difficult but quite time consuming. I ended up with this:
0x100205d8 # POP ESI # RETN
0x1001c348 # POP EAX # RETN
0x100205d5 # SUB EAX, ESI # POP EDI # POP ESI # RETN
0x1001f826 # POP ECX # RETN
0x1001f826 # ADD ECX, ESI # MOV DWORD PTR DS:[EAX], ECX # MOV EAX, 1 # POP EDI # POP ESI # POP EBX # RETN
The first three enables me to put any value into EAX. The last one is quite cool as it both builds the data to be written in ECX and then actually writes it to the address pointed to by EAX. I use the ESI register in both gadgets and both end up popping into it, so both gadgets do a lot of my work and I did not have to use the first "POP ESI # RETN" gadget again. Very cool.
Now, with this chain things took off. I could simply just replay them several times with different data put into ESI, EAX and ECX (the other registers just contain garbage as they are not actually used).
The last part of the exploit needed to adjust the stack to point to my VirtualProtect arguments and I chose this gadget:
0x1001c611 # MOV ESP, EBP # POP EBP # RETN
So I just needed to put the address into EBP first. For that I used these gadgets:
0x10017f66 # POP EBP # RETN
0x1001ff4f # POP EBX # RETN
0x10017a18 # AND AL, 0x22 # SUB EBP, EBX # OR DH, DH # RETN
And it worked! Well, sort of…actually not.
My ROP chain ran beautifully, VirtualProtect was called with all the right parameters, it returned into my shellcode, shikata ga nai ran and decoded the payload, the payload ran and called WinExec…but WinExec failed with a return code meaning ERROR_FILE_NOT_FOUND. Why?
Actually, I had no idea how much of the shellcode actually ran, but I could see that shikata ga nai ran and decoded the payload and that the result matched the one I had chosen. I had to reverse engineer the payload (windows/exec from Metasploit) to discover what went wrong. This is a very cool payload and I will probably write a blog post about it. For now, just know that it makes a call to WinExec and it failed.
I tried building the stack by hand using Immunity Debugger and returing into VirtualProtect. That worked fine and "calc" was executed. Why?
Obviously my ROP code altered something critical, but what?
I then tried a binary search, running half my chain, then building the stack and returning. "calc" ran.
This took a lot of tries so I ended up writing my first Immunity Debugger PyCommand:
import immlib
def main(args):
imm = immlib.Debugger()
stack = imm.getRegs()['ESP']
data = "\xd0\x1a\x80\x7c"
data += "\xe2\x5d\x13\x00"
data += "\xe2\x5d\x13\x00"
data += "\xe2\x00\x00\x00"
data += "\x40\x00\x00\x00"
data += "\x10\x90\x03\x10"
imm.writeMemory(stack, data)
return "[*] PyCommand Executed...YAY"
That sped things up, but "calc" ran every time. Except after having modified the stack pointer…the very last ROP gadget. And then it hit me. The stack ended up not being aligned on a four byte boundary, and apparently that was not a problem for VirtualProtect, nor for shikata ga nai, nor for the shellcode but WinExec had a problem with it.
Aligning the data fixed the problem…I get "calc" now. I have posted a demo video here.
I am not quite happy with the result as there still are problems. I hardcoded some addresses to the stack. They seemed stable on a particular version of Windows, so on the command line for the exploit you choose the version of Windows that you are exploiting (only XP SP 2 and 3 are supported at the moment). However, I am not sure this is true. I did a couple of reboots and the stack was still stable at those addresses, but this occurs to me as being odd. Why have ASLR and not randomize the stack ?
In my next ROP exploit I will calculate all addresses…no more hardcoding.
Lessons learned:
-
Align stack on four byte boundary
-
Finding a useful ROP chain is time consuming
-
When a useful ROP chain has been found things really take off
-
Immunity Debugger PyCommands are very cool and needs more looking into
-
GDB can also be scripted using Python
-
You can learn a lot by reversing Metasploit payloads
-
You can learn a lot by screwing up