题目来源:LockedSecret from GHCTF 2025 新生赛(公开赛道)
官方 WP:GHCTF2025WP - Liv’s blog
Reverse - LockedSecret Hint1:
如果加密函数 IDA 伪代码太乱看不懂,请尝试 Ghidra,或许能帮助你分析算法解出 Flag。
Ghidra 些许情况对比 IDA 有奇效。
用 DIE 查一下壳:

发现用了 UPX 壳(注意这里的 [modified])
脱壳
尝试使用 upx -d
直接脱壳,发现不行:
1 | upx: .\LockedSecret.exe: CantUnpackException: file is modified/hacked/protected; take care!!! |
对于 UPX 壳,大部分情况下应使用 upx -d
脱壳。
这下不知道怎么办了吧,哈哈!
后面查了一下报错信息,得到的答案是:程序的 UPX 特征被篡改过,导致 UPX 不敢直接脱壳。此时需要恢复程序的 UPX 特征。
UPX0
和 UPX1
是加 UPX 壳后的两个区段名。其中 UPX1
区段包含了需要解压的数据块。.rsrc
是程序资源信息区段名,这个区段含有原资源段的完整头部以及图标、Manifest、版本等未被压缩的资源,当然还有 UPX 自身需要的导入信息等(如果程序自身不含资源段,加壳后就是 UPX2
)。UPX0
和 UPX1
可以被随意改成任何字符串,虽然这样改用处不大,但是也能起到伪装的作用。
用任意十六进制编辑器打开程序(我使用 ImHex):

发现 .rsrc
段,那么 LIVV
就是修改后的 UPX 特征。
参考了几个遇到相似问题的文章,发现只需要将 LIVV
的前三个字母 LIV
改成 UPX
即可。
注意查一下字符串,下面还有 LIV
。要把所有的 LIV
都替换成 UPX
。
保存结果,再次使用 upx -d
,发现成功脱壳。
解密
将脱壳后的程序拖进 IDA:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
(重命名了部分函数和对象)
尝试用 Findcrypt 查一下加密算法,结果是空的!这就不好玩了。
然后看一下 sub_401100
:
1 | int sub_401100() |
该函数利用伪随机初始化一个全局数组。
伪随机简介
这里的伪随机 (Pseudo-random) 是统计学意义上的:在给定的随机比特流样本中,1 的数量大致等于 0 的数量,满足这类要求的数字 “一眼看上去” 是随机的。
对于随机数的使用,一般是先播种,然后使用伪随机数函数来获取随机数。不播种会使用默认的种子。这种通过伪随机数函数得到的随机数,就是伪随机数,只要种子固定那么每次生成的随机数序列就会一样。
C 语言中的 srand
、rand
,C++ 的 mt19937
、default_random_engine
等都是伪随机数函数。
具体到本例,第 5 行的 srand
负责伪随机数的初始化,即播种(IDA View 也给 3246389708
标注了 seed
注释)。之后的 rand
生成伪随机数。
(参考 伪随机数问题浅析 - 天工实验室 )
sub_401190
:
1 | int __cdecl sub_401190(int a1) |
被逆向气晕(
我们还是能看出点东西的。第 25-27 行将一串明文 key 与上一个函数初始化的值进行异或计算得到用于加密的 Key。但由于 IDA 的反编译器问题,加密过程反编译代码明显缺乏规律,让人看不出特征。
切换到 Ghidra(通过搜索字符串查找主函数,再跳转):
1 |
|
可以很清楚的看出就是 Tea 加密,不过用重复计算和不同的 sum 值来代替了循环加密。
将第二次计算的 bc46effe
减去 5e2377ff
会发现结果还是 5e2377ff
,所以可以知道 5e2377ff
就是 delta
值,那么这就是一个完整的从 delta
值开始的 Tea 加密。
并且两两计算为一组,可以看出是 8 轮加密的 Tea,并且最后将加密完的值再异或上了 0xf
。
解密脚本:
1 |
|
得到 Flag:NSSCTF{!!!Y0u_g3t_th3_s3cr3t!!!}