C++写壳之高级篇

论坛 期权论坛 期权     
看雪学院   2019-6-9 21:24   3214   0
之前写了写壳基础篇,C++写壳详解之基础篇。现在就来完成写壳高级篇。没有基础篇的知识,那理解高级篇就比较困难,有了写壳基础后,才能在其基础上逐步实现高级功能。

加壳的目的主要是防止别人破解,而想要别人很难破解,我认为要在花指令、混淆和指令虚拟化上花大量的时间及脑力才能做到,这个比较费脑力费时间。我在此就谈谈一些能快速入门的反调试技术,下面难度将逐渐提升。

主要工具: VS2017、x64dbg、OD实验平台:win10 64位
实现功能:反调试、IAT加密、Hash加密、动态解密[h3][/h3]
一反调试
顾名思义,就是阻止别人调试程序,在PEB结构中有一个BegingDebugged标志位专门用于检测是否处于调试状态,为1则处于调试状态,用VS2017测试下列程序:
  1. #include "pch.h"
  2. #include
  3. #include
  4. //反调试1
  5. bool PEB_BegingDebugged()
  6. {
  7. bool BegingDebugged = false;
  8. __asm
  9. {
  10. mov eax, fs:[0x30]; //获取PEB
  11. mov al, byte ptr ds : [eax + 0x2];//获取Peb.BegingDebugged
  12. mov BegingDebugged, al;
  13. }
  14. return BegingDebugged; //如果为1则说明正在被调试
  15. }
  16. int main()
  17. {
  18. if (PEB_BegingDebugged())
  19. {
  20. MessageBoxA(0, "正在被调试", 0, 0);
  21. return 0;
  22. }
  23. std::cout Name + (DWORD)hBase), 0, 0);
  24. if (pImport->FirstThunk)
  25. {
  26. //IAT的地址
  27. PDWORD IAT = PDWORD(pImport->FirstThunk + (DWORD)hBase);
  28. DWORD ThunkRva = 0;
  29. if (pImport->OriginalFirstThunk == 0)
  30. ThunkRva = pImport->FirstThunk;
  31. else
  32. ThunkRva = pImport->OriginalFirstThunk;
  33. PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(ThunkRva + (DWORD)hBase);
  34. //函数的名字
  35. char * dwFunName = 0;
  36. //内层遍历模块中的函数
  37. while (pThunk->u1.Ordinal)
  38. {
  39. //序号导入
  40. if (pThunk->u1.Ordinal & 0x80000000)
  41. {
  42. dwFunName = (char*)(pThunk->u1.Ordinal & 0x7fffffff);
  43. }
  44. //名称导入
  45. else
  46. {
  47. PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME)
  48. (pThunk->u1.Ordinal + (DWORD)hBase);
  49. dwFunName = pImportByName->Name;
  50. }
  51. //获取每个函数的地址
  52. DWORD dwFunAddr = (DWORD)SysGetProcAddress(hModule, dwFunName);
  53. // **加密函数地址**
  54. dwFunAddr ^= 0x13973575;
  55. LPVOID AllocMem = (PDWORD)MyVirtualAlloc(NULL, 0x20, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  56. //构造一段花指令解密的ShellCode
  57. byte OpCode[] = { 0xe8, 0x01, 0x00, 0x00,
  58. 0x00, 0xe9, 0x58, 0xeb,
  59. 0x01, 0xe8, 0xb8, 0x8d,
  60. 0xe4, 0xd8, 0x62, 0xeb,
  61. 0x01, 0x15, 0x35, 0x75,
  62. 0x35, 0x97, 0x13, 0xeb,
  63. 0x01, 0xff, 0x50, 0xeb,
  64. 0x02, 0xff, 0x15, 0xc3 };
  65. //把dwFunAddr写入到解密的ShellCode中
  66. OpCode[11] = dwFunAddr;
  67. OpCode[12] = dwFunAddr >> 0x8;
  68. OpCode[13] = dwFunAddr >> 0x10;
  69. OpCode[14] = dwFunAddr >> 0x18;
  70. //拷贝数据到申请的内存
  71. MyRtlMoveMemory(AllocMem, OpCode, 0x20);
  72. //修改保护属性
  73. DWORD dwProtect = 0;
  74. MyVirtualProtect(IAT, 4, PAGE_EXECUTE_READWRITE, &dwProtect);
  75. //把获取到的加密函数地址填充在导入地址表里面
  76. *(IAT) = (DWORD)AllocMem;
  77. MyVirtualProtect(IAT, 4, dwProtect, &dwProtect);
  78. ++IAT;
  79. ++pThunk;
  80. }
  81. }
  82. ++pImport;
  83. }
  84. }
复制代码

三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. #include "pch.h"
  2. #include
  3. int main()
  4. {
  5. while (true)
  6. {
  7. //用于保存Hash值
  8. unsigned short nHash = 0;
  9. char arr[50] = {}, *p;
  10. p = arr;
  11. printf("请输入API: ");
  12. scanf_s("%s", arr, 50);
  13. while (*p)
  14. {
  15. //先左移11位再右移5位相加后再加上该字符的Ascii
  16. nHash = ((nHash > 5));
  17. nHash = nHash + *p;
  18. p++;
  19. }
  20. printf("Hash值为:0x%X\n", nHash);
  21. }
  22. return 0;
  23. }
复制代码
使用方法是首先使用上述代码对我们需要使用API函数进行Hash加密得到Hash值,然后再写一个Hash值对比字符串的函数(解密),使用该值和系统中的API函数对比,和谁相等,我们就把这个函数的地址获取取出。这样我们就隐晦的得到了所需的函数的地址。
Hash解密代码如下,需要传入2个参数,1是对比函数的地址,2是Hash值:
  1. /****************Hash对比************************************/
  2. _Hash_CmpString://(char* strFunName, int nDigest)
  3. push ebp;
  4. mov ebp, esp;
  5. sub esp, 0x4;
  6. push ebx;
  7. push ecx;
  8. push edx;
  9. mov dword ptr[ebp - 0x4], 0;
  10. xor eax, eax;
  11. xor ecx, ecx;
  12. mov esi, [ebp + 0x8];//strFunName
  13. _Start:
  14. mov al, [esi + ecx];
  15. test al, al;
  16. jz _End;
  17. mov edi, [ebp - 0x4];
  18. shl edi, 0xb; //11的16进制为b
  19. mov edx, [ebp - 0x4];
  20. shr edx, 0x5;
  21. or edi, edx;
  22. add edi, eax;
  23. mov[ebp - 0x4], edi;
  24. inc ecx;
  25. jmp _Start;
  26. _End:
  27. mov esi, [ebp + 0xc];//获取hash值
  28. and edi, 0xffff; //取低16位
  29. cmp edi, esi;//对比hash
  30. mov eax, 0x1;
  31. je _Over;
  32. xor eax, eax;
  33. _Over:
  34. pop edx;
  35. pop ecx;
  36. pop ebx;
  37. mov esp, ebp;
  38. pop ebp;
  39. ret 0x8;
复制代码
上述代码有大大的优化空间,比较懒我就不弄了。

有了Hash加解密,就可以自己实现一个GetProcAddress函数了,在这之后需要获取任何API函数就用自己实现的GetProcAddress函数,这样就是达到更加隐蔽的获取API函数的目的,学会了Hash加解密咱也就脱离了小白的行列了。
代码如下,参数1是所需API的模块基址,参数2是Hash值:(纯汇编获取更能锻炼基本功!)
  1. //自写的GetProcAddress
  2. DWORD MyGetProcAddress(HMODULE hModule, int nDigest)
  3. {
  4. DWORD GetProcAddr = 0;
  5. __asm
  6. {
  7. jmp _Start_Fun;
  8. /*****************自写的GetProcAddress*************************************/
  9. _Fun_GetProcAddress://(dword ImageBase,int nDiegst)
  10. push ebp;
  11. mov ebp, esp;
  12. sub esp, 0xc;
  13. push edx;//保存寄存器
  14. push ebx;
  15. mov edx, [ebp + 0x8]; //DLL基地址如kernel32
  16. mov esi, [edx + 0x3c]; //Dos头的e_lfanew
  17. lea esi, [edx + esi]; //PE头VA(NT头)
  18. mov esi, [esi + 0x78]; //Import表的RVA
  19. lea esi, [edx + esi]; //Import表的VA
  20. mov edi, [esi + 0x1c]; //EAT的RVA .AddressOfFunctions
  21. lea edi, [edx + edi]; //EAT的VA
  22. mov[ebp - 0x4], edi; //EAT的VA保存到局部变量1
  23. mov edi, [esi + 0x20]; //ENT的RVA AddressOfNames
  24. lea edi, [edx + edi]; //ENT的VA
  25. mov[ebp - 0x8], edi; //ENT的VA->Local2
  26. mov edi, [esi + 0x24]; //EOT的RVA
  27. lea edi, [edx + edi]; //EOT的VA
  28. mov[ebp - 0xc], edi; //EOT的VA->Local3
  29. xor ecx, ecx;
  30. jmp _First;
  31. _Begin:
  32. inc ecx;
  33. _First:
  34. mov esi, [ebp - 0x8];//ENT
  35. mov esi, [esi + ecx * 4];//EN RVA
  36. lea esi, [edx + esi];//EN VA
  37. push[ebp + 0xc];
  38. push esi;
  39. call _Hash_CmpString;
  40. test eax, eax;
  41. jz _Begin;//不是则循环
  42. mov esi, [ebp - 0xc];//EOT
  43. xor ebx, ebx;
  44. mov bx, [esi + ecx * 2];//函数所对应的序号
  45. mov esi, [ebp - 0x4];//EAT
  46. mov esi, [esi + ebx * 4];//EA RVA
  47. lea eax, [edx + esi];//函数的地址
  48. pop ebx;
  49. pop edx;
  50. mov esp, ebp;
  51. pop ebp;
  52. retn 0x8;
  53. /****************Hash对比************************************/
  54. _Hash_CmpString://(char* strFunName, int nDigest)
  55. push ebp;
  56. mov ebp, esp;
  57. sub esp, 0x4;
  58. push ebx;
  59. push ecx;
  60. push edx;
  61. mov dword ptr[ebp - 0x4], 0;
  62. xor eax, eax;
  63. xor ecx, ecx;
  64. mov esi, [ebp + 0x8];//strFunName
  65. _Start:
  66. mov al, [esi + ecx];
  67. test al, al;
  68. jz _End;
  69. mov edi, [ebp - 0x4];
  70. shl edi, 0xb;
  71. mov edx, [ebp - 0x4];
  72. shr edx, 0x5;
  73. or edi, edx;
  74. add edi, eax;
  75. mov[ebp - 0x4], edi;
  76. inc ecx;
  77. jmp _Start;
  78. _End:
  79. mov esi, [ebp + 0xc];//获取hash值
  80. and edi, 0xffff;
  81. cmp edi, esi;//对比hash
  82. mov eax, 0x1;
  83. je _Over;
  84. xor eax, eax;
  85. _Over:
  86. pop edx;
  87. pop ecx;
  88. pop ebx;
  89. mov esp, ebp;
  90. pop ebp;
  91. ret 0x8;
  92. _Start_Fun:
  93. pushad;
  94. push nDigest;
  95. push hModule;
  96. call _Fun_GetProcAddress;
  97. mov GetProcAddr, eax;
  98. popad;
  99. }
  100. return GetProcAddr;
  101. }
复制代码
动态获取函数例子(下面的Hash值是乱填的,意思意思下):
  1. MyLoadLibraryExA = (FuLoadLibraryExA)MyGetProcAddress(g_hKernel32, 0xC0D8);
  2. g_hUser32 = MyLoadLibraryExA("user32.dll", 0, 0);
  3. MyMessageBoxW = (FuMessageBoxW)MyGetProcAddress(g_hUser32, 0x1E38);
复制代码

四动态解密
加入动态解密的壳,这无疑是强度较高的壳了,它能够在目标程序运行起来之后,动态的对代码段进行解密。先运行一段代码解密后一部分的代码,然后再运行解密后的代码,可以往复循环,这样破解者只能看见运行着的代码的附近的代码,隔得远的代码处于加密状态,这样就需要花费大量的时间才能破解了,当然想要实现这种高强度,还是需要花费很多时间去设计的,而且要求我们对x86汇编语言有比较深刻理解,这我就分享下我对动态解密理解。

下面我直接根据一个案列来分析动态解密流程:
为了方便演示效果,我在VS中用汇编以动态获取API方式写了一段功能是弹窗的代码,效果如图:


将生成EXE文件用0x32dbg(或者OD)打开后找到弹窗功能的汇编代码,扣取出该段代码16进制字节。扣取字节的操作是在x32dbg中选中该段代码后,右键->复制->数据->C样式ShellCode字符串。



OD中选中代码后右键->数据转换->C++->字节。



把这些字节存在一个字符串数组中,直接用我这个就行了:
  1. char ShellCode[] = "\x60\x83\xEC\x60\xEB\x55\x4D\x65\x73\x73\x61\x67\x65\x42\x6F\x78\x41\x00\x45\x78\x69\x74\x50\x72\x6F\x63\x65\x73\x73\x00\x4C\x6F\x61\x64\x4C\x69\x62\x72\x61\x72\x79\x45\x78\x41\x00\x47\x65\x74\x50\x72\x6F\x63\x41\x64\x64\x72\x65\x73\x73\x00\x75\x73\x65\x72\x33\x32\x2E\x64\x6C\x6C\x00\x48\x65\x6C\x6C\x6F\x20\x47\x72\x65\x61\x74\x20\x4E\x61\x74\x75\x72\x65\x21\x00\xD9\xEE\xD9\x74\x24\xF4\x5A\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76\x1C\x8B\x36\x8B\x5E\x08\x52\x53\xE8\x5A\x00\x00\x00\x8B\xC8\x51\x52\x8D\x42\xC3\x50\x53\xFF\xD1\x5A\x59\x52\x50\x51\x53\xE8\x01\x00\x00\x00\x61\x55\x8B\xEC\x83\xEC\x0C\x8B\x55\x14\x33\xC9\x8D\x72\xE1\x51\x51\x56\xFF\x55\x10\x8B\x55\x14\x8D\x4A\xAB\x51\x50\xFF\x55\x0C\x33\xC9\x8B\x55\x14\x8D\x5A\xEC\x51\x53\x53\x51\xFF\xD0\x8B\x55\x14\x8D\x72\xB7\x56\xFF\x75\x08\xFF\x55\x0C\x51\xFF\xD0\x8B\xE5\x5D\xC2\x10\x00\x55\x8B\xEC\x83\xEC\x0C\x52\x53\x8B\x55\x08\x8B\x72\x3C\x8D\x34\x32\x8B\x76\x78\x8D\x34\x32\x8B\x7E\x1C\x8D\x3C\x3A\x89\x7D\xFC\x8B\x7E\x20\x8D\x3C\x3A\x89\x7D\xF8\x8B\x7E\x24\x8D\x3C\x3A\x89\x7D\xF4\x33\xC0\xEB\x01\x40\x8B\x75\xF8\x8B\x34\x86\x8D\x34\x32\x8B\x5D\x0C\x8D\x7B\xD2\xB9\x0E\x00\x00\x00\xF3\xA6\x75\xE7\x8B\x75\xF4\x33\xDB\x66\x8B\x1C\x46\x8B\x75\xFC\x8B\x34\x9E\x8D\x04\x32\x5B\x5A\x8B\xE5\x5D\xC2\x08\x00";
复制代码
在0x32dbg(或者OD)中选中汇编代码段后右下角会显示选中的字节大小。



下面写一个加密该字符串的代码,编译的时候VS项目属性中配置“C/C++ -> 代码生成 -> 安全检查(禁用GS)”,“连接器 -> 高级 -> 数据执行保护DEP(关闭)”。
  1. //加密函数
  2. void Encoder(char* pData, int nSize)
  3. {
  4. //加密密钥
  5. int nOutKey = 0x15;
  6. //加密后的缓冲区
  7. unsigned char* pBuffer = NULL;
  8. pBuffer = (unsigned char*)new char[nSize + 1];
  9. //对每个字节进行加密
  10. for (int j = 0; j < nSize; j++)
  11. {
  12. pBuffer[j] = pData[j] ^ nOutKey;
  13. }
  14. //打印出每个字节
  15. for (int i = 0; i < nSize; i++)
  16. {
  17. printf("\\x%02X",pBuffer[i]);
  18. }
  19. }
  20. int main()
  21. {
  22. //调用ShellCode查看是否能正常运行
  23. __asm {
  24. lea eax, ShellCode;
  25. push eax;
  26. ret;
  27. }
  28. //加密
  29. Encoder(ShellCode, 257);
  30. getchar();
  31. }
复制代码
加密后会得到一个字节数组,我们把它复制下来存到另一个数组中,然后把他放在解密代码的屁股后面。
开始写解密代码,解密代码才是动态解密中的核心点,重中之重。
这里要说一下GetPC技术,GetPC技术翻译为中文也就是获取指针计数器。在x86汇编中实际上就是获取当前代码EIP的技术。我这用的是call
指令,call  xxx指令相当于 push 下一行代码的EIP + jmp xxx。那么我们直接把XXX改为下一行指令的地址就能获取当前EIP 内联汇编代码为:
  1. //解密函数
  2. __asm {
  3. call _Next;//跳到下一行,并把EIP压入栈
  4. _Next:
  5. pop eax;//获得当前EIP
  6. lea esi, [eax + 0x22];//生成后在OD中查看上一行到最后一行解密代码的长度
  7. xor ecx, ecx;
  8. mov cx, 0x13e;//要解密的字节长度
  9. _DeCode:
  10. mov al, byte ptr ds : [esi + ecx];
  11. xor al, 0x15; //解密密钥
  12. mov byte ptr ds : [esi + ecx], al;
  13. loop _DeCode;
  14. xor[esi + ecx], 0x15;
  15. jmp esi;
  16. }
复制代码
这里要注意的是这段代码后面要紧跟加密后的代码。
在实际的壳代码中,先把需要加密的代码加密后和解密代码组装起来就可以达到动态解密的功能。




- End -



看雪ID:九阳道人    
https://bbs.pediy.com/user-847228.htm


本文由看雪论坛 九阳道人 原创
转载请注明来自看雪社区

热门图书推荐戳
立即购买!



热门文章阅读
1、Art 模式 实现Xposed Native注入
2、base64简单逆向分析(下)
3、使用 RouterSploit 攻击路由器的方法介绍




公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com


↙点击下方“阅读原文”,查看更多干货
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:55
帖子:11
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP