Skip to content

lab-03: 堆上漏洞及其利用

:material-circle-edit-outline: 约 2343 个字 :fontawesome-solid-code: 31 行代码 :material-clock-time-two-outline: 预计阅读时间 8 分钟

[!ABSTRACT]

俞仲炜 3220104929 241201

堆管理器基础

[!QUOTE]

  1. 阅读 example.c 代码,在报告中简述这个目录程序的逻辑;通过 make build 完成对程序的编译和 patch,提供 ldd 执行后的截图;(10 points)
  2. 阅读和运行 test.py 代码,分析打印的 dump*.bin 的内容。要求类似示例图一样将所有申请和释放的对象标记出来,特别标注出 tcache 单向链表管理的对象);(20 points)
  3. test.py 中注释的两行 handle_del 取消注释,再次运行,新产生的 dump*.bin 和之前的相比有何变化?多释放的属于 WilliamJoseph 的堆块由什么结构管理,还位于 tcache 链表上么?请复习课堂上的内容,在报告中进行回答;(10 points)

example.c 逻辑

这是一个用户信息管理系统,允许用户创建、删除、查看和编辑用户信息,此外使用了自定义的内存分配钩子来跟踪内存分配和释放操作

  1. 功能函数
    • user_add():添加新用户,将用户信息存储在类型为 user_infoinfos 数组,会检查内存是否 enough
    • user_del():删除用户,需要验证密码
    • user_show():显示用户信息,需要验证密码
    • user_edit():编辑用户信息,需要验证密码
  2. 辅助函数
    • getline_wrap():从文件描述符读取一行输入
    • heap_debug():转储堆内存到文件
  3. 主循环
    • main() 显示菜单,等待用户选择操作,并调用相应的功能函数

ldd 执行后的截图

image-20241202090122068

dump*.bin 内容分析

[!NOTE]

太多了就不全截出来了,这里 chunk 就分已分配的和未分配的两类,就只分别截部分进行分析

example.c 中,user_add 会先后为 user_info *infochar *intro; 进行 malloc,前者会得到 0x70 大小的 chunk,后者会得到 0x50 大小的 chunk(实际 data 大小要减 0x10)

tcahce bin TOP 会维护满足条件的已释放 chunk 组成的单向链表,如下图所示:

image-20241203095836826

图中一个黑色方框示意一个 chunk 的范围,图中的 chunk 均已被 free

红色箭头示意了挂在 tcache TOP chunk 下的两个单项链表,分别维护着 0x400x60 两种 data 大小的 chunk

可以看到,所有 bk (key)都指向 0x010 处,fd(next)参与构建单向链表

此外可以注意到,tcache bin 里的 chunk 的 P bit 都置 1,以免发生 merge

以及,prev_size 都为 0,符合 tcache bin 的设置

然后我们再看尚未被 free 的 chunk,如下图所示(从蓝色方框开始,往下的都是):

image-20241203100316824

可以看到没有 fdbk

新产生的 dump*.bin 变化

tcache bin 的一个特点是,每个单向链表最多只能容纳 7 个 chunk 以加快重分配速度

原本正好已经 free 了 7 * 2 个 chunk,取消注释后,新的两个被 free 的 chunk 将进入 fast bin

下图为新的被 free 的 2 * 2 个 chunk,左边为变化前,右边为变化后

image-20241203102835557

多释放的属于 WilliamJoseph 的堆块由什么结构管理,还位于 tcache 链表上么

由 fast bin 管理,不在 tcache 链表上,我们可以检查 tcache bin TOP chunk 来确认,TOP chunk 并没有挂载新的链表:

image-20241203103103667

堆上常见漏洞

[!QUOTE]

  1. 找到 uninit/uninit 中的未初始化读漏洞,在报告中给出分析;编写攻击脚本 ,完成对于堆上 flag 内容的窃取; (10 points)
    • 远程环境位于 IP: 8.154.20.109, PORT: 10400
  2. 找到 overflow/overflow.c 中的堆溢出漏洞,编写攻击脚本触发该漏洞;(10 points)
  3. 找到 uaf/uaf 中的释放后使用漏洞,编写攻击脚本触发该漏洞;(10 points)

找到 uninit/uninit 中的未初始化读漏洞

main 主循环开始前,程序进行了一次 0x40 大小的 mallocfree,但是么有清空里面的内容

char* flag = malloc(0x40);
sprintf(flag, "the sacred and awesome flag is %s\n", getenv("FLAG"));
// ....
free(flag);

之后 user_add 里又进行了一次 0x40 大小的 mallocintro,那么这个获得的内存空间就是之前 flag 的内存空间,含有 flag 的信息,我们可以通过 user_show 窃取

编写攻击脚本

只需创建一个空信息账户,并获取其信息,就能将 flag 的信息获取出来

bob_idx = handle_add(b"", b"123456", b"", b"")
handle_show(bob_idx,b"123456")

结果如下图所示:

image-20241202115939523

找到 overflow/overflow.c 中的堆溢出漏洞

可以发现下面这个拿 shell 函数

void backdoor()
{
    system("/bin/sh");
}

此外,我们注意到 user_add 里只给 intro 分配了 0x40 的空间,但是 user_edit 里却可以修改 0x60 的空间,这里出现了堆溢出漏洞

char *intro = malloc(0x40);
    /* ... */
read(0, info->intro, 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):

image-20241202225608658

找到 uaf/uaf 中的释放后使用漏洞

user_del 中有一行被注释,导致被 free 的元素还在“已被分配”数组中,造成 uaf 漏洞

free(info->intro);
free(info);
// infos [index] = NULL;
return;

编写攻击脚本触发该漏洞

uaf.cuser_add 增加了一个输入,为 intro_size,需要将脚本里的 handle_add 进行相应的修改

通过控制 intro_size,我们可以让 chunkfree 进不同的 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")

可见除了部分信息被覆盖为 fdbk,其余信息都成功读取了( motto1 ):

image-20241202230358554

堆上漏洞的利用

[!QUOTE]

  1. 利用 overflow/overflow.c 中的堆溢出漏洞,通过劫持 freelist 的方式(10 points),写 exit GOT 表数据将执行流劫持到 backdoor 函数,从而完成弹 shell,执行 flag.exe 取得 flag(5 points)
    • 远程环境位于 IP: 8.154.20.109, PORT: 10401
  2. 利用 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 函数,完成攻击

远程攻击结果

image-20241202184524384

[!FAILURE] 个人遇到的问题,可跳过

这个脚本我写完后调试了一整天都没找出哪出了问题

最后发现是,在 example.c 中,exit(-1) 是在菜单 default 里,确切来说它就不在菜单里

我当时一扫而过了,之后一直以为 exit(-1) 在 [ 5 ] leave 里

具体来说,我错误的脚本里,最后准备交互的代码是这样的

p.recvuntil(b"[ 5 ] leave\n> ")
p.sendline(b"5")

然后这个 [ 5 ] leave 执行的是 return 0,我是傻逼

确实是我经验不足,正常的程序怎么可能用 exit(-1) 来 leave

这么重要的地方也确实应该在调试过程中回去检查,不应该内耗这么久才发现

利用 uaf/uaf 中的释放后使用漏洞,通过类型混淆的利用方式实现弹 shell

思路

[!reference]

如何利用 Unsorted Bin 泄露 Libc 基地址 - blog of chuj

linux - Why there isn't main_arena in libc.so symbol table, while there is one in malloc.c of glibc2.23? - Stack Overflow

确认 main_arena 相对 libc 的偏移地址_main arena 距离 libc 的偏移-CSDN 博客

unsorted bin 尾节点的 fd 指针会指向 main_arena,借此我们可以通过 uaf 漏洞获取 main_arena 相对地址,进而泄露 libc 基地址,进而获取攻击指令

通过 IDA 反汇编 libc-2.31.so ,确认 main_arena 偏移量为 0x1ECB80,由此可以计算出 libc 基地址

image-20241203115350850

进而可以得到 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 里会先 freeinfo->introfreeinfo,所以我们可以把待执行语句 /bin/sh\0 放进一个用户的 intro 里,再将该用户删除,由此就能拿 shell

远程攻击结果

image-20241202221023204