vnctf2023 - 复现
复现期间去郑州玩了一趟,先复现一下数据安全的 re 题,当时在线下做了nm 90%最后发现密钥都写脸上了就是没出😅😅😅
Butterfly
安卓逆向,先看 AndroidManifest.xml 找 MainActivity

check 函数是 static 方法,明显要逆 so ,解压以后 ida 打开,是个 arm 架构的。
字符串窗口可以搜到 check 字符,查查交叉引用,用 JNIEnv * 修复一些函数以后,发现注册了一个 native 方法


sub_1FD80 就是我们要找的 check 函数,点进去翻翻找到了 yyyyyy 这个函数,显然是被混淆了,但是没有关系,直接选中汇编创建函数然后反编译即可

同时发现相邻的函数

非常可疑,点进去一眼 AES,cipher 就是 yyyyyy 里的 v6,密钥和 iv 都是 1234567890123456

cyberchef 一把梭 flag{welc0me_backTo_obfuscation}
PZGalaxy
直接 view - source 一下就能找到源码,一看是个 RC4 加密
1  | function Leaf(k, p) {  | 
注意这行判断 flag.substring(0, 4) ==  "flag" && date.length == 8 && date.substring(0, 4) == "2023",说明我们需要求的是日期 date ,这个日期的长度是 8,是 RC4 的密钥,我们需要对密钥的后四位进行爆破,把得到的结果输入到网站中就可以获得 flag,和之前西湖论剑 babyRE 的脚本基本一模一样
1  | 
  | 
运行可以得到正确的密钥是 20230127,放到网页中输入一下得到 flag{HitYourSoulAndSeeYouInTheGalaxy}
refuse_re
一打开感觉看不出啥东西,考虑直接动调。

前面就是个判断颜色的按照提示和源码输入 bor 即可绕过,然后发现所有的函数都被 call $5类型的花指令给混淆了,但是没啥用,直接F7就能调到主函数
1  | // local variable allocation has failed, the output may be wrong!  | 
然后一直调,发现是个稍微魔改的AES,密钥扩展好像魔改了,但是没啥用,直接把轮密钥 dump 出来。

需要注意的是魔改了 AddRoundKey,在异或轮密钥以后又异或了 0x23

此外数据分成两轮进行加密,第一轮加密以后 plain2 = (plain2 ^ plain1) + 1,最后是分别比对,可以把 cipher 也给 dump 出来。
给出 exp
1  | 
  | 
运行得到 flag{fa9ad36bd2de1586d944cf7b2935dd91}
dll_puzzle(魔王题部分复现)
大概是👶目前为止遇到过最逆天的逆向题了吧🤔对着 WP 怼了一天大概看懂了一半,后面的解密流程还有一堆包含彩蛋的代码实在看不懂了,晚上看了大爹的视频分享实在是泪目,建议加入睡前故事合集反复观看。
这里分享一种动调的方法,可能会比静态嗯逆要简单一些(可能,应该,会,简单,一些,吧,大概)。
程序启动和调试方法
程序是一个 dll 样本,不能直接运行,需要附加运行,可以用 regsvr32,rundll32,loaddll 等进行启动
1  | regsvr32 RegisterServer.dll  | 
一些静态分析
先扔进 exeinfo ,发现没有壳,是 32-bit dll 文件。

再用 ida 打开程序,入口点停在了 DllEntryPoint 这里

我们随便翻翻里面的函数,sub_1000BD57 里面用到的 __security_cookie 是用于防止栈溢出,显然是系统函数,不用看,后面的样本分析过程中会遇到大量的类似系统函数,我们需要善于利用搜索引擎搜索一些可见字符或者常量等用来识别某个函数到底干了什么。
然后一瞟左边函数窗口发现有 TLSCallBack 函数,可以 ctrl + E 一下并发现题目中的一个彩蛋,同时也能发现回调函数的入口。

然后进回调函数看看,刚点开人就有点麻

只能一步一步分析,先观察一些被调用了很多次的函数,比如 sub_1000B6E0, sub_1000B930这种,出现频率很高而且看起来比较可疑的函数。

sub_1000B6E0
观察回调函数中 sub_1000B6E0 的两个参数,发现第一个参数总是 -2097490458,进入第一个 if 分支,我们来看看这个分支里都干了些什么。

首先用一个全局变量存储了 sub_1000AB30 的返回值,然后再作为参数传入 sub_1000AB60 中,先看sub_1000AB30

一个经典 return 9花指令,简单分析一下

去除的方法就是 call 到 retn 全给 nop 掉,那么我们接着看正常的代码
支线任务:获取 kernel32.dll 基 址
参考 [原创]详解七句汇编获取Kernel32模块地址-编程技术-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)
1  | GetKernel32Base proc  | 

比较一下,发现基本是一模一样,可以认定这几行汇编是为了获得 kernel32 模块的地址。
再来看 sub_1000AB60 函数

先搜搜函数中涉及的一些常量,可能是一些加密算法的初始化变量

一搜发现是 SM3 加密算法,再一看 SM3 算法里的初始化常数

和反编译出来的一模一样,于是实锤,在加密函数的最后发现还异或了第二个参数 a2

猜测应该是从 kernel32.dll 模块中找到某个 API 的 hash 值进行校验之后返回该 API 的地址。
我们回到函数开始的地方

发现了这几行代码,a1 是我们之前获得的 kernel32.dll 的模块的基址,显然这里是从 kernel32.dll 中获取一些信息的操作。 
支线任务: PE文件结构和PEB导出表
[原创]完美实现GetProcAddress-软件逆向-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)
或者这篇 TEB/PEB定位PE文件导入导出表-teb表-初识逆向的博客-CSDN博客
通过 PEView 工具我们也可以找到 Export Table 的地址

导出表的结构为
1  | typedef struct _IMAGE_EXPORT_DIRECTORY {  | 
那么现在再来看这段代码,以及最后的比对代码


a1 是我们之前获得的 kernel32.dll 的模块的基址,显然该函数就是返回了文件基址 + 一个偏移的函数地址,那么 v58 就是 *PIMAGE_EXPORT_DIRECTORY这个结构体指针,取出一些值组后算出来一个 hash 值,比较如果成功,就返回一个地址,并且这个地址的基址是 a1,也就是我们之前通过七行汇编代码来获取的 kernel32.dll 的基址,也就是说,通过这个函数,我们可以动态获取一个 API 的地址,完整函数如下

sub_1000B930
再来看 sub_1000B930 函数

我们可以先看看它在回调函数中是怎么调用的,发现它的返回值作为参数放在了  createMutexA 函数 (synchapi.h) - Win32 apps | Microsoft Learn 里面,同时可以知道第三个参数 v3 应该是一个字符串,也就是说 sub_1000B930 返回的应该也是一个字符串,并且我们注意到 byte_10023E08 里面全是可见字符,应该就是一个单表代换,纯静态分析的时候,模拟执行这个 do-while 循环,返回值是一个 fake_flag。

或者直接抄下来
1  | 
  | 
动态调试
大概搞明白上面两个函数是在干嘛以后,我们可以尝试对文件进行动态调试,我们在 TlsCallBack 和 DllEntryPoint  两个函数中分别下断,然后附加到 regsvr32.exe上进行调试,注意要写命令行参数。

根据之前的分析,猜测执行完 GetAPIAddress 以后可以获得一个返回值 ,我们可以查看寄存器列表来得知返回的 API 名称。
同时,我们步入 GetTableAddress 函数时,可以看到 sub_5981A990 执行完以后,返回值是一个 \0 空字符串,那么猜测该函数是申请一个空字符串,在第一次运行 GetTableAddress 时返回值是

我们接着往下看,一路跑到函数 sub_59811000,点进去一看

发现是一个检测了超级多调试器的反调试函数,但是绕过很简单,改一下返回值就行。接着调试程序,发现一个可疑的条件语句

由于采用了或的关系,且 sub_59811280 函数里面添加了一个 VEH 异常处理

显然也是一个反调试,同时也可以猜测条件语句的后半段也是一段反调试,绕过 VEH 反调试,查看 GetAPIAddress 的返回值,果然 ,发现是 IsDebuggerPresent反调试函数。

进入条件分支内部, 发现还有两个反调试

由于都是 if 条件语句,我们直接全给 nop 掉即可

然后发现了一个之前没遇到过的函数 sub_5981A670

同时我们还观察到如果之前触发了反调试,那么步入的分支是一个类似的函数

看起来只有一些常量不一样,通过一些表的搜索可以看出这是一个 SM4 算法,且在正确的分支中还有一个

这样一个判断函数,用来判定 SM4 的 s_box 是否正确,显然在不同的分支中获得的 s_box 是不同的,所解出来的明文也是不同的,相对应的源码可以在这里找到: NEWPLAN/SMx: 国家商用加密算法 SMx(SM2,SM3,SM4) (github.com) 
到此,回调函数大致分析完了,我们进入 DllEntryPoint 函数中继续跟踪调试。

主要是逆这个函数,我们看到了熟悉的 return 9, patch 掉以后可以显示出下面的代码,发现最后还有花指令

是个 jz-jnz 型的花指令,patch 掉即可,最后在用快捷键 U, P, Alt + P 修一修就行。
然后一直调试,可以看到是在做一堆文件操作,lpFileName 里面变成了 lincense.ini,应该是从这里面读取了某些东西,我们来看看这个 ini 文件里有啥
1  | ;THIS IS AUTO GENERATED BY VN2023 SUITE, DO NOT MODIFY IT MANUALLY!  | 

后面的一些代码是从 GetTableValue 函数里面获取一些特殊的字符串,然后作为参数扔到GetPrivateProfileString 函数 里

猜测是从 lincense.ini 文件中检索相应的字符串,也就是 flag 和 TheAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything 后面的内容,flag 存在 lpReturnedString 中, 后面那个终极浪漫的数字存在 v48 中并且转成数字与一个 key 进行异或,这个异或函数就是 cxxstd_atoi

可以直接写脚本得到正确的 key值
1  | 
  | 
正确的密钥为
1  | key = [0x20, 0x62, 0x32, 0x33, 0x2e, 0x74, 0x76, 0x2f, 0x79, 0x46, 0x52, 0x43, 0x37, 0x68, 0x59, 0x00]  | 
最后就是 sub_10001BA2 函数了,去掉花指令之后反编译看到一个巨大的 if ,一看就是求解一个方程组。
我们把方程组复制出来,放到文本编辑软件中稍微删改一下变成符合 python 语法的条件语句,然后上 z3 求解
1  | from z3 import *  | 
结果为:
1  | cipher = [0xda, 0xb9, 0x49, 0x91, 0xfb, 0xbd, 0xcc, 0x9a, 0x57, 0x11, 0x70, 0xed, 0xe5, 0xa2, 0xac, 0xe1, 0x29, 0x7e, 0x16, 0x29, 0xce, 0x82, 0xa7, 0x3e, 0x8d, 0x7c, 0xbf, 0x20, 0x85, 0xc1, 0xe8, 0xfa, 0x2, 0xba, 0xcb, 0x90, 0xb, 0xa9, 0x74, 0x68, 0xe8, 0x2e, 0x52, 0xfc, 0x42, 0x59, 0xbd, 0x31, 0x7c, 0xa3, 0x5a, 0x88, 0x81, 0x2f, 0x30, 0x10, 0x58, 0xc2, 0x2a, 0x3c, 0x67, 0xca, 0x5, 0x3b, 0xdb, 0xc8, 0x14, 0x67, 0xb6, 0x26, 0x86, 0x6e, 0x29, 0x7a, 0x58, 0x8c, 0x78, 0x28, 0xef, 0x78]  | 
密钥之前已经解出,直接 cyberchef 一下

再转成字符串得到 flag{3f27d7470d8967fd344ec7f1261e64b3}


