首页
论坛
课程
招聘
[原创]指令壳开源
2021-5-6 21:45 11753

[原创]指令壳开源

2021-5-6 21:45
11753

前言

  在把壳子开源之前,我会先对VMProtect1.70.4这个版本做一个简单的分析。在这几天分析过程中,我感受到了VMProtect的威力,并得出一个结论:分析VMProtect非常耗时间,如果没有做好与之长期斗争的准备,很难有实用的成果。另外,由于我第一次分析VMProtect,有分析不对的地方或者术语用得不恰当的地方,希望老萌萌们能指出来,我立即修改,不能误人子弟。分析的样本有三份,我放到了附件里面。
  壳子我是去年写的,当时刚刚看了<<加密与解密>>第四版的第21章,我估摸了一下,自己可以写出来,然后就写出来了。在写之前,我记得当时还在bilibili观看了哈工大姜守旭教授教授的编译原理这门课程的视频,了解了大概就开干,看视频这个操作,对我写指令壳起了一种壮胆的效果。现在要开源,我又熟悉了下整个项目的流程。代码写得有点乱,我会画一张加壳时的流程图做一个直观的说明,和其他一些要点说明,以减少读代码时遇到的困惑。如果遇到调试或者编译问题,可以留言,我看到会及时回复。指令壳源码我会上传到附件。

分析vmp1.70.4

01_速度最快

写一个简单程序来测试:(ps:这个程序在vmp样本一文件夹)

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
#include <Windows.h>
#include <iostream>
 
void __declspec(naked) test_vmp(int a, int b)
{
    /*__asm {
        mov eax,dword ptr[esp+4]
        mov ecx,dword ptr[esp+8]
        add eax,ecx
        ret
    }*/
    __asm {
        xor eax,ebx
        xor eax, ebx
        ret
    }
}
 
int main()
{
    test_vmp(1, 2);
    printf("%d\n", x);
    system("pause");
    return 0;
}

在OD中打开,函数在0x401080这个位置。
clipboard_31_
打开vmp,开始加壳:
clipboard_41_
下拉列表选择最快速度,其他不选择。
clipboard_7_
程序加好壳子后,用OD打开,开始分析:
clipboard_28_
0x401080这儿已经变为jmp了,跳到.vmp0这节里面:
clipboard_2_

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
//入口
00401080| jmp debug_test2.vmp.4A77D2                                       | Debug_test2.cpp:5
 
004A77D2| push 4A781400                                                    |
004A77D7| call debug_test2.vmp.4A6B9F                                      |
 
004A6B9F| jmp debug_test2.vmp.4A703A                                       |
 
004A703A| push esi                                                         | esi:_mainCRTStartup
004A703B| jmp debug_test2.vmp.4A52E8                                       |
 
004A52E8| pushfd                                                           |
004A52E9| push D3AC6D6A                                                    |
004A52EE| mov byte ptr ss:[esp+4],8F                                       |
004A52F3| pushfd                                                           |
004A52F4| pop dword ptr ss:[esp+4]                                         | [esp+4]:___use_sse2_mathfcns+4A78
004A52F8| jmp debug_test2.vmp.4A64C1                                       |
 
004A64C1| call debug_test2.vmp.4A5FE9                                      |
 
004A5FE9| pushad                                                           |
004A5FEA| mov dword ptr ss:[esp+24],ebp                                    | [esp+24]:_mainCRTStartup
004A5FEE| push esp                                                         |
004A5FEF| pushfd                                                           |
004A5FF0| push 72B57CE7                                                    |
004A5FF5| pushfd                                                           |
004A5FF6| mov dword ptr ss:[esp+30],eax                                    | eax:___use_sse2_mathfcns+2D1
004A5FFA| mov byte ptr ss:[esp],cl                                         |
004A5FFD| call debug_test2.vmp.4A701F                                      |
 
004A701F| call debug_test2.vmp.4A5A25                                      |
 
004A5A25| jmp debug_test2.vmp.4A6BC5                                       |
 
 
004A6BC5| mov dword ptr ss:[esp+34],edx                                    | [esp+34]:___use_sse2_mathfcns+2D1
004A6BC9| call debug_test2.vmp.4A5C3D                                      |
 
 
004A5C3D| call debug_test2.vmp.4A6681                                      |
 
004A6681| mov dword ptr ss:[esp+38],edx                                    | [esp+38]:___use_sse2_mathfcns+2D1
004A6685| push dword ptr ss:[esp+8]                                        | [esp+8]:___use_sse2_mathfcns+42C0
004A6689| mov dword ptr ss:[esp+38],ecx                                    | [esp+38]:___use_sse2_mathfcns+2D1, ecx:___use_sse2_mathfcns+2D1
004A668D| push E7FE4EC3                                                    |
004A6692| mov byte ptr ss:[esp],dh                                         |
004A6695| lea esp,dword ptr ss:[esp+3C]                                    |
004A6699| jmp debug_test2.vmp.4A632E                                       |
 
004A632E| btr si,6                                                         |
004A6333| push edi                                                         |
004A6334| push 20AD5139                                                    |
004A6339| xchg esi,ecx                                                     | esi:_mainCRTStartup, ecx:___use_sse2_mathfcns+2D1
004A633B| call debug_test2.vmp.4A6129                                      |
 
004A6129| mov dword ptr ss:[esp+4],ebx                                     |
004A612D| movsx esi,cl                                                     | esi:___use_sse2_mathfcns+2D1
004A6130| pop ecx                                                          | ecx:"U嬱Q梓\x0E"
004A6131| btc di,cx                                                        |
004A6135| pushad                                                           |
004A6136| mov dword ptr ss:[esp+1C],0                                      |
004A613E| stc                                                              |
004A613F| jmp debug_test2.vmp.4A70CF

以上操作是把寄存器压到栈里,执行完后如下图:

 

clipboard_38_


 

寄存器压栈完成后,会先计算出调度表的地址。
[esp+48]这个值就是入口push 4A781400 这个值,经过计算后得到调度表的地址。调度表地址存放在esi寄存器里。

1
2
3
4
5
6
7
8
9
10
004A70CF| mov esi,dword ptr ss:[esp+48]                                    |
004A70D3| btr bp,sp                                                        |
004A70D7| jmp debug_test2.vmp.4A53AF                                       |
 
004A53AF| movzx bp,bl                                                      |
004A53B3| btc bp,si                                                        |
004A53B7| rol esi,18                                                       |
004A53BA| push 124E6496                                                    |
004A53BF| inc esi                                                          |
004A53C0| jmp debug_test2.vmp.4A6C85                                       |

如下,内存5里显示的就是经过加密的调度表:
clipboard

 

然后按F7,到下面那个位置,箭头指向的三步,ebp指向的是真实堆栈,edi就是虚拟机的上下文环境(VMContext)。edi指向的堆栈空间,最大的作用就是存放寄存器。
clipboard_5_

 

clipboard_25_

 

在接下来的步骤中,通过在esi指向的调度表中取值,然后把ebp所指向的寄存器的值,挨个放到edi的虚拟环境中。
clipboard_39_
至此,虚拟机的环境构建完成,准备工作已经做好。


 

接下来进入正题,程序会通过edi寄存器取ebx的值,取两次,第一次的值压入[ebp],第二次压入[ebp+4],然后进入一个handler块进行运算。
这就是那个handler块:

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
004A53F7 | rol ah,6                                                         |
004A53FA | sbb edx,esp                                                      |
004A53FC | mov eax,dword ptr ss:[ebp]                                       |
004A53FF | jmp debug_test2.vmp.4A5BC4   
 
004A5BC4 | cmc                                                              |
004A5BC5 | mov edx,dword ptr ss:[ebp+4]                                     |
004A5BC8 | push 25584F9E                                                    |
004A5BCD | pushad                                                           |
004A5BCE | clc                                                              |
004A5BCF | bt cx,C                                                          |
004A5BD4 | not eax                                                          |
004A5BD6 | pushad                                                           |
004A5BD7 | bt cx,bx                                                         |
004A5BDB | call debug_test2.vmp.4A5D63                                      |
 
004A5D63 | cmc                                                              |
004A5D64 | not edx                                                          |
004A5D66 | cmp edi,F7A6A772                                                 |
004A5D6C | stc                                                              |
004A5D6D | stc                                                              |
004A5D6E | stc                                                              |
004A5D6F | and eax,edx                                                      |
004A5D71 | jmp debug_test2.vmp.4A5A3F                                       |
 
004A5A3F | jmp debug_test2.vmp.4A6E72                                       |
 
004A6E73 | pushfd                                                           |
004A6E74 | mov dword ptr ss:[ebp+4],eax                                     |
004A6E77 | jmp debug_test2.vmp.4A564D                                       |
 
----------------------------------------------------------------------------------------------------------
 
//化简之后
004A53FC | mov eax,dword ptr ss:[ebp]                                       |                                                                            
004A5BC5 | mov edx,dword ptr ss:[ebp+4]                                     |                                                       
004A5BD4 | not eax                                                          |                                                     
004A5D64 | not edx                                                          |                                                          
004A5D6F | and eax,edx                                                      |
004A6E74 | mov dword ptr ss:[ebp+4],eax                                     |
004A6E77 | jmp debug_test2.vmp.4A564D                                       |

可以把上面的handler块命名为Handler_NOT_AND


 

那么,可以把以上的运算过程可以表示成这样:not(ebx) and not(ebx)


 

执行xor eax,ebx这条指令的时候,虚拟机会多次调用Handler_NOT_AND 块,整个流程可以记录为如下形式:

 

T = not(not(ebx) and not(ebx)) and not(not(eax) and not(eax));即为:T = ebx and eax;
S= not(eax) and not(ebx);
最终结果:ret= not(T) and not(S)。
通过写程序来验证,与虚拟机算出来的0x690035一致,说明流程记录没有问题。那么,xor eax,ebx可以用这个表达式来表示:eax = not(ebx and eax) and not(not(eax) and not(ebx))。
clipboard_43_
执行完xor eax,ebx之后,eax寄存器的位置在edi中的会变:
clipboard_35_


 

下一个xor eax,ebx 和上面的操作是一样的,这个操作完了,eax=0x004A3035。
eax寄存器的位置在edi中又变了:
clipboard_42_

 

按F7单步跟,(...省略不重要的部分),接着,程序把edi中所保存的寄存器,再吐出来给ebp所指向的堆栈空间,然后ebp赋值给esp,最后再pop到真实寄存器,退出虚拟机。
clipboard_24_
退出虚拟机:

 

clipboard_33_


02_开启检测调试器

测试程序和上面一样。(ps:这个程序在vmp样本二文件夹)
clipboard_3_
下拉列表选择最快速度,再把调试器勾选上,其他不选择。
clipboard_19_

 

加壳完成后,打开CFF来查看,程序会新增一节.vmp1,程序入口也在这节里面。
clipboard_11_

 

clipboard_30_

 

此外,还构建了一个TLS表:(但在这儿作用似乎不大)
clipboard_27_

 

程序执行到入口后,按F7单步跟,步骤和上面“01速度最快"分析时相差无几,会先构建虚拟机环境。
虚拟环境构建完成后,接着按F7单步跟,我的想法是,很快就能找到一些反调试的线索,但是跟了几个小时,发现不对劲了,和上面“01
速度最快"分析时用手工跟踪,完全不在一个数量级的。天气又大,整个人木在那里。后来,想到在退出虚拟机那个地方下断,方法就是搜索vmp1这节的ret或者ret xx,最终找到两个地方,一个是调度器ret xx,这个不管,另一个就是下图所给出的,在ret 0x40处下断。按F9程序跑起来后,会断在这里,能看到右边寄存器窗口的字符串。这个位置,可以作为过掉检测调试器的突破口。
clipboard_14_
当然,最好的办法应该是在一些能检测出调试器的API函数下断。
vmp1.70.4这个版本,开启调试器检测后,程序会依次调用以下的API来检测是否有调试器存在:

1
2
3
4
5
6
7
IsDebuggerPresent
CheckRemoteDebuggerPresent
GetThreadContext
CloseHandle
NtQueryInformationProcess
NtSetInformationThread
//关于这些函数介绍,看雪里有很多大神发了反调试相关的帖子,搜一下就能找到。

注意:API下断时,不要在头部下断,虚拟机会对有些API函数的头部进行0xCC检测,比如在这个程序中,虚拟机执行到GetThreadContext函数之前,会对GetThreadContext函数的头部进行0xCC检测。建议:没有特殊状况,对API下断时要避开在头部下断。

 

此外,在调用CloseHandle之前,虚拟机会手工构造一个SEH异常处理例程,如果调用成功,没出现异常,那么虚拟机会移除这个SEH。假如调用CloseHandle触发异常,那么将万劫不复,程序进入0x4A99AE后,你会寸步难行,我这儿遇到的是非法写入的异常,程序一直卡在那里。

 

clipboard_10_

 

想过掉CloseHandle检测,可以在CloseHandle头部直接返回(eax=0),然后恢复选区即可。


03_最大保护

测试程序:(ps:这个程序在vmp样本三文件夹)

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
#include <Windows.h>
#include <iostream>
int g_num = 0;
 
void __declspec(naked) test_vmp(int a, int b)
{
    __asm {
        mov eax,[esp+4] // [esp+4] == a
        mov ebx,[esp+8] // [esp+8] == b
        xor eax, ebx
        mov g_num,eax
        ret
    }
}
 
void test2()
{
    test_vmp(0x10, 0x21);
    printf("%X\n", g_num);
}
 
int main()
{   
    test2();   
    system("pause");
    return 0;
}

用OD打开,找到test_vmp函数的位置:0x4010E0。
clipboard_36_

 

打开vmp,开始加壳:
clipboard_22_

 

选择最大保护。
clipboard_26_

 

把加壳后的程序,拖入OD,程序断在了入口处:
clipboard_29_

 

找到0x4010E0,下一个硬件断点,然后按F9让程序跑起来:
clipboard_16_

 

程序断在了这里,按F7单步跟踪:
clipboard_17_

 

程序会先构建虚拟机环境,上面已经分析了,这里省略。
clipboard_18_

 

开始进入正题,因为程序在加壳的时候勾选了隐藏常量和内存保护,对我这种初等选手,所以刚开始的时候就遇到了困难。
在第一条指令(mov eax,[esp+4])中,虚拟机会先对[esp+4]解码,又因为4是常量,所以虚拟机刚开始的时候,会对这个常量解密操作。
大概步骤:程序会在esi指向的调度表读取四个字节,并且在解密过程,还会读取多次,来对常量解密。esp寄存器也是加密了的,解码操作和解码常量差不多。

 

mov eax,[esp+4] 模型是:mov 寄存器,内存
这种模式的指令会走如下的handler块:

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
004A6AFF | 66:0FB6C3                | movzx ax,bl                         |
004A6B03 | F6D0                     | not al                              |
004A6B05 | 66:0FB6C3                | movzx ax,bl                         |
004A6B09 | 66:0FBEC2                | movsx ax,dl                         |
004A6B0D | 8B45 00                  | mov eax,dword ptr ss:[ebp]          |
004A6B10 | 60                       | pushad                              |
004A6B11 | E9 41140000              | jmp debug_test2.vmp.4A7F57          |
 
004A6A55 | 36:8B00                  | mov eax,dword ptr ss:[eax]          |
004A6A58 | 55                       | push ebp                            |
004A6A59 | E9 8A000000              | jmp debug_test2.vmp.4A6AE8          |
 
004A6AE8 | 882C24                   | mov byte ptr ss:[esp],ch            |
004A6AEB | FF3424                   | push dword ptr ss:[esp]             |
004A6AEE | 8945 00                  | mov dword ptr ss:[ebp],eax          |
004A6AF1 | FF3424                   | push dword ptr ss:[esp]             |
004A6AF4 | 9C                       | pushfd                              |
004A6AF5 | 9C                       | pushfd                              |
004A6AF6 | 8D6424 38                | lea esp,dword ptr ss:[esp+38]       |
004A6AFA | E9 83150000              | jmp debug_test2.vmp.4A8082          |
 
------------------------------------------------------
可以化简为:
004A6B0D | 8B45 00                  | mov eax,dword ptr ss:[ebp]          |
004A6A55 | 36:8B00                  | mov eax,dword ptr ss:[eax]          |
004A6AEE | 8945 00                  | mov dword ptr ss:[ebp],eax          |
 
可以把上面的handler块命名为Handler_Reg_Mem

关于xor eax, ebx 指令,上面有分析过,除了垃圾指令,其他没变:

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
004A6113 | push ebp                                   |
004A6114 | lahf                                       |
004A6115 | pushad                                     |
004A6116 | mov eax,dword ptr ss:[ebp]                 |
004A6119 | rcr dh,6                                   |
004A611C | bts dx,7                                   |
004A6121 | bts dx,1                                   |
004A6126 | mov edx,dword ptr ss:[ebp+4]               |
004A6129 | stc                                        |
004A612A | not eax                                    |
004A612C | pushfd                                     |
004A612D | push dword ptr ss:[esp]                    |
004A6130 | not edx                                    |
004A6132 | jmp debug_test2.vmp.4A66E3                 |
004A6137 | not esi                                    |
004A6139 | mov byte ptr ss:[esp],dh                   |
004A613C | pushfd                                     |
004A613D | push C19B900A                              |
004A6142 | pushfd                                     |
004A6143 | lea esp,dword ptr ss:[esp+4C]              |
004A6147 | jmp debug_test2.vmp.4A60CA                 |
 
004A66E3 | clc                                        |
004A66E4 | and eax,edx                                |
004A66E6 | push edi                                   |
004A66E7 | push esi                                   |
004A66E8 | jmp debug_test2.vmp.4A61EF                 |
 
004A61EF | mov dword ptr ss:[ebp+4],eax               |
004A61F2 | mov byte ptr ss:[esp+C],31                 | 31:'1'
004A61F7 | mov byte ptr ss:[esp+C],65                 | 65:'e'
004A61FC | push A8B985C4                              |
004A6201 | mov word ptr ss:[esp+C],sp                 |
004A6206 | pushfd                                     |
004A6207 | pop dword ptr ss:[esp+34]                  |
004A620B | mov byte ptr ss:[esp+8],ah                 |
004A620F | call debug_test2.vmp.4A78E2                |
 
----------------------------------------------------------------------
//可以化简为:
004A6116 | mov eax,dword ptr ss:[ebp]                 |
004A6126 | mov edx,dword ptr ss:[ebp+4]               |
004A612A | not eax                                    |
004A6130 | not edx                                    |
004A66E4 | and eax,edx                                |
004A61EF | mov dword ptr ss:[ebp+4],eax               |

在mov g_num,eax这条指令中,虚拟机对g_num内存地址也是加密了的,解密时候,程序会对esi指向的调度表读取四个字节,并且会读取多次,经过计算最终得到g_num的内存地址。
mov g_num,eax 模型是:mov 内存地址,寄存器
这种模式的指令会走如下handler块:

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
004A69C7 | 04 08                    | add al,8                            |
004A69C9 | 60                       | pushad                              |
004A69CA | 66:05 7B36               | add ax,367B                         |
004A69CE | 8B45 00                  | mov eax,dword ptr ss:[ebp]          |
004A69D1 | 20D6                     | and dh,dl                           |
004A69D3 | 66:F7D2                  | not dx                              |
004A69D6 | 08C2                     | or dl,al                            |
004A69D8 | 8B55 04                  | mov edx,dword ptr ss:[ebp+4]        |
004A69DB | 68 37EDD2A5              | push A5D2ED37                       |
004A69E0 | 66:81FF 7052             | cmp di,5270                         |
004A69E5 | 84CB                     | test bl,cl                          |
004A69E7 | F8                       | clc                                 |
004A69E8 | 83C5 08                  | add ebp,8                           |
004A69EB | FF7424 04                | push dword ptr ss:[esp+4]           |
004A69EF | 66:896424 14             | mov word ptr ss:[esp+14],sp         |
004A69F4 | E9 94F4FFFF              | jmp debug_test2.vmp.4A5E8D          |
 
 
004A5E8D | 8910                     | mov dword ptr ds:[eax],edx          |
004A5E8F | 9C                       | pushfd                              |
004A5E90 | 66:895424 04             | mov word ptr ss:[esp+4],dx          |
004A5E95 | 8D6424 2C                | lea esp,dword ptr ss:[esp+2C]       |
004A5E99 | E9 E4210000              | jmp debug_test2.vmp.4A8082          |
 
-------------------------------------------------------------------------------
可以化简为:
004A69CE | 8B45 00                  | mov eax,dword ptr ss:[ebp]          |
004A69D8 | 8B55 04                  | mov edx,dword ptr ss:[ebp+4]        |
004A69E8 | 83C5 08                  | add ebp,8                           |
004A5E8D | 8910                     | mov dword ptr ds:[eax],edx          |
 
可以把上面的handler块命名为Handler_Mem_Reg

小结

  关于隐藏常量和内存保护的解密过程,只是跟了几遍,了解了大概流程,没有具体分析,退出虚拟机时,加密寄存器,这个解密模式和隐藏常量和内存保护的解密过程似乎差不多。总的来说,这次分析过程是失败的,因为隐藏常量、内存保护以及离开虚拟机时加密寄存器的解密过程没有分析出来,只是把汇编指令在虚拟机中的handler块找出来了。我觉得,这些解密操作,正是vmprotect虚拟机最精华的部分之一,在跟踪这些解密操作的时候,我脑袋都大了,暂时先搁在这儿,做一些更有意义的事情(^_^)。这节可以省略不看。如果有像我一样的初等选手,跟起来又有点费劲,又想了解这个解密过程的,可以在看雪搜搜,有很多大神都应该分析过。

指令壳项目开源说明

0x00 纲要

项目名称:指令壳框架
功能:可以对32位可执行程序加壳(*.exe)
编译器:vs2019(编译模式采用的是Debug模式,也就是调试模式)
开发语言:C、C++、内联汇编
解决方案:一个解决方案,两个项目(VMProtect、Stub,VMProtect是现实核心功能,Stub是外壳部分)

0x01 项目说明以及一些注意事项

  • 程序用win32编写的,没用MFC或者QT。
    • 在整个项目中,会用到汇编引擎和反汇编引擎,汇编引擎用的是XEDparse,反汇编引擎用的是BeaEngine。
    • 此外,我定义了几个主要模块:指令分析器,垃圾模块构造指令器,IAT加密(解密)模块,反调试模块。
    • 没有处理异常,也就是说,加了异常处理的函数,不要加壳。

Common文件夹里封装了一些类:
CString类即是字符串操作的类,支持字符串和整型混合相加(字符串+(DWORD)16进制/10进),生成一个字符串。(注意:加16进制时,前面要加DWORD表示这是16进制。)
PE类封装了处理PE文件格式一些函数,比如文件拉伸、修复重定位表、添加新节等等。
FileOpenration类是文件操作类,封装了打开文件、删除文件、保存文件、创建子进程等等一些函数。

 

clipboard_8_
clipboard_40_

 

在加壳过程中,要频繁用到内存申请、内存释放的操作,为了防止内存泄漏,我封装了一个类(AllocMemory)用来申请内存,
这个类的作用就是只管申请内存,不用管释放,这个类会自动释放内存:

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
#pragma once
#include <vector>
#include <basetsd.h>
using namespace std;
 
class AllocMemory
{
    vector<char*>p;   
public:
    virtual ~AllocMemory()
    {
        for (int i = 0; i < p.size(); i++)
        {
            if (p[i]==0)
            {
                continue;
            }
            free(p[i]);
            p[i] = 0;
        }
        p.clear();
    }
 
public:
    template<typename T>
    T auto_malloc( ULONG_PTR MAXSIZE)
    {
        T tmp = (T)malloc(MAXSIZE);
        memset((char*)tmp, 0, MAXSIZE);
        p.push_back((char*)tmp);
        return tmp;
    }
};

0x02 流程图

vmp编写流程图

 

程序外观:
clipboard_1_
clipboard_12_

 

实验:对test_vmp函数加壳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __declspec(naked) test_vmp(int a, int b)
{
    __asm {
        mov eax, [esp + 4]
        mov eax, [esp + 4]
        mov eax, [esp + 4]
        mov eax,[esp+4] // [esp+4] == a
        mov ebx,[esp+8] // [esp+8] == b
        xor eax, ebx
        mov g_num,eax
        ret
    }
}
int main()
{
    test_vmp(1,2);
    system("pause");
    return;
}

拖入OD,在0x401010这个位置:
clipboard_23_

 

打开vmp_1.0,开始加壳:
clipboard_44_
clipboard_21_

 

回到项目,点击编译:
clipboard_34_

 

用OD打开,经过加了花指令的从IAT表拷贝过来的API的跳转地址,每次执行时,样式都不一样。
第一次打开:
clipboard_20_

 

用OD第二次打开:
clipboard_4_

 

此外,对加了该指令壳的函数,每次进入该函数后,指令也会变,这些操作都是在外壳中完成的,具体请参考Stub项目。

0x03 指令分析器

  指令分析器的作用:把要保护的指令,翻译为中间表示。我用的是BeaEngine引擎,所以在解析指令的时候,需要遵循BeaEngine反汇编引擎的规则。
指令分析器的主框架如下:

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
//解析要保护的指令,翻译为中间表示
void MiddleRepresent(DISASM disAsm)
{
/*----------------------------------------------------------------------------------*/
/*    1、是否有操作3                                   */
/*----------------------------------------------------------------------------------*/
    if (NO_ARGUMENT != disAsm.Argument3.ArgType)
    {
        switch (disAsm.Argument3.ArgType & 0xF0000000)
        {
        case REGISTER_TYPE: //寄存器
            break;
        case MEMORY_TYPE: //内存
            break;
        case CONSTANT_TYPE://常数
            break;
        default:
            break;
        }
    }
 
 /*----------------------------------------------------------------------------------*/
/*    2、是否有操作2                                   */
/*----------------------------------------------------------------------------------*/
    if (NO_ARGUMENT != disAsm.Argument2.ArgType)
    {
        switch (disAsm.Argument2.ArgType & 0xF0000000)
        {
        case REGISTER_TYPE: //寄存器
            break;
        case MEMORY_TYPE: //内存
            break;
        case CONSTANT_TYPE://常数
            break;
        default:
            break;
        }
    }
 
 /*----------------------------------------------------------------------------------*/
/*    3、是否有操作1                                   */
/*----------------------------------------------------------------------------------*/
    if (NO_ARGUMENT != disAsm.Argument1.ArgType)
    {
        switch (disAsm.Argument1.ArgType & 0xF0000000)
        {
        case REGISTER_TYPE: //寄存器
            break;
        case MEMORY_TYPE: //内存
            break;
        case CONSTANT_TYPE://常数
            break;
        default:
            break;
        }
    }
 
 /*----------------------------------------------------------------------------------*/
/*    4、处理普通handler                                   */
/*----------------------------------------------------------------------------------*/
 
//省略...
 
/*----------------------------------------------------------------------------------*/
/*    5、判断是否有辅助handler                                   */
/*----------------------------------------------------------------------------------*/
    if (
        0x10000000 != disAsm.Argument1.ArgType ||
        0x10000000 != disAsm.Argument2.ArgType ||
        0x10000000 != disAsm.Argument3.ArgType
        )
    {
            if (NO_ARGUMENT != disAsm.Argument1.ArgType)
            {
            switch (disAsm.Argument1.ArgType & 0xF0000000)
            {
            case REGISTER_TYPE: //寄存器
                break;
            case MEMORY_TYPE: //内存
                break;
            case CONSTANT_TYPE://常数
                break;
            default:
                break;
            }
        }
     }
}

上面这个解析器,对一条指令是从右往左解析的,比如这条指令:mov eax,eax
翻译为中间表示就是:

1
2
3
4
vPushReg  VR_ecx  //操作2
vPushReg  VR_eax //操作1
vMOV            //普通handler
vPopReg   VR_eax //辅助handler

handler操作和数据是分别保存的,仍然以上面那条指令为例:

1
2
3
4
5
6
7
8
9
10
11
vPushReg  VR_ecx  //操作2
vPushReg  VR_eax //操作1
vMOV            //普通handler
vPopReg   VR_eax //辅助handler
 
把VR_ecx、VR_eax、VR_eax分离出来保存在一个数据表的结构体中。
翻译就可以这样表示了:
vPushReg
vPushReg
vMOV          
vPopReg

内存操作处理起来比较麻烦,至少对我来说是如此,MemoryMiddle()函数用来专门处理内存操作。
例如这条指令 mov dword ptr[eax+ecx*4+0x401000],eax,可以译成如下的中间表示:

1
2
3
4
5
6
7
8
9
vPushReg   //eax
vPushImm4  //4
vPushReg4  //ecx
vMUL_MEM  //*
vPushReg4 //eax
vAdd4    //+
vPushImm4 //0x401000
vAdd     //+
vWriteMemDs4

此外,局部变量的操作,比如这条指令:mov dword ptr[ebp-0x8],eax,仍然可以用MemoryMiddle函数来翻译:

1
2
3
4
vPushImm4  //0xFFFFFFF8
vPushReg4 //ebp
vAdd4
8会被BeaEngine引擎解析为0xFFFFFFF8,ebp-0x80xFFFFFFF8+ebp是等价的

下面举个完整的例子:

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
void _declspec(naked) _stdcall  code_vm_test(int x)
{
    //MessageBoxA(NULL, 0, 0, 0);
    _asm {
        sub esp,0x150
        push eax
        push ecx
        push edx
        lea ecx, code_vm_test
        add ecx,10h      
        push ecx
        pop dword ptr[g_num + 4]
        jmp L14
        sub esp,0x150
        L14:     
        mov ecx,1       
        xor eax,eax
        mov ah,10h
        mov bl,30h
        L13:
        add ecx,1
        add ah,bl
        cmp ecx,0x10
        jle L13
        //je L11
        add eax,0x432
       mov ebx,4
       mov ecx,1
       mov byte ptr[g_num + ebx + ecx * 4],ah
        //mov word ptr[g_num + ebx + ecx * 4],ax       
        //mov dword ptr[g_num+ebx+ecx*4],eax
        jmp L12
       //L11:
        mov g_num,eax
 
        call test2
       L12:
        mov eax, 01h   //eax=1:取CPU序列号
        xor edx, edx
        cpuid
        mov acpuid, eax
        mov dl,byte ptr[acpuid]
        mov lcpuid, edx
 
        pop edx
        pop ecx
        pop eax
        add esp,0x150
        retn 4
    }
}

上面这个函数,翻译为中间表示如下:

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
VMStartVM_2
vPushImm4
vPushReg4
vSUB4
vPopReg4
VCheckESP
vPushReg4
vPUSH
vPushReg4
vPUSH
vPushReg4
vPUSH
vPushImm4
vReadMemDs4
vPushReg4
vPopReg4
vPushImm4
vPushReg4
vAdd4
vPopReg4
vPushReg4
vPUSH
vRetnNOT_
vNotSimulate
vResumeStart_
vPushImm4
vJMP
vPushImm4
vPushImm4
vPushReg4
vSUB4
vPopReg4
VCheckESP
vPushImm4
vPushReg4
vMOV4
vPopReg4
vPushReg4
vPushReg4
vXOR4
vPopReg4
vPushImm4
vPushReg1_above
vMOV4
vPopReg1_above
vPushImm4
vPushReg1_low
vMOV4
vPopReg1_low
vPushImm4
vPushReg4
vAdd4
vPopReg4
vPushReg1_low
vPushReg1_above
vAdd4
vPopReg1_above
vPushImm4
vPushReg4
vCMP
vPushImm4
vJLE
vPushImm4
vPushImm4
vPushReg4
vAdd4
vPopReg4
vPushImm4
vPushReg4
vMOV4
vPopReg4
vPushImm4
vPushReg4
vMOV4
vPopReg4
vPushReg1_above
vPushImm4
vPushReg4
vMUL_MEM
vPushReg4
vAdd4
vPushImm4
vAdd4
vWriteMemDs1
vPushImm4
vJMP
vPushImm4
vPushReg4
vPushImm4
vWriteMemDs4
vPushImm4
vRetnNOT_
vCALL
vResumeStart_
vPushImm4
vPushReg4
vMOV4
vPopReg4
vPushReg4
vPushReg4
vXOR4
vPopReg4
vRetnNOT_
vNotSimulate
vResumeStart_
vPushReg4
vPushImm4
vWriteMemDs4
vPushImm4
vReadMemDs1
vPushReg1_low
vPopReg1_low
vPushReg4
vPushImm4
vWriteMemDs4
vPushReg4
vPopReg4
vPOP4
vPushReg4
vPopReg4
vPOP4
vPushReg4
vPopReg4
vPOP4
vPushImm4
vPushReg4
vAdd4
vPopReg4
VCheckESP
vPushImm4
vRETN

0x04 垃圾指令构造器

垃圾指令构造器的设计很简单,对我来说,难点在于垃圾指令的选择,有些指令是不能作为垃圾指令,改变普通寄存器的指令我没有用,比如AAA指令,会改变eax寄存器的值。
下面是垃圾指令的构造器核心函数:

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
//生成垃圾指令
CString VMLoader2::ProduceRubbishOpecode(char* reg04, char* reg05)
{
    VMTable vmtbl = vmtable32[SrandNum(0, m_vmlength)];
    CString str = vmtbl.strInstruction;
    //1、目的操作
    switch (vmtbl.optype[0])
    {
    case NONETYPE://没有操作数
        break;
    case IMMTYPE://立即数
    {
        if (8 == vmtbl.bitnum[0])
        {
            str = str + " " + 4;
        }
        else if (16 == vmtbl.bitnum[0])
        {
            str = str + " " + 4;
        }
        else
        {
            str = str + " " + 8;
        }
 
    }
        break;
    case REGTYPE://寄存器
    {
 
        if (8 == vmtbl.bitnum[0])
        {
            for (int i = 0; i < 14; i++)
            {
                if (stricmp(reg04, regname_[2][i]) == 0)
                {
                    str = str + " " + regname_[0][i];
                    break;
                }
            }           
        }
        else if (16 == vmtbl.bitnum[0])
        {
            for (int i = 0; i < 14; i++)
            {
                if (stricmp(reg05, regname_[2][i]) == 0)
                {
                    str = str + " " + regname_[1][i];
                    break;
                }
            }
        }
        else
        {
            str = str + " " + reg05;
        }
    }
        break;
    case MEMTYPE://内存
    {//随机选择vmp1节中没有用到的内存
        DWORD dnum = SrandNum(m_vmps.vmp1_startaddr+0x4000, m_vmps.vmp1_startaddr+0x5000);
        CString memstr = dnum;
        if (8 == vmtbl.bitnum[0])
        {
            str = str + " byte ptr[" + memstr.GetString() + "]";
        }
        else if (16 == vmtbl.bitnum[0])
        {
            str = str + " word ptr[" + memstr.GetString() + "]";
        }
        else
        {
            str = str + " dword ptr[" + memstr.GetString() + "]";
        }
    }
        break;
    default:
        break;
    }
 
    //2、源操作数
    switch (vmtbl.optype[1])
    {
    case NONETYPE://没有操作数
        break;
    case IMMTYPE://立即数
    {
        if (8 == vmtbl.bitnum[1])
        {
            str = str + "," + 4;
        }
        else if (16 == vmtbl.bitnum[1])
        {
            str = str + "," + 8;
        }
        else
        {
            str = str + "," + 4;
        }
    }
        break;
    case REGTYPE://寄存器(操作数2的寄存器可以在8个寄存器中任意选择)
    {
        if (0 == stricmp(vmtbl.strInstruction,"xchg"))
        {//如果是xchg,寄存器则选择reg04,或者reg05
            if (8 == vmtbl.bitnum[1])
            {
                for (int i = 0; i < 14; i++)
                {
                    if (stricmp(reg05, regname_[2][i]) == 0)
                    {
                        str = str + "," + regname_[0][i];
                        break;
                    }
                }
 
            }
            else if (16 == vmtbl.bitnum[1])
            {
                for (int i = 0; i < 14; i++)
                {
                    if (stricmp(reg04, regname_[2][i]) == 0)
                    {
                        str = str + "," + regname_[1][i];
                        break;
                    }
                }
            }
            else
            {
                str = str + "," + reg04;
            }
            break;
        }
        if (8 == vmtbl.bitnum[1])
        {
            str = str + "," + regname_[0][SrandNum(0, 8)];
        }
        else if (16 == vmtbl.bitnum[1])
        {
            str = str + "," + regname_[1][SrandNum(0, 8)];
        }
        else
        {
            str = str + "," + regname_[2][SrandNum(0, 8)];
        }
    }
        break;
    case MEMTYPE://内存
    {//随机选择vmp1节内的地址,或者选esp寄存器
        DWORD dnum = SrandNum(m_vmps.vmp1_startaddr, m_vmps.vmstartaddr);
        CString memstr = dnum;
        const char* memchr[5] = { memstr.GetString(),"esp+20","esp+28","esp+0x30","esp+0x14" };
        const char* srandstr = memchr[SrandNum(0, 5)];
 
        if (8 == vmtbl.bitnum[1])
        {
            str = str + ",byte ptr[" + srandstr + "]";
        }
        else if (16 == vmtbl.bitnum[1])
        {
            str = str + ",word ptr[" + srandstr + "]";
        }
        else
        {
            str = str + ",dword ptr[" + srandstr + "]";
        }
    }
        break;
    default:
        break;
    }
 
    return str;
}

ProduceRubbishOpecode函数,每被调用一次就可以构造一条垃圾指令。

0x05 handler的设计

把要用到的handler全部放到一个表格中归类整理,如下图:
191710p9gyvz88fxe8i1qd
先来举一个例子,比如指令:xor eax,eax

1
2
3
4
5
翻译为中间表示:
vPushReg4
vPushReg4
vXOR4
vPopReg4

上面每一个中间表示的handler都有对应一个函数:

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
CString vPushReg4(char* VR0, char* VR1)
{
    CString str = "mov ";
    str = str + VR0 +",dword ptr[ebp]\n" ;
    str = str + "add ebp,4\n";
    str = str + "xor " + VR0 + "," + dataencrypt + "\n";
    str = str + "mov "+ VR0 +",dword ptr [edi+"+ VR0 +"*4]\n";
    str = str + "push "+ VR0 +"\n";
    return str;
}
 
CString vXOR4(char* VR0, char* VR1)
{
    CString str = "mov ";
    str = str + VR0 + ",dword ptr[esp]\n";
    str = str + "mov " + VR1 + ",dword ptr[esp+4]\n";
    str = str + "xor " + VR0 + "," + VR1 + "\n";
    str = str + "add esp,8\n";
    str = str + "push " + VR0 + "\n";
    return str;
}
 
CString vPopReg4(char* VR0, char* VR1)
{
    CString str = "mov ";
    str = str + VR0 + ",dword ptr[ebp]\n";
    str = str + "xor " + VR0 + "," + dataencrypt + "\n";
    str = str + "add ebp,4\n";
    str = str + "pop dword ptr[edi+" + VR0 + "*4]\n";
    return str;
}

vmtest.h和vmtest.cpp分别存放了所有handler块的声明和具体实现。请参考VMProtect项目。

0x06 IAT加密

IAT解密模块、反调试模块以及花指令构造器,都在Stub项目中,Stub.dll动态库是整个程序的外壳部分。
IAT加密过程:
第一步把IAT表转存到一个临时数据结构中,然后清除IAT和INT表,最后把临时数据结构中的函数名称加密。这步是在VMProtect项目中完成的。
第二步在Stub中解密这个临时数据结构,解密之后,再加密,并且加上花指令。
花指令构造器具体实现在JunkCode.cpp文件中。以下列出花指令构造器的核心函数:

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
//这是一个多跳、往回跳的花指令构造器,之后跳到真实指令。
void JunkCode_::SrandJunkCode()
{
    BUFFERSTRUCT_ buffer;
    buffer.value = jncode_one;
    buffer.match = 1;
    g_buffer.push_back(buffer);
 
    buffer.value = buffer.match = 0;
    g_buffer.push_back(buffer);
 
    char x = jncode[rand_v() % 4];
    buffer.value = x;
    g_buffer.push_back(buffer);
 
 
    if (x == 0xFF)
    {
        buffer.value = second[rand_v() % 2];
        g_buffer.push_back(buffer);
    }
 
    int y = rand_v() % 3;
 
    for (int i = 0; i < y; i++)
    {
        buffer.value = randsss[rand_v() % RANDSSS];
        g_buffer.push_back(buffer);
    }
 
 
    buffer.value = jncode_one;
    buffer.match = 0x3;
    buffer.jmpmatch = 0x2;
    g_buffer.push_back(buffer);
 
    buffer.value = buffer.match = buffer.jmpmatch = 0;
    g_buffer.push_back(buffer);
 
    x = jncode[rand_v() % 4];
    buffer.value = x;
    g_buffer.push_back(buffer);
 
 
    if (x == 0xFF)
    {
        buffer.value = second[rand_v() % 2];
        g_buffer.push_back(buffer);
    }
 
    y = rand_v() % 3;
 
    for (int i = 0; i < y; i++)
    {
        buffer.value = randsss[rand_v() % RANDSSS];
        g_buffer.push_back(buffer);
    }
 
    for (int i = 0; i < 5; i++)
    {
        if (i == 0)
        {
            buffer.jmpmatch = 1;
            buffer.recodemodify = 1;
            buffer.value = moveax[i];
            g_buffer.push_back(buffer);
            buffer.jmpmatch = buffer.recodemodify = 0;
            continue;
        }
        buffer.value = moveax[i];
        g_buffer.push_back(buffer);
    }
 
 
    buffer.value = jncode_one;
    buffer.match = 0x2;
    g_buffer.push_back(buffer);
 
    buffer.value = buffer.match = buffer.jmpmatch = 0;
    g_buffer.push_back(buffer);
 
 
    x = jncode[rand_v() % 4];
    buffer.value = x;
    g_buffer.push_back(buffer);
 
 
    if (x == 0xFF)
    {
        buffer.value = second[rand_v() % 2];
        g_buffer.push_back(buffer);
    }
 
    y = rand_v() % 2;
 
    for (int i = 0; i < y; i++)
    {
        buffer.value = randsss[rand_v() % RANDSSS];
        g_buffer.push_back(buffer);
    }
 
    for (int i = 0; i < 7; i++)
    {
        if (i == 0)
        {
            buffer.jmpmatch = 3;    //3
            buffer.value = jmpoep[i];
            g_buffer.push_back(buffer);
            buffer.match = buffer.jmpmatch = 0;
            continue;
        }
 
        buffer.value = jmpoep[i];
        g_buffer.push_back(buffer);
    }
 
    //修复数据
    vector_< BUFFERSTRUCT_>::iterator iter_buff = g_buffer.begin();
    vector_< BUFFERSTRUCT_>::iterator iter_buff_1 = g_buffer.begin();
    for (int i = 0; i < g_buffer.size(); i++)
    {
 
        if ((*iter_buff).match != 0)
        {
            int temp = (*iter_buff).match;
            for (int j = 0; j < g_buffer.size(); j++)
            {
                if (temp == (*iter_buff_1).jmpmatch)
                {
                    (*(iter_buff + 1)).value= j - i - 2;
                    iter_buff_1 = g_buffer.begin();
                    break;
 
                }
                ++iter_buff_1;
            }
        }
        ++iter_buff;
    }
}

0x07 补充

1)怎么添加handler块?

测试的时候,我只是对常用的指令添加了handler块,还有很多指令是没有处理的,那么,程序在加壳过程中,如果有jmp或者jxx跳转到未知指令(未知指令是指没有添加handler的指令,找不到匹配),就会出错,此时,则应该先检查是否有未知指令,并添加相应的handler块。
添加方式:以inc指令为例子
第一步:
在vmtest.h中添加声明CString vINC(char VR0, char VR1);
clipboard_15_

 

第二步:在vmtest.cpp中实现其函数功能。
clipboard_6_

 

第三步:
在VMLoader2.cpp,把55改成56,在g_FunName数组里添加{vINC,"inc ","vINC "},注意"inc "和"vINC ",后面有一个空格,
不然程序在匹配inc指令的时候匹配不上,就会把inc当成不可模拟指令来处理。
clipboard_13_

2)写在末尾

  这个加壳程序,设计上有先天缺陷,这可以归咎于我正向开发的基础不扎实,还有就是只掌握了编译原理的一些皮毛知识,好些地方都有点乱,像是硬怼的,很多地方现在还能看到打斗的痕迹。整个程序,由于在设计上的缺陷,使得虚拟机不能对寄存器进行轮转操作。另外,汇编指令是直接换成handler块的,中间没有先对汇编指令进行任何变形。所以,这只是一个模拟vmprotect的最最简单的指令壳子。

3)编译问题

编译时,请采用Debug和x86模式:
clipboard_9_

 

编译时,可能会遇到的编码错误:
clipboard_37_

 

Stub项目运行库选择多线程(/MT)
clipboard88


[培训] 优秀毕业生寄语:恭喜id咸鱼炒白菜拿到远超3W月薪的offer,《安卓高级研修班》火热招生!!!

最后于 2021-5-7 18:28 被舒默哦编辑 ,原因: 更正错误
上传的附件:
收藏
点赞33
打赏
分享
打赏 + 1.00
打赏次数 1 金额 + 1.00
 
赞赏  demoscene   +1.00 2021/05/24 精品文章~
最新回复 (43)
雪    币: 9183
活跃值: 活跃值 (8383)
能力值: ( LV12,RANK:310 )
在线值:
发帖
回帖
粉丝
wmsuper 活跃值 5 2021-5-6 23:20
2
1

雪    币: 625
活跃值: 活跃值 (1091)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
库尔 活跃值 2021-5-7 01:45
3
1
现在逆向VMP全都研究编译器了...
雪    币: 1581
活跃值: 活跃值 (2234)
能力值: ( LV9,RANK:176 )
在线值:
发帖
回帖
粉丝
nevinhappy 活跃值 2 2021-5-7 08:43
4
1
感谢分享!!!
雪    币: 656
活跃值: 活跃值 (289)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
gxkyrftx 活跃值 2021-5-7 09:18
5
1
牛啊
雪    币: 14305
活跃值: 活跃值 (1438)
能力值: (RANK:648 )
在线值:
发帖
回帖
粉丝
KevinsBobo 活跃值 8 2021-5-7 10:22
6
1
感谢分享!致敬!
雪    币: 5310
活跃值: 活跃值 (2429)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
jmpcall 活跃值 2 2021-5-7 11:38
7
1
真大神!
雪    币: 1268
活跃值: 活跃值 (2421)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
风中小筑V 活跃值 2021-5-7 12:06
8
1
感谢分享!
雪    币: 62
活跃值: 活跃值 (256)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
值得怀疑 活跃值 2021-5-7 12:12
9
1
厉害啊·!~
雪    币: 330
活跃值: 活跃值 (251)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
Loopher 活跃值 2021-5-7 12:41
10
1
牛贝塔
雪    币: 7254
活跃值: 活跃值 (8258)
能力值: ( LV12,RANK:340 )
在线值:
发帖
回帖
粉丝
SSH山水画 活跃值 4 2021-5-7 13:12
11
1

雪    币: 1937
活跃值: 活跃值 (2691)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
舒默哦 活跃值 1 2021-5-7 13:21
12
1
统一回复,谢谢老萌萌们捧场。以后有问题,还得向你们请教。
雪    币: 1040
活跃值: 活跃值 (289)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Rookietp 活跃值 2021-5-7 14:21
13
1

以后还更新吗,建议放gitee github

最后于 2021-5-7 14:23 被Rookietp编辑 ,原因:
雪    币: 651
活跃值: 活跃值 (660)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
LexSafe 活跃值 2021-5-7 14:28
14
1
好家伙
雪    币: 1331
活跃值: 活跃值 (3455)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
supperlitt 活跃值 2021-5-7 15:08
15
1
666666666666
雪    币: 7252
活跃值: 活跃值 (492)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
sky东 活跃值 2021-5-7 15:15
16
1
感谢分享!致敬!
雪    币: 1042
活跃值: 活跃值 (461)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leyongwei 活跃值 2021-5-7 15:28
17
1
666666送你,别客气哈。。
雪    币: 1367
活跃值: 活跃值 (891)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
欧阳休 活跃值 2021-5-7 16:01
18
1
雪    币: 70
活跃值: 活跃值 (225)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Archar 活跃值 2021-5-7 16:08
19
1
双手大拇指
雪    币: 7831
活跃值: 活跃值 (6818)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2021-5-7 17:11
20
1
感谢分享
雪    币: 2517
活跃值: 活跃值 (1103)
能力值: ( LV8,RANK:124 )
在线值:
发帖
回帖
粉丝
KuCha128 活跃值 1 2021-5-7 17:25
21
1
感谢分享

"此外,还构建了一个STL表:(但在这儿作用似乎不大)"
这里有个小错误, STL->TLS
雪    币: 1937
活跃值: 活跃值 (2691)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
舒默哦 活跃值 1 2021-5-7 18:29
22
0
KuCha128 感谢分享[em_63] "此外,还构建了一个STL表:(但在这儿作用似乎不大)" 这里有个小错误, STL->TLS
已改
雪    币: 270
活跃值: 活跃值 (838)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 活跃值 1 2021-5-7 19:13
23
1
感谢分享!
雪    币: 657
活跃值: 活跃值 (665)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Oday小斯 活跃值 2021-5-7 23:30
24
1
感谢分享
雪    币: 6436
活跃值: 活跃值 (686)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Ally Switch 活跃值 2021-5-8 07:40
25
1
大佬终于来看雪发文了
游客
登录 | 注册 方可回帖
返回