CISCN2024-Reverse-VT
前言
这题不想吐槽什么,利用开源的混淆项目将代码混淆就是一道题。由于逆向功底不够且花指令样式随机多变,只能手动去除花指令,这部分就花了一个小时左右。当时比赛时看到一坨混淆直接放弃了,现在重新做一遍发现其实花点时间其实也是可以解出的。有附带原题和Patched后的exe以及i64文件。
分析
程序分析&开源混淆项目
首先是例常Die,发现和叠Buff一样的一系列特征。
![]()
后续在Github上找到了这个混淆的项目(obfus.h),只是用一系列特征来伪造。
![]()
大概阅览了一下这个混淆项目,就是利用一堆花指令和逻辑混淆处理代码,然后封装了一些常用函数。
![]()
这个项目最新版的GetProcAddress是自己重新封装实现的,而通过dbg断点发现题目程序的GetProcAddress是可以断下来的,所以应该是某个历史版本而非最新,至少是在这个版本之前。
![]()
去除花指令
这个程序花指令含以下几种(可能没截全),大多都是常规花指令,可以直接按U,再跳过指定字节数,按C重构代码。
![]()
![]()
![]()
![]()
然后以下是一个特殊情况的花指令,图1是花指令,图2是去花后的,需要跳过箭头处jmp的第一个0xEB字节,然后再重构代码。
![]()
![]()
然后接下来就是重复的操作,这部分可以利用idc脚本进行一键去除,由于我做的时候为了保证去除所有花指令,就都手动操作,花了一个小时左右。
去花后程序API
去花重构函数过程中,可以发现有很多这样类似的函数,其实就是之前在开源混淆项目的代码中看到的那些自封装函数,就是封装起来调用系统API的一个代理函数。
大概有这么多,和开源项目里面差不多一致,不过并没有所有api都被调用到。(红叉处是后续分析功能自命名的)
![]()
![]()
Main函数分析
通过start函数进入main函数,发现有反调试代码(如IsDebuggerPresent),且在以下代码段发现有通过调用GetCommandLineA获取程序运行的命令行。
![]()
调试尝试运行后发现程序会使用到命令行运行程序附带的前两个参数。下图这部分代码就是在main函数中将第二个命令行参数通过atoi转成ProcessId进行后续操作,所以第二个参数就应该是某个进程的ID。
![]()
在下面发现有创建线程的代码,可以看到是创建mark2函数线程(自命名的)。
![]()
尝试带参调试,看看能不能看mark2里面做了什么。第一个参数随便填,第二个参数随便填一个进程ID(必须要正在运行的进程,如果不是真实PID则Main函数不会阻塞,而是会跳到最后调用Exit函数)。
![]()
然后断点在Main函数开头处,使用ScyllaHide插件进行一键去反调试。
![]()
调试运行发现没法执行到创建线程的这个代码段,看汇编段发现是这边判断ecx等于0,所以跳转走了,没有执行。
![]()
由于不知道这边ecx需要什么条件,直接下条件断点在cmp处,将ecx设置为1,强制执行下面的创建线程代码,便可以进入mark2函数进行下一步分析。
![]()
Mark2函数分析
发现有类似被加密的数据(以下称encFlag),部分变量名和函数已经被我重命名了。
![]()
param1_bytes_2指针进入两层跳转过去发现,数据指向的是我们参数1字符串unhex后的字节数据,所以就可以猜测给param1_bytes_2赋值的函数就是类似unhex函数。
![]()
然后下面encFlag赋值后进行了重赋值,利用之前的encFlag值,调用一个函数生成了一系列key值,然后用key值异或上param1_bytes_2,这边是i%2,所以就一直循环异或这两个字节,也就是我们输入的参数1进行unhex后的两个字节。
![]()
由于encFlag之前的值是固定的,所以生成的一系列Key也是固定的,可以直接利用条件断点在给Key赋值的地方将他输出。
![]()
得到以下Key值列表。
![]()
在encFlag重赋值下面,调用了一个计算call传入encFlag,计算返回一个4字节数值。
![]()
且在下面部分可以看到对v22做了一个判断,判断是否为0xF703DF16,若不是,则会执行到这个return直接返回,不会继续执行下面部分代码。
![]()
在下面未执行代码中看到了类似对encFlag进行解密的函数Call,并且在下面看到了判断解密后数据结尾是否是为'}',如果是就break。
![]()
且在最底下代码看到一个printf函数的调用,输出了解密后的字符串。
![]()
程序流程总结
首先要带两个参数运行程序,第一个参数是4长度字符串,第二个参数是一个正在运行的某个进程ID。
满足某个条件创建线程执行mark2函数进行函数主解密流程。
将第一个参数进行unhex,转成2字节数据,与固定异或列表进行一次异或计算,再调用一个计算call得到一个4字节数据,并且必须是0xF703DF16。
将通过参数1处理后的加密数据进行解密得到flag,然后printf输出。
所以最主要是就是分析calc函数,用代码模拟calc函数进行爆破,得到密钥,也就是参数一那两个字节,接下来就分析calc函数。
calc函数分析
从上文可以知道calc函数第一个参数传入一个48长度字节数组,第二个参数是48,那么第二个参数应该就是输入数据的长度。
通过动调走一遍流程,确定关键计算代码,忽略其他的逻辑混淆。
最外层是通过len进行一次数据遍历,内层是进行8次的循环。
![]()
![]()
![]()
可以用c++重写出原始的calc函数代码。
复制代码 隐藏代码
uint32_t calc(uint8_t* data, int len)
{
uint32_t ret_value = -1;
for (int count = 0; count < len; count++)
{
ret_value ^= data[count];
for (int i = 0; i 8; i++)
{
if (ret_value & 1)
{
ret_value = (ret_value >> 1) ^ 0xEDB88320;
}
else
{
ret_value >>= 1;
}
}
}
return ~ret_value;
}
爆破密钥(参数一)
复制代码 隐藏代码
#include
uint32_t calc(uint8_t
* data, int len)
{
uint32_t ret_value = -1;
for (int count = 0; count < len; count++)
{
ret_value ^= data[count];
for (int i = 0; i 8; i++)
{
if (ret_value & 1)
{
ret_value = (ret_value >> 1) ^ 0xEDB88320;
}
else
{
ret_value >>= 1;
}
}
}
return ~ret_value;
}
int main()
{
short Param1 = 0;
// 爆破2字节
for (int i = 0; i 0xffff; i++)
{
Param1 = i;
// ida条件断点得到的key值列表
uint8_t KeyList[]{
82,225,68,226,57,225,94,155,81,220,
25,152,80,146,57,193,80,158,82,130,
39,130,38,231,83,128,36,128,66,220,
57,158,2,148,39,129,69,131,81,147,
2,128,68,129,68,129,68,129 };
uint8_t Enc[48]{};
uint8_t* pParam1 = (uint8_t*)(uint64_t)(&Param1);
// calc之前的异或计算
for (int j = 0; j 48; j++)
{
Enc[j] = pParam1[j % 2] ^ KeyList[j];
}
auto calc_value = calc(Enc, 48);
if (calc_value == 0xF703DF16)
{
printf("Cracked:%02X%02X\n", pParam1[0], pParam1[1]);
break;
}
}
return0;
}
输出"Cracked:79BC",得到密钥为79BC。
获取Flag
输入密钥和一个正在运行的进程ID,再次进行调试。
![]()
发现可以正常走到decrypt_flag函数调用处。
![]()
单步执行完decrypt_flag函数,跳转到flag指针处,即可看到解密后的flag。
flag{MjExNTY3MzE3NTQzMjI=}
![]()
心得
这次全流程做下来感觉其实面对复杂的混淆和花指令得有耐心去分析,耐心的动调观察数据变化以及注释关键点,这样才能更好理顺思路和理解代码流程。
这次花指令全都是手动去除,因为怕写脚本处理的花指令不完整,也就是担心去除的不完整导致程序执行错乱。现在发现其实是可以参考开源的那个混淆项目,看他用到了哪些花指令,然后结合实际汇编进行分析,应该是可以将所有花指令的情况都列出来的,然后进行一键脚本去除。
-官方论坛
www.52pojie.cn
👆👆👆