第一次做 CTF 逆向题,太难了!
我之前从来没打过 CTF。拿到这道题的时候毫无头绪,所以我只好到网上疯狂 Google 相关知识。可惜逆向这个东西是速成不了的,经过几周几次高并发尝试之后,我放弃了。
打这篇blog的时候再次失败了。打到一半电脑没电,几百字的 blog(近期最长啊!)就没了啊orz我恨
2022 Update: 现在这篇已经快 1k 字了!反咕咕大成功!
题源
司虎老师的《网络安全基础与应用》选做题 happyreverse:happyreverse
废话
剪掉了!(其实是因为压力太大,没力气再码一遍了)
最远尝试
损坏 rar 修复
在老师提供的课程主页上拿到 rar 的时候解压不了。刚开始的时候我是在 iPad 上解压的,换了 n 个软件也不行,回去用电脑仍然未果。所以我考虑这个压缩文件已经损坏。
用了两个小时,配置好 Windows 官方 VMware 虚拟机之后把它放入找的 rar 修复软件里,顺利修复(实际上用 WinHex 看了下前面有缺失的 HEX 字符,对应填补后就可以了)。解压后是 MFC 写的应用程序。
查壳
拿到 MFC 应用程序之后开始准备逆向,第一步应该是看下有没有加壳,所以找了 peid 看一下。
刚开始下的 peid 有毛病,告诉我是穿山甲壳,傻傻的信了:
所以找了 n 小时的破壳脚本和教程,没有任何进展。开始思考是不是根本就没壳,换了看起来正常一点的 peid 结果是没壳的(最后找老师确认也是这样)。
白跑一场,不过没壳是好事。
开始逆向
动态调试 ollydbg + 静态 IDA Pro。
IDA Pro 静态调试
逆向过后查 string 找到 flag 位置:
找到 data 段位置:
查交叉引用 XREF:
找到对应汇编代码段,反汇编试一下:
在标黄的地方。同时这个函数也是加/解密的主函数。
我已经将一些伪代码(未命名的)通过具体行为分析猜测它对应 C 的哪些函数了。重要代码段如下:
// 省略了前面的变量声明部分
__alloca_probe();
v48 = a1;
your_flag = (const WCHAR *)a3;
v46 = (wchar_t *)a2;
v45 = (unsigned int)&savedregs ^ __security_cookie;
v3 = sub_407915();
if ( v3 == 0 )
return_error(-2147467259); // 应该是初始化部分,如果没初始化成功就返回错误信息
v4 = ((int (__thiscall *)(int (__stdcall ***)(int, int), unsigned int, wchar_t *, const WCHAR *))(*v3)[3])(
v3,
v45,
v46,
your_flag);
*(_DWORD *)ArgList = v4 + 16;
v49 = 0;
v5 = *(_DWORD *)v4;
if ( *(_DWORD *)(v4 + 4) )
{
if ( *(_DWORD *)(v4 + 12) >= 0 )
{
if ( _InterlockedDecrement((volatile signed __int32 *)(v4 + 12)) <= 0 )
(*(void (__stdcall **)(int))(**(_DWORD **)v4 + 4))(v4);
*(_DWORD *)ArgList = (*(int (__thiscall **)(int, int))(*(_DWORD *)v5 + 12))(v5, v48) + 16;
}
else
{
if ( *(_DWORD *)(v4 + 8) < 0 )
return_error(-2147024809);
*(_DWORD *)(v4 + 4) = 0;
**(_WORD **)ArgList = 0;
}
}
your_flag = (const WCHAR *)ArgList; // flag
v6 = mfc_stuff(1001); // MFC 相关函数,和题目本身没啥关系
sub_40F9D4(v6, (int)your_flag);
if ( *(_DWORD *)(*(_DWORD *)ArgList - 12) > 0 )
{
v7 = sub_407915();
if ( v7 == 0 )
return_error(-2147467259);
string_input = (LPCWSTR)(((int (__thiscall *)(int (__stdcall ***)(int, int), int))(*v7)[3])(v7, v48) + 16);
LOBYTE(v49) = 1;
v8 = *(const WCHAR **)ArgList;
if ( ((1 - *(_DWORD *)(*(_DWORD *)ArgList - 4)) | *(_DWORD *)(*(_DWORD *)ArgList - 8)) < 0 )
{
modify_v15((int *)ArgList, 0);
v8 = *(const WCHAR **)ArgList;
}
your_flag = v8; // flag
scanf((int)&string_input, &The_key_you_typed_is, v8); // 接收输入字符串
your_flag = string_input; // 将输入字符串赋值给 your_flag 变量
v9 = (HWND *)mfc_stuff(1002); // MFC 相关函数
sub_4119E3(v9, your_flag); //
printf(L"\r\n", 2u); // 打印字符串:“尝试利用该密钥进行解密:”
printf(&Try_to_use_this_key, '\r'); // 打印字符串:“尝试利用该密钥进行解密:”
your_flag = string_input; // 将输入字符串赋值给 your_flag 变量
v10 = (HWND *)mfc_stuff(1002); // MFC 相关函数
sub_4119E3(v10, your_flag);
v24 = 0;
memset(&v25, 0, 0x7FEu);
if ( ((1 - *(_DWORD *)(*(_DWORD *)ArgList - 4)) | *(_DWORD *)(*(_DWORD *)ArgList - 8)) < 0 ) // 检查输入长度
modify_v15((int *)ArgList, 0);
v22 = 0;
memset(&v23, 0, 0x7FEu);
sub_401140();
sub_401000(2 * wcslen((const unsigned __int16 *)&v22));
memset(&v26, 0, 0x100u);
sub_4011B0(wcslen((const unsigned __int16 *)&v24));
// 隐藏 flag (加密过的)
*(_DWORD *)v27 = 316603392;
v28 = 521699724;
v29 = -1896982701;
v30 = 23873730;
v31 = -1217616604;
v32 = -653821873;
v33 = 147644523;
v34 = 1788871112;
v35 = -401501575;
v36 = -670459317;
v37 = -247200577;
v38 = 691461817;
v39 = 800007765;
v40 = -1582862633;
v41 = 779312504;
v42 = -1253129845;
v43 = -1463759142;
v44 = -434474754;
v45 = -729680901;
v46 = (wchar_t *)254367706;
your_flag = (const WCHAR *)-1086890754;
sub_401260(v27); //加密函数
// 错误处理
v11 = sub_407915();
if ( !v11 )
return_error(-2147467259);
v19 = (wchar_t *)(((int (__thiscall *)(int (__stdcall ***)(int, int)))(*v11)[3])(v11) + 16);
your_flag = (const WCHAR *)v27;
LOBYTE(v49) = 2;
scanf((int)&v19, L"%s", v27);
v12 = v19;
// 处理输入长度
if ( ((1 - *((_DWORD *)v19 - 1)) | *((_DWORD *)v19 - 2)) < 0 )
{
modify_v15((int *)&v19, 0);
v12 = v19;
}
// 如果输入正确
if ( _wcsnicmp(v12, L"flag{", '\x05') )
{
your_flag = (const WCHAR *)5;
v46 = &STRING_the_key_you_typed_is_WRONG;
}
// 输入错误了
else
{
printf(L"Flag : ", '\a');
your_flag = (const WCHAR *)*((_DWORD *)v12 - 3);
v46 = v12;
}
// 打印你输入的 flag
printf(v46, (size_t)your_flag);
v13 = string_input;
your_flag = string_input;
// MFC 相关函数
v14 = (HWND *)mfc_stuff(1002);
sub_4119E3(v14, your_flag);
LOBYTE(v49) = 1;
if ( _InterlockedDecrement((volatile signed __int32 *)v12 - 1) <= 0 )
{
v15 = **((_DWORD **)v12 - 4);
your_flag = v12 - 8;
(*(void (__stdcall **)(wchar_t *))(v15 + 4))(v12 - 8);
}
LOBYTE(v49) = 0;
if ( _InterlockedDecrement((volatile signed __int32 *)v13 - 1) <= 0 )
{
v16 = **((_DWORD **)v13 - 4);
your_flag = v13 - 8;
(*(void (__stdcall **)(LPCWSTR))(v16 + 4))(v13 - 8);
}
}
else
{
sub_406D72((int)&STRING_Please_Input_Your_key, 0, 0);
}
v49 = -1;
result = (const WCHAR *)(*(_DWORD *)ArgList - 16);
if ( _InterlockedDecrement((volatile signed __int32 *)(*(_DWORD *)ArgList - 16 + 12)) <= 0 )
{
v18 = **(_DWORD **)result;
your_flag = result;
result = (const WCHAR *)(*(int (__stdcall **)(const WCHAR *))(v18 + 4))(result);
}
return result;
}
看到 flag 已经被加密了。主函数里面没有加密函数。找到其他函数:
int __usercall sub_401260@<eax>(int result@<eax>, int a2@<ecx>)
{
int v2; // esi
int v3; // edi
_BYTE *v4; // ecx
int v5; // esi
unsigned __int8 v6; // dl
int v7; // edi
int v8; // esi
unsigned __int8 v9; // dl
int v10; // edi
int v11; // esi
unsigned __int8 v12; // dl
int v13; // edi
int v14; // esi
unsigned __int8 v15; // dl
int v16; // edi
int v17; // esi
unsigned __int8 v18; // dl
int v19; // edi
unsigned __int8 v20; // dl
signed int v21; // [esp+Ch] [ebp-4h]
v2 = 0;
v3 = 0;
v4 = (_BYTE *)(a2 + 1);
v21 = 14;
do
{
v5 = (v2 + 1) % 256;
v6 = *(_BYTE *)(v5 + result);
v7 = (v6 + v3) % 256;
*(_BYTE *)(v5 + result) = *(_BYTE *)(v7 + result);
*(_BYTE *)(v7 + result) = v6;
*(v4 - 1) ^= *(_BYTE *)((v6 + *(unsigned __int8 *)(v5 + result)) % 256 + result);
v8 = (v5 + 1) % 256;
v9 = *(_BYTE *)(v8 + result);
v10 = (v9 + v7) % 256;
*(_BYTE *)(v8 + result) = *(_BYTE *)(v10 + result);
*(_BYTE *)(v10 + result) = v9;
*v4 ^= *(_BYTE *)((v9 + *(unsigned __int8 *)(v8 + result)) % 256 + result);
v11 = (v8 + 1) % 256;
v12 = *(_BYTE *)(v11 + result);
v13 = (v12 + v10) % 256;
*(_BYTE *)(v11 + result) = *(_BYTE *)(v13 + result);
*(_BYTE *)(v13 + result) = v12;
v4[1] ^= *(_BYTE *)((v12 + *(unsigned __int8 *)(v11 + result)) % 256 + result);
v14 = (v11 + 1) % 256;
v15 = *(_BYTE *)(v14 + result);
v16 = (v15 + v13) % 256;
*(_BYTE *)(v14 + result) = *(_BYTE *)(v16 + result);
*(_BYTE *)(v16 + result) = v15;
v4[2] ^= *(_BYTE *)((v15 + *(unsigned __int8 *)(v14 + result)) % 256 + result);
v17 = (v14 + 1) % 256;
v18 = *(_BYTE *)(v17 + result);
v19 = (v18 + v16) % 256;
*(_BYTE *)(v17 + result) = *(_BYTE *)(v19 + result);
*(_BYTE *)(v19 + result) = v18;
v4[3] ^= *(_BYTE *)((v18 + *(unsigned __int8 *)(v17 + result)) % 256 + result);
v2 = (v17 + 1) % 256;
v20 = *(_BYTE *)(v2 + result);
v3 = (v20 + v19) % 256;
*(_BYTE *)(v2 + result) = *(_BYTE *)(v3 + result);
*(_BYTE *)(v3 + result) = v20;
v4[4] ^= *(_BYTE *)((v20 + *(unsigned __int8 *)(v2 + result)) % 256 + result);
v4 += 6;
--v21;
}
while ( v21 );
return result;
}
这个就是加密算法了啊,实际上是类 RC 4 算法,这之后应该跑一下脚本对之前的密文进行解码,但能力实在不够了 TAT 就到此为止了。
Ollydbg 动态调试
把文件拖入到 Ollydbg 打断点看了下跑起来时候的具体行为,具体是:
- 接收输入input
- 把 input base64 编码一遍
- 把编码后的 input 继续 rc4 加密一遍
- 对 hard-code 的 flag (放在上面了)进行同样操作
- 比较,得出结果
大概是这样一个流程。
感想
应该是自己第一次做 ctf reverse 题,感觉难度比在网上搜罗到的所有rc4/mfc题解都难一个等级,各种输入函数和mfc组件函数混在一起,真的感觉很难。
可惜这门课除了这道题外,别的什么也没有教。这就让像我这样的入门小白来说,短时间内做出这种题基本上[我感觉]是没戏的。
不过好的一点是现在会了一丢丢逆向知识,解个入门赛签到题估计应该是可以的?