lab-03: 堆上漏洞及其利用
[!ABSTRACT]
俞仲炜 3220104929 241201
堆管理器基础
[!QUOTE]
- 阅读
example.c
代码,在报告中简述这个目录程序的逻辑;通过make build
完成对程序的编译和 patch,提供 ldd 执行后的截图;(10 points)- 阅读和运行
test.py
代码,分析打印的dump*.bin
的内容。要求类似示例图一样将所有申请和释放的对象标记出来,特别标注出 tcache 单向链表管理的对象);(20 points)- 将
test.py
中注释的两行handle_del
取消注释,再次运行,新产生的dump*.bin
和之前的相比有何变化?多释放的属于William
和Joseph
的堆块由什么结构管理,还位于 tcache 链表上么?请复习课堂上的内容,在报告中进行回答;(10 points)
example.c 逻辑
这是一个用户信息管理系统,允许用户创建、删除、查看和编辑用户信息,此外使用了自定义的内存分配钩子来跟踪内存分配和释放操作
- 功能函数
user_add()
:添加新用户,将用户信息存储在类型为user_info
的infos
数组,会检查内存是否 enoughuser_del()
:删除用户,需要验证密码user_show()
:显示用户信息,需要验证密码user_edit()
:编辑用户信息,需要验证密码
- 辅助函数
getline_wrap()
:从文件描述符读取一行输入heap_debug()
:转储堆内存到文件
- 主循环
main()
显示菜单,等待用户选择操作,并调用相应的功能函数
ldd 执行后的截图
dump*.bin
内容分析
[!NOTE]
太多了就不全截出来了,这里 chunk 就分已分配的和未分配的两类,就只分别截部分进行分析
在 example.c
中,user_add
会先后为 user_info *info
和 char *intro;
进行 malloc
,前者会得到 0x70
大小的 chunk,后者会得到 0x50
大小的 chunk(实际 data 大小要减 0x10)
tcahce bin TOP 会维护满足条件的已释放 chunk 组成的单向链表,如下图所示:
图中一个黑色方框示意一个 chunk 的范围,图中的 chunk 均已被 free
红色箭头示意了挂在 tcache TOP chunk 下的两个单项链表,分别维护着 0x40
和 0x60
两种 data 大小的 chunk
可以看到,所有 bk
(key)都指向 0x010
处,fd
(next)参与构建单向链表
此外可以注意到,tcache bin 里的 chunk 的 P bit 都置 1,以免发生 merge
以及,prev_size 都为 0,符合 tcache bin 的设置
然后我们再看尚未被 free
的 chunk,如下图所示(从蓝色方框开始,往下的都是):
可以看到没有 fd
和 bk
新产生的 dump*.bin
变化
tcache bin 的一个特点是,每个单向链表最多只能容纳 7 个 chunk 以加快重分配速度
原本正好已经 free
了 7 * 2 个 chunk,取消注释后,新的两个被 free
的 chunk 将进入 fast bin
下图为新的被 free
的 2 * 2 个 chunk,左边为变化前,右边为变化后
多释放的属于 William
和 Joseph
的堆块由什么结构管理,还位于 tcache 链表上么
由 fast bin 管理,不在 tcache 链表上,我们可以检查 tcache bin TOP chunk 来确认,TOP chunk 并没有挂载新的链表:
堆上常见漏洞
[!QUOTE]
- 找到
uninit/uninit
中的未初始化读漏洞,在报告中给出分析;编写攻击脚本 ,完成对于堆上 flag 内容的窃取; (10 points)
- 远程环境位于 IP:
8.154.20.109
, PORT:10400
- 找到
overflow/overflow.c
中的堆溢出漏洞,编写攻击脚本触发该漏洞;(10 points)- 找到
uaf/uaf
中的释放后使用漏洞,编写攻击脚本触发该漏洞;(10 points)
找到 uninit/uninit
中的未初始化读漏洞
在 main
主循环开始前,程序进行了一次 0x40
大小的 malloc
并 free
,但是么有清空里面的内容
char* flag = malloc(0x40);
sprintf(flag, "the sacred and awesome flag is %s\n", getenv("FLAG"));
// ....
free(flag);
之后 user_add
里又进行了一次 0x40
大小的 malloc
给 intro
,那么这个获得的内存空间就是之前 flag
的内存空间,含有 flag
的信息,我们可以通过 user_show
窃取
编写攻击脚本
只需创建一个空信息账户,并获取其信息,就能将 flag
的信息获取出来
结果如下图所示:
找到 overflow/overflow.c
中的堆溢出漏洞
可以发现下面这个拿 shell 函数
此外,我们注意到 user_add
里只给 intro
分配了 0x40
的空间,但是 user_edit
里却可以修改 0x60
的空间,这里出现了堆溢出漏洞
编写攻击脚本触发该漏洞
我们尝试利用 user1 覆写 user2 的信息:
user_idx1 = handle_add(b"user1", b"pwd1", b"intro1", b"motto1")
user_idx2 = handle_add(b"user2", b"pwd2", b"intro2", b"motto2")
payload = b""
payload += b"A" * 0x40
payload += p64(0)
payload += p64(0)
payload += p64(0x114514)
handle_edit(user_idx1, b"pwd1", b"user1", payload, b"motto1")
执行 handle_show(user_idx2, b"pwd2")
,可见 user2 的信息成功被修改(14 45 11
):
找到 uaf/uaf
中的释放后使用漏洞
user_del
中有一行被注释,导致被 free
的元素还在“已被分配”数组中,造成 uaf 漏洞
编写攻击脚本触发该漏洞
uaf.c
在 user_add
增加了一个输入,为 intro_size
,需要将脚本里的 handle_add
进行相应的修改
通过控制 intro_size
,我们可以让 chunk
被 free
进不同的 bin
我们尝试读取一个已被删除的用户信息:
user_idx1 = handle_add(b"user1", b"pwd1", b"64", b"intro1", b"motto1")
handle_del(user_idx1, b"pwd1")
handle_show(user_idx1, b"pwd1")
可见除了部分信息被覆盖为 fd
和 bk
,其余信息都成功读取了( motto1
):
堆上漏洞的利用
[!QUOTE]
- 利用
overflow/overflow.c
中的堆溢出漏洞,通过劫持 freelist 的方式(10 points),写 exit GOT 表数据将执行流劫持到backdoor
函数,从而完成弹 shell,执行flag.exe
取得 flag(5 points)
- 远程环境位于 IP:
8.154.20.109
, PORT:10401
- 利用
uaf/uaf
中的释放后使用漏洞,通过类型混淆的利用方式构建任意地址读写原语(10 points),进而通过内存破坏实现弹 shell,执行flag.exe
取得 flag;(5 points)
- 远程环境位于 IP:
8.154.20.109
, PORT:10402
- 注:相比于上个目标,这个程序开启了 RELRO 保护,故无法破坏 GOT 表内容;
- 提示:回到最开始的
example.c
,位于 libc 内存中有其他的攻击目标可以作为写的对象来实现控制流劫持。
通过劫持 freelist 的方式利用 overflow/overflow.c
中的堆溢出漏洞
思路
已知 0x40
大小的 chunk 会先挂到 tcache bin 等待复用,chunk 在内存上的位置并不会发生改变,所以我们可以将第二个 chunk 先 free
进 tcache bin,再利用第一个 chunk 的堆溢出漏洞覆写掉第二个 chunk 的 fd
,使其指向 GOT 中 exit
项的地址 ,形成 tcacheTOP->第二个chunk->GOT[exit]
的情景
之后将第二个 chunk malloc
出来,tcache bin 就会重组单向链表,形成 tcacheTOP->GOT[exit]
的情景
这样下一次 malloc
得到的 chunk 就是 GOT [exit] 所处的内存了,可以直接将其修改为 backdoor
函数的地址
这样调用 exit
就会调用 backdoor
函数,完成攻击
远程攻击结果
[!FAILURE] 个人遇到的问题,可跳过
这个脚本我写完后调试了一整天都没找出哪出了问题
最后发现是,在
example.c
中,exit(-1)
是在菜单default
里,确切来说它就不在菜单里我当时一扫而过了,之后一直以为
exit(-1)
在 [ 5 ] leave 里具体来说,我错误的脚本里,最后准备交互的代码是这样的
然后这个 [ 5 ] leave 执行的是
return 0
,我是傻逼确实是我经验不足,正常的程序怎么可能用
exit(-1)
来 leave这么重要的地方也确实应该在调试过程中回去检查,不应该内耗这么久才发现
利用 uaf/uaf
中的释放后使用漏洞,通过类型混淆的利用方式实现弹 shell
思路
[!reference]
unsorted bin 尾节点的 fd
指针会指向 main_arena
,借此我们可以通过 uaf 漏洞获取 main_arena
相对地址,进而泄露 libc
基地址,进而获取攻击指令
通过 IDA 反汇编 libc-2.31.so
,确认 main_arena
偏移量为 0x1ECB80
,由此可以计算出 libc 基地址
进而可以得到 system
和 __free_hook
的地址
接下来就是通过类型混淆的利用方式构建任意地址读写原语,通过内存破坏实现弹 shell
我们可以创建再创建两个用户 user3 和 user4,并先后删除,使得两个 user_info
结构对应的 chunk 进入 tcache bin
然后再创建一个新用户,malloc
会先给其分配 user4 的 user_info
chunk;同时我们再设置其 intro size 为 0x68
,即 user_info
的大小,这样 malloc
会给这个新用户的 intro
分配 user3 的 user_info
chunk
如此,我们就可以修改 user3 user_info
chunk 里的 char *intro
,将其值改为 free_hook_addr
现在我们只需要修改 user3 的 “intro” 的内容,就可以实现对 __free_hook
的修改;我们将其改为 system_addr
,如此我们调用 free
时实际上就会调用 system
注意到 user_del
里会先 free
掉 info->intro
再 free
掉 info
,所以我们可以把待执行语句 /bin/sh\0
放进一个用户的 intro 里,再将该用户删除,由此就能拿 shell