来源:https://bbs.pediy.com/thread-251267.htm 看雪论坛
作者:九阳道人
之前在写了写壳基础篇,现在就来完成写壳高级篇。没有基础篇的知识,那理解高级篇就比较困难。有了写壳基础后,才能在其基础上逐步实现高级功能,加壳的目的主要是防止别人破解,而想要别人很难破解,我认为要在花指令、混淆和指令虚拟化上大量的时间及脑力才能做到,这个比较费脑力费时间。我在此就说说一些能快速入门的反调试技术,下面说的难度将逐渐提升。
主要工具: VS2017、x64dbg、OD
实验平台:win10 64位
实现功能:反调试、IAT加密、Hash加密、动态解密。
一、反调试
顾名思义,就是阻止别人调试程序,在PEB结构中有一个BegingDebugged标志位专门用于检测是否处于调试状态,为1则处于调试状态,用VS2017测试下列程序:
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 "pch.h" #include <iostream> #include <windows.h> / / 反调试 1 bool PEB_BegingDebugged() { bool BegingDebugged = false; __asm { mov eax, fs:[ 0x30 ]; / / 获取PEB mov al, byte ptr ds : [eax + 0x2 ]; / / 获取Peb.BegingDebugged mov BegingDebugged, al; } return BegingDebugged; / / 如果为 1 则说明正在被调试 } int main() { if (PEB_BegingDebugged()) { MessageBoxA( 0 , "正在被调试" , 0 , 0 ); return 0 ; } std::cout << "Hello World!
" ; getchar(); } |
敲完代码后按Ctrl+F5直接运行可以输出Hello World!,按F5以调试方式运行程序则会弹出反调试窗口,说明这种方法检测是否正在被调试成功!
但是坏消息是用某些论坛的OD运行这程序依然能正常运行,检测不到反调试,现在的各个平台的OD基本都有一个插件是StrongOD,他能干掉PEB中所有检测反调试的标志位!所以以上方法基本被淘汰了。
所以这里介绍一个可以在OD中也能反调试的方法,有一个可以同时在0环和3环运行的函数NtQueryInformationProcess,它的主要作用是查看进程相关的各种信息,在这把它用于检测调试。
我们给它第二个参数传入ProcessDebugPort,当输出的查询信息为0xFFFFFFFF时,则此程序处于被调试状态。代码如下:
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
|
#include "pch.h" #include <iostream> #include <windows.h> #include <winternl.h> #pragma comment(lib,"ntdll.lib") / / 反调试 2 bool NQIP_ProcessDebugPort() { int nDebugPort = 0 ; NtQueryInformationProcess( GetCurrentProcess(), / / 目标进程句柄 ProcessDebugPort, / / 查询信息的类型 &nDebugPort, / / 输出查询的信息 sizeof(nDebugPort), / / 查询类型的大小 NULL); return nDebugPort = = 0xFFFFFFFF ? true : false; } int main() { if (NQIP_ProcessDebugPort()) { MessageBoxA( 0 , "正在被调试" , 0 , 0 ); return 0 ; } std::cout << "Hello World!
" ; getchar(); } |
编译生成程序后使用OD打开再运行,弹出窗口,反调试成功,我在这测试了15PB的OD和吾爱破解的OD均能有效。掌握了反调试的方法,我们可以把它放在壳代码的各个角落,检测到调试就马上退出程序,多放置几个阴人位置,这样就能增加破解的难度了!
二、IAT加密
要对IAT加密前提条件是对PE文件比较熟悉。
IAT也就是导入函数的地址表,程序在加载到内存后IAT中填充的都是函数的地址,使用OD打开随意exe文件,我这就打开QQ.exe,找到第一个HEX数据是FF15开头的CALL代码,右键查看内存地址。
从图中知:如果IAT没加密反汇编代码一目了然就能看见用的的什么API,加密的目的就是即使调用函数也不能一眼就看出调用的是什么函数(有字符串提示)。
内存窗口的地址是IAT每个元素的地址,数值一列是IAT存储的数据,注释中也解释了它们是什么API的地址。
IAT加密原理就是:
- 遍历导入表获取每个函数的IAT地址(对应上图内存栏中地址的值)
- 取出IAT地址的内容,就是函数的地址(上图内存栏中数值的值),把该函数地址进行加密后得到一个数据(我这就是异或了一个值0x13973575进行了加密)
- 申请一段内存,其中存放解密上述的数据得到真地址,然后调用该地址的代码。
- 把申请的内存地址放入IAT地址对应的数值中。
完成以上步骤后IAT就被加密了,当然第3步当中可以进行适当的混淆和加花指令别人就更加看不出来了。
加密后再查看反汇编代码就没有字符串提示了,再查看该地址内存数据也没有字符串注释了,效果图如下:
在汇编窗口Ctrl+G输入04A90000查看,其中的代码有花指令如下:
去掉花指令后其汇编代码最终成功调用真正函数的地址了,就是:
具体操作就是在壳代码解压缩,解密后,再进行IAT修复加密,遍历IAT代码我就不解释了。
IAT加密源码:
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
|
/ / 修复并加密IAT void MendIAT() { HMODULE hBase = (HMODULE)g_hModule; auto pImport = (PIMAGE_IMPORT_DESCRIPTOR)(g_Sc.dwImportRVA + (DWORD)hBase); / / 外层遍历模块 while (pImport - >Name) { / / 获取当前模块地址 HMODULE hModule = MyLoadLibraryExA((char * )(pImport - >Name + (DWORD)hBase), 0 , 0 ); if (pImport - >FirstThunk) { / / IAT的地址 PDWORD IAT = PDWORD(pImport - >FirstThunk + (DWORD)hBase); DWORD ThunkRva = 0 ; if (pImport - >OriginalFirstThunk = = 0 ) ThunkRva = pImport - >FirstThunk; else ThunkRva = pImport - >OriginalFirstThunk; PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(ThunkRva + (DWORD)hBase); / / 函数的名字 char * dwFunName = 0 ; / / 内层遍历模块中的函数 while (pThunk - >u1.Ordinal) { / / 序号导入 if (pThunk - >u1.Ordinal & 0x80000000 ) { dwFunName = (char * )(pThunk - >u1.Ordinal & 0x7fffffff ); } / / 名称导入 else { PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME) (pThunk - >u1.Ordinal + (DWORD)hBase); dwFunName = pImportByName - >Name; } / / 获取每个函数的地址 DWORD dwFunAddr = (DWORD)SysGetProcAddress(hModule, dwFunName); / / * * 加密函数地址 * * dwFunAddr ^ = 0x13973575 ; LPVOID AllocMem = (PDWORD)MyVirtualAlloc(NULL, 0x20 , MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); / / 构造一段花指令解密的ShellCode byte OpCode[] = { 0xe8 , 0x01 , 0x00 , 0x00 , 0x00 , 0xe9 , 0x58 , 0xeb , 0x01 , 0xe8 , 0xb8 , 0x8d , 0xe4 , 0xd8 , 0x62 , 0xeb , 0x01 , 0x15 , 0x35 , 0x75 , 0x35 , 0x97 , 0x13 , 0xeb , 0x01 , 0xff , 0x50 , 0xeb , 0x02 , 0xff , 0x15 , 0xc3 }; / / 把dwFunAddr写入到解密的ShellCode中 OpCode[ 11 ] = dwFunAddr; OpCode[ 12 ] = dwFunAddr >> 0x8 ; OpCode[ 13 ] = dwFunAddr >> 0x10 ; OpCode[ 14 ] = dwFunAddr >> 0x18 ; / / 拷贝数据到申请的内存 MyRtlMoveMemory(AllocMem, OpCode, 0x20 ); / / 修改保护属性 DWORD dwProtect = 0 ; MyVirtualProtect(IAT, 4 , PAGE_EXECUTE_READWRITE, &dwProtect); / / 把获取到的加密函数地址填充在导入地址表里面 * (IAT) = (DWORD)AllocMem; MyVirtualProtect(IAT, 4 , dwProtect, &dwProtect); + + IAT; + + pThunk; } } + + pImport; } } |
三、Hash加密
为什么要进行Hash加密?因为在逆向工作者逆向的过程中,字符串信息对他们来说很重要,如果看见了一个API函数的字符串,那么他大概就能知道这段代码大概的功能了,轻而易举就能破解掉,为了阻止这种事件发生,那么Hash加密在这就能发挥出很大作用。
众所周知1个字节是8位,这代表他表示2的8次方个数,也就是256种可能,如果我们把它的一个数据代表一个系统中的函数(API),相当于给函数一个序号,那么1个字节就能存储256个函数的信息,那2个字节就能存储2的16次方也就是65536个API函数,这真是大大的好消息, windows系统中的API函数也就几千个,2个字节存储其全部API函数信息真是绰绰有余。
而让这2个字节的数据代表一个函数,这个数据我们称它为Hash值,因此需要设计一个算法。我在这设计是方法是定义一个2字节类型(short)的数据,分别把nHash值先左移11位再右移5位后相加,再加上API函数中一个字符的Ascii码,以此循环遍历完整个API函数的所有字符,得到一个我们需要的Hash值。在之前写壳基础篇中提到过壳代码中的API是动态获取的,那么我们在动态获取的时候使用Hash值更能提高隐蔽性,使破解者不易发现我们所要使用的是哪个函数。
具体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
|
#include "pch.h" #include <iostream> int main() { while (true) { / / 用于保存 Hash 值 unsigned short nHash = 0 ; char arr[ 50 ] = {}, * p; p = arr; printf( "请输入API: " ); scanf_s( "%s" , arr, 50 ); while ( * p) { / / 先左移 11 位再右移 5 位相加后再加上该字符的Ascii nHash = ((nHash << 11 ) | (nHash >> 5 )); nHash = nHash + * p; p + + ; } printf( "Hash值为:0x%X
" , nHash); } return 0 ; } |
使用方法是首先使用上述代码对我们需要使用API函数进行Hash加密得到Hash值,然后再写一个Hash值对比字符串的函数(解密),使用该值和系统中的API函数对比,和谁相等,我们就把这个函数的地址获取取出。这样我们就隐晦的得到了所需的函数的地址。
Hash解密代码如下,需要传入2个参数,1是对比函数的地址,2是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
|
/ * * * * * * * * * * * * * * * * Hash 对比 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / _Hash_CmpString: / / (char * strFunName, int nDigest) push ebp; mov ebp, esp; sub esp, 0x4 ; push ebx; push ecx; push edx; mov dword ptr[ebp - 0x4 ], 0 ; xor eax, eax; xor ecx, ecx; mov esi, [ebp + 0x8 ]; / / strFunName _Start: mov al, [esi + ecx]; test al, al; jz _End; mov edi, [ebp - 0x4 ]; shl edi, 0xb ; / / 11 的 16 进制为b mov edx, [ebp - 0x4 ]; shr edx, 0x5 ; or edi, edx; add edi, eax; mov[ebp - 0x4 ], edi; inc ecx; jmp _Start; _End: mov esi, [ebp + 0xc ]; / / 获取 hash 值 and edi, 0xffff ; / / 取低 16 位 cmp edi, esi; / / 对比 hash mov eax, 0x1 ; je _Over; xor eax, eax; _Over: pop edx; pop ecx; pop ebx; mov esp, ebp; pop ebp; ret 0x8 ; |
上述代码有大大的优化空间,比较懒我就不弄了。
有了Hash加解密,就可以自己实现一个GetProcAddress函数了,在这之后需要获取任何API函数就用自己实现的GetProcAddress函数,这样就是达到更加隐蔽的获取API函数的目的,学会了Hash加解密咱也就脱离了小白的行列了。
代码如下,参数1是所需API的模块基址,参数2是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
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
|
/ / 自写的GetProcAddress DWORD MyGetProcAddress(HMODULE hModule, int nDigest) { DWORD GetProcAddr = 0 ; __asm { jmp _Start_Fun; / * * * * * * * * * * * * * * * * * 自写的GetProcAddress * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / _Fun_GetProcAddress: / / (dword ImageBase, int nDiegst) push ebp; mov ebp, esp; sub esp, 0xc ; push edx; / / 保存寄存器 push ebx; mov edx, [ebp + 0x8 ]; / / DLL基地址如kernel32 mov esi, [edx + 0x3c ]; / / Dos头的e_lfanew lea esi, [edx + esi]; / / PE头VA(NT头) mov esi, [esi + 0x78 ]; / / Import表的RVA lea esi, [edx + esi]; / / Import表的VA mov edi, [esi + 0x1c ]; / / EAT的RVA .AddressOfFunctions lea edi, [edx + edi]; / / EAT的VA mov[ebp - 0x4 ], edi; / / EAT的VA保存到局部变量 1 mov edi, [esi + 0x20 ]; / / ENT的RVA AddressOfNames lea edi, [edx + edi]; / / ENT的VA mov[ebp - 0x8 ], edi; / / ENT的VA - >Local2 mov edi, [esi + 0x24 ]; / / EOT的RVA lea edi, [edx + edi]; / / EOT的VA mov[ebp - 0xc ], edi; / / EOT的VA - >Local3 xor ecx, ecx; jmp _First; _Begin: inc ecx; _First: mov esi, [ebp - 0x8 ]; / / ENT mov esi, [esi + ecx * 4 ]; / / EN RVA lea esi, [edx + esi]; / / EN VA push[ebp + 0xc ]; push esi; call _Hash_CmpString; test eax, eax; jz _Begin; / / 不是则循环 mov esi, [ebp - 0xc ]; / / EOT xor ebx, ebx; mov bx, [esi + ecx * 2 ]; / / 函数所对应的序号 mov esi, [ebp - 0x4 ]; / / EAT mov esi, [esi + ebx * 4 ]; / / EA RVA lea eax, [edx + esi]; / / 函数的地址 pop ebx; pop edx; mov esp, ebp; pop ebp; retn 0x8 ; / * * * * * * * * * * * * * * * * Hash 对比 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / _Hash_CmpString: / / (char * strFunName, int nDigest) push ebp; mov ebp, esp; sub esp, 0x4 ; push ebx; push ecx; push edx; mov dword ptr[ebp - 0x4 ], 0 ; xor eax, eax; xor ecx, ecx; mov esi, [ebp + 0x8 ]; / / strFunName _Start: mov al, [esi + ecx]; test al, al; jz _End; mov edi, [ebp - 0x4 ]; shl edi, 0xb ; mov edx, [ebp - 0x4 ]; shr edx, 0x5 ; or edi, edx; add edi, eax; mov[ebp - 0x4 ], edi; inc ecx; jmp _Start; _End: mov esi, [ebp + 0xc ]; / / 获取 hash 值 and edi, 0xffff ; cmp edi, esi; / / 对比 hash mov eax, 0x1 ; je _Over; xor eax, eax; _Over: pop edx; pop ecx; pop ebx; mov esp, ebp; pop ebp; ret 0x8 ; _Start_Fun: pushad; push nDigest; push hModule; call _Fun_GetProcAddress; mov GetProcAddr, eax; popad; } return GetProcAddr; } |
动态获取函数例子(下面的Hash值是乱填的,意思意思下):
1
2
3
|
MyLoadLibraryExA = (FuLoadLibraryExA)MyGetProcAddress(g_hKernel32, 0xC0D8 ); g_hUser32 = MyLoadLibraryExA( "user32.dll" , 0 , 0 ); MyMessageBoxW = (FuMessageBoxW)MyGetProcAddress(g_hUser32, 0x1E38 ); |
四、动态解密
加入动态解密的壳,这无疑是强度较高的壳了,它能够在目标程序运行起来之后,动态的对代码段进行解密。先运行一段代码解密后一部分的代码,然后再运行解密后的代码,可以往复循环,这样破解者只能看见运行着的代码的附近的代码,隔得远的代码处于加密状态,这样就需要花费大量的时间才能破解了,当然想要实现这种高强度,还是需要花费很多时间去设计的,而且要求我们对x86汇编语言有比较深刻理解,这我就分享下我对动态解密理解。
下面我直接根据一个案列来分析动态解密流程:
为了方便演示效果,我在VS中用汇编以动态获取API方式写了一段功能是弹窗的代码,效果如图
将生成EXE文件用0x32dbg(或者OD)打开后找到弹窗功能的汇编代码,扣取出该段代码16进制字节。扣取字节的操作是在x32dbg中选中该段代码后,右键->复制->数据->C样式ShellCode字符串
OD中选中代码后右键->数据转换->C++->字节
把这些字节存在一个字符串数组中,直接用我这个就行了:
1
|
char ShellCode[] = "x60x83xECx60xEBx55x4Dx65x73x73x61x67x65x42x6Fx78x41x00x45x78x69x74x50x72x6Fx63x65x73x73x00x4Cx6Fx61x64x4Cx69x62x72x61x72x79x45x78x41x00x47x65x74x50x72x6Fx63x41x64x64x72x65x73x73x00x75x73x65x72x33x32x2Ex64x6Cx6Cx00x48x65x6Cx6Cx6Fx20x47x72x65x61x74x20x4Ex61x74x75x72x65x21x00xD9xEExD9x74x24xF4x5Ax64x8Bx35x30x00x00x00x8Bx76x0Cx8Bx76x1Cx8Bx36x8Bx5Ex08x52x53xE8x5Ax00x00x00x8BxC8x51x52x8Dx42xC3x50x53xFFxD1x5Ax59x52x50x51x53xE8x01x00x00x00x61x55x8BxECx83xECx0Cx8Bx55x14x33xC9x8Dx72xE1x51x51x56xFFx55x10x8Bx55x14x8Dx4AxABx51x50xFFx55x0Cx33xC9x8Bx55x14x8Dx5AxECx51x53x53x51xFFxD0x8Bx55x14x8Dx72xB7x56xFFx75x08xFFx55x0Cx51xFFxD0x8BxE5x5DxC2x10x00x55x8BxECx83xECx0Cx52x53x8Bx55x08x8Bx72x3Cx8Dx34x32x8Bx76x78x8Dx34x32x8Bx7Ex1Cx8Dx3Cx3Ax89x7DxFCx8Bx7Ex20x8Dx3Cx3Ax89x7DxF8x8Bx7Ex24x8Dx3Cx3Ax89x7DxF4x33xC0xEBx01x40x8Bx75xF8x8Bx34x86x8Dx34x32x8Bx5Dx0Cx8Dx7BxD2xB9x0Ex00x00x00xF3xA6x75xE7x8Bx75xF4x33xDBx66x8Bx1Cx46x8Bx75xFCx8Bx34x9Ex8Dx04x32x5Bx5Ax8BxE5x5DxC2x08x00" ; |
在0x32dbg(或者OD)中选中汇编代码段后右下角会显示选中的字节大小。
下面写一个加密该字符串的代码,编译的时候VS项目属性中配置“C/C++ -> 代码生成 -> 安全检查(禁用GS)”,“连接器 -> 高级 -> 数据执行保护DEP(关闭)”。
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
|
/ / 加密函数 void Encoder(char * pData, int nSize) { / / 加密密钥 int nOutKey = 0x15 ; / / 加密后的缓冲区 unsigned char * pBuffer = NULL; pBuffer = (unsigned char * )new char[nSize + 1 ]; / / 对每个字节进行加密 for ( int j = 0 ; j < nSize; j + + ) { pBuffer[j] = pData[j] ^ nOutKey; } / / 打印出每个字节 for ( int i = 0 ; i < nSize; i + + ) { printf( "\x%02X" ,pBuffer[i]); } } int main() { / / 调用ShellCode查看是否能正常运行 __asm { lea eax, ShellCode; push eax; ret; } / / 加密 Encoder(ShellCode, 257 ); getchar(); } |
加密后会得到一个字节数组,我们把它复制下来存到另一个数组中,然后把他放在解密代码的屁股后面。
开始写解密代码,解密代码才是动态解密中的核心点,重中之重。
这里要说一下GetPC技术,GetPC技术翻译为中文也就是获取指针计数器。在x86汇编中实际上就是获取当前代码EIP的技术。我这用的是call 指令,call xxx指令相当于 push 下一行代码的EIP + jmp xxx。 那么我们直接把XXX改为下一行指令的地址就能获取当前EIP 内联汇编代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/ / 解密函数 __asm { call _Next; / / 跳到下一行,并把EIP压入栈 _Next: pop eax; / / 获得当前EIP lea esi, [eax + 0x22 ]; / / 生成后在OD中查看上一行到最后一行解密代码的长度 xor ecx, ecx; mov cx, 0x13e ; / / 要解密的字节长度 _DeCode: mov al, byte ptr ds : [esi + ecx]; xor al, 0x15 ; / / 解密密钥 mov byte ptr ds : [esi + ecx], al; loop _DeCode; xor[esi + ecx], 0x15 ; jmp esi; } |
这里要注意的是这段代码后面要紧跟加密后的代码。
在实际的壳代码中,先把需要加密的代码加密后和解密代码组装起来就可以达到动态解密的功能。