专栏名称: 吾爱破解论坛
吾爱破解论坛致力于软件安全与病毒分析的前沿,丰富的技术版块交相辉映,由无数热衷于软件加密解密及反病毒爱好者共同维护,留给世界一抹值得百年回眸的惊艳,沉淀百年来计算机应用之精华与优雅,任岁月流转,低调而奢华的技术交流与探索却
目录
相关文章推荐
潮司电商客服外包  ·  315名单出来了!这些类目商家将受影响 ·  6 小时前  
潮司电商客服外包  ·  315名单出来了!这些类目商家将受影响 ·  6 小时前  
西藏市场监管  ·  3·15曝光问题,多地连夜查处! ·  昨天  
龙视新闻联播  ·  “一天流水20亿元”!手机抽奖“疯狂敛财”. ... ·  昨天  
龙视新闻联播  ·  “一天流水20亿元”!手机抽奖“疯狂敛财”. ... ·  昨天  
黑龙江政务  ·  保护消费者权益,这些知识一起了解! ·  2 天前  
黑龙江政务  ·  保护消费者权益,这些知识一起了解! ·  2 天前  
厦门广电  ·  传Windows停止供货!华为PC或全面转向 ... ·  2 天前  
厦门广电  ·  传Windows停止供货!华为PC或全面转向 ... ·  2 天前  
51好读  ›  专栏  ›  吾爱破解论坛

2024 CISCN x 长城杯初赛Reverse赛题vt Writeup——攻城攻心的混淆大师

吾爱破解论坛  · 公众号  · 互联网安全  · 2025-03-16 08:46

主要观点总结

文章分享了作者在初赛结束后,经过深入研究,成功对一道当时无人能解的难题进行了解构和解析。作者首先介绍了赛题初窥,对程序进行了深入的分析,并发现了保护器和虚拟机保护的特征。接着,作者通过去除语法混淆和语义混淆,利用idc脚本实现了静态分析,并找到了核心逻辑。文章还探讨了程序逻辑分析,包括反调试分析、核心逻辑和命令行参数分析,最终得出了获取flag的步骤。作者通过具体的数据分析和实例,详细描述了题目的精妙之处,同时也指出了题目的不足之处。

关键观点总结

关键观点1: 赛题初窥

作者介绍了赛题的特点,包括保护器和虚拟机保护的特征,并进行了深入分析。

关键观点2: 去除混淆

作者通过idc脚本实现了静态分析,去除了语法混淆和语义混淆,使程序更容易被理解。

关键观点3: 程序逻辑分析

作者详细分析了程序逻辑,包括反调试分析、核心逻辑和命令行参数分析,得出了获取flag的步骤。

关键观点4: 精妙之处与不足

作者讨论了题目的精妙之处,同时也指出了题目的不足之处,认为它更适合出现在时间更为充裕的竞赛中。


正文

作者 坛账号: VirusCollector

前言
在初赛结束近两月之际,笔者在复盘过程中意外发现了这道当时无人能解的难题。经过两日深入的探索与钻研,笔者终于成功地对这道赛题进行了全面的解构。在品味破译flag所带来的喜悦之余,笔者亦深感此题蕴含了诸多精妙之处,特撰此文与诸君分享。
一、 赛题初窥
拿到样本re.exe,照例先拖入DIE分析,结果让人大吃一惊:

DIE检测出了如此多的保护器,几乎涵盖了平常见过的所有种类。再拖入 IDA 看一眼:
从上面灰色、红色的部分占据绝大部分可以看出,整个程序几乎没有多少正确识别的函数,似乎比较符合 虚拟机 保护的特征。倘若到此你直接将它拖入回收站的话,则正中出题人下怀,被其用精妙的心理战击退。仔细观察程序的代码,可以发现有意义的指令分别位于.text和UPX0段,且指令间存在很明显的被花指令混淆的痕迹,而其它与虚拟机有关的段(.vmp0、.winlice等)均没有任何代码,这表明程序并未使用虚拟化保护,只是采取增加特定段来欺骗DIE。此外,直接运行程序会迅速闪退,而调试运行程序会一直运行下去(比赛时运行了几个小时都没结束......),这都大大增强了“不战而屈人之兵”的效果。

二、 去除语法混淆 在初步分析后,我们可以发现不能像该比赛中的逆向dump赛题那样通过简单的黑盒测试分析结果,必须要先去除花指令的语法混淆,从而实现静态分析。由于程序使用了大量的花指令,逐个patch耗时且容易出错,笔者考虑使用idc脚本实现该功能。
该样本的花指令主要有以下几种类型: 1. jz + 1
该类型花指令跳过了一个字节,特征码为0F 84 01 00 00 00。如图所示,由简单的数据流分析可知,0x401599处的指令一定会跳转,因此0x40159F处的指令永远不会到达,patch方法为直接nop该字节。

2. jz + 2

同jz + 1类型,特征码为0F 84 02 00 00 00,直接patch跳过的2个字节即可。
3. jz + 8

同jz + 1类型,特征码为0F 84 08 00 00 00,直接patch跳过的8个字节即可。
4. jge + 6

该类型花指令特征码为0F 8D 06 00 00 00,其不能像前几种一样通过数据流分析直接观察出一定跳转,但若程序执行到0x401AFB处,则最终一定会崩溃。笔者的做法偏向保守, 将跳过的6个字节前2个patch为ud2指令,后4个指令patch为nop指令 ,这样静态分析得到的伪代码也能提示跳转到此处会产生BUG。
5. jmp + 6

同jz + 1类型,特征码为E9 06 00 00 00 00(多使用一个00避免误报),直接patch跳过的6个字节即可。
6. jmp - 23

同jge + 6类型,特征码为EB E9, 将该指令patch为ud2指令,其后的4个字节patch为nop指令。

7. cmp al, 0E9h

同jge + 6类型,特征码为3C E9, 将该指令patch为ud2指令,其后的4个字节patch为nop指令。
8. 特殊
  • 0x413824处的指令jmp     qword ptr cs:45BF481Dh非法, 查找交叉引用发现0x413826处有跳转到这里的条件分支 ,因此将该处前2个字节patch为ud2指令。
  • 0x41669E处的两个00直接patch为nop指令。
  • 0x41878D处patch一个ud2指令加上9字节的nop,否则会干扰sub_418779的函数分析。

综上所述,我们可以编写出处理花指令混淆的解混淆idc脚本:
#include static main(){    auto seg, current_ea, ea;    // 遍历所有段    for (seg = get_first_seg(); seg != BADADDR; seg = get_next_seg(seg))    {        auto seg_name = get_segm_name(seg);        // 检查段名是否符合要求        if (seg_name != ".text" && seg_name != "UPX0")        {            //Message("跳过段: %s (0x%X)\n", seg_name, seg);            continue;        }        Message("正在处理段: %s (0x%X)\n", seg_name, seg);        // 获取段的起始和结束地址        auto start_ea = seg;        auto end_ea = get_segm_end(seg);        current_ea = start_ea;        auto pattern1 = "0F 84 01 00 00 00";        while (1)        {            ea = find_binary(current_ea, SEARCH_DOWN, pattern1);            if (ea > end_ea || ea == BADADDR)                break;            Message("Found pattern1 at 0x%X\n", ea);            patch_byte(ea + 60x90);               current_ea = ea + 1;        }        current_ea = start_ea;        auto pattern2 = "0F 84 02 00 00 00";        while (1)        {            ea = find_binary(current_ea, SEARCH_DOWN, pattern2);            if (ea > end_ea || ea == BADADDR)                break;            Message("Found pattern2 at 0x%X\n", ea);            patch_word(ea + 60x9090);                         current_ea = ea + 1;        }        current_ea = start_ea;        auto pattern3 = "0F 84 08 00 00 00";        while (1)        {            ea = find_binary(current_ea, SEARCH_DOWN, pattern3);            if (ea > end_ea || ea == BADADDR)                break;            Message("Found pattern3 at 0x%X\n", ea);            patch_qword(ea + 60x9090909090909090);            current_ea = ea + 1;        }        current_ea = start_ea;        auto pattern4 = "0F 8D 06 00 00 00";        while (1)        {            ea = find_binary(current_ea, SEARCH_DOWN, pattern4);            if (ea > end_ea || ea == BADADDR)                break;            Message("Found pattern4 at 0x%X\n", ea);            patch_word(ea + 60x0B0F);                         patch_dword(ea + 80x90909090);            current_ea = ea + 1;        }        current_ea = start_ea;        auto pattern5 = "E9 06 00 00 00 00";        while (1)        {            ea = find_binary(current_ea, SEARCH_DOWN, pattern5);            if (ea > end_ea || ea == BADADDR)                break;            Message("Found pattern5 at 0x%X\n", ea);            patch_word(ea + 50x0B0F);                         patch_dword(ea + 70x90909090);            current_ea = ea + 1;        }        current_ea = start_ea;        auto pattern6 = "EB E9";        while (1)        {            ea = find_binary(current_ea, SEARCH_DOWN, pattern6);            if (ea > end_ea || ea == BADADDR)                break;            Message("Found pattern6 at 0x%X\n", ea);            patch_word(ea, 0x0B0F);                               patch_dword(ea + 20x90909090);            current_ea = ea + 1;        }        current_ea = start_ea;        auto pattern7 = "3C E9";        while (1)        {            ea = find_binary(current_ea, SEARCH_DOWN, pattern7);            if (ea > end_ea || ea == BADADDR)                break;            Message("Found pattern7 at 0x%X\n", ea);            patch_word(ea + 20x0B0F);              // ud2            patch_word(ea + 40x9090);              // nop            current_ea = ea + 1;        }    }    patch_word(0x4138240x0B0F);    patch_word(0x41669E0x0B0F);    patch_word(0x41878D0x0B0F);    patch_qword(0x41878F0x9090909090909090);    patch_byte(0x4187970x90);       Message("Finished.\n");}
运行脚本后,将patch后文件保存为re-new1.exe,IDA打开此文件后,发现大部分函数已经被识别出来:

手工把剩下没有自动识别成函数的位置按P键生成函数,至此我们完全实现了语法解混淆:


三、 去除语义混淆
从start函数中,不难发现sub_4098F0为main函数,F5反编译代码如下:


可以发现程序还是非常难以阅读,不过此时笔者发现下面区域的字节都是固定常数0-9:


交叉引用发现它们只被读而没被写,因此可以分别重命名为byte0-9:
重新分析反编译代码,发现它们被用来实现语法混淆,例如155行的条件分支永远为真,v147永远为0等。因此我们可以考虑把对这些常量的引用的指令变成对常数引用的指令,利用IDA的优化来去除这些永真永假跳转。以byte0为例,其指令形式为movsx   ecx/eax/edx, cs:byte0,占7个字节,我们可以将其改为mov     ecx/eax/edx, 0(占5个字节),再填充2字节的nop指令,对byte0-byte9使用idc脚本去除混淆,代码如下:
#include <idc.idc>static main(){    auto seg, current_ea, mnemonic, op1, op2;    // 遍历所有段    for (seg = get_first_seg(); seg != BADADDR; seg = get_next_seg(seg))    {        auto seg_name = get_segm_name(seg);        // 检查段名是否符合要求        if (seg_name != ".text" && seg_name != "UPX0")        {            //Message("跳过段: %s (0x%X)\n", seg_name, seg);            continue;        }        Message("正在处理段: %s (0x%X)\n", seg_name, seg);        // 获取段的起始和结束地址        auto start_ea = seg;        auto end_ea = get_segm_end(seg);        // 遍历段中的每一条指令        current_ea = start_ea;        while (current_ea < end_ea && current_ea != BADADDR)        {            // 获取指令的助记符和操作数            mnemonic = print_insn_mnem(current_ea);            op1 = print_operand(current_ea, 0);            op2 = print_operand(current_ea, 1);            // 检查是否是目标指令            if (mnemonic == "movsx" && op2 == "cs:byte0")            {                Message("Target Ins at: 0x%X\n", current_ea);                if (op1 == "eax")                {                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码                }                else if (op1 == "ecx")                {                    patch_byte(current_ea, 0xB9);       // mov ecx, imm32 的操作码                }                else if (op1 == "edx")                {                    patch_byte(current_ea, 0xBA);       // mov edx, imm32 的操作码                }                // 设置 imm32 = 0                patch_dword(current_ea + 10);      // imm32 = 0                patch_word(current_ea + 50x9090);  // nop                create_insn(current_ea);             // 重新分析指令            }            // 检查是否是目标指令            if (mnemonic == "movsx" && op2 == "cs:byte1")            {                Message("Target Ins at: 0x%X\n", current_ea);                if (op1 == "eax")                {                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码                }                else if (op1 == "ecx")                {                    patch_byte(current_ea, 0xB9);       // mov ecx, imm32 的操作码                }                else if (op1 == "edx")                {                    patch_byte(current_ea, 0xBA);       // mov edx, imm32 的操作码                }                // 设置 imm32 = 1                patch_dword(current_ea + 11);      // imm32 = 1                patch_word(current_ea + 50x9090);  // nop                create_insn(current_ea);             // 重新分析指令            }            // 检查是否是目标指令            if (mnemonic == "movsx" && op2 == "cs:byte2")            {                Message("Target Ins at: 0x%X\n", current_ea);                if (op1 == "eax")                {                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码                }                else if (op1 == "ecx")                {                    patch_byte(current_ea, 0xB9);       // mov ecx, imm32 的操作码                }                else if (op1 == "edx")                {                    patch_byte(current_ea, 0xBA);       // mov edx, imm32 的操作码                }                // 设置 imm32 = 2                patch_dword(current_ea + 12);      // imm32 = 2                patch_word(current_ea + 50x9090);  // nop                create_insn(current_ea);             // 重新分析指令            }            // 检查是否是目标指令            if (mnemonic == "movsx" && op2 == "cs:byte3")            {                Message("Target Ins at: 0x%X\n", current_ea);                if (op1 == "eax")                {                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码                }                else if (op1 == "ecx")                {                    patch_byte(current_ea, 0xB9);       // mov ecx, imm32 的操作码                }                else if (op1 == "edx")                {                    patch_byte(current_ea, 0xBA);       // mov edx, imm32 的操作码                }                // 设置 imm32 = 3                patch_dword(current_ea + 13);      // imm32 = 3                patch_word(current_ea + 50x9090);  // nop                create_insn(current_ea);             // 重新分析指令            }            // 检查是否是目标指令            if (mnemonic == "movsx" && op2 == "cs:byte4")            {                Message("Target Ins at: 0x%X\n", current_ea);                if (op1 == "eax")                {                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码                }                else if (op1 == "ecx")                {                    patch_byte(current_ea, 0xB9);       // mov ecx, imm32 的操作码                }                else if (op1 == "edx")                {                    patch_byte(current_ea, 0xBA);       // mov edx, imm32 的操作码                }                // 设置 imm32 = 4                patch_dword(current_ea + 14);      // imm32 = 4                patch_word(current_ea + 50x9090);  // nop                create_insn(current_ea);             // 重新分析指令            }            // 检查是否是目标指令            if (mnemonic == "movsx" && op2 == "cs:byte5")            {                Message("Target Ins at: 0x%X\n", current_ea);                if (op1 == "eax")                {                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码                }                else if (op1 == "ecx")                {                    patch_byte(current_ea, 0xB9);       // mov ecx, imm32 的操作码                }                else if (op1 == "edx")                {                    patch_byte(current_ea, 0xBA);       // mov edx, imm32 的操作码                }                // 设置 imm32 = 5                patch_dword(current_ea + 15);      // imm32 = 5                patch_word(current_ea + 50x9090);  // nop                create_insn(current_ea);             // 重新分析指令            }            // 检查是否是目标指令            if (mnemonic == "movsx" && op2 == "cs:byte6")            {                Message("Target Ins at: 0x%X\n", current_ea);                if (op1 == "eax")                {                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码                }                else if (op1 == "ecx")                {                    patch_byte(current_ea, 0xB9);       // mov ecx, imm32 的操作码                }                else if (op1 == "edx")                {                    patch_byte(current_ea, 0xBA);       // mov edx, imm32 的操作码                }                // 设置 imm32 = 6                patch_dword(current_ea + 16);      // imm32 = 6                patch_word(current_ea + 50x9090);  // nop                create_insn(current_ea);             // 重新分析指令            }            // 检查是否是目标指令            if (mnemonic == "movsx" && op2 == "cs:byte7")            {                Message("Target Ins at: 0x%X\n", current_ea);                if (op1 == "eax")                {                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码






请到「今天看啥」查看全文