首页
社区
课程
招聘
[原创] 看雪 2023 KCTF 年度赛 第十二题 深入内核
2023-10-1 04:36 8187

[原创] 看雪 2023 KCTF 年度赛 第十二题 深入内核

2023-10-1 04:36
8187

name输入KCTF,在0x452582下断点,ecx就指向正确的serial




全文完,剩下的都是一些碎碎念


用时9小时12分22秒,其中1小时吃午饭(还没吃完一血就出了,顿感事情不妙,但也瞬间没什么动力了:一血时间过短,后面在怎么做的快的基本上只能拿到一半多点的难度分,还着啥急),8小时陷入在各种无用的垃圾代码和虚假代码中找不到最终的判断点在哪里(不过也顺带理了一些程序的逻辑);最后找到答案时心情五味杂陈……

解题过程没什么值得说的技术点,主要记录下心路历程和爬坑过程。

IDA打开,满屏的代码混淆,但又有一种扑面而来的熟悉感。
众所周知,看雪CTF比赛的防守方很喜欢把自己以前出过的题加些修改再重新出。再看出题战队,果然混淆方法与 看雪 2022·KCTF 春季赛 第九题 同归于尽 有明显的相似性:

大量垃圾代码+似是而非的虚假代码+隐藏控制流

区别在于,本题没有反调试。

但对于一个一向不喜欢动态调试的人来说,遇到代码混淆的题真的极度不爽。(同样是出一道题,出成无混淆的算法类型的题又小又难格局大,即使做不出来也是做题者知识面不够,更容易收获好评)

sub_40485D连同49A0E8和49A0E0两个全局变量被大量调用且有多层递归。sub_40485D在IDA中无法反编译,但在Ghidra中可以,能看到里面几乎全是对自己的递归调用,但唯有一处例外,即0x405a09处的 (**(code **)(*param_1 + 4))(); ,所以能够确定这个函数只影响控制流,没有实际逻辑。

两个全局变量指向的地址附近有off_48F11C、off_48F18C、off_48F1B0三组函数指针,其中第三组是空的库函数,第二组与sub_40485D很像都是不能反编译的混淆函数,第一组则看起来正常很多(后面的分析表明,这一组确实有真实逻辑)。

全局字符串能看到48F1E8的"Success, GoodJob!"以及48F208的"error",交叉引用对应到sub_41F227和sub_438078(都在off_48F11C表里,但后面的分析表明,这两个函数虽然被调用到但里面触及这两个字符串的分支条件永远不会成立)(在此处耗费了很多时间)。

从输入输出入手,在程序调用系统函数时中断,回溯调用栈可以找到sub_46FB42是getchar,sub_41DFAB是printf。而getchar的唯一调用者是sub_4525EB(调用点在0x45AD88),这里也是真实处理输入的地方,而且name和serial都是在这里输入的。printf虽然调用处很多,但实际输出的地方也在sub_4525EB,而且无论输出是banner还是最终的success或error,调用点都在0x45DE9C。
到这里线索也中断了,因为从输入到输出不是靠控制流连接的.

后面分析表明,sub_4525EB是最重要的函数,这里有输入输出以及调用各种计算函数的逻辑。排除掉里面大量调用sub_40485D的混淆以外,还使用了类似控制流平坦化的混淆,主要逻辑是分块的,根据传参的情况经过一些运算处理后决定走到哪个部分。

动态调试发现sub_4525EB被sub_460D0E调用(也被),调用点在0x463197,是间接调用,而且这里是循环调用。这里的调用决定了进入sub_4525EB后会走哪个分支,所sub_460D0E是一个类似分发器的角色。

线索再次中断。尝试对输入下数据断点跟踪,发现数据复制的次数有点多,跟不太下去。接下来只好具体分析sub_4525EB调用的每个函数。(当然,从一血时间不足一小时看,显然路已经走偏了;但苦于到现在也没找到检验逻辑在哪里,似乎也没有更好的办法了)

回到getchar的调用点0x45AD88,向下看,sub_41E152是把输入值作为value以一个不知道哪里来的字符为key插入一个map中。而sub_41E152引用的第一个参数是sub_41E152的第一个参数加四。

花费大量精力标记结构体(此处耗时相当长),然后找参数加四值的交叉引用,发现了另一个函数sub_41E38D是从map中取值的。而且,这里对map调用的模板函数名称暴露了一些信息:std::_Tree<std::_Tmap_traits<std::pair<tagCALLGRAPHNODE *,Symbol *>,tagVirtualCallee *,CallGraphNodeSymbolPairLess,UtcPoolAllocator<8,std::pair<std::pair<tagCALLGRAPHNODE *,Symbol *> const,tagVirtualCallee *>>,0>>::_Find_lower_bound<std::pair<tagCALLGRAPHNODE *,Symbol *>> ,表明这个map是变量表(Symbol),而sub_41E152是自定义虚拟机的基本块逻辑,sub_460D0E则是决定基本块调用顺序的分发逻辑。

突破口在确认map是变量表之后。对sub_41E38D下断点,跟踪所有对它的调用,逐步发现了这里有对serial的加密、解密等运算,以及sub_45241B(调用点在0x45AE1C)是对长度的检查,里面的sub_4520C5(调用点在0x4524B7)似乎是处理数学比较运算(它的第三个参数就是字符串"!="之类的运算符)。

正准备沿着对sub_41E38D的调用跟踪从输入到输出的流程,突然在9次(大概)之后在调试器寄存器窗口重新发现了serial的字面值。回溯调用栈到sub_4524E7,在0x452582有一个明显的std::string判等(sub_469F92),且两个参数值都是serial的值?!

重启程序,name换成KCTF,在0x452582下断点,发现sub_469F92的第二个参数没变,而第一个参数变了。提取出来,验证这就是正确的serial……

很尴尬,不知道是否为出题人有意留下的破解弱点。又想到了 看雪 2022 KCTF 秋季赛 第十一题 衣锦昼行 也是像这样在某个时刻让明文的目标serial出现在了内存中。也许本题的一血仍然是像当时一样用CE找内存看的??
但总之,如果正面解程序中的虚拟机和运算,复杂程序一个小时应该绝对不够。如果说这个破绽是出题人预期之内的,那只能说其实对于攻击方而言整道题目变成了一个捉迷藏的游戏,谁能最先在各种混淆中找到这个位置谁就胜利,无疑放大了随机性,而且加上看雪CTF计分方式有些过于偏重时间(对防守方的难度分以及攻击分的积分都是如此,例如今年的第二题做出人数那么多,难度分却比本题还高甚至达成了火力值第一,只因为最开始题目有缺陷导致直到作者补充说明之后题目才有可解性使得一血出现时间很长……),这种随机性对攻击方而言真的不知道说是好事还是坏事;而如果出题人本意是希望攻击方破解程序的虚拟机+混淆+算法,那这种正向计算加一个简单比较的方式只能算巨大的非预期。


p.s. 这篇文章有很多空行,但是粘贴到论坛后都消失了,所有段落都挤在了一起。想知道论坛的编辑器应该如何保留空行?


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

收藏
点赞3
打赏
分享
最新回复 (2)
雪    币: 3507
活跃值: (17964)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2023-10-1 19:00
2
0
感谢分享,挺喜欢看前辈的碎碎念
雪    币: 19410
活跃值: (29069)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-10-1 21:15
3
1
感谢分享
游客
登录 | 注册 方可回帖
返回