第一次在大比赛出题,本来出了4个逆向,最后只上了两个,也是Re所有题目里面最难的两个(mmm和loda)
Multiple Magic Matrices
出题记录
第一部分的约束矩阵:灵感来源于朋友发我的一张 5*5 逻辑bingo图,有点像小学那种推断谁说的对谁说的错的脑筋急转弯题,当时发现转为布尔矩阵后用z3进行约束能出结果,遂扩展到 8*8,又加入了一些条件,本意也是想让用AI直接提取约束条件,然后写z3脚本跑出来约束
第二部分的加密算法:来源于不久之前想自己设计的基于洛书幻方的分组加密算法,随便搓的,同构还算简单,没有什么拐弯抹角的加密方式
第三部分的数织:灵感同样来自朋友,随便搓了个字符画当图案用 (后面还为能让加密结果尽量贴近这个图案找了半天的最佳iv值)
WP
main函数:
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
先查看对于key的校验:
1 | __int64 __fastcall sub_140002CC3(__int64 a1) |
输入的是一个uint64,在sub_140001AAA转为bit,然后填充入matrix中,查看matrix的校验函数:
1 | _BOOL8 sub_140001BDB() |
其中有很多对矩阵的约束,转为python的z3脚本:
1 | from z3 import * |
得到解:1001011111111111010000000110100111010110000110000111000110111110
(十进制为10952543642096005566
),此时程序打印出来一个图案:
1 | □ □ □ □ □ □ □ □ □ □ □ □ □ ║ 2 2 1 3 |
显然,这是一个数织问题,我们可以通过在线网站恢复出这里所需的数织图案:
1 | ████ ████ ██ ██████║ 2 2 1 3 |
接下来跟踪这个数织是如何构造的,注意到之前密钥正确后被转为了hex并插入了一个字节0xEF(97ff4069d61871beef
),查看后续逻辑:
1 | _DWORD *__fastcall sub_140002F00(const char *a1, _DWORD *a2) |
此函数初始化了一个类似base64的映射表(结尾是_!),然后把字符换成在表中的索引,并转为6位2进制,之后在main函数中要求bit总数大于144,相当于输入的字符长度大于24,继续查看sub_140003070:
1 | __int64 __fastcall sub_140003070(__int64 a1, int a2, __int64 a3) |
这是一个很明显的滑动窗口加密,一次带入144bit(18字节),key使用刚才的key,还初始化了一个额外的iv值(注意key和iv都是9字节),查看核心加密函数:
1 | __int64 __fastcall sub_140001780(_DWORD *a1, _BYTE *a2, _BYTE *a3, int a4) |
是一个很明显的分组加密算法,一组9字节,采用CBC模式,用到了两个矩阵,一个是洛书幻方(492357816),还有一个是S盒,同构这个加密算法:
1 | class Luo_Shu: |
加密完成后,有一个函数将比特流转为布尔方阵:
1 | _DWORD *__fastcall sub_14000334F(_DWORD *a1, int a2, int *a3) |
会将比特流转为比其长度大一点的最小方阵,剩下位置填充为0,继续查看最后一个函数:
1 | __int64 __fastcall sub_1400036CC(int a1, int a2, _DWORD *a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7) |
校验了方阵是否为13*13的(169bit),如果是则对其某些位置取反,根据之前得到的目标matrix,可以恢复出来原矩阵的形态为:
1 | 1100101100101 |
发现是162bit,也就是输入是27字节,编写脚本解密:
1 | charset = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!") |
flag{M4g1c_m47r1x_15_ev3rYwH3rE!}
Let Operator Dance Alltime!!!
出题记录
这题本来该更难的, 但是觉得太难把payload换了 (之前的payload是一个更难的题)
外层是PE加载器,能用密钥解密payload并通过环境变量传参,用一个多步操作映射迷宫约束了密钥
payload魔改自于分布式计算项目LODA,是一个类汇编语言解释器,主要解决数学问题,其在github上开源并拥有官方文档和官方在线运行可视化,payload层的主要思路也是diff源码,从main开始查找魔改点
WP
程序有UPX,脱壳.
脱壳后发现程序代码段很少,有一个巨大的数据段,且有几个额外段.prt
、.prd
、.prb
.
给出的main函数显然是假的,解密后也是fakeflag,注意到.prt段的函数sub_1400035C5
中有反调,如果有调试器则跳转至main,这说明起始函数应当位于.prt段中,不难发现是sub_140003659
.
sub_140003659的起始部分打印有字符串加密,要求输入一个密钥通过sub_1400030E7
的检验,sub_1400030E7是一个迷宫,检验输入的字符串是否在指定字符集A-Za-z0-9_!中,若存在则将其转换为对应的索引,还有一些其他校验(比如有4个下划线,结尾是感叹号等),然后解密迷宫地图,转为比特矩阵并走迷宫.
注意到每个字符控制的不止一步,而是很多步的组合,最终要到27,47的位置,输入的字符串会被xor 0xA作为解密后续数据的密钥.
解析迷宫并编写脚本求解:
1 | mapping = { |
得到密钥7H15_Is_4_73tr15_m4z3!
,发现可以继续向后执行逻辑了,之后是一个单字节解密和一个PE文件加载器,在
1 | CurrentProcess = GetCurrentProcess(); |
中读取了名为LODA_ARGS的环境变量值,并将其按空格拆分后解析,传入payload(实际为dll),函数在v22(lpBaseAddress, 1, v53);
这一行进入dll内部,在此之前可以选择dump整个dll以便分析,也可以继续在动态调试中跟进.
之后进入dll,如果没有在LODA_ARGS中传参,那么程序会直接退出,我们可以试试创建LODA_ARGS并随机输入一些内容进行盲测,注意到此时弹出了Input your flag:
,我们可以去dll中查找这个字符串:
1 | // Hidden C++ exception states: #wind=2 |
此即为main函数,注意到程序在我们输入flag后弹出了一条消息:Unknown command:
,这说明LODA_ARGS中的第一个参数是一个指令,我们可以搜索字符串或者使用万能的help进行盲测,弹出:
1 | Welcome to LODA CTF version. More information at https://loda-lang.org/ |
给出了一个网站https://loda-lang.org/,可以在这里查看LODA语言的官方文档,以及在github上获取原项目的源代码和编译程序.
注意到LODA是一门类汇编语言,不具有显式的堆栈结构,只有无数个不限制数值上限的寄存器$0,$1,$2…$n,且$0是主要寄存器,一般情况下不支持手动初始化值,只支持初始化偏移量代入,最终程序结束后$0的值会作为返回值输出.
LODA编译生成的exe文件是一个解释器而非编译器,因此在题目没有显式给出额外的可执行汇编代码的情况下,汇编代码一定以某种方式存于dll内部,并通过特殊的arg进行触发.
尝试各种指令后发现只有evaluate没有被ban,其余的指令均被禁止使用,那么可以推测evaluate指令的解析逻辑被魔改,我们先从cmd/main.cpp(也就是上面的程序主函数)开始,寻找所有被魔改的函数,首先查看Settings.parseArgs:
1 | // Hidden C++ exception states: #wind=2 |
注意到其中多出了一个字符串解密逻辑,byte处解密后的值为Loda_Lang
,可惜暂时还无法查到这个字符串的xref,继续查看main函数,找到这一行sub_180022040((__int64)&cin, (__int64)&input);
,其中的input必定有xref,共计xref到两个主要的函数:sub_18004CCB0
和sub_18004D1C0
,而这两个函数恰好均为eval/evaluator.cpp中的函数,可以发现输入的input每8字节按大端序被转为uint64后作为LODA-VM执行器的主寄存器$0的初始化值:
1 | ... |
接下来查看main中的dispatch函数,在其中找到以下几行:
1 | if ( n4 == 8 && !(unsigned int)sub_18013ED70(v28, "evaluate", 8) ) |
进入其中的函数(Commands::evaluate):
1 | ... |
注意到了明显的密文比较逻辑,有一段巨大的密文byte_180154070
,长达4148字节,这就是用于比较的密文,在其上还有一段异或逻辑:
1 | if ( v11 ) |
注意到密钥v26来源于qword_18018C010
(此时还是空的),查询这个数组的xref,查到一个函数sub_1800F1070
:
1 | ... |
注意到了明显的依然使用这个key进行循环异或解密的数组byte_18015A320
,而继续查询key的xref,可以发现其正式parseArgs中自解密的Loda_Lang
,因此两个密文全部可以解密,第一串密文解密得到:
1 | 583816102810052800589434335069219286405826406630421989889686064356068205809909774121462460601326025288509488060186540672688592635715723433315421893336296834148679608422191981907424653118438603335430859349845434037593630964251937832355292049020864523737762582094808993539638654563699089038209584551358553991909861266732226673248842442203439010762618318248702523232866474166818905065779215570910502879852007937495130, |
是十个巨大的数,猜测这就是程序运算后输出的结果,那么flag就应当长达80字节,继续对第二段密文进行解密:
1 | mov $1, $0 |
发现这正是题目的LODA汇编源代码,不难发现这是一个TEA加密和一个多质数RSA加密(中间的循环是junkcode,根据loda官方文档,若循环标签在循环过程中不自减,则循环不执行),编写脚本解密即可:
1 | from Crypto.Util.number import * |
flag{G4m3_1ik3_D1l_DecRypt&L04d3r_w17H_L0d4_14nG-yE7_4nO7h3R_4s53mB1y_L4NgUAg3!}
谢罪
第一次给大比赛出题,本来排查下来觉得应该不会有什么问题的,但是实际比赛过程中还是出现了一点问题(比如mmm的少量多解问题和loda的约束迷宫未能完全约束的非预期问题等),今后在出题的时候会更认真检查潜在的漏洞,尽量避免这些问题再发生