[长城杯 2024]vt-批量去花+去不透明词+crc32爆破

Posted by Qmeimei10086 on January 22, 2026

前言

这次依旧是复现,我什么时候才能自己自己写出这种难题 (´;ω;`),不过指出了了网上的一些文章的对程序理解的小误区
这是一道很有难度的题目,确实放长城杯不太合适,而且程序设计的很创新,创的我直呼何意味

分析

先拖进DIE查个壳
DIE 逗我雷霆呢,这么多,不过直接拖进ida里,复现大部分函数都可以识别,区段表里多了很多段比如vmp0,upx0等,估计是故意加的一些特征,不影响分析

去花

随便点开一个函数,比如sub_4098F0,发现有花指令,而且不少,我们考虑用脚本去画
用脚本去花的核心是找特征码并且patch

  1. jz + 1
    .text:000000000040158F 31 DB                             xor     ebx, ebx
    .text:0000000000401591 31 C0                             xor     eax, eax
    .text:0000000000401593 89 C3                             mov     ebx, eax
    .text:0000000000401595 89 D3                             mov     ebx, edx
    .text:0000000000401597 31 C2                             xor     edx, eax
    .text:0000000000401599 0F 84 01 00 00 00                 jz      near ptr loc_40159F+1
    .text:000000000040159F
    .text:000000000040159F                   loc_40159F:                             ; CODE XREF: .text:0000000000401599↑j
    .text:000000000040159F                                                           ; sub_407AE7+2A↓p
    .text:000000000040159F 20 90 C9 C3 55 48                 and     [rax+4855C3C9h], dl
    

分析一下
特征码为0F 84 01 00 00 00,分析发现永远会来到loc_40159F+1,所以把跳过的哪一个字节nop了

  1. jz + 2
    特征码:0F 84 02 00 00 00同理,跳这么点距离,应该百分百会越过这两个字节,直接nop掉
  2. jz + 8
    特征码 0F 84 08 00 00 00
    同上
  3. jge + 6
    .text:0000000000409903 B8 60 83 05 00                    mov     eax, 58360h
    .text:0000000000409908 49 89 C2                          mov     r10, rax
    .text:000000000040990B 4C 89 D1                          mov     rcx, r10
    .text:000000000040990E E8 6A 97 00 00                    call    loc_41307D
    .text:0000000000409913 0F BE 0D 3B 97 00                 movsx   ecx, cs:byte_413055
    .text:0000000000409913 00
    .text:000000000040991A 39 C8                             cmp     eax, ecx
    .text:000000000040991C 0F 8D 06 00 00 00                 jge     near ptr loc_409927+1
    

特征码: 0F 8D 06 00 00 00
不确定call loc_41307D之后会不会改变eax,所以这里patch保守一点,将跳过的6个字节前2个patch为ud2指令,后4个指令patch为nop指令,这样静态分析得到的伪代码也能提示跳转到此处会产生BUG

  1. jmp + 6 特征码 E9 06 00 00 00 00 ,必跳,直接nop6字节
  2. jmp - 23
    .text:0000000000404504                   loc_404504:                             ; CODE XREF: .text:000000000040451D↓j
    .text:0000000000404504 E9 05 00 00 00                    jmp     loc_40450E
    .text:0000000000404509                   ; ---------------------------------------------------------------------------
    .text:0000000000404509
    .text:0000000000404509                   loc_404509:                             ; CODE XREF: .text:00000000004044FA↑j
    .text:0000000000404509 B8 28 87 00 00                    mov     eax, 8728h
    .text:000000000040450E
    .text:000000000040450E                   loc_40450E:                             ; CODE XREF: .text:loc_404504↑j
    .text:000000000040450E 8B 4D F8                          mov     ecx, [rbp-8]
    .text:0000000000404511 0F AF C8                          imul    ecx, eax
    .text:0000000000404514 83 F9 00                          cmp     ecx, 0
    .text:0000000000404517 0F 84 06 00 00 00                 jz      loc_404523
    .text:000000000040451D EB E9                             jmp     short near ptr loc_404504+4
    

一个jz loc_404523向下跳6个字节,下面是一个jmp
虽然不能判断jz会不会跳转,但是jmp往上条到一个奇怪的地方,那不是毁了,所以大概率是不能让他跳的,将该指令patch为ud2指令,其后的4个字节patch为nop指令,合起来6字节
特征码是EB E9

  1. cmp al, 0E9h
    .text:0000000000403D7F 0F BE 05 CF F2 00                 movsx   eax, cs:byte_413055
    .text:0000000000403D7F 00
    .text:0000000000403D86 81 F8 61 03 00 00                 cmp     eax, 361h
    .text:0000000000403D8C 0F 86 16 00 00 00                 jbe     loc_403DA8
    .text:0000000000403D92 B8 A8 03 00 00                    mov     eax, 3A8h
    .text:0000000000403D97 49 89 C2                          mov     r10, rax
    .text:0000000000403D9A 4C 89 D1                          mov     rcx, r10
    .text:0000000000403D9D E8 BD F2 00 00                    call    sub_41305F
    .text:0000000000403DA2 3C E9                             cmp     al, 0E9h
    .text:0000000000403DA4 B3 01                             mov     bl, 1
    

    byte_413055是0,jbe loc_403DA8必跳转,所以可以把后面几条patch为ud2指令,其后的4个字节patch为nop指令。,防止ida分析跳过来的情况
    特征码:3C E9

最后网上复制来的脚本用

1
2
3
4
5
6
7
8
9
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
92
93
94
95
96
97
98
99
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
#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;
        }
 
    }
 
 
 
    Message("Finished.\n");
}

将patch后文件保存为re-new1.exe,IDA打开此文件后,发现大部分函数已经被识别出来,不行手动重建一下

去不透明词

进入主函数sub_4098F0发现好多0x309 + byte_41305B + byte_413058 - byte_41305E这种计算的表达式在if里,点进去一看发现他们是0,1, 2….,9
bytes
全部改名为_0,_1,_2…_9
number 这下看懂了,这些都是可以算出来的值表达式,但是ida却因为这是从内存里拿出来的值不敢直接算,所以保留了这些分支
然后我就学到了一个很聪明的方法,利用ida本身强大的分析能力去除不透明词
比如

movsx   ecx, cs:_2

我们可以把它改成

mov     ecx, 2

这样子ida就会直接算出来,就可以判断能否到达这个分支,进而自动的帮我们去掉这个可能无法到达的分支

1
2
3
4
5
6
7
8
9
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
92
93
94
95
96
97
98
99
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:_0")
            {
                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:_1")
            {
                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:_2")
            {
                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:_3")
            {
                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:_4")
            {
                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:_5")
            {
                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:_6")
            {
                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:_7")
            {
                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:_8")
            {
                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:_9")
            {
                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");
}

去除无用函数

两个高频函数

1
2
3
4
5
6
7
8
__int64 __fastcall sub_41307D(unsigned int a1)
{
  if ( byte_40C0FB == a1 )
    return byte_40C0FB;
  _RAX = sub_41305F(79802);
  __asm { cpuid }
  return a1;
}

这个函数就是把的值直接变为返回值,
Windows x64调用约定中,参数依次使用rcx, rdx, r8, r9传递,返回值是rax
所以call sub_41307D等价于mov rax, rcx
另一个

1
2
3
4
5
6
__int64 sub_413162()
{
  _RAX = 0;
  __asm { cpuid }
  return 1;
}

等价于mov rax, 1
第三个

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall sub_4131D6(__int64 a1, unsigned int a2)
{
  int v3; // [rsp+2Ch] [rbp-4h]

  v3 = sub_41307D(a2);
  if ( !v3 )
    return 0;
  if ( v3 != 1 )
    return sub_41307D(a2);
  sub_413162(1);
  return sub_41307D(1u) != 0;
}

这个函数百分比放回第二个参数a2,所以等价于mov rax, rdx
pacth脚本

1
2
3
4
5
6
7
8
9
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 == "sub_41307D")
                {
                    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 == "sub_413162")
                {
                    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 == "sub_4131D6")
                {
                    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");
}

这下舒服多了

代码分析

1
2
3
4
5
6
7
8
9
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
92
93
94
95
96
97
98
99
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
__int64 __fastcall sub_4098F0(int argc, __int64 __argv)
{
  BOOL v2; // eax
  __int64 v3; // rcx
  __int64 detect_debug_again_1; // rax
  const CHAR *lpProcName; // rax
  __int64 pid; // rax
  BOOL v7; // eax
  __int64 v8; // rcx
  __int64 msvcrt; // rax
  char *exit_name; // rax
  FARPROC aexit; // [rsp+58h] [rbp-688h]
  HMODULE hModule; // [rsp+60h] [rbp-680h]
  double v14; // [rsp+80h] [rbp-660h]
  double v15; // [rsp+B0h] [rbp-630h]
  double v16; // [rsp+B8h] [rbp-628h]
  _DEBUG_EVENT DebugEvent; // [rsp+E8h] [rbp-5F8h] BYREF
  DWORD v18[2]; // [rsp+198h] [rbp-548h]
  LPVOID lpParameter; // [rsp+1A0h] [rbp-540h]
  LPTHREAD_START_ROUTINE verify_input_func_addr; // [rsp+1A8h] [rbp-538h]
  SIZE_T dwStackSize; // [rsp+1B0h] [rbp-530h]
  LPSECURITY_ATTRIBUTES lpThreadAttributes; // [rsp+1B8h] [rbp-528h]
  double v24; // [rsp+1C8h] [rbp-518h]
  double pid_is_alive; // [rsp+1D0h] [rbp-510h]
  __int64 v26; // [rsp+1D8h] [rbp-508h]
  DWORD Pid_I_input; // [rsp+1E4h] [rbp-4FCh]
  double v29; // [rsp+1F0h] [rbp-4F0h]
  double a_import_value; // [rsp+1F8h] [rbp-4E8h]
  __int64 v31; // [rsp+200h] [rbp-4E0h]
  struct _PROCESS_INFORMATION ProcessInformation; // [rsp+208h] [rbp-4D8h] BYREF
  struct _STARTUPINFOA StartupInfo; // [rsp+220h] [rbp-4C0h] BYREF
  FARPROC aSprintf; // [rsp+288h] [rbp-458h]
  HMODULE hModule_1; // [rsp+290h] [rbp-450h]
  CHAR CommandLine[1024]; // [rsp+298h] [rbp-448h] BYREF
  LPSTR cmd_i_input; // [rsp+698h] [rbp-48h]
  __int64 v38; // [rsp+6A0h] [rbp-40h]
  __int64 v39; // [rsp+6A8h] [rbp-38h]
  __int64 v40; // [rsp+6B0h] [rbp-30h]
  __int64 v41; // [rsp+6B8h] [rbp-28h]
  __int64 v42; // [rsp+6C0h] [rbp-20h]
  _BOOL8 v43; // [rsp+6C8h] [rbp-18h]
  _BOOL8 v44; // [rsp+6D0h] [rbp-10h]
  __int64 v45; // [rsp+6D8h] [rbp-8h]

  v45 = 0;
  v2 = 1;
  if ( !IsDebuggerPresent() )
  {
    v44 = get_a_new_process_retcod() == 0;
    if ( !(2 * (0 / v44) / 2) )
      v2 = 0;
  }
  v43 = v2;
  if ( v2 )
  {
    sub_413110(*&qword_40C4D0);
    dead_loop();
    while ( 1 )
      ;
  }
  v42 = 0;
  v41 = 0;
  v40 = 0;
  v39 = 0;
  v38 = 0;
  IsDebuggerPresent();
  0 = 0;
  cmd_i_input = GetCommandLineA();
  memset_w(CommandLine, 0, 0x400u);
  detect_debug_again_1 = get_msvcrt_name(v3);
  hModule_1 = sub_401DFC(detect_debug_again_1);
  lpProcName = sub_4052FB();
  aSprintf = GetProcAddress(hModule_1, lpProcName);
  LODWORD(pid) = GetCurrentProcessId();
  (aSprintf)(CommandLine, aSD, cmd_i_input, pid);// "%s %d"
  memset(&StartupInfo, 0, sizeof(StartupInfo));
  memset(&ProcessInformation, 0, sizeof(ProcessInformation));
  first_arg = *(__argv + 8);
  StartupInfo.cb = 104;
  v31 = 0;
  a_import_value = (21 - (argc == 3));
  v29 = (19
       - *run_vm(*&qword_40C508, -157668960, *&qword_40C500, *&qword_40C4F8, *&qword_40C4F0, *&qword_40C4E8).m128i_i64);
  if ( *run_vm(*&qword_40C520, -457636272, a_import_value, *&qword_40C518, v29, *&qword_40C510).m128i_i64 == 0 )
  {
    Pid_I_input = atoi(*(__argv + 16));
    v26 = 0;
    pid_is_alive = (21 - DebugActiveProcess(Pid_I_input));
    v24 = (19
         - *run_vm(*&qword_40C548, -157664832, *&qword_40C540, *&qword_40C538, *&qword_40C530, *&qword_40C528).m128i_i64);
    if ( *run_vm(*&qword_40C560, -457632144, pid_is_alive, *&qword_40C558, v24, *&qword_40C550).m128i_i64 == 0 )
    {
      lpThreadAttributes = 0;
      dwStackSize = 0;
      verify_input_func_addr = verify_input;
      lpParameter = 0;
      *v18 = 0;
      CreateThread(0, 0, verify_input_func_addr, 0, 0, 0);
      do
      {
        WaitForDebugEvent(&DebugEvent, 0xAu);
        if ( DebugEvent.dwDebugEventCode == 5 )
          break;
        v16 = (21 - (0 == 1));
        v15 = (19
             - *run_vm(*&qword_40C588, -157653824, *&qword_40C580, *&qword_40C578, *&qword_40C570, *&qword_40C568).m128i_i64);
      }
      while ( *run_vm(*&qword_40C5A0, -457621136, v16, *&qword_40C598, v15, *&qword_40C590).m128i_i64 );
    }
  }
  v14 = (19
       - *run_vm(*&qword_40C5C8, -157646256, *&qword_40C5C0, *&qword_40C5B8, *&qword_40C5B0, *&qword_40C5A8).m128i_i64);
  v7 = *run_vm(*&qword_40C5E0, -457613568, (21 - (argc == 2)), *&qword_40C5D8, v14, *&qword_40C5D0).m128i_i64 == 0;
  v8 = v7;
  if ( v7 )
  {
    CreateProcessA(0, CommandLine, 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInformation);// 创建一附加自己的函数
    Sleep(0x1388u);
  }
  msvcrt = get_msvcrt_name(v8);
  hModule = sub_401DFC(msvcrt);
  exit_name = get_exit_name();
  aexit = GetProcAddress(hModule, exit_name);
  return (aexit)(0);
}

依然有不少混淆,比如有些系统函数是通过GetProcAddress获取的,sub_401DFC这个函数通过大量的嵌套调才到本体等
run_vm是一个黑盒函数,看起来像虚拟机,传入一堆乱七八糟的数,不过里面只有一个值是变得,所以我们只要分析变化的值是多少为true就行
大概的调用流程是

  1. 检测调试器,如果检测到调试器就死循环
    所以我们在oep下个断点,然后用scyllahide直接注入就行
  2. argc==2时,创建一个,获取自身pid,创建新的进程,参数为原本的第一个参数 + 父进程的pid,从这里可以看出
    1
    
    (aSprintf)(CommandLine, aSD, cmd_i_input, pid);// "%s %d"
    
  3. 子进程的argc==3,先用DebugActiveProcess检测父进程活性,如果是活跃的会创建一个线程跑verify_input_func_addr函数
    创建好会waitfordebug,如果被debug就会退出程序,不过这有点奇怪,谁来debug?我原本以为是父进程再次来调试子进程,完成同步(sync)退出,可是没找到相关代码,我发现没断在brak上,倒是while do没跑几轮就结束了,估计是runvm这边帮他跑出去的,如果有知道有什么用的请告诉我
  4. 父进程5s后自动退出,不过此时verify_input_func_addr线程应该以及完成工作
    所以verify_input_func_addr函数是重点

这是一个很有意思的题,出题者的正确思路应该是子进程附加父进程,但是假如我们只有输入2个参数,(命令的exe本身算一个),创建一个新进程,但是父进程在5s后会自动退出,这会加大我们分析的难度
不过有个避开的方法,我们可以自己搞一个活跃的进程,输入进去,就可以顺利断在verify_input_func_addr上

verify_input_func_addr分析

稍微修补后的代码

1
2
3
4
5
6
7
8
9
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
92
93
94
95
96
97
98
99
__int64 verify_input()
{
  size_t arg1_len; // rax
  double char_1; // xmm0_8
  size_t Size; // r8
  LPDWORD lpNumberOfCharsWritten; // r9
  void *lpReserved; // [rsp+20h] [rbp-100h]
  signed int i; // [rsp+70h] [rbp-B0h]
  signed int _len; // [rsp+74h] [rbp-ACh]
  void *decrypted_flag; // [rsp+78h] [rbp-A8h]
  double v9; // [rsp+A0h] [rbp-80h]
  int data_hash; // [rsp+BCh] [rbp-64h]
  int n48; // [rsp+E4h] [rbp-3Ch]
  unsigned __int8 data[48]; // [rsp+E8h] [rbp-38h] BYREF
  _BYTE *input; // [rsp+118h] [rbp-8h]

  arg1_len = my_strlen(first_arg);
  input = unhex(first_arg, arg1_len);
  data[0] = 90;
  data[1] = -23;
  data[2] = 76;
  data[3] = -22;
  data[4] = 65;
  data[5] = -23;
  data[6] = 102;
  data[7] = -93;
  data[8] = 89;
  data[9] = -28;
  data[10] = 33;
  data[11] = -96;
  data[12] = 88;
  data[13] = -102;
  data[14] = 65;
  data[15] = -55;
  data[16] = 88;
  data[17] = -90;
  data[18] = 90;
  data[19] = -118;
  data[20] = 47;
  data[21] = -118;
  data[22] = 46;
  data[23] = -17;
  data[24] = 91;
  data[25] = -120;
  data[26] = 44;
  data[27] = -120;
  data[28] = 74;
  data[29] = -28;
  data[30] = 65;
  data[31] = -90;
  data[32] = 10;
  data[33] = -100;
  data[34] = 47;
  data[35] = -119;
  data[36] = 77;
  data[37] = -117;
  data[38] = 89;
  data[39] = -101;
  data[40] = 10;
  data[41] = -120;
  data[42] = 76;
  data[43] = -119;
  data[44] = 76;
  data[45] = -119;
  data[46] = 76;
  data[47] = -119;
  for ( n48 = 0; n48 < 48; ++n48 )
  {
    char_1 = *run_vm(*&qword_40C480, -79543808, (21 - data[n48]), *&qword_40C478, *&qword_40C470, *&qword_40C468).m128i_i64;
    data[n48] = input[n48 % 2] ^ char_1;
  }
  data_hash = calc_hash(data, 48);
  v9 = (19
      - *run_vm(*&qword_40C4A8, -157693040, *&qword_40C4A0, *&qword_40C498, *&qword_40C490, *&qword_40C488).m128i_i64);
  if ( *run_vm(*&qword_40C4C0, -457660352, (21 - (data_hash != -150741226)), *&qword_40C4B8, v9, *&qword_40C4B0).m128i_i64 )
  {
    decrypted_flag = malloc_w(0x1Eu);
    memset_w(decrypted_flag, 0, 0x1Fu);
    _len = decrypt_again(data, decrypted_flag, Size);
    for ( i = 0; i < _len; ++i )
    {
      if ( *(decrypted_flag + i) == '}' )
      {
        *(decrypted_flag + i + 1) = 0;
        break;
      }
    }
    detect_debug();
    empty_fun();
    j_j_WriteConsoleA(0x101, &lpBuffer_, decrypted_flag, lpNumberOfCharsWritten, lpReserved);
    0 = 1;
    return 0;
  }
  else
  {
    0 = 1;
    return 0;
  }
}

流程

  1. 先将我们输入的值结果unhex(必须要输入大写的)
  2. 初始化一串数组,先对每一个的元素用run_vm进行某个奇妙的运算,得到的值与的到的hex前两位进行异或,最后回写
    由于有个% 2,好像只会不断的去第1,第2位,所以我们输入的只有这前两位有效
    vm_run是一个黑盒函数,但是他结果的运算是固定的,所以得到的值也是固定的
    我们在这里下个断点
    xor     eax, ecx
    

    观察eax是我们输入的那个hex,所以ecx就是char_1
    我们编辑这个断点,触发时执行

    1
    
    Message("RCX = 0x%x\n", rcx); 0
    

    可以的到

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    uint8_t 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,
    };
    
  3. 然后是一个计算hash的函数,hash是-150741226就会进入下一步
  4. 对经过一轮运算的data进行下一步解密,最后的到的就是flag,用j_j_WriteConsoleA输出
    不过有一个检测最后以为是不是大括号的操作,但是没啥用,姑且当成作者的提示
    总结一下就是
    1
    2
    3
    4
    5
    
    初始化data
    for i in range(0,48):
     data[i] = enc_func(data[i]) xor input_hex[0或者1]
    if modify_hash(data) == -150741226:
     print(decrypt(data))
    

    明显不可逆,我们无法直接反推flag,也无法反推密钥
    但是那个hash校验很关键,由于我们输入的只有前两位有用,我们只需要爆破就行

1
2
3
4
5
6
7
8
9
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
#include <stdio.h>
#include <stdint.h>

unsigned int calc_hash(uint8_t* p_enc, int n48)
{
    uint8_t* v3; // rcx
    int n8; // [rsp+78h] [rbp-8h]
    unsigned int v6; // [rsp+7Ch] [rbp-4h]
    int n48_1; // [rsp+98h] [rbp+18h]

    n48_1 = n48;
    v6 = -1;
    while (n48_1--)
    {
        v3 = p_enc++;
        v6 ^= *v3;
        for (n8 = 0; n8 < 8; ++n8)
        {
            if ((v6 & 1) != 0)
                v6 = (v6 >> 1) ^ 0xEDB88320;
            else
                v6 >>= 1;
        }
    }
    return ~v6;
}

uint8_t 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,
};



int main()
{
    uint8_t data[48];
    uint8_t input[2];
    for (int a = 0; a < 0xff; ++a)
    {
        for (int b = 0; b < 0xff; ++b)
        {
            input[0] = a;
            input[1] = b;
            for (int c = 0; c < 48; ++c) {
                data[c] = input[c % 2] ^ num[c];
            }
            if (calc_hash(data, 48) == 0xF703DF16)
            {
                printf("%x %x\n", a, b);
                goto success;
            }
        }
    }
success:
    return 0;
}


运行后得到 79 BC

FLAG

输入命令

.\re.exe 79BC
flag{MjExNTY3MzE3NTQzMjI=}