XYCTF_Reverse出题小记
本文最后更新于 2025年4月8日 下午
XYCTF_Reverse出题小记
写在前面
被好朋友墨水师傅邀请到XYCTF2025的出题群里,十分开心也颇具纪念意义,我们就是因为XYCTF2024而结缘,从选手到出题人身份的转变也让我倍感荣幸。虽然赛中出现了各种非预期和更换了一次附件的问题,但我希望各位师傅从这次比赛能有所收获,感谢大家对我题目的认可,我也会继续努力,成为想要成为的自己。
“要有最朴素的生活和最遥远的梦想,即使明天天寒地冻,山高水远,路远马亡。”
Moon
附件里给了一个 main.py和moon.pyd 。
那么可能有人会问,什么是.pyd
文件呢?
.pyd
文件是 Python 动态模块(Python Dynamic Module) 的一种,它本质上是一个 Windows 下的动态链接库(DLL)文件,但扩展名是 .pyd
,以便 Python 能识别并导入。
.pyd
文件允许你用 C、C++ 或其他编译型语言编写 Python 模块,然后作为一个模块供 Python 导入和调用,就像普通的 .py
文件一样。
你可以这样使用:
1 |
|
很多人赛中来询问我关于python版本的问题,显示模块导入不成功,目前见过的pyd文件大多都可以在字符串表里找到关于python版本号的问题
比如这里,我们可以看到python版本为3.11
类比去年ciscn初赛的rand0m,也是同样的,能看到python版本为3.12
打开main.py我们发现是导入了moon模块然后调用了moon.check_flag来进行验证(顺便说一下这题推荐了毛姆的《月亮与六便士》哈哈哈)
我们打开moon.pyd搜索字符串,能定位到sub_180002550函数
里面大概呈现了所有的字符串
1 |
|
我们能看到有xor_crypt,seed,random,甚至密文的十六进制值在这里也进行了体现
那么我们下一步就是要找到xor的位置,其实就是寻找函数PyNumber_Xor,搜索引用发现在函数sub_180001370处有逻辑
先xor然后写进一个list里面
往后找比较,找PyObject_RichCompare找到sub_180001B70
其实看到这里就不难猜到是random.seed去生成随机数而且进行xor了,而事实也的确如此
现在我们需要找到随机数种子(当然也可以直接根据flag头flag{爆破)
观察到xor的v8来自off_18000B618,一交叉引用发现很多,然后找到了函数sub_180003010
随机数种子是0x114514,当然也可以有快速定位的方法,关注函数PyLong_FromLong,这是对数值进行处理的函数,然后分析到这里我们就可以写解密脚本了
最终解密脚本如下:
1 |
|
这里后面有个非预期的解法(也可能是我太菜了),import moon模块,然后help(moon)
可以得到以下信息:
这里就能猜出来大概逻辑了,ida找密文就行了
最终flag:
1 |
|
” 希望大家都能在满地都是六便士的街上,抬起头看到自己的月光“
Lake
出了个old school的编程语言pascal,最早的国内OI比赛的时候用的这个,也是人生学的第一门编程语言,出题也有纪念意义了。
对于pascal的编译环境配置这里提一下:现在的fpc编译器已经没有直接支持x64版本的了,如果想在win下配置fpc,那么需要先下载32位的编译器,然后再在同目录下安装交叉编译器,这样就能得到可进行64位编译的fpc pascal编译器了。
这里先把题目源码放上:
1 |
|
然后我们拿到这个程序运行一下发现是类似于打字机的效果,然后推测肯定有sleep函数
通过sleep函数跟到主函数
1 |
|
观察到实现了一个类vm,然后后面的sub_1000019B0是个字节位移的加密,灵感来源2024熵密杯某题(
1 |
|
然后可以发现密文和vm的opcode(这里一开始产生了多解,给各位解题的师傅带来了不好的体验,在这里说声抱歉)
opcode的格式是
1 |
|
各操作与代码对应关系为:
1 |
|
那么我们先还原位移加密然后再爆破vm部分其实就可以了。
解题脚本如下:
1 |
|
最终得到flag:
flag{L3@rn1ng_1n_0ld_sch00l_@nd_g3t_j0y}
梭罗的《瓦尔登湖》非常值得一读(又双叒叕植入书籍推荐哈哈)
“最大的收获和价值远不能受到人们的赞赏。我们很容易怀疑它们是否存在。我们很快把它们忘记了。它们是最高的现实。也许最惊人、最真实的事实从来没有在人与人之间交流过。我每天生命的最真实的收获,就像朝霞暮霭那样难以捉摸,无法言传。那是我抓到的一点儿星尘,一片彩虹。”
Summer
出这题的设想和灵感来自于我的CTF领路人,然后一个有趣的Haskell题目就应运而生了。
函数式语言的最大特点就是惰性计算和使用惰性列表来进行存储,惰性计算就是在调用一个值之前不会对这个值进行计算,惰性列表存储类似于数据结构中的链表,存在头指针,然后跟着的是存储的数据,数据存储是不连续的,也就是为什么这个题的密文比较难寻找的原因了。
照例放一下题目的源码:
1 |
|
拿到这个题目,没有什么好入手的办法,动调或者摁撕汇编了(预期其实就是这两种方法)
很多人应该找到了那个反编译Haskell的库(https://github.com/gereeter/hsdecomp)
,但是实在是太老了,反编译不了是预期的。所以我们只能硬着头皮进行调试
以下部分分析思路来自5m10v3师傅,我觉得写得非常好,就放到这里跟大家一块分享
进入 main 函数,可以看到 hs_main 以 ZCMain_main_closure 作为参数,指向 haskell 程序的真正入口点。
ZCMain_main_closure里面可以看到有Main_main_info,这个函数也调用了很多底层的函数
ghczminternal_GHCziInternalziBase这个函数的第一个参数指向了sub_40F318,而sub_40F318又调用sub_40F2D0,这个函数其实就是打印字符串
ghczminternal_GHCziInternalziList_length_info这个函数是检测长度的,我们可以断在这里看密文和密钥的长度
这里的这个rbx最后实际存储了长度,第一次断在这里获得我们输入的长度,第二次断下获得密钥长度0x16,第三次断下获得密文长度0x32
我们继续向下调试,在字符串列表能看到一串Inkleqmp%q]Ncqv]Qwoogp,推测这就是被加密的密钥,那么我们调试到loc_43E370的时候发现这里经过了一些处理,会有字符出现,再次断到这里发现这里就是对密钥的处理
(这里是需要连续跑到三次才能跑到下一个字符,第一次会返回字符实际的值,第二次返回一个1,第三次会返回对应的长度,比如1,2)
和被加密的密钥对比,发现密钥异或了0x2,那么真正的密钥就是Klingsor’s_Last_Summer(克林索尔的最后夏天)
我们继续往下调试,发现这里其实不是单纯解密了密钥,因为密钥是循环被写入一个位置的,会调试出大于0x16的序列,最后一次的返回值是0xFF
循环写入密钥,并且最后返回的长度是0xFF,我们可以联想到RC4的KSA过程,这里基本就可以确定是rc4的加密算法了,那么接下来是寻找密文的过程,通过搜索Haskell的惰性存储方式,了解到非字符串的数据是以惰性链表的形式存储的,它的内存存储方式和 C 语言中的数组非常不同,不是连续的内存块,而是由链式结构组成。
每个节点保存两个指针:一个指向当前的数据值,一个指向下一个列表元素(即尾部)
那么我们已知密钥出现在data段,那么去data段寻找密文
很快就发现了在密钥附近有链式的存储结构
然后就可以提取出密文,密文为
1 |
|
尝试用密文解密rc4发现仍然不对,但是稍微根据flag头思考一下就会发现解得的值跟密文差了xor 0x23,那么就能判断rc4之后还xor了0x23,当然直接动调单步跟也是可以的。
Haskell这类的函数式语言程序最大的问题在于下内存读写断点是无法跟到比较逻辑的,这或许就是惰性计算的魅力所在吧。
最终解题脚本如下:
1 |
|
得到flag:
flag{Us3_H@sk3ll_t0_f1nd_th3_truth_1n_th1s_Summ3R}
希望师傅们通过这道题可以初窥Haskell逆向的冰山一角,本人也是在出题的过程中慢慢学习的Haskell相关知识,另外推荐《克林索尔的最后夏天》,黑塞用诗意的笔调勾画了克林索尔的形象。
“于是我愿重走这条路,带着不同的感触,聆听小溪,凝视夜空,一次又一次。”
写在最后
笔行至此,忽然想到某老贼书里酒德麻衣悠悠唱起的和歌:”或许是不知梦的缘故,流离之人追逐幻影“,就以一句对其的应和作为结尾吧:
”人总要抱紧什么才知道自己真的存在,哪怕那只是幻影“