首页
社区
课程
招聘
[原创]实现多种反调试,SMC,虚拟机保护和一机一码等保护措施的一个程序(期末作业)
2020-2-10 19:48 10837

[原创]实现多种反调试,SMC,虚拟机保护和一机一码等保护措施的一个程序(期末作业)

2020-2-10 19:48
10837

摘要:利用反调试,虚拟机保护,关键函数和字符串隐藏,SMC加密技术,一机一码,花指令等技术实现一个软件保护的demo。

关键字:反调试,SMC,花指令,VMProtect,一机一码。

目录

一.     相关技术介绍和实现原理... 5

1.    什么是软件保护?如何做软件保护... 5

2.    花指令... 6

3.    SMC加密技术... 6

4.    用户名和序列号加密算法设计... 7

5.    隐藏GetProcAddress 7

6.       VMProtectSDK 保护... 8

7.       反调试原理... 8

1)       IsDebuggerPresent. 8

2)       CheckRemoteDebuggerPresent. 9

3)       NtSetInformationThread(). 9

4)       PebIsDebugged(). 9

5)       HeapFlags(). 9

6)       NtQueryInformationProcess(). 10

7)       异常处理... 10

8)       前台窗口... 11

9)       进程快照方式... 11

10)     硬件断点... 11

11)     软件断点... 11

二.程序一实现... 11

1.    MFC实现一个CreakMe. 11

2.    SMC加密技术... 12

3.    SMC外部加密程序.. 14

4.    花指令... 16

1.       花指令1,函数跳转.. 16

2.       花指令2. 18

5.    用户名一机一码实现... 19

6.    序列号加密算法... 20

7.    隐藏GetProcAddress函数.. 21

8.       VMProtectSDK 保护... 23

9.    反调试实现(代码和注释,原理在第一部分).. 25

10.      KeyGen. 31

三.程序二实现... 32

1.    基于花指令或SMC技术,实现程序的静态反汇编逆向分析保护。... 32

2.    基于反动态调试技术,实现程序的反动态调试保护,要求实现多重反动态调试保护技术。... 36

3.       基于序列号保护技术,实现程序的软件版权保护。要求序列号实现和用户名相关并一机一码,不能使用硬编码序列号。... 40

四.功能测试... 43

1.       程序正常执行流程:... 43

1)    第一个程序... 43

2)    第二个程序... 45

2.       简单逆向程序一... 54

五.总结... 57

六.附录... 59

一. 相关技术介绍和实现原理

1.      什么是软件保护?如何做软件保护

随着计算机技术的迅猛发展以及软件技术的快速成长,计算机软件技术不仅对国民经济建 设、信息安全起着重要作用,而且还是提高科技创新能力,增强技术竞争实力的重要组成部 分。由于软件的复制、拷贝是件很容易的事,所以导致非法复制、盗版软件之风的泛滥。

在这种形势下,为了防止软件的非法复制、盗版,保护软件开发商的利益,研制者和销售商对自己的软件进行技术保护和数据加密,以此来保护计算机软件著作权人的权益,鼓励计算机软件的 开发与应用,促进软件产业和国民经济信息化的发展。软件保护技术是软件开发者寻找各种有效方 法和技术来维护软件版权,增加其盗版的难度, 或延长软件破解的时间,尽可能防止软件被非法使用。

软件技术上分为很多不同的分支,主要包括:加密、防篡改、软件水印、软件多样化、反逆向技术、虚拟机、基于网络的保护和基于硬件的保护等。

1.加密:软件的代码以加密形式保护,在执行代码前进行解密操作,是一种应用最广的软件保护技术,对代码进行加密,并在软件运行前解密就是所谓的加壳。

2.加壳技术:源于加密技术但是后来由于其使用的广泛性,逐渐自成一派,而且综合使用了其它各种软件保护技术,可认为是软件保护技术的一种应用。

3.软件防篡改:在软件中加入一些特殊的机制,使得其他人试图修改软件时,软件做出拒绝执行、随机崩溃或者删除自身文件等保护软件的行为。防篡改算法要完成两个基本任务:

第一个是检查程序是否被修改,第二个是在发现代码被修改时执行相应的反制措施。

4.软件水印:在软件中嵌入唯一的标识以证明开发者对软件版权的所有权,从而防止因软件被盗版损害开发者利益。

5.软件多样化:一个软件可以生成不同的副本,让每个副本都各不相同以至于攻击者破解了软件的一个副本,不能用于其它副本,防止利用已知的漏洞进行攻击或者通过注册机进行盗版。

6.代码混淆:主要的目的是保护软件中的一些重要信息不被轻易获得,通过一系列的混淆方法,使非软件开发方通过逆向工程获得软件源代码的难度增大、时间增长,从而达到保护软件结构和数据的目的。代码混淆对逆向工程的抵抗作用明显,作为加密技术的补充和发展,受到越来越多的关注。它的出现使得攻击者难以通过IDA等工具反汇编、反编译逆向分析出程序的源码或中间码,从而获得程序的逻辑和算法。

7.反调试:在软件中加入各种调试器和虚拟机的探测器,一旦发现程序被调试或者在虚拟机中运行离开采取退出或自毁等防护措施,避免程序被分析。

8.反逆向技术:通过各种方式使攻击者无法获取和逆向程序的代码,又可进一步细分为反调试、代码混淆、自修改代码、代码分离等。

9.自修改代码( self-modifying code, SMC ):程序运行期间修改或产生代码的一种机制。自修改保护机制是有效抵御静态逆向分析的代码保护技术之一,广泛应用于软件保护和恶意代码等领域。计算机病毒等恶意代码的作者通常采用该技术动态修改内存中的指令来达到对代码加密或变形的目的,从而躲过杀毒软件的检测与查杀,或者增加恶意代码逆向分析人员对代码进行分析的难度。

10. 虚拟机软件保护:属于自修改代码的一种,近年来逐渐发展为一条独立的软件保护方法分支。虚拟机保护就是将某段程序编译成具有特定意义的一段代码,这段代码不能在目标机器上直接执行,要通过解释器模拟执行。虚拟机代码在可执行文件中只是一块数据,反汇编工具是不能反编译虚拟机代码的,因为虚拟机代码是在运行过程中解释执行的。虚拟机保护方法的局限性在于其设计机制复杂,开发成本较高,而且经过此方法保护的程序容量会大大增加,造成的时间和空间开销都很大。

2.      花指令

在原程序中添加一些汇编指令,添加后不影响原程序的正常功能,但是能使反汇编工具反汇编错误。花指令是一段汇编指令,对于程序而言,花指令是无用的汇编代码,花指令的有无不影响程序的运行。

原理:根据反汇编工具的反汇编算法,构造代码和数据,在指令流中插入很多“数据垃圾”,干扰反汇编软件的判断,从而使得它错误地确定指令的起始位置,杜绝了先把程序代码列出来再慢慢分析的做法。

编写花指令的基本原则:保持堆栈平衡以及花指令不会影响程序的功能。堆栈平衡是指执行花指令前和执行后esp保持不变。

3.      SMC加密技术

(注:原理介绍使用其他博客文章内容,会在最后给出博客地址)

所谓SMC(Self Modifying Code)技术,就是一种将可执行文件中的代码或数据进行加密,防止别人使用逆向工程工具(比如一些常见的反汇编工具)对程序进行静态分析的方法,只有程序运行时才对代码和数据进行解密,从而正常运行程序和访问数据。计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。现在,很多加密软件(或者称为“壳”程序)为了防止Cracker(破解者)跟踪自己的代码,也采用了动态代码修改技术对自身代码进行保护。以下的伪代码演示了一种SMC技术的典型应用:

proc main:

............

IF .运行条件满足

  CALL DecryptProc (Address of MyProc);对某个函数代码解密

  ........

  CALL MyProc                           ;调用这个函数

  ........

  CALL EncryptProc (Address of MyProc);再对代码进行加密,防止程序被Dump

......

end main

 然后写外部函数对区段进行解密操作。

4.      用户名和序列号加密算法设计

对用户名的唯一性获取,先获得CPU的制造商,这里需要借助cpuid的汇编指令。CPUID操作码是一个面向x86架构的处理器补充指令,它的名称派生自CPU识别,作用是允许软件发现处理器的详细信息。Win32k 平台上,获取CPUID的办法主要有两种,一种是利用 WMI 另一种是利用 x86 汇编的 cpuid 指令,而最快的办法就是通过汇编了,而且 WMI 与汇编之间效率上的差距的确有点让人难以忍受,WMI 获取 CPUID 的效率几乎接近了一秒钟,而利用 cpuid 指令的办法,大概是几个 us 时间的问题,调用代码在第二部分实现。

然后根据得到的字符串和进程PID进行异或,这里这个异或虽然在输入的时候会麻烦一点,但是在调试的时候利用GetCurrentID()函数得到的进程id是调试器的,会对反调试有作用。

序列号的处理是利用得到的用户名和CPU制造商,然后先解密字符串“20171120051”,再根据这个字符串的数值取出CPU制造商的字符串对应位置的字符。再把得到的字符根据学号数值插入到用户名对应的位置形成序列号。实现代码见第二部分。

5.      隐藏GetProcAddress

隐藏方式就是根据它在kernel32里面的作用,自己写一个函数去实现它。函数获得DLL句柄(也就是DLL的内存映射基地址),和需要找的函数字符串。根据导出表去寻找这个函数的函数地址,导出表结构如下:

typedef struct _IMAGE_EXPORT_DIRECTORY { 

    DWORD   Characteristics; 

    DWORD   TimeDateStamp; 

    WORD    MajorVersion; 

    WORD    MinorVersion; 

    DWORD   Name; 

    DWORD   Base; 

    DWORD   NumberOfFunctions; 

    DWORD   NumberOfNames; 

    DWORD   AddressOfFunctions;     // RVA from base of image 

    DWORD   AddressOfNames;         // RVA from base of image 

    DWORD   AddressOfNameOrdinals;  // RVA from base of image 

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; 

Base  函数以序号导出的时候的序号基数,从这个数开始递增 

NumberOfFunctions 本dll一共有多少个导出函数,不管是以序号还是以函数名导出 

NumberOfFunctions 本dll中以能够以函数名称导出的函数个数(注意,说一下,其实函数里的  每一个函数都能通过序号导出,但是为了兼容性等等,也给一些函数提供用函数名称来导出)  AddressOfFunctions  指向一个DWORD数组首地址,共有NumberOfFunctions 个元素,每一个元素都是一个函数地址  AddressOfNames 指向一个DWORD数组首地址,共有NumberOfNames个元素,每一个元素都是一个字符串(函数名字符串)首地址  AddressOfNameOrdinals指向一个WORD数组首地址,共有NumberOfNames个元素,每一个元素都是一个函数序号  我们说的最后俩数组,其实是一种一一对应的关系,假如分别叫 dwNames[] 和 dwNamesOrdinals[],  假如dwNames[5]的值(这个指是一个地址,前面都说了)指向的字符串等于“GetValue”,那么dwNamesOrdinals[5]的值  (这个指是一个序号,前面都说了),就是GetValue导出函数的序号啦,这时候就需要用到第一个数组了,假如名字叫dwFuncAddress[], GetValue的导出地址就是  dwFuncAddress[dwNamesOrdinals[5]] + 模块基址 。

   代码实现部分在第二部分。借助了Stack Overflow的部分,后面会给出我参考的博客地址。

6.   VMProtectSDK 保护

    虚拟机保护技术,是指将代码翻译为机器和人都无法识别的一串伪代码字节流;在具体执行时再对这些伪代码进行一一翻译解释,逐步还原为原始代码并执行。

这段用于翻译伪代码并负责具体执行的子程序就叫作虚拟机VM(好似一个抽象的CPU)。它以一个函数的形式存在,函数的参数就是字节码的内存地址。大作业当中调用了最新的VMProtect3.3版本的SDK进行代码虚拟或者变异。实现过程和使用方法见第二部分实现部分。在被加密之后的函数位置不是原来的指令了,而是一些垃圾跳转和指令,逆向的时候需要寻找bytecode解密出跳转表来找正常的代码逻辑。

7.   反调试原理

(注:这里原理介绍借用了看雪帖子的文章)

1)   IsDebuggerPresent

IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。

2)   CheckRemoteDebuggerPresent

CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。

3)   NtSetInformationThread()

这个也是NtDLL里面的结构体,你可以在当前线程里调用NtSetInformationThread,调用这个函数时,如果在第二个参数里指定0x11这个值(意思是ThreadHideFromDebugger)

4)   PebIsDebugged()

Windows操作系统维护着每个正在运行的进程的PEB结构,它包含与这个进程相关的所有用户态参数。这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态。进程运行时,位置fs:[30h]指向PEB的基地址。为了实现反调试技术,恶意代码通过这个位置检查BeingDebugged标志,这个标志标识进程是否正在被调试。

5)   HeapFlags()

Reserved数组中一个未公开的位置叫作ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置。ProcessHeap位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。这些属性叫作ForceFlags和Flags。Win10偏移地址是0x44。下面是一个win10堆栈结构表:


6)   NtQueryInformationProcess()

这个函数是Ntdll.dll中一个API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。大作业中使用的是debugPort。

7)   异常处理

    进程中发生异常时若SEH未处理或注册的SEH不存在,会调用UnhandledExceptionFilter,它会运行系统最后的异常处理器。UnhandledExceptionFilter内部调用了前面提到过的NtQueryInformationProcess以判断是否正在调试进程。若进程正常运行,则运行最后的异常处理器;若进程处于调试,则将异常派送给调试器。SetUnhandledExceptionFilter函数可以修改系统最后的异常处理器。下面的代码先触发异常,然后在新注册的最后的异常处理器内部判断进程正常运行还是调试运行。进程正常运行时pExcept->ContextRecord->Eip+=4;将发生异常的代码地址加4使得其能够继续运行;进程调试运行时产生无效的内存访问异常,从而无法继续调试。这里是设置的异常时除数是0。但是需要在OllyDbg中,选择Options->Debugging Options->Exceptions来设置把异常传递给应用程序。不然没效果。

8)   前台窗口

  可以使用FindWindowA或者GetWindowTextA获得窗口句柄来比较窗口名字,可以自己设置一个黑名单进行比较。通配符暂时实现不了。

9)   进程快照方式

通过CreateToolhelp32Snapshot()函数获得运行程序的京城快照,然后定义一个PROCESSENTRY32结构体,根据句柄返回的文件名进行循环比较黑名单的文件名。

10)  硬件断点

在OllyDbg的寄存器窗口按下右键,点击View debug registers可以看到DR0、DR1、DR2、DR3、DR6和DR7这几个寄存器。DR0、Dr1、Dr2、Dr3用于设置硬件断点,由于只有4个硬件断点寄存器,所以同时最多只能设置4个硬件断点。DR4、DR5由系统保留。  DR6、DR7用于记录Dr0-Dr3中断点的相关属性。如果没有硬件断点,那么DR0、DR1、DR2、DR3这4个寄存器的值都为0。

11)  软件断点

软件断点是通过修改目标地址代码为0xCC(INT3/BreakpointInterrupt)来设置的断点。通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。这里以普通断点和函数断点分别举例。

二.程序一实现

1.      MFC实现一个CreakMe

这部分是参考C++中文网的demo和它的消息机制,具体代码见后面附件或者打包文件,不做过多阐述。

2.      SMC加密技术

(注:这部分只展示实现的情况和相关代码,测试阶段会比较调试的时候区别)

 首先把需要保护的加密算法保护起来:

#pragma code_seg(".JIANG")

void fun()

{

       MessageBox(NULL, TEXT("wrong"), TEXT("错误"), MB_ICONINFORMATION);

}

string Fun1(string userkey, string cpu)  //处理之后的用户名,和CPU型号  处理序列号函数

{

       string name = "kihnhhkiilh";  //20171120051

       char a[50];

       char CPU[50];

       strcpy_s(CPU, cpu.c_str());

       for (int i = 0; i < name.length(); i++)

       {

              char k = name[i] ^ 'Y';

              a[i] = CPU[hex2int(k)];

       }

       for (int i = 0; i < name.length(); i++)

       {

              char k = name[i] ^ 'Y';

              string s(1, a[i]);

              userkey.insert(hex2int(k), s);

       }

       return userkey;

}

#pragma code_seg()

#pragma comment(linker, "/SECTION:.JIANG,ERW")

     面对之前的问题,就是try_except里面不能放置类,所以这里在后面解密阶段直接进行解密,不进行try结构来检查函数是否解密完成。解密(加密)操作就是遍历找到区块,然后进行先解密一个字符串,再和这个字符串进行循环异或,再进行移位运算。

遍历找区段名字:

void SMC_De(char* pBuf, char* key)     //SMC解密函数

{

       const char* szSecName = ".JIANG";

       short nSec;

       PIMAGE_DOS_HEADER pDosHeader;

       PIMAGE_NT_HEADERS pNtHeader;

       PIMAGE_SECTION_HEADER pSec;

       pDosHeader = (PIMAGE_DOS_HEADER)pBuf;

       pNtHeader = (PIMAGE_NT_HEADERS)& pBuf[pDosHeader->e_lfanew];

       nSec = pNtHeader->FileHeader.NumberOfSections;

       pSec = (PIMAGE_SECTION_HEADER)& pBuf[sizeof(IMAGE_NT_HEADERS) + pDosHeader->e_lfanew];

       for (int i = 0; i < nSec; i++)

       {

              if (strcmp((char*)& pSec->Name, szSecName) == 0)

              {

                     int pack_size;

                     char* packStart;

                     pack_size = pSec->SizeOfRawData;

                     packStart = &pBuf[pSec->VirtualAddress];

                     xorPlus(packStart, pack_size, key, strlen(key));

                     return;

              }

              pSec++;

       }

}

然后调用异或和移位加密算法:

void xorPlus(char* soure, int dLen, char* Key, int Klen)   //异或之后再移位

{

       for (int i = 0; i < dLen;)

       {

              for (int j = 0; (j < Klen) && (i < dLen); j++, i++)

              {

                     soure[i] = soure[i] ^ Key[j];

                     soure[i] = ~soure[i];

              }

       }

       for (int i = 0; i < dLen; i++)

       {

              char m = soure[i];

              soure[i] = soure[dLen - i - 1];

              soure[dLen - i - 1] = m;

       }

}

加密的字符串要先进行解密操纵再待到后面运算:

string KeyBuffer = "86y­6+4y ,787y
70/<+*0- y,4kihnhhkiilh";

       for (int i = 0; i <KeyBuffer.length(); i++)

       {

              KeyBuffer[i] = KeyBuffer[i] ^ 'Y';

       }

3.      SMC外部加密程序

首先选择需要进行加密的文件,这部分代码和之前实验的一样:

//取得文件路径部分

       TCHAR szFilePath[MAX_PATH];

       OPENFILENAME ofn = { 0 };

       memset(szFilePath, 0, MAX_PATH);

       memset(&ofn, 0, sizeof(ofn));

       ofn.lStructSize = sizeof(ofn);

       ofn.hwndOwner = NULL;

       ofn.hInstance = GetModuleHandle(NULL);

       ofn.nMaxFile = MAX_PATH;

       ofn.lpstrInitialDir = ".";

       ofn.lpstrFile = szFilePath;

       ofn.lpstrTitle = "选择PE文件";

       ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;

       ofn.lpstrFilter = "(*.*)\0*.exe;*.dll\0";

       GetOpenFileName(&ofn);

       if (szFilePath == NULL)

       {

              MessageBox(NULL, "打开文件错误", NULL, NULL);

              //return 0;

       }

       //创建文件句柄

       HANDLE hFile;

       char KeyBuffer[MAX_PATH] = "ao Form yunNan University Num:20171120051";

       hFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

       if (hFile == INVALID_HANDLE_VALUE)

       {

              MessageBox(NULL, TEXT("打开文件失败"), NULL, NULL);

              return;

       }

打开程序之后就跟解密的思路是一样的了,先找到需要进行加密的区段,然后对区段进行异或和移位加密:

void xorPlus(char* soure, int dLen, char* Key, int Klen)

{

       for (int i = 0; i < dLen;)

       {

              for (int j = 0; (j < Klen) && (i < dLen); j++, i++)

              {

                     soure[i] = soure[i] ^ Key[j];

                     soure[i] = ~soure[i];

              }

       }

       for (int i = 0; i < dLen; i++)

       {

              char m = soure[i];

              soure[i] = soure[dLen - i - 1];

              soure[dLen - i - 1] = m;

       }

}

void SMC(HANDLE hFile, char* key)

{

       // SMC 加密XX区段

       HANDLE hMap;

       const char* szSecName = ".JIANG";

       char* pBuf;

       int size;

       short nSec;

       PIMAGE_DOS_HEADER pDosHeader;

       PIMAGE_NT_HEADERS pNtHeader;

       PIMAGE_SECTION_HEADER pSec;

       size = GetFileSize(hFile, 0);

       hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, size, NULL);

       if (hMap == INVALID_HANDLE_VALUE)

       {

       _viewf:

              MessageBox(NULL, TEXT("映射失败"), NULL, NULL);

              return;

       }

       pBuf = (char*)MapViewOfFile(hMap, FILE_MAP_WRITE | FILE_MAP_READ, 0, 0, size);

       if (!pBuf) goto _viewf;

       pDosHeader = (PIMAGE_DOS_HEADER)pBuf;

       pNtHeader = (PIMAGE_NT_HEADERS)& pBuf[pDosHeader->e_lfanew];

       if (pNtHeader->Signature != IMAGE_NT_SIGNATURE)

       {

              MessageBox(NULL, TEXT("不是有效的win32 可执行文件"), NULL, NULL);

              goto _clean;

       }

       nSec = pNtHeader->FileHeader.NumberOfSections;

       pSec = (PIMAGE_SECTION_HEADER)& pBuf[sizeof(IMAGE_NT_HEADERS) + pDosHeader->e_lfanew];

       for (int i = 0; i < nSec; i++)

       {

              if (strcmp((char*)& pSec->Name, szSecName) == 0)

              {

                     int pack_size;

                     char* packStart;

                     pack_size = pSec->SizeOfRawData;

                     packStart = &pBuf[pSec->PointerToRawData];

                     xorPlus(packStart, pack_size, key, strlen(key));

                     MessageBox(NULL, TEXT("加密成功"), NULL, NULL);

                     goto _clean;

              }

              pSec++;

       }

       MessageBox(NULL, TEXT("未找到JIANG区段,加密失败"), NULL, NULL);

_clean:

       UnmapViewOfFile(pBuf);

       CloseHandle(hMap);

       return;

}

   这个只能加密自己需要保护的程序,保护不了加密不了其他程序。

4.      花指令

1.   花指令1,函数跳转

有很多花指令的设计思路是用垃圾参数和字节来填充空间,然后利用反汇编工具的工作模式不能正确解读造成干扰。我设计的思路是去干扰调试者,能让反汇编工具正常工作而且函数是正常,但是函数没有实际意义,而是一堆循环。

首先是定义三个基本函数,这三个基本函数的出口都是一个具体的数值,但是出口数据可以影响另一个函数的运行,不管怎么相互调用,最后的结果都是1,平坦化设计思路。三个基本函数如下:

#define FUN0                 lo0  //垃圾函数

#define FUN1                 lo1 

#define FUN2                 lo2

#define FUN3                 lo3

static inline int FUN0(void)

{

       volatile int i = 138, j = 1949;

       if ((i++) % 2 > 0)   j *= i;

       if (j < 0)  i *= 2;

       else return 0;

       i = 1;

       while (i++ < 2) { j /= i; j++; i++; }

       return i;

}

static inline int FUN1(void)

{

       volatile int i = 21, j = 75;

       if ((i--) % 3 > 0)     j *= i;

       if (j > 1)  i *= 3;

       else return 1;

       i = 1;

       while (i++ < 3) { j /= i; j--; i++; }

       return j;

}

static inline int FUN2(void)

{

       volatile int i = 56, j = 17;

       if ((i--) % 5 > 0)     j *= i;

       if (j > 2)  i *= 5;

       else return 0;

       i = 1;

       while (i++ < 5) { j *= i;  j += 3; i += 3; }

       return i;

}

static inline int FUN3(void)

{

       volatile int i = 1909, j = 131;

       if ((i--) % 7 > 0)     j *= i;

       if (j > 3)  i *= 7;

       else return 1;

       i = 1;

       while (i++ < 7) { j /= i;   j -= 5; i += 5; }

       return i;

}

然后定义的花指令就可以这样调用:

#define _FLOWER_FUN_0 {if(FUN2())FUN1();if(FUN0()) FUN3();if(FUN1()) FUN2();if(FUN3()) FUN1(); \

                          if(FUN1())FUN0();if(FUN2()) FUN3();if(FUN3()) FUN1();if(FUN1()) FUN0();}

#define _FLOWER_FUN_1 {if(FUN3())FUN1();if(FUN1()) FUN2();if(FUN2()) FUN0();if(FUN0()) FUN1(); \

                          if(FUN2())FUN1();if(FUN0()) FUN3();if(FUN1()) FUN2();if(FUN3()) FUN1();}

If语句得到的值不会去影响下面的判断,只要最后return 0就行。

2.   花指令2

asInvoker:

父进程是什么权限级别,那么此应用程序作为子进程运行时就是什么权限级别。

默认情况下用户启动应用程序都是使用 Windows 资源管理器(explorer.exe)运行的;在开启了 UAC 的情况下,资源管理器是以标准用户权限运行的。于是对于用户点击打开的应用程序,默认就是以标准用户权限运行的。如果已经以管理员权限启动了一个程序,那么这个程序启动的子进程也会是管理员权限。典型的情况是一个应用程序安装包安装的时候使用管理员权限运行,于是这个安装程序在安装完成后启动的这个应用程序进程实例就是管理员权限的。有时候这种设定会出现问题,你可以阅读 在 Windows 系统上降低 UAC 权限运行程序(从管理员权限降权到普通用户权限)。

requireAdministrator

此程序需要以管理员权限运行。在资源管理器中可以看到这样的程序图标的右下角会有一个盾牌图标。


用户在资源管理器中双击启动此程序,或者在程序中使用 Process.Start 启动此程序,会弹出 UAC 提示框。点击“是”会提权,点击“否”则操作取消。

highestAvailable

此程序将以当前用户能获取的最高权限来运行。这个概念可能会跟前面说的 requireAdministrator 弄混淆。

如果你指定为 highestAvailable:当你在管理员账户下运行此程序,就会要求权限提升。资源管理器上会出现盾牌图标,双击或使用 Process.Start 启动此程序会弹出 UAC 提示框。在用户同意后,你的程序将获得完全访问令牌(Full Access Token)。当你在标准账户下运行此程序,此账户的最高权限就是标准账户。受限访问令牌(Limited Access Token)就是当前账户下的最高令牌了,于是 highestAvailable 已经达到了要求。资源管理器上不会出现盾牌图标,双击或使用 Process.Start 启动此程序也不会出现 UAC 提示框,此程序将以受限权限执行。对进程控制块的操作,有管理员权限的程序就能控制。


5.      用户名一机一码实现

首先获得CPU制造商,调用函数如下:

string GetManID()//获取制造商信息

{

       char ID[25];//存储制造商信息

       memset(ID, 0, sizeof(ID));//先清空数组 ID

       ExeCPUID(0);//初始化

       memcpy(ID + 0, &debx, 4);//制造商信息前四个字符复制到数组

       memcpy(ID + 4, &dedx, 4);//中间四个

       memcpy(ID + 8, &decx, 4);//最后四个

       //如果返回 char * ,会出现乱码;故返回 string 值

       return string(ID);

}

然后里面的ExeCPUID(0),是另一个函数,CPUID指令是intel IA32架构下获得CPU信息的汇编指令,可以得到CPU类型,型号,制造商信息,商标信息,序列号,缓存等一系列CPU相关的东西。前面原理部分有介绍。

void ExeCPUID(DWORD veax)//初始化CPU

{

       __asm

       {

              mov eax, veax

              cpuid

              mov deax, eax

              mov debx, ebx

              mov decx, ecx

              mov dedx, edx

       }

}

   所以用户名的唯一性就是得到的CPU生产商再和进程pid进行循环异或:(至于为什么还是选择进程pid参与计算,是因为在动态调试的时候,用GetCurrentProcessId()函数返回的可能是调试器的进程id,后面调试会讲。)

processId = GetCurrentProcessId();   //进程pid

string username(string CPU, DWORD pid)  //异或拼接

{

       string cur_str = to_string(long long(pid));

       int j = 0;

       for (int i=0; i < CPU.length(); i++)

       {

              CPU[i] = CPU[i] ^cur_str[j];

              j++;

              if (j == cur_str.length() - 1)

                     j = 0;

       }

       return CPU;

}

6.      序列号加密算法

序列号的加密思路是,函数接收刚刚生成的唯一序列号和CPU制造商,然后解密关键字符串“20171120051”,这个字符串预先跟Y进行了异或,使用的时候需要先解密。接着根据学号这个字符串转换成的数组,根据具体数值取出CPU制造商对应的字符,如数字2,就是取出GenuineIntel第二位字符n。然后取出字符之后,再根据数值把字符插入到“用户名”的对应位置,得到序列号。算法如下:

string Fun1(string userkey, string cpu)  //处理之后的用户名,和CPU型号  处理序列号函数

{

       string name = "kihnhhkiilh";  //20171120051

       char a[50];

       char CPU[50];

       strcpy_s(CPU, cpu.c_str());

       for (int i = 0; i < name.length(); i++)

       {

              char k = name[i] ^ 'Y';

              a[i] = CPU[hex2int(k)];

       }

       for (int i = 0; i < name.length(); i++)

       {

              char k = name[i] ^ 'Y';

              string s(1, a[i]);

              userkey.insert(hex2int(k), s);

       }

       return userkey;

}

7.      隐藏GetProcAddress函数

原理在之前的部分已经介绍了,这里给出实现的代码和一些注释:

DWORD MyGetProcAddress(

       HMODULE hModule,    // DLL的句柄,就是模板映射的基地址

       LPCSTR lpProcName   // 需要查找的函数名字或者序号

)

{

       int i = 0;

       char* pRet = NULL;

       PIMAGE_DOS_HEADER pImageDosHeader = NULL;

       PIMAGE_NT_HEADERS pImageNtHeader = NULL;

       PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;

       pImageDosHeader = (PIMAGE_DOS_HEADER)hModule;

       pImageNtHeader = (PIMAGE_NT_HEADERS)((DWORD)hModule + pImageDosHeader->e_lfanew);

       pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)hModule + pImageNtHeader->OptionalHeader.DataDirectory

              [IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

       DWORD dwExportRVA = pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

       DWORD dwExportSize = pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

       DWORD* pAddressOfFunction = (DWORD*)(pImageExportDirectory->AddressOfFunctions + (DWORD)hModule);

       DWORD* pAddressOfNames = (DWORD*)(pImageExportDirectory->AddressOfNames + (DWORD)hModule);

       DWORD dwNumberOfNames = (DWORD)(pImageExportDirectory->NumberOfNames);

       DWORD dwBase = (DWORD)(pImageExportDirectory->Base);

       WORD* pAddressOfNameOrdinals = (WORD*)(pImageExportDirectory->AddressOfNameOrdinals + (DWORD)hModule);

       //这个是查一下是按照什么方式(函数名称or函数序号)来查函数地址的 

       DWORD dwName = (DWORD)lpProcName;

       if ((dwName & 0xFFFF0000) == 0)

       {

              goto xuhao;

       }

       for (i = 0; i < (int)dwNumberOfNames; i++)

       {

              char* strFunction = (char*)(pAddressOfNames[i] + (DWORD)hModule);

              if (strcmp(strFunction, (char*)lpProcName) == 0)

              {

                     pRet = (char*)(pAddressOfFunction[pAddressOfNameOrdinals[i]] + (DWORD)hModule);

                     goto _exit11;

              }

       }

       //这个是通过以序号的方式来查函数地址的 

xuhao:

       if (dwName < dwBase || dwName > dwBase + pImageExportDirectory->NumberOfFunctions - 1)

       {

              return 0;

       }

       pRet = (char*)(pAddressOfFunction[dwName - dwBase] + (DWORD)hModule);

_exit11:

       //判断得到的地址有没有越界 

       if ((DWORD)pRet<dwExportRVA + (DWORD)hModule || (DWORD)pRet > dwExportRVA + (DWORD)hModule + dwExportSize)

       {

              return (DWORD)pRet;

       }

       char pTempDll[100] = { 0 };

       char pTempFuction[100] = { 0 };

       strcpy(pTempDll, pRet);

       char* p = strchr(pTempDll, '.');

       if (!p)

       {

              return (DWORD)pRet;

       }

       *p = 0;

       strcpy(pTempFuction, p + 1);

       strcat(pTempDll, ".dll");

       LPWSTR k[100] = { 0 };

       TCHAR aaa[31];

       MultiByteToWideChar(0, 0, pTempDll , 31, aaa, 62);

       HMODULE h = LoadLibrary(aaa);

       if (h == NULL)

       {

              return (DWORD)pRet;

       }

       return MyGetProcAddress(h, pTempFuction);

}

   根据句柄得到映射地址,然后判断是字符串函数名字还是序号导出(这一块代码需要借助,没写过)。再在导出表中得到函数的映射地址。

8.   VMProtectSDK 保护

首先下载最新的vmp,可以从官网进行下载,下载之后根目录下有一个example文件夹:


然后点击里面的VC++,里面有一个头文件和一个资源文件:


把这两个文件在项目里面包含它。然后就可以调用了。官方的头文件里面有很多内容:


要注意虚拟机的调用的顺序,特别是涉及到堆栈操作的话。

大作业中对整个反调试函数进行虚拟操作:


也可以选择其他的方式,比如代码变异等。默认就是代码虚拟。

9.      反调试实现(代码和注释,原理在第一部分)

在写大作业之前,我就把论坛里面常见的反调试技术都实现了一遍。把整个反调试弄成一个头文件里面,并用一个主函数包含起来。这样就可以像花指令那样想在哪放就在哪放。实现的原理见前面第一章,这里只给出实现代码,原理见第一部分。

用win  API函数实现的反调试:

BOOL Fan1()

{

       return IsDebuggerPresent();

}

BOOL Fan2()

{

       BOOL ret;

       CheckRemoteDebuggerPresent(GetCurrentProcess(), &ret);

       return ret;

}

检查进程控制块PEB结构函数:

bool PebIsDebugged()  //检查PEB  BeingDebugged标志位

{

       char result = 0;

       __asm

       {

              // 进程的PEB地址放在fs这个寄存器位置上

              mov eax, fs: [30h]

              // 查询BeingDebugged标志位

              mov al, BYTE PTR[eax + 2]

              mov result, al

       }

       return result != 0;

}

检查NtGlobal标志位:

bool PebNtGlobalFlags()  //检查NtGlobal标志位  NtGlobal标志位

{

       int result = 0;

       __asm

       {

              // 进程的PEB

              mov eax, fs: [30h]

              // 控制堆操作函数的工作方式的标志位

              mov eax, [eax + 68h]

              // 操作系统会加上这些标志位FLG_HEAP_ENABLE_TAIL_CHECK,

              // FLG_HEAP_ENABLE_FREE_CHECK and FLG_HEAP_VALIDATE_PARAMETERS,

              // 它们的并集就是x70

              and eax, 0x70   //&

              mov result, eax

       }

       return result != 0;

}

检查堆栈标志位,堆结构见原理部分:

bool HeapFlags()   //检查堆头信息

{

       int result = 0;

       __asm

       {

              // 进程的PEB

              mov eax, fs: [30h]

              mov eax, [eax + 18h]

              mov eax, [eax + 44h]  //win7 win10

              mov result, eax

       }

       return result != 0;

}

利用Ntdll里面的NtQueryInformationProcess检查是否是反调试,我用0x7号位置,也就是debugPort位置,由于是调用外部dll,所以需要定义一个引用指针,GetProcAddress用的是自己的函数:

typedef NTSTATUS(WINAPI* NtQueryInformationProcessPtr)(   //NtQueryInformationProcess函数检查端口返回值

       HANDLE processHandle,

    PROCESSINFOCLASS ProcessInformationClass,

       PVOID processInformation,

       ULONG processInformationLength,

       PULONG returnLength);

int NtQueryInformationProcess()

{

       int debugPort = 0;

       HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll "));   //调用Ntdll链接库

       NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)MyGetProcAddress(hModule, "NtQueryInformationProcess");

       if (NtQueryInformationProcess(GetCurrentProcess(), (PROCESSINFOCLASS)7, &debugPort, sizeof(debugPort), NULL))

              printf("[ERROR NtQueryInformationProcessApproach] NtQueryInformationProcess failed\n");

       else

              return debugPort ==-1;  

       return 0;    //返回端口是0就不在调试

}

跟上面的调用方法是一样的,就是调用NTdll里面的结构体,但是要定义外部引用指针:

typedef NTSTATUS(*NtSetInformationThreadPtr)(HANDLE threadHandle,  // NtSetInformationThread方法

       THREADINFOCLASS threadInformationClass,

       PVOID threadInformation,

       ULONG threadInformationLength);

void NtSetInformationThread()   //你可以在当前线程里调用NtSetInformationThread,调用这个函数时,如果在第二个参数里指定0x11这个值(意思是ThreadHideFromDebugger)

{

       HMODULE hModule = LoadLibrary(TEXT("Ntdll.dll"));

       NtSetInformationThreadPtr NtSetInformationThread = (NtSetInformationThreadPtr)MyGetProcAddress(hModule, "NtSetInformationThread");

       NtSetInformationThread(GetCurrentThread(), (THREADINFOCLASS)0x11, 0, 0);

}

设置异常处理,并且自己添加一个异常,但是有一个问题是这个添加之后程序会报错,就是因为正常执行程序的时候如果有异常没有设置处理机制:

LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS* pei)

{

       //MessageBox(NULL, TEXT("反调试8  经过异常处理函数"), TEXT("信息"), MB_ICONINFORMATION);

       SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)

              pei->ContextRecord->Eax);

       // 修改寄存器eip的值

       pei->ContextRecord->Eip += 2;

       // 告诉操作系统,继续执行进程剩余的指令(指令保存在eip里),而不是关闭进程

       return EXCEPTION_CONTINUE_EXECUTION;

}

bool UnhandledExceptionFilterApproach()

{

       SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);

       __asm

       {

              // 将eax清零

              xor eax, eax

              // 触发一个除零异常

              div eax

       }

       return false;

}

寻找前台窗口的两个方法:

BOOL Fan10()   //找窗口

{

       if (FindWindowA("OLLYDBG", NULL) != NULL || FindWindowA("WinDbgFrameClass", NULL) != NULL || FindWindowA("QWidget", NULL) != NULL)

       {

              return TRUE;

       }

       else

       {

              return FALSE;

       }

}

BOOL Fan11()   //前台窗口

{

       char fore_window[1024];

       GetWindowTextA(GetForegroundWindow(), fore_window, 1023);

       if (strstr(fore_window, "WinDbg") != NULL || strstr(fore_window, "x64_dbg") != NULL || strstr(fore_window, "OllyICE") != NULL || strstr(fore_window, "OllyDBG") != NULL || strstr(fore_window, "Immunity") != NULL)

       {

              return TRUE;

       }

       else

       {

              return FALSE;

       }

}

利用进程快照的方式,获得所有进程信息,然后根据PROCESSNTRY32结构体定义的用户名字符串比较黑名单软件名:

BOOL Fan12()

{

       DWORD ID;

       DWORD ret = 0;

       PROCESSENTRY32 pe32;

       pe32.dwSize = sizeof(pe32);

       HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

       if (hProcessSnap == INVALID_HANDLE_VALUE)

       {

              return FALSE;

       }

       BOOL bMore = Process32First(hProcessSnap, &pe32);

       while (bMore)

       {

              if (TCHAR2STRING2(pe32.szExeFile) =="OLLYDBG.EXE"  || TCHAR2STRING2(pe32.szExeFile) ==  "OllYICE.exe" || _stricmp((char*)pe32.szExeFile, "x64_dbg.exe") == 0 || _stricmp((char*)pe32.szExeFile, "windbg.exe") == 0 || _stricmp((char*)pe32.szExeFile, "ImmunityDebugger.exe") == 0)

              {

                     MessageBox(NULL, TEXT("反调试12"), TEXT("信息"), MB_ICONINFORMATION);

                     return TRUE;

              }

              bMore = Process32Next(hProcessSnap, &pe32);

       }

       CloseHandle(hProcessSnap);

       return FALSE;

}

软件断点检测:这部分代码参考某论坛代码,原理就是下了断点之后程序会多了INT3(CC)byte,所以大小会变。

BOOL Fan13()   //软件断点检测

{

       PIMAGE_DOS_HEADER pDosHeader;

       PIMAGE_NT_HEADERS32 pNtHeaders;

       PIMAGE_SECTION_HEADER pSectionHeader;

       DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);

       pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;

       pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);

       pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +

              (WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);

       DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;

       DWORD dwCodeSize = pSectionHeader->SizeOfRawData;

       BOOL Found = FALSE;

       __asm

       {

              cld

              mov     edi, dwAddr

              mov     ecx, dwCodeSize

              mov     al, 0CCH

              repne   scasb

              jnz     NotFound

              mov Found, 1

              NotFound:

       }

       return Found;

}

硬件断点检测,需要借助一个结构体Contetxt得到进程上下文:

BOOL Fun14()   //检查四个硬件断点

{

       CONTEXT context;

       HANDLE hThread = GetCurrentThread();

       context.ContextFlags = CONTEXT_DEBUG_REGISTERS;

       GetThreadContext(hThread, &context);

       if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3 != 0)

       {

              return TRUE;  //在调试状态

       }

       return FALSE;

}

10.    KeyGen

命令行编程,接收测试的程序pid,返回用户名和序列号,就是之前的加密算法的逆运算。

#include<iostream>

using namespace std;

int hex2int(char c)

{

       if ((c >= 'A') && (c <= 'Z'))

       {

              return c - 'A' + 10;

       }

       else if ((c >= 'a') && (c <= 'z'))

       {

              return c - 'a' + 10;

       }

       else if ((c >= '0') && (c <= '9'))

       {

              return c - '0';

       }

}

int main(int argv, char *argc[])

{

       string cpu = "GenuineIntel";

       string pid(argc[1]);

       int j = 0;

       for (int i = 0; i < cpu.length(); i++)

       {

              cpu[i] = cpu[i] ^ pid[j];

              j++;

              if (j == pid.length() - 1)

                     j = 0;

       }

       cout << cpu << endl;

       string cpu2 = "GenuineIntel";

       string name = "kihnhhkiilh";

       char a[50];

       char CPU[50];

       strcpy_s(CPU, cpu2.c_str());

       for (int i = 0; i < name.length(); i++)

       {

              char k = name[i] ^ 'Y';

              a[i] = CPU[hex2int(k)];

       }

       for (int i = 0; i < name.length(); i++)

       {

              char k = name[i] ^ 'Y';

              string s(1, a[i]);

              cpu.insert(hex2int(k), s);

       }

       cout << cpu << endl;

       return 0;

 }


四.功能测试

1.   程序正常执行流程:

1)       第一个程序

首先编译一下程序,然后用外部SMC加密程序进行加密:


显示加密成功:


然后点击运行程序:


输入错误用户名会提示错误:


只有用户名对了才能去判断序列号对不对。现在命令行调用注册机,并查看此时的PID:



如果用户名对了,序列号不对会提示错误:


输入正确的用户名和序列号是没有弹窗提示的。


附录

1.        反调试https://bbs.pediy.com/thread-225740.htm

2.        反调试https://www.jianshu.com/p/0f6e796e813f

3.        SMC技术https://blog.csdn.net/PandaOS/article/details/46575441

4.        SMC动态加解密https://blog.csdn.net/jinhill/article/details/8587575


借助了很多前辈的东西,感谢感谢。


附件第一个源.cpp是keygen


相关程序有点大,我放百度网盘了。 

链接:https://pan.baidu.com/s/1SDia-ALVLFzrRFt-olFX_Q 

提取码:imim 

复制这段内容后打开百度网盘手机App,操作更方便哦


login就是源程序,login_De是SMC保护。


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2020-2-13 16:40 被kanxue编辑 ,原因:
上传的附件:
收藏
点赞9
打赏
分享
最新回复 (20)
雪    币: 9934
活跃值: (2554)
能力值: ( LV6,RANK:87 )
在线值:
发帖
回帖
粉丝
Lixinist 1 2020-2-10 19:58
2
0
我还以为什么期末作业让你们写vm。。。
雪    币: 2873
活跃值: (1607)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chixiaojie 2020-2-10 21:21
3
0
感觉超级难
雪    币: 2011
活跃值: (2296)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
evilbeast 1 2020-2-11 03:55
4
0
收藏
雪    币: 8286
活跃值: (4816)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
v0id_ 2020-2-11 08:44
5
0
写的不错,支持了
雪    币: 4255
活跃值: (8410)
能力值: ( LV9,RANK:181 )
在线值:
发帖
回帖
粉丝
nevinhappy 2 2020-2-11 09:31
6
0
不好,感觉要精。谢谢分享。
雪    币: 2510
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_xghoecki 2020-2-11 11:51
7
1
感谢分享
雪    币: 6
活跃值: (980)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lookzo 2020-2-14 14:09
8
0
前面还可以,后面竟然直接用了vmp
雪    币: 310
活跃值: (1917)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
niuzuoquan 2020-2-15 09:25
9
0
mark
雪    币: 1202
活跃值: (895)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
program杨 2020-2-16 20:07
10
0
mark
雪    币: 19586
活跃值: (60093)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2020-2-17 11:53
11
0
感谢分享!
雪    币: 5123
活跃值: (4429)
能力值: ( LV5,RANK:65 )
在线值:
发帖
回帖
粉丝
gamehack 2020-2-25 12:03
12
0
写的很好,收藏了,感谢分享!
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_ekqjtpuf 2020-4-5 23:05
13
0
大佬真的强,收藏学习了
雪    币: 83
活跃值: (1042)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
killpy 2 2020-4-6 18:52
14
0
很强
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_wjtmsski 2020-4-9 15:54
15
0
很好很强大
雪    币: 248
活跃值: (3784)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
luskyc 2020-4-10 11:14
17
0
不管是什么保护,具体情况具体对待,重要的是见招拆招
雪    币: 33
活跃值: (318)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
Dascolee 2020-4-11 13:00
18
0
很强
雪    币: 3574
活跃值: (2130)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_acmeqfbq 2020-4-14 19:51
19
0
厉害
雪    币: 3383
活跃值: (3427)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 1 2020-4-14 19:57
20
0
感谢分享!
雪    币: 23
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_tfcmlgee 2020-5-6 06:16
21
0
厉害
游客
登录 | 注册 方可回帖
返回