-
-
[原创]HITB CTF 2018 gundam分析
-
发表于: 2021-8-22 23:18 17358
-
这道题主要考察tcache poisoning技术(修改tcache 中chunk的next指针),涉及到内存地址泄露、double free等技术,题目难度不大,但是对于入门选手来说,要跨过各种坑,成功利用漏洞获得系统控制权也不是简单容易的事。
一、gundam结构分析
二、内存泄露
三、双重释放漏洞(double free)
四、总结遇到的各种坑
一、gundam结构分析
(一) 基本结构:
通过逆向分析,可以知道,gundam结构如下:
包含一个结构体,命名为gundam,一个指向该结构体的指针factory。实际上在建立gundam的过程中,有两次malloc过程:
语句#1申请0x28字节的堆内存,用于创建结构体gundam(factory指针指向该结构体)。
语句#2申请0x100uLL字节的内存,用于保存gundam的name,返回地址buf指向该chunk的用户数据部分。
使用命令gdb gundam启动pwndbg,创建8个gundam之后,使用heap命令查看chunk的状态如下:
每个gundam包含两个chunk,一个大小为0x31的factory,另一个大小为0x111的name。
创建8个gundam,再释放,可以看到前7个进入tcache,最后一个进入unsortedbin:
(二)gundam中的关键函数
该函数在读入用户输入的gundam name时,没有对字符串末尾进行处理(加上'\x00')。
destroy函数删除gundam,首先将gundam->flag置0,然后释放gundam->name。而在释放name后,并没有将name指针置空。
blow_up函数将已经flag置0的factroy释放,并将factroy指针置空,减少gundam的数量,但是仍然没有将gundam->name指针置空,存在double free漏洞。
二、内存泄露
由前面分析得知,由于tcache机制本身的限制,当释放的chunk数量超过7个,会充满tcache bin ,超出的chunk根据大小放到unsorted bin。当chunk被free放入tcache bin时,会在该chunk对应的用户数据区(fd区域)写入tcache_entry, 指向下一个位于tcache bin 中的chunk的用户数据,从而构成单链表。
前面分析,函数构造gundam时,对于用户的输入字符串没有进行处理,即末尾增加截断字符“\x00”,而申请的堆空间有0x100字节,没有初始化,导致存在泄露信息的可能。
通过gdb.attach()动态跟进调试gundam,在构造8个gundam处设置断点,前7个的name 是‘AAAAAAA’,最后一个的name是'BBBBBBB',查看最后一个gundam->name的内存,可以看到0x7fe02bafbc78这个地址,而该地址是main_arena+88地址:
我们知道,tcache结构位于heap的最前端,也是一个堆块,其中包含数组entries,用于放置64个bins的地址,数组counts存放每个bins中chunk的数量。
通过vmmap命令,可以看到heap的开始位置为0x55e22cd98000:
使用x/26gx 0x55e22cd98000+0x10继续查看该地址处的值:
可以看到,tcache中已经存放了7个chunk,所以第8个chunk放入unsorted bin。
认真观察可以发现,地址0x55e22cd980c8处的指针0x000055e22cd98a10正好就是tcache bin头结点指向的第一个chunk,也就是最后被释放加入tcache的chunk(第7个chunk):
我们知道,一个gundam包含两个chunk,大小为0x30,另一个为0x110。我们由此出发,寻找第8个chunk。
通过连续申请创建8个gundam,第7个gundam的地址加上2个chunk的大小,就是第8个chunk的地址。
使用x/26gx 0x000055e22cd98a10+0x30+0x110-0x10,可以看到第8个chunk的地址为0x55e22cd98b40,其bk和fd指针都指向0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址):
思考为什么会泄露地址0x7f566befac78(main_arena+88)?因为在反复的gundam创建和释放过程中,伴随着0x55e22cd98b40+0x10(基址会变,偏移不变)处数据的变化。在创建第8个chunk时,地址0x55e22cd98b40+0x10(基址会变,偏移不变)存放gundam->name,由于我们输入name字符串为'BBBBBBB',只有7个字节(加上末尾的0x0a正好8个字节),0x55e22cd98b40+0x18后面内存地址处的值没有专门处理。在释放第8个chunk时,通过前面的分析可以知道第8个chunk的地址0x55e22cd98b40处,其bk和fd指针都指向0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)。而再次创建第8个chunk时,由于gundam构造函数没有对用户输入的字符串末尾进行处理,也没有对其申请的chunk进行过初始化,导致上次释放时写入的bk指针的值0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)仍然存在。利用者就可以通过查找'BBBBBBB'字符串进而找到泄露的地址0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)。前提是,必须至少有两次创建和释放8个以上gundam的过程,才能使第八个chunk地址的偏移0x18处存在泄露地址。
知道了泄露地址,我们再查找libc基地址,紧挨着heap的libc-2.26.so对应的地址0x7f566bb4f000就是libc基地址:
泄露地址0x7f566befac78-libc基地址0x7f566bb4f000=偏移0x3ac78是一个固定值。由于每次运行时程序加载到地址空间的基址会随机变化,可以用泄露地址减去该偏移得到对应的libc的基地址,进而计算出free_hook_addr和system_addr地址。
三、双重释放漏洞
libc-2.26没有对tcache 中的double free进行安全性检查,直到libc-2.28,才增加了对tcache 中的double free的安全性检查。前面我们在分析gundam的关键函数时知道,destroy函数和blow_up函数都没有将gundam->name置空,存在着双重释放的漏洞。
对于前面我们创建的8个gundam,我们先依次destroy序号为2、1、0的gundam,可以看到有3个空闲的chunk进入tcache:
使用heap命令观察chunk状态:
0x5627336a1a00地址处chunk为0号gundam的name对应的chunk,最后进入tcache,位于tcache指向的第一个位置。此时factroy还没有释放,大小为0x31的chunk还没有free。
再次destroy序号为0的gundam,可以看到:
此时,tcache中只有一个chunk,且该chunk的fd指针指向自身。为什么重复destroy了0号gundam,反而造成tcache中空闲chunk减少2个?
如上图所示,1表示destroy3个gundam后,tcache bin的状态。
当free一个chunk时,该chunk的fd指针会指向tcache bin中的第一个chunk,然后tcache bin的头结点指向刚free掉的chunk,依次类推,保证tcache bin的头结点指向的chunk永远是最后freed的。
当再次destroy 0号gundam时,对应的chunk(已在tcache中)会再次被free,并修改fd指针,从而使chunk 0的fd指向自身,从而完成double free。
通过前面计算出的泄露地址与libc基地址的偏移,可以在获得泄露地址的前提下,动态算出libc基地址的,进而计算出free_hook_addr和system_addr地址。
再次构造gundam,分别以free_hook_addr、'/bin/sh\x00'字符串和system_addr作为name参数:
1.构造第1个gundam时,参数为free_hook_addr的地址,glibc会从tcache bin 中找到空闲的chunk,此时为chunk0,tcache_get 操作会将tcache->entries[tc_idx]指向的第一个chunk返回,并使entries[tc_idx]指向下一个chunk。由于chunk0的fd指向自身,tcache->entries[tc_idx]仍然指向chunk0,同时chunk0的fd被改写成free_hook_addr;
从上图可以看到,chunk0的fd指向_free_hook地址。
2.构造第2个gundam时,参数为'/bin/sh\x00'字符串,glibc会从tcache bin 中找到空闲的chunk,此时仍然为chunk0,tcache_get 操作会使chunk0返回,使tcache->entries[tc_idx]指向前面chunk0的fd,即free_hook_addr,同时使chunk0的fd改写为'/bin/sh\x00'字符串;
从上图可以看到,tcache->entries[tc_idx]已经指向_free_hook地址。
从上图可以看到,chunk0的fd处为'/bin/sh\x00'字符串。
3.构造第3个gundam时,参数为system_addr地址,glibc会从tcache bin 中找到空闲的chunk,因为此时tcache->entries[tc_idx]指向free_hook_addr,就在free_hook_addr处写上system_addr。
从上图可以看到,free_hook_addr处已经写上system_addr。
通过以上3步操作,成功使system_addr与free_hook绑定,再执行1个destroy操作,即可启动system。
因此,选择destroy含有'/bin/sh\x00'字符串的1号gundam,成功执行shell:
四、总结遇到的各种坑
调试过程遇到不少问题,首先是如何在本地系统上调试针对libc-2.26.so的程序gundam。通过在网上查找资料,将libc.so.6分别作为LD_PRELOAD环境变量和ELF()的参数进行设置,根据libc.so.6在内存地址空间的基地址和泄露地址的偏移量计算出偏移,exploit脚本能够执行到最后,但是无法执行用户输入命令,输入命令就会报出segment fault。vmmap显示的内存中的链接器仍然是本地系统glibc-2.27的链接器ld-2.27.so。同时,使用gdb直接加载gundam,在提示符下通过语句设置环境变量的方式,根本无法启动调试。最后想到是libc版本的问题,从服务器下载glibc的源码,进行编译,生成libc-2.26.so,解决了编译的问题。再就是动态跟进调试的问题。使用gdb直接加载gundam,并逐句调试的方式效率太低,最后想到在python脚本中使用gdb.attach()和pause()语句,可以动态跟进调试,效率提高不少。最后就是为什么会出现泄漏地址的问题?而这恰恰是解决问题的关键。查了网上很多资料,只讲了可以找到该泄漏地址,但没有讲这个问题的原因,给利用造成了很多不便。经过反复调试,弄清了造成地址泄露的原因是:两次以上构造和销毁gundam造成第8个chunk的bk处保留了原来指向unsortedbin的指针,导致再次构造gundam时,关键地址被泄露。
struct gundam{
uint32_t flag;
char
*
name;
char
type
[
24
];
}gundam;
struct gundam
*
factory[
9
]
struct gundam{
uint32_t flag;
char
*
name;
char
type
[
24
];
}gundam;
struct gundam
*
factory[
9
]
s
=
malloc(
0x28
)
#1
buf
=
malloc(
0x100uLL
);
#2
s
=
malloc(
0x28
)
#1
buf
=
malloc(
0x100uLL
);
#2
__int64 sub_B7D()
{
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
);
memset(s,
0
,
0x28uLL
);
buf
=
malloc(
0x100uLL
);
if
( !buf )
{
puts(
"error !"
);
exit(
-
1
);
}
printf(
"The name of gundam :"
);
read(
0
, buf,
0x100uLL
);
*
((_QWORD
*
)s
+
1
)
=
buf;
printf(
"The type of the gundam :"
);
__isoc99_scanf(
"%d"
, &v1);
if
( v1 <
0
|| v1 >
2
)
{
puts(
"Invalid."
);
exit(
0
);
}
strcpy((char
*
)s
+
16
, &aFreedom[
20
*
v1]);
*
(_DWORD
*
)s
=
1
;
for
( i
=
0
; i <
=
8
;
+
+
i )
{
if
( !factory[i] )
{
factory[i]
=
s;
break
;
}
}
+
+
dword_20208C;
}
return
0LL
;
}
__int64 sub_B7D()
{
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
);
memset(s,
0
,
0x28uLL
);
buf
=
malloc(
0x100uLL
);
if
( !buf )
{
puts(
"error !"
);
exit(
-
1
);
}
printf(
"The name of gundam :"
);
read(
0
, buf,
0x100uLL
);
*
((_QWORD
*
)s
+
1
)
=
buf;
printf(
"The type of the gundam :"
);
__isoc99_scanf(
"%d"
, &v1);
if
( v1 <
0
|| v1 >
2
)
{
puts(
"Invalid."
);
exit(
0
);
}
strcpy((char
*
)s
+
16
, &aFreedom[
20
*
v1]);
*
(_DWORD
*
)s
=
1
;
for
( i
=
0
; i <
=
8
;
+
+
i )
{
if
( !factory[i] )
{
factory[i]
=
s;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创]large chunk分配过程调试 12795
- [原创]BCTF 2018 House of Atum分析 11861
- [原创]HITB CTF 2018 gundam分析 17359
- [原创][原创]Unsorted Bin 利用后续 5807