-
-
[原创]看雪.深信服 2021 KCTF 春季赛 第九题 平定战乱
-
发表于: 2021-5-26 15:55 9417
-
pwn题二连:
linux aarch64的elf文件,没开pie(所以text段和data段地址固定),没开full relro(所以got表可写),开了NX(数据不可执行,但是否生效要看环境)
菜单堆题
ida逆向,发现开了ollvm混淆,找了几个去混淆的工具,要么跑不起来,要么没效果/有问题(求推荐目前最好用的去混淆工具)
直接硬看,识别出几个函数:
sub_403CE0:add
sub_404400:edit
sub_4041B4:delete
sub_403908:readline,遇到\n结尾则改成\0,不以\n结尾则不变(所以没有0截断)
sub_403B10:readint,先readline然后对字符串调用strtol
sub_402058:自己实现的malloc
sub_400994:自己实现的free
add:分配的堆块指针保存在0x4161B0开始的char *数组的第一个空位置,最多能保存16个;size保存在0x4160AC开始的unsigned int数组中
edit:给一个[0, 16)的index,如果对应index的位置不为空则可以写入最多size个字节
delete:给一个[0, 16)的index,free掉对应index的堆块
漏洞在delete中,free后没有把指针置零,后续的edit仍能写入,造成UAF
malloc和free比较复杂,混淆去不掉根本看不动。搭环境动态调试,在ubuntu上可以直接安装:
启动:
调试:
经过反复试验,堆的设计应该是这样的:
与glibc的堆很相似,chunk按照内存地址从小到大连续排布,根据prev_size和size可以找到前一个和后一个堆块。
在malloc时,用户传入的size先向上对齐到16字节,然后再加上16字节(用来保存prev_size和size),对应到最终分配的chunk的size
size域的最低一个bit指示前一个堆块是否空闲,1表示前一个堆块是空闲的,0表示前一个堆块在使用中(与glibc的PREV_INUSE正好相反)
返回给用户的指针指向chunk的fd域
free的堆块由fd和bk组成双向链表,注意fd和bk指向的是chunk的fd域的地址而不是头部,全局变量0x416190(也是一个struct chunk结构)是链表的头(与glibc的unsorted_bin类似)。链表为空时0x416190的fd和bk都指向自己的fd域的地址(0x4161A0)。
对同样大小的chunk,0x416110的数组的对应下标保存了指向最后释放的chunk的头部(prev_size域)的指针(类似于glibc的fastbin)。
0x4160A0保存着当前已分配区域的末尾的指针(类似于glibc的topchunk)
free的过程:
把堆块从尾部插入"unsorted bin"(从0x416190的bk开始),然后同时插入对应size的"fastbin"中(如果"fastbin"该位置非空,替换之)
malloc的过程:
整个堆分配器没有堆块的拆分与合并
UAF能任意覆盖fd和bk(注意输入没有0截断),很容易想到2.23时代针对双向链表的经典攻击方法unlink:
假设当前堆块是P,则把P从双链表解链的操作为:
因此如果能控制P->fd和P->bk,就有机会实现任意写
但是,malloc和free中有很多检查需要bypass掉:
看描述,这些检查其实glibc里也有,只是不知道题目里的触发时机和条件是否和glibc一样。
(堆块的分配原理可以通过动态调试看出来,但这些检查就不容易看出来了(因为正常情况下不会触发)。OLLVM的恶心之处在这里才真正体现出来,如果没有混淆,通过静态分析能很容易找出这些检查在什么情况下会触发)
混淆去不掉,只好猜+动态调试验证。
"Double link list error!":双链表完整性检查,一般发生在即将解链的时候,检查P->fd->bk == P && P->bk->fd == P。题目里的fd和bk指向的是chunk的fd域,因此有一点微调,但基本一致,可以用经典方法绕过。
"Size check!":在用2.23常规堆题的经典unlink方法试验时总是触发这个错误,完全搞不明白,但好像把待unlink的堆块的fd指向的堆块的下一个堆块的size赋值正确就能通过这个检查?
另外注意到malloc里有很对对size域& 0xF0
的操作很可疑,因此怀疑size域实际上只有最低的一个字节有用,试验发现确实如此。
下面开始构造,这是全局的content数组:
0x4161B0:index 0
0x4161B8:index 1
0x4161C0:index 2
0x4161C8:index 3
0x4161D0:index 4 fakechunk2
0x4161D8:index 5 fakechunk2
0x4161E0:index 6 fakechunk2
0x4161E8:index 7 fakechunk2 -> 0x4161C0 (-> changed to 0x416200)
0x4161F0:index 8 fakechunk3
0x4161F8:index 9 fakechunk3
0x416200:index 10 fakechunk3 -> 0x4161C0 (-> changed to 0x4161E0)
0x416208:index 11 fakechunk3
0x416210:index 12 fakechunk4
0x416218:index 13 fakechunk4
0x416220:index 14
0x416228:index 15
注意到第一次分配的堆块的地址总是0x30结尾,因此先add(0xe0, "0")分配一个0xf0大小的堆块,这样第二个堆块的地址就会以0x120结尾
然后不断的add(0x10)再delete,将index 1到index 13的全部填充为同一个大小为0x20的堆块地址,且这个地址恰好也以0x20结尾
然后直接用libc2.23时代的经典unlink攻击:UAF修改这个堆块的fd为0x4161E0,bk为0x416200(因为程序没开pie,所以这两个地址是固定的)
注意(P->fd - 0x10)->bk = 0x4161D0->bk = (0x4161E8) = P, (P->bk - 0x10)->fd = 0x4161F0->fd = *(0x416200) = P,满足双向链表的完整性检查
另外保证0x4161D8、0x4161F8、0x416218三个地方& 0xF0以后等于0x20,保证不出现"Size Check!"
(实际上应该用不到这么多堆块,这里为了不深究检查的触发条件就多构造了一些值)
然后分配一个新的0x20大小的堆块(malloc(0x10)),触发unlink,结果是0x416200处的值被修改为了0x4161E0,0x4161E8处的值被修改为了0x416200
至此得到了任意写:(受限于readline的\n截断,待写入的地址和值不能包含"\n")
先通过index 10把0x4161E0(index6)的值改为目标地址,再通过index6向目标地址写内容
下一步是如何通过任意写获得shell,在题目给出libc之后变得非常简单:
查到strtol在libc的偏移是0x358a8,system在libc的偏移是0x3dda8,二者相差不超过0x10000,因此经过地址随机化之后有概率保证地址的高6字节都相同。
对于地址的低16位,其中低12位是不受随机化影响的,剩下的四位可以爆破
利用任意写,只改低2个字节(partial overwrite,也是经典方法),有1/16的概率把strtol的got表值覆盖为system函数的地址。而strtol在readint时被调用,第一个参数是给菜单的输入,是完全可控的,可以输入"/bin/sh"获得shell
最终的exp:(1/16的成功率)
后记:
因为题目刚开始没给libc,所以在获得任意写之后首先思考的是如何leak,然而程序里没有输出堆块内容的函数。
考虑过通过修改stdout泄露、dl-resolve直接搞system,觉得都不可行。
利用任意写改掉got表函数地址的最低字节然后看程序能否正常运行,搞出了部分函数的最低字节地址,然后去libc database搜索并没有结果(看了题目最后给出的libc,是linaro的,大概database里没收录)
考虑题目是不是偷懒用qemu-aarch64启动的(如果是这样,NX和ASLR都是无效的,也就是说可以直接在bss段写shellcode跳过去),本地成功了远程不行,沟通后确认远程是用qemu-system-aarch64启动的(偷鸡失败)。
直接猜system函数低12位的地址爆破:看了多个版本的libc,strtol和system的距离都很近,这样只要猜对就有1/16的概率成功,而aarch64 libc的导出函数都是0或8结尾,所以开512个进程爆破应该是可以接受的。
太过暴力,想了想还是决定先放了,厚颜无耻的询问能否提供libc(打算如果不给再爆破),然后出题方很大方的给了。
题目不给libc应该是想考察如何获得任意读。这一点最开始确实没反应过来,沟通后才恍然大悟:
前面获得shell的方法是把strtol覆盖为system,利用第一个参数可控传入"/bin/sh"字符串。
由于第一个参数可控,如果把strtol覆盖为printf在plt表的地址,就得到了一个格式化字符串漏洞(而且格式化串在栈上,还能反复利用),可以任意地址读/写。
利用任意读,可以遍历linkmap获得libc的基地址(linkmap的地址保存在got表前面,0x415ff0处,linkmap+0x18是指向下一个linkmap的指针,linkmap+0x8是指向共享库的名称的指针,limkmap+0x0是共享库的基地址),然后dump出libc的文件头或者用pwntools的DynELF获得system函数的偏移
$
file
main
main: ELF
64
-
bit LSB executable, ARM aarch64, version
1
(SYSV), dynamically linked, interpreter
/
lib
/
ld
-
linux
-
aarch64.so.
1
,
for
GNU
/
Linux
3.7
.
0
, stripped
$
file
main
main: ELF
64
-
bit LSB executable, ARM aarch64, version
1
(SYSV), dynamically linked, interpreter
/
lib
/
ld
-
linux
-
aarch64.so.
1
,
for
GNU
/
Linux
3.7
.
0
, stripped
$ checksec main
Arch: aarch64
-
64
-
little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (
0x400000
)
$ checksec main
Arch: aarch64
-
64
-
little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (
0x400000
)
apt
-
get install qemu
-
user libc6
-
arm64
-
cross gdb
-
multiarch
apt
-
get install qemu
-
user libc6
-
arm64
-
cross gdb
-
multiarch
qemu
-
aarch64
-
L
/
usr
/
aarch64
-
linux
-
gnu .
/
main
qemu
-
aarch64
-
L
/
usr
/
aarch64
-
linux
-
gnu .
/
main
qemu
-
aarch64
-
g
1234
-
L
/
usr
/
aarch64
-
linux
-
gnu .
/
main
gdb
-
multiarch
-
ex
"target remote localhost:1234"
qemu
-
aarch64
-
g
1234
-
L
/
usr
/
aarch64
-
linux
-
gnu .
/
main
gdb
-
multiarch
-
ex
"target remote localhost:1234"
struct chunk {
uint64_t prev_size;
uint64_t size;
uintptr_t fd;
uintptr_t bk;
};
struct chunk {
uint64_t prev_size;
uint64_t size;
uintptr_t fd;
uintptr_t bk;
};
NEXT
=
P
-
>fd
PREV
=
P
-
>bk
NEXT
-
> bk
=
PREV
PREV
-
>fd
=
NEXT
NEXT
=
P
-
>fd
PREV
=
P
-
>bk
NEXT
-
> bk
=
PREV
PREV
-
>fd
=
NEXT
"Size check!"
"Wrong heap! "
"Wrong ptr!"
"Wrong size!"
"Wrong next_chunk size!"
"Wrong pre_chunk size!"
"Double link list error!"
"Top chunk error!"
"Size check!"
"Wrong heap! "
"Wrong ptr!"
"Wrong size!"
"Wrong next_chunk size!"
"Wrong pre_chunk size!"
"Double link list error!"
"Top chunk error!"
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课