吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3283|回复: 26
收起左侧

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

  [复制链接]
VirusCollector 发表于 2025-2-21 14:57
本帖最后由 VirusCollector 于 2025-2-25 15:36 编辑

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

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

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

2. jz + 2
3.png
同jz + 1类型,特征码为0F 84 02 00 00 00,直接patch跳过的2个字节即可。
3. jz + 8
5.png
同jz + 1类型,特征码为0F 84 08 00 00 00,直接patch跳过的8个字节即可。
4. jge + 6
6.png
该类型花指令特征码为0F 8D 06 00 00 00,其不能像前几种一样通过数据流分析直接观察出一定跳转,但若程序执行到0x401AFB处,则最终一定会崩溃。笔者的做法偏向保守,将跳过的6个字节前2个patch为ud2指令,后4个指令patch为nop指令,这样静态分析得到的伪代码也能提示跳转到此处会产生BUG。
5. jmp + 6
7.png
同jz + 1类型,特征码为E9 06 00 00 00 00(多使用一个00避免误报),直接patch跳过的6个字节即可。
6. jmp - 23
8.png
同jge + 6类型,特征码为EB E9, 将该指令patch为ud2指令,其后的4个字节patch为nop指令。

7. cmp al, 0E9h
9.png
同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脚本:

[C] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <idc.idc>
  
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 + 6, 0x90);  
            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 + 6, 0x9090);            
            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 + 6, 0x9090909090909090);
            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 + 6, 0x0B0F);            
            patch_dword(ea + 8, 0x90909090);
            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 + 5, 0x0B0F);            
            patch_dword(ea + 7, 0x90909090);
            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 + 2, 0x90909090);
            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 + 2, 0x0B0F);              // ud2
            patch_word(ea + 4, 0x9090);              // nop
            current_ea = ea + 1;
        }
  
    }
  
    patch_word(0x413824, 0x0B0F);
    patch_word(0x41669E, 0x0B0F);
    patch_word(0x41878D, 0x0B0F);
    patch_qword(0x41878F, 0x9090909090909090);
    patch_byte(0x418797, 0x90);  
  
    Message("Finished.\n");
}



运行脚本后,将patch后文件保存为re-new1.exe,IDA打开此文件后,发现大部分函数已经被识别出来: 10.png

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

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

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

交叉引用发现它们只被读而没被写,因此可以分别重命名为byte0-9: 14.png
重新分析反编译代码,发现它们被用来实现语法混淆,例如155行的条件分支永远为真,v147永远为0等。因此我们可以考虑把对这些常量的引用的指令变成对常数引用的指令,利用IDA的优化来去除这些永真永假跳转。以byte0为例,其指令形式为movsx   ecx/eax/edx, cs:byte0,占7个字节,我们可以将其改为mov     ecx/eax/edx, 0(占5个字节),再填充2字节的nop指令,对byte0-byte9使用idc脚本去除混淆,代码如下:
[C] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#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 + 1, 0);      // imm32 = 0
                patch_word(current_ea + 5, 0x9090);  // 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 + 1, 1);      // imm32 = 1
                patch_word(current_ea + 5, 0x9090);  // 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 + 1, 2);      // imm32 = 2
                patch_word(current_ea + 5, 0x9090);  // 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 + 1, 3);      // imm32 = 3
                patch_word(current_ea + 5, 0x9090);  // 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 + 1, 4);      // imm32 = 4
                patch_word(current_ea + 5, 0x9090);  // 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 + 1, 5);      // imm32 = 5
                patch_word(current_ea + 5, 0x9090);  // 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 + 1, 6);      // imm32 = 6
                patch_word(current_ea + 5, 0x9090);  // 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 的操作码
                }
                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 = 7
                patch_dword(current_ea + 1, 7);      // imm32 = 7
                patch_word(current_ea + 5, 0x9090);  // nop
                create_insn(current_ea);             // 重新分析指令
            }
  
            // 检查是否是目标指令
            if (mnemonic == "movsx" && op2 == "cs:byte8")
            {
                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 = 8
                patch_dword(current_ea + 1, 8);      // imm32 = 0
                patch_word(current_ea + 5, 0x9090);  // nop
                create_insn(current_ea);             // 重新分析指令
            }
  
            // 检查是否是目标指令
            if (mnemonic == "movsx" && op2 == "cs:byte9")
            {
                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 = 9
                patch_dword(current_ea + 1, 9);      // imm32 = 9
                patch_word(current_ea + 5, 0x9090);  // nop
                create_insn(current_ea);             // 重新分析指令
            }
  
  
            // 移动到下一条指令(避免死循环)
            auto next_ea = next_head(current_ea, end_ea);
            if (next_ea == BADADDR || next_ea <= current_ea)
                break;
  
            current_ea = next_ea;
        }
    }
  
    Message("Finished.\n");
}


执行后发现程序反编译得到的代码中去除了很多用于混淆的永真永假分支:
15.png
此时,笔者发现三个使用频率最高的函数也使用了语义混淆,例如sub_41307D(重命名为return_arg1)返回第一个参数的值:
16.png
类似地,可以发现sub_413162(重命名为return_num1)返回固定值1,sub_4131D6(重命名为return_arg2)返回第二个参数的值,我们可以继续编写idc脚本,把对这3个函数的调用(call指令)换成对应的对rax/eax操作的指令,代码如下:
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#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);
  
            // 检查是否是目标指令
            if (mnemonic == "call")
            {
                if (op1 == "returnarg1")
                {
                    Message("Target Ins at: 0x%X (call returnarg1)\n", current_ea);
  
                    // 替换为 mov rax, rcx (3 字节)
                    patch_byte(current_ea, 0x48);       // REX.W 前缀
                    patch_byte(current_ea + 1, 0x89);   // mov 操作码
                    patch_byte(current_ea + 2, 0xC8);   // modrm: mov r/m64, r64 (rax = rcx)
  
                    // 填充 2 字节的 nop 指令
                    patch_byte(current_ea + 3, 0x90);   // nop
                    patch_byte(current_ea + 4, 0x90);   // nop
  
                    // 重新分析指令
                    create_insn(current_ea);
                }
                else if (op1 == "return_num1")
                {
                    Message("Target Ins at: 0x%X (call return_num1)\n", current_ea);
  
                    // 替换为 mov eax, 1 (5 字节)
                    patch_byte(current_ea, 0xB8);       // mov eax, imm32 的操作码
                    patch_dword(current_ea + 1, 1);     // imm32 = 1
  
                    // 重新分析指令
                    create_insn(current_ea);
                }
                else if (op1 == "return_arg2_except1695")
                {
                    Message("Target Ins at: 0x%X (call return_arg2_except1695)\n", current_ea);
  
                    // 替换为 mov rax, rdx (3 字节)
                    patch_byte(current_ea, 0x48);       // REX.W 前缀
                    patch_byte(current_ea + 1, 0x89);   // mov 操作码
                    patch_byte(current_ea + 2, 0xD0);   // modrm: mov r/m64, r64 (rax = rdx)
  
                    // 填充 2 字节的 nop 指令
                    patch_byte(current_ea + 3, 0x90);   // nop
                    patch_byte(current_ea + 4, 0x90);   // nop
  
                    // 重新分析指令
                    create_insn(current_ea);
                }
            }
  
            // 移动到下一条指令(避免死循环)
            auto next_ea = next_head(current_ea, end_ea);
            if (next_ea == BADADDR || next_ea <= current_ea)
                break;
  
            current_ea = next_ea;
        }
    }
  
    Message("Finished.\n");
}


执行后发现反编译生成的代码缩减了一大半,至此我们基本实现了语义解混淆,剩下的混淆内容可以在后续分析中去除:
17.png



四、 程序逻辑分析
1.  反调试分析
18.png
程序首先调用IsDebuggerPresent判断是否被调试,如果正在被调试则直接进入无限循环,这也符合之前笔者探究所得的情况。若未处于调试模式,则再调用sub_41992F(重命名为IsInDebugger)函数,判断其返回值是否为0,为0则会触发除以0异常而退出。
19.png
进入IsInDebugger函数,可以发现其首先创建了一个进程并获取其退出码,退出码不为0则直接返回退回码(非0值),表明未处于调试状态。
20.png
21.png
可以看到该线程通过判断Dr7寄存器是否被设置来判断是否设置了硬件断点,不论判断结果如何,都会清空所有硬件断点。
22.png
23.png
最后再次通过间接调用IsDebuggerPresent判断调试状态并返回,这里的func_前缀开头的函数(func_LoadLibrary、func_GetModuleHandle、func_GetProcAddress),其内部都跳转了很多次才到达有意义的部分(例如func_LoadLibrary:sub401DFC->sub_401ACD->...->sub_418779),这也算是一种语义混淆。
2.  核心逻辑
(1) 命令行参数类型
24.png
25.png
分析main函数后半部分,容易看出a3只会在argc为3和不为3的时候取值20和21,a5是一个定值,经过sub_4133F3函数计算后返回值决定了能否进入sub_40885A(重命名为VerifyInput)判断输入是否正确。
26.png
显然sub_4133F3函数的伪代码表明其是一个类似虚拟机的函数,该函数的第1个参数和第2个参数经过运算后得到opcode,再执行对应操作,静态分析比较困难。这里笔者直接绕过反调试使用动态分析,调试发现argc为3时能够进入main函数80行,即我们需要输入3个参数。
对第90行的条件分支也可以使用相同的方法,可发现当DebugActiveProcess返回非0值时能进入main函数第92行,这表明我们的第3个命令行参数需要输入一个正在运行的进程的PID。
(2) 命令行参数内容 27.png
最后来看VerifyInput函数,动态调试可知v0为命令行第2个参数的长度,v10将该字符串参数转为字节数组,例如"12BC"转为{0x12,0xBC}。(注意:只能将字符串的大写字母进行转换,小写字母无法转换)
28.png
可以发现,由于cipher数组初始化为定值,故65-68行的循环中,每次v7的值也为定值,再依次与我们输入的前两个字节异或,最后所得的cipher数组通过sub_407E53(重命名为ModifiedCRC32)计算Hash值,若该值为0xF703DF16则第71行条件分支为真,程序对cipher数组解密并输出flag。因此我们只需动调得到48个v7的值,再根据ModifiedCRC32的算法爆破2字节即可得到输入的第二个参数。代码如下:
[C] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
unsigned char num[] =
{
        0x52, 0xE1, 0x44, 0xE2, 0x39, 0xE1, 0x5E, 0x9B,
        0x51, 0xDC, 0x19, 0x98, 0x50, 0x92, 0x39, 0xC1,
        0x50, 0x9E, 0x52, 0x82, 0x27, 0x82, 0x26, 0xE7,
        0x53, 0x80, 0x24, 0x80, 0x42, 0xDC, 0x39, 0x9E,
        0x2, 0x94, 0x27, 0x81, 0x45, 0x83, 0x51, 0x93,
        0x2, 0x80, 0x44, 0x81, 0x44, 0x81, 0x44, 0x81,
};
  
unsigned int hash(unsigned char* data, unsigned int len)
{
        unsigned int res = -1;
        for (int i = 0; i < len; ++i)
        {
                res ^= data[i];
                for (int j = 0; j < 8; ++j)
                {
                        if (res & 1)
                                res = (res >> 1) ^ 0xEDB88320;
                        else
                                res >>= 1;
                }
        }
        return ~res;
}
  
int main()
{
        for (int a = 0; a < 0xff; ++a)
        {
                for (int b = 0; b < 0xff; ++b)
                {
                        unsigned char data[48];
                        for (int i = 0; i < 48; ++i)
                        {
                                data[i] = (i % 2) ? (b ^ num[i]) : (a ^ num[i]);       
                        }
                        if (hash(data, 48) == 0xF703DF16)
                        {
                                printf("%x %x\n", a, b);
                                goto success;
                        }
                }
        }
  
success:
        return 10086;
}


最终程序输出为79 bc,这表明我们的命令行第二个参数内容为"79BC"即可。

五、 输出结果
结合上述分析,我们得到了获取flag的步骤:
  • 打开一个新进程(例如notepad.exe),获取其PID。
  • 运行原始的re.exe,参数为79BC PID。
29.png
30.png
运行后得到结果flag{MjExNTY3MzE3NTQzMjI=}。
六、 总结

行文至此,笔者不禁再次感叹这道题目的精妙之处——它在多个层面上都运用了巧妙的混淆技术:从表面上看,题目伪装成被虚拟机大量保护的程序,与解题者展开心理博弈;在内部实现上,则采用了大量花指令进行语法混淆,并通过引入常量运算和函数运算得到固定结果、增加永真永假分支、函数多次跳转、包装系统API等手段进行语义混淆。此外,题目还运用了自创的虚拟化技术对关键运算函数进行混淆,并针对硬件断点实施了反调试措施。在逻辑分析层面,选手需要绕过反调试机制获取异或数组、运用爆破思想,并输入一个进程PID作为条件。总体而言,这道题目在设计和实现上都堪称优秀。
然而,笔者也认为这道题目也存在一定的不足之处,即它更适合出现在时间更为充裕的竞赛中。在初赛阶段,其性价比相对较低,可能无法充分发挥其应有的挑战性和价值。
0.png
0.png
4.png

vt.zip

33.57 KB, 下载次数: 17, 下载积分: 吾爱币 -1 CB

题目附件

免费评分

参与人数 14吾爱币 +15 热心值 +12 收起 理由
A_DUST + 1 + 1 用心讨论,共获提升!
InfiniteBoy + 1 用心讨论,共获提升!
ioyr5995 + 1 + 1 我很赞同!
笙若 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
libin302010 + 1 + 1 我很赞同!
神奇的人鱼 + 1 + 1 用心讨论,共获提升!
kn0sky + 2 + 1 我很赞同!
BIGSMATER + 1 + 1 我很赞同!
bGl1 + 1 热心回复!
小菜鸟一枚 + 1 + 1 用心讨论,共获提升!
唐小样儿 + 1 + 1 我很赞同!
diodio + 1 + 1 谢谢@Thanks!
qq3bot + 1 + 1 谢谢@Thanks!
星辰丿 + 1 + 1 牛逼!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

rea1 发表于 2025-3-10 20:57
分析的真妙,确实,如果是现在的话第一时间确实会先怀疑是不是加了一堆特征码,但是赛场上时间紧迫,恰好被拿捏心思了,看到一堆壳后果断先放弃做别的,实在是心服口服。
zbfdyw 发表于 2025-2-23 08:27
这个是真的牛,不服都不行,能够举一反三,甚至连出题的意图和不足都能分析的头头是道,黑客学校的校长非你莫属!
8987 发表于 2025-2-21 19:03
小小梦想家007 发表于 2025-2-21 20:40
膜拜大神的第二天
fengjicheng 发表于 2025-2-22 08:38
膜拜大神的第三天
diodio 发表于 2025-2-22 10:21
好详细,感谢分享,下来膜拜学习
YYSUNFENG 发表于 2025-2-23 18:34
好详细,感谢分享,下来膜拜学习
xfwww 发表于 2025-2-24 09:44
感谢分享解题思路,遇事多思考。
endriver 发表于 2025-2-24 15:11
跟破案一样,抽丝剥茧,一步一步找到事情的真相
kll545012 发表于 2025-2-24 15:24
感谢楼主分享!
膜拜大神的第4天
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-4-1 07:55

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表