如何解决魔改后的加密算法
- 1 视频也可以含恶意代码吗
- 2 Frida 入门
- 3 虚拟机保护
- 4 C#逆向入门
- 5 安卓刷机一点通
- 6 Z3-Solver教程
- 7 出题人的“恶意”
- 8 编译器为我做了什么
- 9 CSAPP Lab2 Note
- 10 Hash系列算法
- 11 程序脱壳
- 12 安卓逆向介绍
- 13 IDA类型修复
- 14 IDA疑难杂症
- 15 花指令
- 16 反动态调试技术与应对手段
- 17 AES加密算法
- 18 如何解决魔改后的加密算法 本文
- 19 常见加密算法识别与变种分析
- 20 TEA 加密算法
- 21 RC4 加密算法
- 22 Base64 编码
- 23 IDA Dynamic Debugging
- 24 IDA Usage With Reverse Basic
题目来源:LockedSecret from GHCTF 2025 新生赛(公开赛道) 官方 WP:GHCTF2025WP - Liv’s blog
Reverse - LockedSecret Hint1: 如果加密函数 IDA 伪代码太乱看不懂,请尝试 Ghidra,或许能帮助你分析算法解出 Flag。 Ghidra 些许情况对比 IDA 有奇效。
用 DIE 查一下壳:
发现用了 UPX 壳(注意这里的 [modified])
脱壳
尝试使用 upx -d 直接脱壳,发现不行:
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:
int __cdecl main(int argc, const char **argv, const char **envp){ char v4; // [esp+0h] [ebp-108h] char v5; // [esp+0h] [ebp-108h] int i; // [esp+0h] [ebp-108h] char input[256]; // [esp+4h] [ebp-104h] BYREF
memset(input, 0, sizeof(input)); printf("Input your flag:", v4); scanf("%32s", (char)input); if ( strlen(input) != 32 ) { printf("Wrong length!\n", v5); system("pause"); exit(0); } sub_401100(); sub_401190(input); for ( i = 0; i < 32; ++i ) { if ( flag[i] != input[i] ) { printf("Wrong!\n", i); system("pause"); exit(0); } } printf("Right!\n", i); system("pause"); return 0;}(重命名了部分函数和对象)
尝试用 Findcrypt 查一下加密算法,结果是空的!这就不好玩了。
然后看一下 sub_401100:
int sub_401100(){ int i; // [esp+0h] [ebp-4h]
srand(3246389708u); for ( i = 0; i < 8; ++i ) { if ( i ) dword_4043D8[i] = dword_4043D4[i] ^ rand(); else dword_4043D8[0] = rand(); dword_4043D8[i] %= 256; } return 0;}该函数利用伪随机初始化一个全局数组。
伪随机简介
这里的伪随机 (Pseudo-random) 是统计学意义上的:在给定的随机比特流样本中,1 的数量大致等于 0 的数量,满足这类要求的数字“一眼看上去”是随机的。
对于随机数的使用,一般是先播种,然后使用伪随机数函数来获取随机数。不播种会使用默认的种子。这种通过伪随机数函数得到的随机数,就是伪随机数,只要种子固定那么每次生成的随机数序列就会一样。
C 语言中的 srand、rand,C++ 的 mt19937、default_random_engine 等都是伪随机数函数。
具体到本例,第 5 行的 srand 负责伪随机数的初始化,即播种(IDA View 也给 3246389708 标注了 seed 注释)。之后的 rand 生成伪随机数。
请注意,C 的 rand 函数默认以 srand(1) 为种子生成伪随机数。在同一种子下,rand 生成的伪随机数列相同。由于标准做法1不允许多次更换种子,因此只要知道了一个种子值,那么该程序下所有 rand 函数生成的伪随机数列均相同。Python 的 random 库也提供了这样的 rand 和 seed 函数。
(参考 伪随机数问题浅析 - 天工实验室 )
再往下面看:
int __cdecl sub_401190(int a1){ unsigned int v1; // ecx unsigned int v2; // eax int result; // eax int v4; // [esp+0h] [ebp-7Ch] unsigned int v5; // [esp+4h] [ebp-78h] unsigned int v6; // [esp+14h] [ebp-68h] unsigned int v7; // [esp+18h] [ebp-64h] unsigned int v8; // [esp+1Ch] [ebp-60h] unsigned int v9; // [esp+20h] [ebp-5Ch] unsigned int v10; // [esp+24h] [ebp-58h] unsigned int v11; // [esp+28h] [ebp-54h] unsigned int v12; // [esp+30h] [ebp-4Ch] unsigned int v13; // [esp+38h] [ebp-44h] unsigned int v14; // [esp+3Ch] [ebp-40h] unsigned int v15; // [esp+40h] [ebp-3Ch] unsigned int v16; // [esp+44h] [ebp-38h] unsigned int v17; // [esp+48h] [ebp-34h] int i; // [esp+4Ch] [ebp-30h] int v19; // [esp+50h] [ebp-2Ch] int v20[4]; // [esp+54h] [ebp-28h] BYREF int Src[5]; // [esp+64h] [ebp-18h] BYREF
strcpy((char *)Src, "IamTheKeyYouKnow"); for ( i = 0; i < 15; ++i ) *((_BYTE *)Src + i) ^= LOBYTE(dword_4043D8[i % 8]); memcpy(v20, Src, sizeof(v20)); v19 = 4; do { v17 = *(_DWORD *)(a1 + 8 * (4 - v19) + 4); v16 = *(_DWORD *)(a1 + 8 * (4 - v19)) + ((v20[1] + (v17 >> 5)) ^ (v17 + 1579382783) ^ (v20[0] + 16 * v17)); v15 = v17 + ((v20[3] + (v16 >> 5)) ^ (v16 + 1579382783) ^ (v20[2] + 16 * v16)); v14 = v16 + ((v20[1] + (v15 >> 5)) ^ (v15 - 1136201730) ^ (v20[0] + 16 * v15)); v13 = v15 + ((v20[3] + (v14 >> 5)) ^ (v14 - 1136201730) ^ (v20[2] + 16 * v14)); v12 = v13 + ((v20[3] + ((v14 + ((v20[1] + (v13 >> 5)) ^ (v13 + 443181053) ^ (v20[0] + 16 * v13))) >> 5)) ^ (v14 + ((v20[1] + (v13 >> 5)) ^ (v13 + 443181053) ^ (v20[0] + 16 * v13)) + 443181053) ^ (v20[2] + 16 * (v14 + ((v20[1] + (v13 >> 5)) ^ (v13 + 443181053) ^ (v20[0] + 16 * v13))))); v1 = v14 + ((v20[1] + (v13 >> 5)) ^ (v13 + 443181053) ^ (v20[0] + 16 * v13)) + ((v20[1] + (v12 >> 5)) ^ (v12 + 2022563836) ^ (v20[0] + 16 * v12)); v11 = v12 + ((v20[3] + (v1 >> 5)) ^ (v1 + 2022563836) ^ (v20[2] + 16 * v1)); v10 = v1 + ((v20[1] + (v11 >> 5)) ^ (v11 - 693020677) ^ (v20[0] + 16 * v11)); v9 = v11 + ((v20[3] + (v10 >> 5)) ^ (v10 - 693020677) ^ (v20[2] + 16 * v10)); v8 = v10 + ((v20[1] + (v9 >> 5)) ^ (v9 + 886362106) ^ (v20[0] + 16 * v9)); v7 = v9 + ((v20[3] + (v8 >> 5)) ^ (v8 + 886362106) ^ (v20[2] + 16 * v8)); v6 = v8 + ((v20[1] + (v7 >> 5)) ^ (v7 - 1829222407) ^ (v20[0] + 16 * v7)); v2 = v7 + ((v20[3] + (v6 >> 5)) ^ (v6 - 1829222407) ^ (v20[2] + 16 * v6)); v5 = v2 + ((v20[3] + ((v6 + ((v20[1] + (v2 >> 5)) ^ (v2 - 249839624) ^ (v20[0] + 16 * v2))) >> 5)) ^ (v6 + ((v20[1] + (v2 >> 5)) ^ (v2 - 249839624) ^ (v20[0] + 16 * v2)) - 249839624) ^ (v20[2] + 16 * (v6 + ((v20[1] + (v2 >> 5)) ^ (v2 - 249839624) ^ (v20[0] + 16 * v2))))); *(_DWORD *)(a1 + 8 * (4 - v19)) = (v6 + ((v20[1] + (v2 >> 5)) ^ (v2 - 249839624) ^ (v20[0] + 16 * v2))) ^ 0xF; *(_DWORD *)(a1 + 8 * (4 - v19) + 4) = v5 ^ 0xF; v4 = v19; result = --v19; } while ( v4 ); return result;}被逆向气晕(
我们还是能看出点东西的。第 25-27 行将一串明文 key 与上一个函数初始化的值进行异或计算得到用于加密的 Key。但由于 IDA 的反编译器问题,加密过程反编译代码明显缺乏规律,让人看不出特征。
切换到 Ghidra(通过搜索字符串查找主函数,再跳转):
void __cdecl FUN_00401190(int param_1)
{ uint uVar1; uint uVar2; bool bVar3; uint local_34; int local_30; int local_2c; int local_28; int local_24; int local_20; byte local_1c [20]; uint local_8;
local_8 = DAT_00404000 ^ (uint)&stack0xfffffffc; local_1c[0] = 'I'; local_1c[1] = 'a'; local_1c[2] = 'm'; local_1c[3] = 'T'; local_1c[4] = 'h'; local_1c[5] = 'e'; local_1c[6] = 'K'; local_1c[7] = 'e'; local_1c[8] = 'y'; local_1c[9] = 'Y'; local_1c[10] = 'o'; local_1c[0xb] = 'u'; local_1c[0xc] = 'K'; local_1c[0xd] = 'n'; local_1c[0xe] = 'o'; local_1c[0xf] = 'w'; local_1c[0x10] = 0; for (local_34 = 0; (int)local_34 < 0xf; local_34 = local_34 + 1) { uVar1 = local_34 & 0x80000007; if ((int)uVar1 < 0) { uVar1 = (uVar1 - 1 | 0xfffffff8) + 1; } local_1c[local_34] = local_1c[local_34] ^ (byte)*(undefined4 *)(&DAT_004043d8 + uVar1 * 4); } local_2c = 0; local_28 = 0; local_24 = 0; local_20 = 0; memcpy(&local_2c,local_1c,0x10); local_30 = 4; do { uVar1 = *(uint *)(param_1 + 4 + (4 - local_30) * 8); uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x5e2377ff ^ (uVar1 >> 5) + local_28) + *(int *)(param_1 + (4 - local_30) * 8); uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x5e2377ff ^ (uVar2 >> 5) + local_20) + uVar1; uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0xbc46effe ^ (uVar1 >> 5) + local_28) + uVar2; uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0xbc46effe ^ (uVar2 >> 5) + local_20) + uVar1; uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x1a6a67fd ^ (uVar1 >> 5) + local_28) + uVar2; uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x1a6a67fd ^ (uVar2 >> 5) + local_20) + uVar1; uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x788ddffc ^ (uVar1 >> 5) + local_28) + uVar2; uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x788ddffc ^ (uVar2 >> 5) + local_20) + uVar1; uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0xd6b157fb ^ (uVar1 >> 5) + local_28) + uVar2; uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0xd6b157fb ^ (uVar2 >> 5) + local_20) + uVar1; uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x34d4cffa ^ (uVar1 >> 5) + local_28) + uVar2; uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x34d4cffa ^ (uVar2 >> 5) + local_20) + uVar1; uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0x92f847f9 ^ (uVar1 >> 5) + local_28) + uVar2; uVar1 = (uVar2 * 0x10 + local_24 ^ uVar2 + 0x92f847f9 ^ (uVar2 >> 5) + local_20) + uVar1; uVar2 = (uVar1 * 0x10 + local_2c ^ uVar1 + 0xf11bbff8 ^ (uVar1 >> 5) + local_28) + uVar2; *(uint *)(param_1 + (4 - local_30) * 8) = uVar2 ^ 0xf; *(uint *)(param_1 + 4 + (4 - local_30) * 8) = (uVar2 * 0x10 + local_24 ^ uVar2 + 0xf11bbff8 ^ (uVar2 >> 5) + local_20) + uVar1 ^ 0xf; bVar3 = local_30 != 0; local_30 = local_30 + -1; } while (bVar3); FUN_0040173b(local_8 ^ (uint)&stack0xfffffffc); return;}可以很清楚的看出就是 Tea 加密,不过用重复计算和不同的 sum 值来代替了循环加密。
将第二次计算的 bc46effe 减去 5e2377ff 会发现结果还是 5e2377ff,所以可以知道 5e2377ff 就是 delta 值,那么这就是一个完整的从 delta 值开始的 Tea 加密。
并且两两计算为一组,可以看出是 8 轮加密的 Tea,并且最后将加密完的值再异或上了 0xf。
解密脚本:
#include <iostream>#include <Windows.h>
void tea_decrypt(uint32_t v[2], const uint32_t k[4]){ uint32_t v0 = v[0], v1 = v[1];
uint32_t sum = 0x5E2377FF * 8;
uint32_t delta = 0x5E2377FF;
for (uint32_t i = 0; i < 8; i++) { v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]); v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]); sum -= delta; }
v[0] = v0; v[1] = v1;}
int main(){ unsigned char Key[] = { 0x2D,0xF7,0x3D,0x42,0x01,0x9A,0xF5,0x05,0x1D,0xCF,0x3F,0x63,0x22,0x91,0xD1,0x77 }; unsigned char EncFlag[] = { 0xDC,0x45,0x1E,0x03,0x89, 0xE9,0x76,0x27,0x47,0x48, 0x23,0x01,0x70,0xD2,0xCE, 0x64,0xDA,0x7F,0x46,0x33, 0xB1,0x03,0x49,0xA3,0x27, 0x00,0xD1,0x2C,0x37,0xB3, 0xBD,0x75 }; for (int i = 0; i < 4; i++) { *(uint32_t*)(EncFlag + i * 8) ^= 0xf; *(uint32_t*)(EncFlag + i * 8 + 4) ^= 0xf; tea_decrypt((uint32_t*)(EncFlag + i * 8), (uint32_t*)Key); }
printf("%.32s\n",EncFlag); return 0;}得到 Flag:NSSCTF{!!!Y0u_g3t_th3_s3cr3t!!!}
Footnotes
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!