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
远程攻击结果
