查看原文
其他

实现一个压缩壳,并给它加点“料”

橘喵Cat 看雪学苑 2022-09-06


本文为看雪论坛优秀文章

看雪论坛作者ID:橘喵Cat





前言


实现压缩壳,必须对PE格式十分熟悉,其次,解压缩代码需要编写shellcode,也是十分麻烦的环节。有了两者的结合,我们才能写好一个真正的压缩壳。





设计思路


首先上一张图,让大家直观地感受到一个壳程序是如何运行起来的。


左边是壳PE,壳程序有一个PE头,节表1是空节,用来存放解压缩后的原程序PE,节表2此时存储的是压缩后的原PE。节表3则是壳代码节,壳PE运行起来后,首先就是进入入口点,运行节表3的代码,解压缩节表2,然后将结果覆盖PE头+节表1的位置,修复完导入表、重定位表,jmp到原程序的入口点处即可。

原理不变,我这里加了点“料”,新增了节4和节5,存储了相关的信息,让压缩壳的脱壳过程变难,往后看就知道了。





壳代码实现


1.为了生成一个新的壳PE,我们一步步来,首先是PE头,俗话说靠山吃山,靠水吃水,这个PE头我就直接拿原程序的PE头来代替了,只不过需要改一些数据:

NumberOfSections --节表数量,要改为5AddressOfEntryPoint --入口点,要改为节表3里的代码入口SizeOfImage --PE在内存中的大小,要改为新的PE的内存大小pSecHdr --节表头,要拓展为5个节表 void CPacker::GetNewPeHdr(){ //拷贝原PE的PE头 m_dwNewPeHdrSize = m_pNtHdr->OptionalHeader.SizeOfHeaders; m_pNewPeHdr = new BYTE[m_dwNewPeHdrSize]; CopyMemory(m_pNewPeHdr, m_pDosHdr, m_dwNewPeHdrSize); //修改 auto pDosHdr = (PIMAGE_DOS_HEADER)m_pNewPeHdr; auto pNtHdr = (PIMAGE_NT_HEADERS)(m_pNewPeHdr + pDosHdr->e_lfanew); auto pSecHdr = (PIMAGE_SECTION_HEADER) ((LPBYTE)&pNtHdr->OptionalHeader + pNtHdr->FileHeader.SizeOfOptionalHeader); pNtHdr->FileHeader.NumberOfSections = 5; pNtHdr->OptionalHeader.AddressOfEntryPoint = m_newSecHdr[2].VirtualAddress; pNtHdr->OptionalHeader.SizeOfImage = m_newSecHdr[4].VirtualAddress + m_newSecHdr[4].Misc.VirtualSize; //清空DataDirectory目录 ZeroMemory(pNtHdr->OptionalHeader.DataDirectory, sizeof(pNtHdr->OptionalHeader.DataDirectory)); //修改新的节表头 CopyMemory(pSecHdr, m_newSecHdr, sizeof(m_newSecHdr)); }


2.新PE头里一些需要修改的数据,比如SizeOfImage,我们目前还没有,需要等我们构造出节表1、2、3、4、5之后,才知道。接下来,先构造节表1的表头,节表1是个空节,它的大小只要够存放原程序的节表即可,多给一点也没关系,我这里直接给了SizeOfImage。(由于是空节,所以这里并不需要考虑节表1的数据内容)

//空节 strcpy((char*)m_newSecHdr[0].Name, ".cr42"); m_newSecHdr[0].Misc.VirtualSize = m_pNtHdr->OptionalHeader.SizeOfImage; m_newSecHdr[0].VirtualAddress = m_pSecHdr[0].VirtualAddress; m_newSecHdr[0].SizeOfRawData = 0; m_newSecHdr[0].PointerToRawData = 0; m_newSecHdr[0].Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;


3.接着是节表2,这个节要存放原PE的压缩数据,先设计表头(使用了PointerToRelocations和PointerToLinenumbers这两个没啥用的字段,存放压缩大小信息,留着给后面shellcode用)。

//压缩数据节 strcpy((char*)m_newSecHdr[1].Name, ".data"); m_newSecHdr[1].Misc.VirtualSize = GetAlign(m_dwComSecSize, m_pNtHdr->OptionalHeader.SectionAlignment); m_newSecHdr[1].VirtualAddress = m_newSecHdr[0].VirtualAddress + m_newSecHdr[0].Misc.VirtualSize; m_newSecHdr[1].SizeOfRawData = m_dwComSecSize; m_newSecHdr[1].PointerToRawData = m_pNtHdr->OptionalHeader.SizeOfHeaders; m_newSecHdr[1].Characteristics = IMAGE_SCN_MEM_READ; m_newSecHdr[1].PointerToRelocations = m_dwComSize; //压缩后大小 m_newSecHdr[1].PointerToLinenumbers = m_dwSrcPeSize;//压缩前大小


然后将PE压缩,压缩前我把节表、导入表、重定位表保存并清空,到时候由shellcode进行还原。

bool CPacker::GetCompressData(){ COMPRESSOR_HANDLE hCompressor = NULL; BOOL Success = CreateCompressor( COMPRESS_ALGORITHM_XPRESS_HUFF, NULL, &hCompressor ); m_pComData = new BYTE[m_dwSrcPeSize + 0x28]; LPBYTE m_pSrcPeTmp = new BYTE[m_dwSrcPeSize]; CopyMemory(m_pSrcPeTmp, m_pSrcPe, m_dwSrcPeSize); PIMAGE_DOS_HEADER m_pDosHdrTmp = (PIMAGE_DOS_HEADER)m_pSrcPeTmp; PIMAGE_NT_HEADERS m_pNtHdrTmp = (PIMAGE_NT_HEADERS)(m_pSrcPeTmp + m_pDosHdrTmp->e_lfanew); PIMAGE_SECTION_HEADER m_pSecHdrTmp = (PIMAGE_SECTION_HEADER) ((LPBYTE)&m_pNtHdrTmp->OptionalHeader + m_pNtHdrTmp->FileHeader.SizeOfOptionalHeader); //1.在压缩前,把节表保存并清空 nSecNum = m_pNtHdrTmp->FileHeader.NumberOfSections; CopyMemory(m_pSaveSecHdr, m_pSecHdrTmp, nSecNum * 40); ZeroMemory(m_pSecHdrTmp, nSecNum * 40); //2.把导入表保存并清空 m_pImportAddr = m_pNtHdrTmp->OptionalHeader.DataDirectory[1].VirtualAddress; nImpSize = m_pNtHdrTmp->OptionalHeader.DataDirectory[1].Size; ZeroMemory(&(m_pNtHdrTmp->OptionalHeader.DataDirectory[1]), 8); //3.把重定位表保存并清空 m_pRelocAddr = m_pNtHdrTmp->OptionalHeader.DataDirectory[5].VirtualAddress; nRelocSize = m_pNtHdrTmp->OptionalHeader.DataDirectory[5].Size; ZeroMemory(&(m_pNtHdrTmp->OptionalHeader.DataDirectory[5]), 8); Success = Compress( hCompressor, m_pSrcPeTmp, m_dwSrcPeSize, m_pComData, m_dwSrcPeSize + 0x28, &m_dwComSize ); return true;}


4.接着是节表3,该节表存放的是解压缩PE、还原导入表、重定位表,运行原程序的至关重要的shellcode,先设计表头。

//代码节 strcpy((char*)m_newSecHdr[2].Name, ".text"); m_newSecHdr[2].Misc.VirtualSize = GetAlign(m_dwCodeSecSize, m_pNtHdr->OptionalHeader.SectionAlignment); m_newSecHdr[2].VirtualAddress = m_newSecHdr[1].VirtualAddress + m_newSecHdr[1].Misc.VirtualSize; m_newSecHdr[2].SizeOfRawData = m_dwCodeSecSize; m_newSecHdr[2].PointerToRawData = m_newSecHdr[1].PointerToRawData + m_newSecHdr[1].SizeOfRawData; m_newSecHdr[2].Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;


至于节表3的数据内容,也就是shellcode,等我把节表、PE设计完,我再说。


5.接着是节表4,节表4是我额外增加的一个,用来存储步骤3中保存的原节表表头、原导入表、原重定位表,首先设计节表4表头。

//存放原节表、导入表、重定位表的节 strcpy((char*)m_newSecHdr[3].Name, ".info"); m_newSecHdr[3].Misc.VirtualSize = GetAlign(m_dwTableSecSize, m_pNtHdr->OptionalHeader.SectionAlignment); m_newSecHdr[3].VirtualAddress = m_newSecHdr[2].VirtualAddress + m_newSecHdr[2].Misc.VirtualSize; m_newSecHdr[3].SizeOfRawData = m_dwTableSecSize; m_newSecHdr[3].PointerToRawData = m_newSecHdr[2].PointerToRawData + m_newSecHdr[2].SizeOfRawData; m_newSecHdr[3].Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;


然后把刚才保存的原节表表头、原导入表、原重定位表信息,按顺序写入到缓冲区里。

bool CPacker::GetTable(){ m_dwTableSize = nSecNum * 40 + 4 + 8 + 8; m_pTable = new BYTE[m_dwTableSize]; RtlCopyMemory(m_pTable, &nSecNum, 4); RtlCopyMemory(m_pTable + 4, m_pSaveSecHdr, nSecNum * 40); RtlCopyMemory(m_pTable + 4 + nSecNum * 40, &m_pImportAddr, 4); RtlCopyMemory(m_pTable + 4 + nSecNum * 40 + 4, &nImpSize, 4); RtlCopyMemory(m_pTable + 4 + nSecNum * 40 + 4 + 4, &m_pRelocAddr, 4); RtlCopyMemory(m_pTable + 4 + nSecNum * 40 + 4 + 4 + 4, &nRelocSize, 4); return true;}


6.还剩最后一个节表5,是一个空节,壳代码还原原PE时,要还原导入表,于是这个节的作用就体现出来了,shellcode在这里玩了一波偷梁换柱,直接毙掉了x64dbg的脱壳后导入表自动修复功能,等会介绍shellcode的时候你们就知道了。设计节表5的表头:

//绕过x64搜索导入表的节(空节) strcpy((char*)m_newSecHdr[4].Name, ".imp"); m_newSecHdr[4].Misc.VirtualSize = GetAlign(0x10000, m_pNtHdr->OptionalHeader.SectionAlignment); m_newSecHdr[4].VirtualAddress = m_newSecHdr[3].VirtualAddress + m_newSecHdr[3].Misc.VirtualSize; m_newSecHdr[4].SizeOfRawData = 0; m_newSecHdr[4].PointerToRawData = m_newSecHdr[3].PointerToRawData + m_newSecHdr[2].SizeOfRawData; m_newSecHdr[4].Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;


至此,新的壳PE结构我们就设计好了,然后将新PE头、节表234写入到新文件(节表1、5是空节,不用写入),就大功告成,加壳完毕!

bool CPacker::WriteNewPe(CString strNewPe){ //创建文件 HANDLE hFile = CreateFile(strNewPe, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); //写入PE头 DWORD dwBytesWrited = 0; WriteFile(hFile, m_pNewPeHdr, m_dwNewPeHdrSize, &dwBytesWrited, NULL); //写入数据节 WriteFile(hFile, m_pComSec, m_dwComSecSize, &dwBytesWrited, NULL); //写入代码节 WriteFile(hFile, m_pCodeSec, m_dwCodeSecSize, &dwBytesWrited, NULL); //写入存放表数据节 WriteFile(hFile, m_pTableSec, m_dwTableSecSize, &dwBytesWrited, NULL); CloseHandle(hFile); return true;}





shellcode代码实现


由于壳PE可能是随机基址,所以执行shellcode时,一定要确保它的代码跟地址无关。我的shellcode是在VS上编译的,为了保证VS不会生成多余的代码,要修改以下几个设置:

1.使用Release版(Debug会加地址有关代码)2.不使用main函数,自己定义一个,放在链接器->高级->入口点(main不是程序真正入口点)3.关掉代码生成->安全检查4.关掉增强指令集5.关掉全程序优化


然后就是最麻烦的shellcode代码编写了。首先要清楚shellcode代码的功能:

1.解压缩节表2里面的压缩数据2.将解压缩数据覆盖到PE头处,连带着空节表1也被覆盖3.还原节表、导入表、重定位表


1.要解压缩,那么必定要调用库的API,如果直接调用的话,call的就是死地址,违背了地址无关原则。那么使用LoadLibrary然后GetProcAddress?显然也不行,LoadLibrary和GetProcAddress也是死地址,所以我们要自己实现LoadLibrary和GetProcAddress的功能。


首先通过_PEB来拿到kernel32的模块基址。

HMODULE GetKernel32(){ HMODULE hKer; __asm { mov eax, dword ptr fs:[0x30] mov eax, dword ptr[eax + 0x0C] mov eax, dword ptr[eax + 0x0C] mov eax, dword ptr[eax] mov eax, dword ptr[eax] mov eax, dword ptr[eax + 0x18] mov hKer, eax } return hKer;}


然后通过kernel32的导出函数表,拿到GetProcAddress函数地址,代码如下:

FARPROC MyGetProcAddress(HMODULE hMod, LPCSTR lpProcName) { IMAGE_DOS_HEADER* pDosHdr; IMAGE_NT_HEADERS* pNTHdr; IMAGE_EXPORT_DIRECTORY* pExpDir; DWORD pAddrTbl; DWORD pNameTbl; DWORD pOrdTbl; //解析dos头 pDosHdr = (IMAGE_DOS_HEADER*)hMod; //nt头 pNTHdr = (IMAGE_NT_HEADERS*)(pDosHdr->e_lfanew + (DWORD)hMod); //获取导出表 pExpDir = (IMAGE_EXPORT_DIRECTORY*)(pNTHdr->OptionalHeader.DataDirectory[0].VirtualAddress + (DWORD)hMod); //导出函数地址表 pAddrTbl = (DWORD)(pExpDir->AddressOfFunctions + (DWORD)hMod); //导出函数名称表 pNameTbl = (DWORD)(pExpDir->AddressOfNames + (DWORD)hMod); //导出序号表 pOrdTbl = (DWORD)(pExpDir->AddressOfNameOrdinals + (DWORD)hMod); //判断是序号还是名称 if ((int)lpProcName & 0xffff0000) { //名称 int i = 0; while (i < pExpDir->NumberOfNames) { //获取名称地址 int nNameOff = (int)(*(DWORD*)(pNameTbl + i * 4) + (DWORD)hMod); //字符串比较 if (((char*)nNameOff)[0] == 'G'&& ((char*)nNameOff)[1] == 'e'&& ((char*)nNameOff)[2] == 't'&& ((char*)nNameOff)[3] == 'P'&& ((char*)nNameOff)[4] == 'r'&& ((char*)nNameOff)[5] == 'o'&& ((char*)nNameOff)[6] == 'c'&& ((char*)nNameOff)[7] == 'A'&& ((char*)nNameOff)[8] == 'd'&& ((char*)nNameOff)[9] == 'd'&& ((char*)nNameOff)[10] == 'r'&& ((char*)nNameOff)[11] == 'e'&& ((char*)nNameOff)[12] == 's'&& ((char*)nNameOff)[13] == 's') { //找到了, 从导出序号表取出函数地址下标 int nOrdinal = *(WORD*)(pOrdTbl + i * 2); //从导出地址表,下标寻址,获取导出函数地址 int nFuncAddr = *(DWORD*)(pAddrTbl + nOrdinal * 4); //不是转发 nFuncAddr += (int)hMod; //返回地址 if (nFuncAddr != NULL) { return (FARPROC)nFuncAddr; } } i++; } } else { //序号 int nOrdinal = (DWORD)lpProcName - pExpDir->Base; //从导出地址表,下标寻址,获取导出函数地址 int nFuncAddr = *(DWORD*)(pAddrTbl + nOrdinal * 4); //返回地址 if (nFuncAddr != NULL) { return (FARPROC)(nFuncAddr + (DWORD)hMod); } } return 0;}


有了GetProcAddress函数地址和kernel32的基址,同理就能拿到LoadLibrary函数地址了。(注意定义字符串变量时,用单个字符一个一个排列,在汇编里面看就是db出来的,否则字符串会有一个常量区地址,影响shellcode的通用性)

char szLoadLibrary[] = { 'L', 'o','a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', '\0' }; pEnv->pfnLoadLibraryA = (PFN_LoadLibraryA)pEnv->pfnGetProcAddress(hKer, szLoadLibrary);


2.有了LoadLibrary和GetProcAddress,就可以使用任何库函数了,解压缩便是小菜一碟。

//有了LoadLibrary和GetProcAddress,就可以使用任意函数了 //获取解压缩相关函数 char szCab[] = { 'C','a','b','i','n','e','t', '\0' }; HMODULE hCab = pEnv->pfnLoadLibraryA(szCab); char szCreateDecompressor[] = { 'C','r','e','a','t','e','D','e','c','o','m','p','r','e','s','s','o','r', '\0' }; pEnv->pfnCreateDecompressor = (PFN_CreateDecompressor)pEnv->pfnGetProcAddress(hCab, szCreateDecompressor); char szDecompress[] = { 'D','e','c','o','m','p','r','e','s','s', '\0' }; pEnv->pfnDecompress = (PFN_Decompress)pEnv->pfnGetProcAddress(hCab, szDecompress); char szVirtualAlloc[] = { 'V','i','r','t','u','a','l','A','l','l','o','c', '\0' }; pEnv->pfnVirtualAlloc = (PFN_VirtualAlloc)pEnv->pfnGetProcAddress(hKer, szVirtualAlloc); char szVirtualProtect[] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t','\0' }; pEnv->pfnVirtualProtect = (PFN_VirtualProtect)pEnv->pfnGetProcAddress(hKer, szVirtualProtect); //解压缩 LPBYTE pPEBuff = (LPBYTE)env.pfnVirtualAlloc(NULL, dwDeComSize, MEM_COMMIT, PAGE_READWRITE); DECOMPRESSOR_HANDLE hDecompressor; BOOL bSuccess = env.pfnCreateDecompressor( COMPRESS_ALGORITHM_XPRESS_HUFF, NULL, &hDecompressor ); DWORD dwDecompressedBufferSize = 0; bSuccess = env.pfnDecompress( hDecompressor, pComData, dwComSize, pPEBuff, dwDeComSize, &dwDecompressedBufferSize );


3.解压缩完毕,得到了原PE,接下来就是将原PE覆盖到现在的PE头+节表1的地方,同时还原节表头、导入表和重定位表,相当于LoadPE的功能了。

#define VirtualProtect pEnv->pfnVirtualProtectDWORD MyLoadLibrary(LPBYTE pPEBuff, Environment* pEnv, LPBYTE pTableBuf, LPBYTE pMyImpBuf) { DWORD dwImageBase; HANDLE hFile; HANDLE hFileMap; LPVOID pPEBuf; IMAGE_DOS_HEADER* pDosHdr; IMAGE_NT_HEADERS* pNTHdr; IMAGE_SECTION_HEADER* pSecHdr; DWORD dwNumOfSecs; IMAGE_IMPORT_DESCRIPTOR* pImpHdr; DWORD dwSizeOfHeaders; IMAGE_IMPORT_DESCRIPTOR hdrZeroImp; HMODULE hDll; DWORD dwOep; DWORD dwOldProc; IMAGE_BASE_RELOCATION* pReloc; DWORD dwOfReloc; DWORD dwOff; //RtlZeroMemory(&hdrZeroImp, sizeof(IMAGE_IMPORT_DESCRIPTOR)); pPEBuf = pPEBuff; //解析 //dos 头 pDosHdr = (IMAGE_DOS_HEADER*)pPEBuf; //nt头 pNTHdr = (IMAGE_NT_HEADERS*)(pDosHdr->e_lfanew + (DWORD)pPEBuf); //还原节表 DWORD nSecNum = *(DWORD*)pTableBuf; mymemcpy((void*)((DWORD)&pNTHdr->OptionalHeader + pNTHdr->FileHeader.SizeOfOptionalHeader), pTableBuf + 4, nSecNum * 40); //还原导入表 mymemcpy(&(pNTHdr->OptionalHeader.DataDirectory[1].VirtualAddress), pTableBuf + 4 + nSecNum * 40, 4); mymemcpy(&(pNTHdr->OptionalHeader.DataDirectory[1].Size), pTableBuf + 4 + nSecNum * 40 + 4, 4); //还原重定位表 mymemcpy(&(pNTHdr->OptionalHeader.DataDirectory[5].VirtualAddress), pTableBuf + 4 + nSecNum * 40 + 4 + 4, 4); mymemcpy(&(pNTHdr->OptionalHeader.DataDirectory[5].Size), pTableBuf + 4 + nSecNum * 40 + 4 + 4 + 4, 4); //选项头信息 dwSizeOfHeaders = pNTHdr->OptionalHeader.SizeOfHeaders; //自己的模块基址 dwImageBase = (DWORD)GetModuleBase(); dwOff = dwImageBase - pNTHdr->OptionalHeader.ImageBase; //新旧ImageBase的偏移差 dwOep = pNTHdr->OptionalHeader.AddressOfEntryPoint + dwImageBase; //节表 dwNumOfSecs = pNTHdr->FileHeader.NumberOfSections; pSecHdr = (IMAGE_SECTION_HEADER*)((DWORD)&pNTHdr->OptionalHeader + pNTHdr->FileHeader.SizeOfOptionalHeader); //拷贝PE头 VirtualProtect((LPVOID)dwImageBase, pNTHdr->OptionalHeader.SizeOfHeaders, PAGE_EXECUTE_READWRITE, &dwOldProc); mymemcpy((void*)dwImageBase, pPEBuf, dwSizeOfHeaders); VirtualProtect((LPVOID)dwImageBase, pNTHdr->OptionalHeader.SizeOfHeaders, dwOldProc, &dwOldProc); //按照节表,拷贝节区数据 int i = 0; IMAGE_SECTION_HEADER* dwSecTmp = pSecHdr; while (i < dwNumOfSecs) { //目标 DWORD dwDstMem = dwImageBase; dwDstMem += dwSecTmp->VirtualAddress; //源 DWORD dwSrcFile = (DWORD)pPEBuf + dwSecTmp->PointerToRawData; //拷贝 VirtualProtect((LPVOID)dwDstMem, dwSecTmp->SizeOfRawData, PAGE_EXECUTE_READWRITE, &dwOldProc); mymemcpy((void*)dwDstMem, (void*)dwSrcFile, dwSecTmp->SizeOfRawData); VirtualProtect((LPVOID)dwImageBase, pNTHdr->OptionalHeader.SizeOfHeaders, dwOldProc, &dwOldProc); i++; dwSecTmp = (IMAGE_SECTION_HEADER*)((char*)dwSecTmp + sizeof(IMAGE_SECTION_HEADER)); } //获取导入表 if (pNTHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress != 0) { pImpHdr = (IMAGE_IMPORT_DESCRIPTOR*)(dwImageBase + pNTHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); //处理导入表 IMAGE_IMPORT_DESCRIPTOR* pImpHdrTmp = pImpHdr; DWORD dwNum = 0; int nImpNum = 0; while (true) { //判断结束,全0项结束 if (memcmp(pImpHdrTmp, &hdrZeroImp, sizeof(IMAGE_IMPORT_DESCRIPTOR)) == 0) { break; } //判断字段, 为空则结束 if (pImpHdrTmp->Name == NULL || pImpHdrTmp->FirstThunk == NULL) { break; } //加载dll hDll = pEnv->pfnLoadLibraryA((LPCSTR)(dwImageBase + pImpHdrTmp->Name)); //获取导入地址表, IAT DWORD dwIAT = pImpHdrTmp->FirstThunk + dwImageBase; DWORD dwINT = dwIAT; //获取导入名称表, INT if (pImpHdrTmp->OriginalFirstThunk != NULL) { dwINT = pImpHdrTmp->OriginalFirstThunk + dwImageBase; } //遍历导入名称表 while (*(DWORD*)(dwINT) != 0) { if ((*(DWORD*)pImpHdrTmp) >> 31) { //序号导入, 获取序号 dwNum = *(DWORD*)pImpHdrTmp; dwNum = (dwNum << 16) >> 16; } else { //名称导入 dwNum = *(DWORD*)pImpHdrTmp; dwNum += dwImageBase; dwNum += 2; } //获取函数地址后,先不要把地址直接写入IAT //而是先将.imp节地址写入IAT(每16个字节写一次) //在.imp节里写代码指令push 函数地址 retn //如果函数名是_acmdln,那么这里就不要混淆导入表 if (((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[0] == '_' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[1] == 'a' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[2] == 'c' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[3] == 'm' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[4] == 'd' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[5] == 'l' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[6] == 'n') { *(DWORD*)dwIAT = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2)); } else { DWORD dwMyAddr = (DWORD)pMyImpBuf + nImpNum * 0x10; DWORD dwFuncAddr = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2)); *(DWORD*)dwIAT = dwMyAddr; *(BYTE*)dwMyAddr = 0x68; //push *(DWORD*)(dwMyAddr + 1) = dwFuncAddr; //真实函数地址 *(BYTE*)(dwMyAddr + 5) = 0xC3; //retn } dwIAT += 4; dwINT += 4; nImpNum++; } pImpHdrTmp = (IMAGE_IMPORT_DESCRIPTOR*)((char*)pImpHdrTmp + sizeof(IMAGE_IMPORT_DESCRIPTOR)); } } if (pNTHdr->OptionalHeader.DataDirectory[5].VirtualAddress != 0) { //定位重定位表 pReloc = (IMAGE_BASE_RELOCATION*)(pNTHdr->OptionalHeader.DataDirectory[5].VirtualAddress + dwImageBase); dwOfReloc = pNTHdr->OptionalHeader.DataDirectory[5].Size; int nSize = 0; while (nSize < dwOfReloc) { //数组首地址 int nOff = (DWORD)pReloc + 8; //数组元素个数 int nCnt = (pReloc->SizeOfBlock - 8) >> 1; //遍历数组 int j = 0; while (j < nCnt) { //取出一项 int nDataOff = *(WORD*)(nOff + j * 2); //判断是否是有效重定位项 if (nDataOff & 0x00003000) { //修正 nDataOff = nDataOff & 0x0fff; //页偏移 nDataOff = nDataOff + pReloc->VirtualAddress; nDataOff = nDataOff + dwImageBase; *(int*)nDataOff = *(int*)nDataOff + dwOff; } j++; } //处理下一个分页 nSize += pReloc->SizeOfBlock; pReloc = (IMAGE_BASE_RELOCATION*)((char*)pReloc + pReloc->SizeOfBlock); } } return dwOep;}


这里重点要介绍的,就是还原导入表的过程,也是整篇文章的核心主题,加点“料”。我在遍历还原导入表时,并没有直接将API的地址填入到IAT里,而是将节表5的地址,从起始位置开始,每隔16个字节,将地址填入到IAT里,然后在对应的节表5地址上填入push 真实函数地址 + retn的汇编指令。这样一来,原PE程序运行调用API时,就会跳到节表5里面,再从节表5里面跳到真实API地址,直接干掉了x64dbg的脱壳导入表自动修复功能。

//获取函数地址后,先不要把地址直接写入IAT //而是先将.imp节地址写入IAT(每16个字节写一次) //在.imp节里写代码指令push 函数地址 retn //如果函数名是_acmdln,那么这里就不要混淆导入表 if (((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[0] == '_' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[1] == 'a' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[2] == 'c' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[3] == 'm' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[4] == 'd' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[5] == 'l' && ((LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2))[6] == 'n') { *(DWORD*)dwIAT = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2)); } else { DWORD dwMyAddr = (DWORD)pMyImpBuf + nImpNum * 0x10; DWORD dwFuncAddr = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)((*(DWORD*)(dwINT)) + dwImageBase + 2)); *(DWORD*)dwIAT = dwMyAddr; *(BYTE*)dwMyAddr = 0x68; //push *(DWORD*)(dwMyAddr + 1) = dwFuncAddr; //真实函数地址 *(BYTE*)(dwMyAddr + 5) = 0xC3; //retn }




总结


最后来看看效果,对扫雷进行加壳,加壳后的程序可以正常运行。

先用x64找到真实入口点,进行一波dump操作。

dump后无法直接运行。

然后去x64里使用自动搜索修复导入表的功能,可以看到,IAT这里存放的压根就不是真实API的地址,所以x64也无法识别出来。

搞定!‍





看雪ID:橘喵Cat

https://bbs.pediy.com/user-home-940656.htm

*本文由看雪论坛 橘喵Cat 原创,转载请注明来自看雪社区



# 往期推荐

1.formbook脱壳记

2.CVE-2021-1732提权漏洞学习笔记

3.带加密字符串的.NET样本分析的一些技巧

4.对一个随身WIFI设备的漏洞挖掘尝试

5.[安全运维向]模拟搭建小型企业内网

6.某设备CoAP协议漏洞挖掘实战






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存