SekaiCTF 2025 Reverse WP
SekaiCTF 2025 Reverse WP
总结来说,这次XCTF的逆向题质量都非常高(除了lua),每一题都非常有质量,非常推荐做。
Miku-music-machine
考点:控制流保护、SMC、迷宫
main核心代码如下,要求输入50长度字符串,异或上全局的一个数组,然后每个字节进行四步计算,&3来决定执行不同计算,然后Call每轮计算的v8值下标的函数,播放对应音乐。
v7,v8初始值22,最后判断v7是否等于418。
可以看到函数列表里面是给dwMsg赋值,用于播放不同的midi音乐。
看对v7、v8的操作猜测是类似迷宫的移动,导出函数列表,发现一共是441个函数,正好是21**2,说明是21x21的地图,上面的+=21和-=21也就对应的下移和上移操作,++和–对应右移和左移。
随便输入SEKAI{}
格式五十长度字符串进行调试,发现程序跑到一半就异常退出,调试发现是这边Call函数列表的函数时候出错了,这边可以看到这个是控制流保护下才有的Call指令。
断点该处控制流保护Call,调试发现正常的一些函数就会经过上两个jmp rax
跳转到对应函数,部分函数则不符合条件,会到最底下的jmp
,然后触发异常。
这边直接使用Patch代码,遍历所有函数,输出可执行的函数下标。
Call的部分代码直接该成,使用r15来遍历441个函数。
1 | .text:00007FF7290F4890 loc_7FF7290F4890: |
然后顶上r12赋值的地方之后直接jmp到这段代码头
1 | .text:00007FF7290F4839 lea r12, unk_7FF7290F0000 |
调试运行,在Call内部将最底下jmp
改成ret
,然后给两个jmp rax
设置条件断点输出r15。
1 | auto r15 = GetRegValue("r15"); |
运行输出结果,以下就是可以Call的函数下标。
1 | 22,24,25,26,28,29,30,31,32,33,34,35,36,37,38,39,40,43,47,55,57,59,61,64,65,66,67,68,69,70,71,72,74,75,76,78,80,82,87,93,97,99,106,107,108,110,112,114,115,116,117,118,120,121,122,123,124,131,133,139,143,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,164,165,166,169,179,187,190,192,194,196,197,198,199,200,201,202,204,206,208,213,215,217,219,223,225,227,229,232,234,235,236,237,238,240,241,242,244,246,247,248,249,250,253,259,267,271,274,275,276,277,278,279,280,281,282,283,284,285,286,288,290,291,292,295,299,301,303,305,307,309,311,313,316,318,319,320,322,324,326,328,330,332,334,339,341,345,349,351,355,358,359,360,362,364,365,366,367,368,370,372,373,374,376,379,381,383,387,393,395,397,400,402,404,405,406,408,409,410,412,413,414,416,418, |
上面得知该地图是21*21,而且这边可以看出可以Call的函数下标第一个刚好是v7初始值22,最后418刚好是代码最后对v7的校验值。
所以可以知道上面下标就是地图可走路的下标,而其他的则是墙壁,所以Call对应墙壁的函数才会触发异常,是利用控制流保护函数名单来做的校验。
使用代码输出该地图,可以看到确实是标准的一个地图,起点22,终点418。
然而从地图看,通路仅有一条,并且路程就44步,不符合题目要求输入的50长度字符串要求的200步,且在运行的时候发现有些Call还是会异常。
可以在异常的函数中发现这边有个int,而其他正常函数是没有的,查看这段代码交叉调用,可以看到有另一个函数对他这边做了xor处理。
实际调试发现只要先调用了底下这个Call对上面的做xor处理,上面的函数就可以正常调用。
使用IDC脚本遍历出所有带的Xor函数。
1 |
|
可以整理出以下对应Xor以及被Xor函数的表。
1 | { |
使用代码绘制出这8个函数下标处在地图上,然后大写字母对应Xor函数,小写字母对应被Xor函数。
1 |
|
得到以下地图,看地图就可以知道Xor的意义,也就是得先走到Xor函数位置,触发函数对小写字母处的函数进行Xor,也就是所谓的“解锁”,让小写字母位置函数可以Call,也就是可以通过。
综上需要走到大写字母处去解锁对应小写字母处位置,才能通过那格。
我是直接写了个交互程序,自己按最短路走了一遍迷宫,发现刚好是200步,就可以将路径数据转为输入字节异或后的数据,再异或即可得到Flag。
交互代码:
1 |
|
走完所有路程到终点,按Home输出路程大小以及路程01数据。
解密代码:
1 |
|
SEKAI{https://www.youtube.com/watch?v=J---aiyznGQ}
Sekai-Craft
考点:MC计分板命令,魔改Tea
核心代码就在Sekai-Craft\mvm\datapacks\mvm\data\mvm\function\mvm.mcfunction
中。
可以先用代码提取出里面的Delta、Key[4]、Cipher三个数据。
1 |
|
通过观察加密过程,他是把正常4字节运算展开到位单位计算,所以显得复杂,把代码按一块一块分析比较方便。
最后分析结果如下,魔改点就一处,直接异或上key[sum & 3]和key[(sum >> 11) & 3],而标准的是有加上sum值的。
1 | v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (key[sum & 3]); |
通过搜索部分代码可知,一共执行了64轮计算,由于有4个4字节密文也就是两组加密,所以Tea加密是32轮计算。
解密代码:
1 |
|
SEKAI{s3k41cr4tg00d:^)}
Alchemy Master
质量最高的题。
考点:服务端逆向,DLL远程线程注入,函数Hook,代码编译,CL编译中元数据计算
server.py
要求输入CPP的代码,然后写到solution.cpp,传入solution.cpp目录参数执行launch.exe。
1 | from pathlib import Path |
launch.exe
main函数,创建运行cl.exe
,传入编译参数以及server.py传来的源码路径。
加载本地plugin.dll
,在cl.exe
远程申请内存空间,然后将plugin.dll
内存数据写到cl.exe
进程中,再使用CreateRemoteThread
远程调用cl.exe
中的LoadLibraryA
函数,加载运行刚刚写入的plugin.dll
,这个操作也就是所谓的远程注入DLL。
plugin.dll
查看字符串,可以找到调用该字符串的函数。
往函数上层走,可以看到这边使用了Hook,方框处是被Hook函数地址,箭头处是Hook调用函数。
在下面箭头的函数中可以看到另外两个Hook,针对clxx.dll
和c2.dll
DLL的Hook,Hook调用函数都是同一个,也就是第一张输出Flag的地方。
接下来就是对cl.exe
的动调,因为该plugin.dll
是注入到该进程的。
动调方法:传参源码目录运行launch.exe
,然后断点在注入前的代码,使用plugin.dll
的IDA项目附加到当前运行中的cl.exe
,然后再运行launch.exe
的断点,plugin.dll
那边的IDA就会断下。
可以看到Hook了c2.dll
中的某个函数,开头jmp到自己的函数中。
发现Hook函数这边进来,也就输出Flag的函数中,将被Hook函数执行得到的结果传入。
通过输入不同的solution.cpp代码来调试,发现数据结构如下(下文会提到如何得到),第一个int是不同代码对应的编码,第二个int16是用来计算的操作数,最后的int16是未知数据,没用到这边不管。
此处v3是输入进来的一个CodeType
的链式数组结构,对应的是solution.cpp编译过程中不同类型代码对应的数据,v5是全局CodeType
数组,v5与v3的代码编码进行循环判断当前v3代码是哪个类型,最后取出对应代码编码的操作码。
使用上面全局取出的对应代码编码的操作码,对另一个全局数组进行–或者++操作。
下面+1LL那行代码实际就是通过操作计算得到对应全局数组的指针,然后++。
最后全局数组++–计算完的结果要与另一个目标数组数据完全相等,然后输出Flag。
从全局的编码数据来看,一共有9种用到的不同的代码编码,且操作数不一定相等,可以通过输入各种solution.cpp代码,来动调计算,看看不同的代码编码会对qword_7FFE52FA9AB0
的全局数据哪几个下标数据进行–以及++。
被计算的全局数组数据以及最终目标数据
1 | Original = [0x734, 0x0, 0x0, 0x0, 0xBBC, 0x0, 0xB63] |
代码编码分析
以下是不同solution.cpp代码在编译中Hook函数取到的对应CodeType数据,也就是上面的v3指针中的数据。
1 | int a() |
通过上面几个不同代码的交叉分析,可以得到一些代码编码对应的实际代码类型,共有9种代码编码,这边只识别出其中6种。
1 | 0x91e -> unk |
然后就是动调不同代码编码对应的计算操作。
一共动调到7种编码,另外两个没有识别的并没有动调到过,第一个数组是对全局数组进行++的下标,第二个数组是对全局数组进行–的下标。
发现下面已经识别出的6种代码编码对应的操作中,已经包含0-6所有下标的计算,所以理论上可以仅通过这六个代码编码使用Z3求出各个代码所需要的数量,让所有代码编码进行计算后,全局数组数据会符合目标数组数据。
1 | //{code, add_index, dec_index} |
Z3求解目标代码
使用Z3求解各个不同代码编码所需要的代码数量。
注:上文分析中throw命令会占用0x8a2和0x899两个代码编码,而0x899又是Call的编码,所以最后所需Call的代码数量要减去throw的代码数量,因为throw已经帮Call占用了一部分计算
1 | from z3 import * |
最后得到的目标代码文件目录传到launch.exe即可输出Flag。
代码发送获得Flag
1 | from pwn import * |