From 79980398ccbd823c26fc67548ee2a63a3411803b Mon Sep 17 00:00:00 2001 From: CatMe0w Date: Fri, 6 Dec 2024 08:26:18 -0700 Subject: [PATCH] Update --- ...56\265\346\200\273\347\273\223-catme0w.md" | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 "source/_posts/2024\347\247\213\345\206\254\345\255\243\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\270\211\351\230\266\346\256\265\346\200\273\347\273\223-catme0w.md" diff --git "a/source/_posts/2024\347\247\213\345\206\254\345\255\243\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\270\211\351\230\266\346\256\265\346\200\273\347\273\223-catme0w.md" "b/source/_posts/2024\347\247\213\345\206\254\345\255\243\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\270\211\351\230\266\346\256\265\346\200\273\347\273\223-catme0w.md" new file mode 100644 index 0000000000..450e43925d --- /dev/null +++ "b/source/_posts/2024\347\247\213\345\206\254\345\255\243\345\274\200\346\272\220\346\223\215\344\275\234\347\263\273\347\273\237\350\256\255\347\273\203\350\220\245\347\254\254\344\270\211\351\230\266\346\256\265\346\200\273\347\273\223-catme0w.md" @@ -0,0 +1,155 @@ +--- +title: 2024秋冬季开源操作系统训练营第三阶段总结-catme0w +date: 2024-12-06 23:25:00 +tags: + - author:CatMe0w + - repo:https://github.com/CatMe0w +--- + +你们好,我是catme0w,来分享一下我在内存分配器挑战中的经历。 + +也看看我在二阶段的文章吧:https://rcore-os.cn/blog/2024/11/10/2024%E7%A7%8B%E5%86%AC%E5%AD%A3%E5%BC%80%E6%BA%90%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E8%AE%AD%E7%BB%83%E8%90%A5%E7%AC%AC%E4%B8%80%E3%80%81%E4%BA%8C%E9%98%B6%E6%AE%B5%E6%80%BB%E7%BB%93-catme0w/ + +# Round 1:更好的TLSF分配器 + +一开始拿到题目的时候,是想尝试一下改善“正常的分配器”的,也就是实现通用分配器,因为那时想着,毕竟在内核空间之中搞小动作太过容易,以非正常方式刷分数显得太cheaty了一点也不好玩。 + +粗略了解过后,认识到原来TLSF实现所使用的rlsf库已经是分配器算法的较优实现了:https://github.com/yvt/rlsf#measured-performance 。尝试调整了一下rlsf的参数(FLLEN和SLLEN),发现并没有什么变化,无法突破170。 + +认识到试图优化TLSF似乎是没有前途的;改善TLSF的路线,宣告失败。 + +# Round 2:面向测例分配器 + +那么,我们不得不从头开始写我们自己的东西了。 + +要了解分数由什么决定,也就是什么决定了indicator数量的上限,进一步地,了解每一步中内存分配行为和内存碎片的变化。 + +不难看出,测例会释放掉偶数项的块,而保留奇数项。 + +由此,我们可以针对性地“攻击”这一点——研究测例的分配模式,追踪每一次分配是否是“不会被释放”的那一批,将其连续排列在一个特殊的区域中,使得内存碎片完全不会出现,利用率达到惊人的100%。 + +可当我准备动手时,意识到这个东西在现实世界里没有意义,它只能针对这个测例起效。至少在我看来,这已经和非正常方式没有太大差异了。 + +那么,要不……一不做二不休,干脆直接把非正常方式做到极致? + +# Round 3:假的分配器 + +从测例的行为可以了解到,它只会验证块的最后一个字节是否正确,那么思路就很简单了,追踪每次分配行为,如果是外层的Vec,就给它用真的分配器;如果是内层Vec,就只分配两个字节,然后返回伪造的内存地址! + +可是,说的倒轻巧,思路是简单不假,可是究竟有什么方法精确地识别一个堆分配操作来自哪里呢?答案是没门!更不要提伪造内存地址会有多难做。 + +我确实尝试了一下,但是……很快放弃了。 + +值得一提的是,分配器的部分不直接在内核中,没有println可用,因而调试极其困难。 + +必须再换一个思路才行。 + +# Round 4:他不体面你就帮他体面分配器 + +注意到:每次分配的内存大小由base+delta构成,base指数增长,从32开始,每次为之前的两倍,直到达到512 KB;delta为indicator数量。 + +释放的规律上文已提过,偶数项释放,奇数项保留。 + +那么,新的思路由此诞生:追踪indicator的数量,确定当前测例位于哪一“回合”,进而精确计算出哪些分配会被测例保留,记下它们的内存地址…… + +然后,从分配器的代码内,释放掉它们! + +他不体面,你就帮他体面! + +### + +我最开始思路还要更简单些,只是记录下所有分配的内存地址,当遇到大于512 KB的释放时,就释放掉之前记录的所有地址。 + +但这样会有一个显而易见的问题:double free——测例本身还会释放一半呢。 + +由于测例很简单,我一开始不认为这是个问题,但在我无数次撞在LoadFault上之后,我投降了…… + +### + +以下是我最终的详细思路: + +具体不会被释放的块,长度为64+delta、256+delta、1024+delta……。 + +将其base值,也就是64、256、1024……维护在分配器内的一个Vec中。 + +当分配器命中这些值时,把它们的内存地址和大小保存在另一个Vec中。 + +当分配器命中下一“回合”的32+delta,也就是第一次分配时,标志前一“回合”结束,此时,将记录到的那些不会被测例释放的内存块释放掉。 + +由此,我们便消除了测例的“内存泄漏”,内存将永远无法被耗尽。 + +### + +但没那么简单,这个“正确的思路”第一版实现仍然会停止在130,并且不是no memory,而是触发了LoadFault,内存越界了。 + +经观察,我所记录的那些“我要帮它体面”的内存块中,恰好有某项命中了外层Vec的扩张大小…… + +由于临近截止,我没有时间仔细去寻找它们到底命中哪个长度了,只能暂时绕过它。 + +在indicator足够大之前,每回合均不释放开头的两项。 + +### + +一阵操作之后,我终于第一次看到了indicator数量超越170。 + +或者说,我终于得到了梦寐以求(?)的无尽indicator。 + +在何时停止好呢?来做一个114514吧,多有纪念意义啊。 + +只是,它在65536就停了下来,显示no memory。 + +何故呢? + +原来,测例中负责释放的那部分(free_pass)所使用的delta长度是u8,只有负责分配的部分(alloc_pass)才是usize。65536是u8的上限,不是我的上限。 + +我的114514,梦碎了。 + +### + +“这不可能!” + +我大叫起来。 + +此时临截止不到一个小时了,我分明记得之前看到过有人把indicator刷到了六位数。 + +他们怎么绕过u8的上限的? + +# Round ?:一种基于kernel exploit的分配器 + +想要打破u8,那就得让free_pass不执行。 + +得发挥更强大的想象力,寻找更下作(?)的手段。 + +那么,这其中的花样可就多了…… + +此时的操作系统仍是unikernel,没有虚拟内存,不存在用户空间,所有的代码共用同一个(物理)地址空间。 + +你可以找到测例里alloc_pass那个循环下标的内存地址,然后直接把它改成114514…… + +或者,直接覆盖掉println…… + +甚至,还可以想办法在我们能允许修改的这一小块代码中弄出些use-after-free,来点缓冲区溢出,来点ROP…… + +### + +但我想,已经刷到六位数的那些人用的并不是这样的方法。他们会怎么做呢?比赛结束之后再去看看吧。 + +# 100% Honest Review + +在最开始,我对参与这项挑战的兴趣不大。我不喜欢这种有零和博弈味道的规则——早提交可能失去优化机会,晚提交可能因结果相同而落后。 + +ArceOS已经很难了,题目若再有压力,我会直接躺平。我承认我很脆弱。 + +话虽这么说,我还是来了。尽管我很晚才开始了解挑战的细节,而到那时,已经有人发掘出了非正常方式。 + +那时我的反应与群里一位的想法几乎一致:这不就彻底没意思了吗? + +但当我亲自上手这个题目时,想法还是逐渐发生了变化。~~哎真香~~ + +纵使前期有些不愉快,最终还是收获了不少快乐与知识。 + +### + +但倘若你问我,还想不想再玩一次这样的挑战题目的话…… + +我的回答是否定的。