写在前面的话:
前面几篇文章,我们介绍了如何获取kernerl32.dll导出函数地址的方法;
并在此基础上,编写了ShellCode,实现了动态加载DLL以及解析API地址;
但是,似乎还称不上Perfect,我们能够获取到LoadLibrary和GetProcAddress,事情就结束了吗?
我们仍然需要给他们push一些个参数,那些API的名字,占用了我们ShellCode的大部分空间;(如果API较多的话)
这使得我们的ShellCode看上去不那么美妙,因此,对API做HASH势在必行;
那也许有朋友会问:做了HASH,总有一处还原的地方吧,如果不还原,那程序里就一定有字符串存在;否则,GetProcAddress怎么玩呢?
也因此,我们对Kernel32.dll导出表的解析,就需要一般化一下了;让它不止适应于kernel32.dll,而是windows下的任何32位的PE文件;
(64位类似,解析PE,都一样,笔者就拿32位举例了,有兴趣的朋友也可以自行解析)
如果能够做到,那我们的HASH才会有意义,因为,ENT里就有API名字了;
因此,在开始HASH运算前,我们先来搞一下之前的那部分程序;
零:导出表一般化解析
0. 先来看下PE的DOS头结构
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
我们需要关注的是最后一个字段,这个里的内容是NT头的偏移,首先,看下,这个字段在本结构体的偏移60(0x3C)
也就是说,[BaseAddr+0x3C]就是e_lfanew的值,因此,NT头的首地址BaseAddr+e_lfanew;
那再看下NT头的结构:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; // NT头中的文件头20Byte typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; // NT头中的扩展头 typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
我们要找什么呢,导出表的RVA,导出表是扩展头里的第0号元素;因此,计算出的数据目录表[导出表]相对NT头的偏移,就是0x78;
至此,我们通过分析DOS头和NT头结构,得到了下面的信息:
0、e_lfanew = [BaseAddr+0x3C]
1、NTStartVA:BaseAddr + e_lfnew
2、ExportStartRVA:[NTStartVA + 0x78]
3、ExportStartVA:BaseAddr + ExportStartRVA
到这一步,接下来就需要看下导出表的结构了
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;
需要的字段,EAT/ENT/EOT,我们在上边的分析中,其实已经得到了这个导出表结构体的首地址了,就是ExportStartVA,那就简单了
4、EATRVA = [ExportStartVA + 0x1C] -> EAT = BaseAddr + EATRVA
5、ENTRVA = [ExportStartVA + 0x20] -> ENT = BaseAddr + ENTRVA
6、EOTRVA = [ExportStartVA + 0x24] -> EOT = BaseAddr + EOTRVA
至此,分析结束,开始编写代码;
一、代码(为了便于理解,咱们封装成一些裸函数)
0、获取基址
void __declspec(naked) GetKernelBase() { _asm { push ebp; mov ebp, esp; sub esp, 0x0C; mov eax, fs:[0x30]; // PEB mov eax, [eax + 0xC]; // LDR mov eax, [eax + 0xC]; // InLoadOrderModuleList, exe mov eax, [eax]; // nt.dll mov eax, [eax]; // kernel32.dll mov eax, dword ptr ds : [eax + 0x18]; // BaseAddr; mov esp, ebp; pop ebp; ret; } }
1、解析导出表,部分关键代码(全部代码,考虑下,还是在我们写完HASH算法后贴出来)
_asm{ push ebp; mov ebp, esp; sub esp, 0x10; push ebx; push ecx; push esi; push edi; ... mov [ebp - 0x4], eax; // [ebp - 0x4] -> BaseAddr mov eax, [eax + 0x3C]; // e_lfanew add eax, [ebp - 0x4]; // NTStartVA mov eax, [eax + 0x78]; // ExportStartRVA add eax, [ebp - 0x4]; // ExportStart_VA mov ebx, [eax + 0x1C]; // EATRVA add ebx, [ebp - 0x4]; // EAT mov [ebp - 0x8], ebx; // [ebp - 0x8] -> EAT mov ebx, [eax + 0x20]; // ENTRVA add ebx, [ebp - 0x4]; // ENT mov [ebp - 0xC], ebx; // [ebp - 0xC] -> ENT mov ebx, [eax + 0x24]; // EOTRVA add ebx, [ebp - 0x4]; // EOT mov [ebp - 0x10], ebx; // [ebp - 0x10] -> EOT ... pop edi; pop esi; pop ecx; pop ebx; mov esp, ebp; pop ebp; ret; }
2、接下来就要考虑如何实现HASH算法了
要求:尽量简单,又不失功能;(不同的API的HASH碰撞几率越小越好,同时ShellCode里,要兼顾体积)
/* * @1 API * @2 Length */ void __declspec(naked) ApiHash() { _asm { push ebp; mov ebp, esp; sub esp, 0x8; mov dword ptr[ebp - 0x4], 0x6B821B17; // Init Hash Value mov dword ptr[ebp - 0x8], 0; // Init Local Var jmp short _begin; _loop: mov eax, [ebp + 0x8]; // eax = srcApi add eax, 0x1; // eax = srcApi + 1 mov[ebp + 0x8], eax; // srcApi++ mov ecx, [ebp - 0x8]; // ecx = i add ecx, 0x1; // ecx += 1 mov[ebp - 0x8], ecx; // i++ _begin: mov edx, [ebp - 0x8]; // edx = i cmp edx, [ebp + 0xC]; // edx vs len jnb short _end; // if (edx >= len) exit; mov eax, [ebp - 0x4]; // eax = Hash shl eax, 0x5; // eax = Hash << 5 mov ecx, [ebp + 8]; // ecx = srcApi movsx edx, byte ptr[ecx]; // edx = *srcApi add eax, edx; // eax = Hash << 5 + *srcApi mov ecx, [ebp - 0x4]; // ecx = Hash shr ecx, 0x2; // ecx = Hash >> 2 add eax, ecx; // eax = Hash << 5 + *srcApi + Hash >> 2 xor eax, [ebp - 0x4]; mov[ebp - 0x4], eax; // Hash ^= (Hash << 5 + *srcApi + Hash >> 2); jmp short _loop; _end: mov eax, [ebp - 0x4]; // eax = Hash mov esp, ebp; pop ebp; ret 0x8; } }
3、既然HASH算法也有了,在开始编写获取API的函数之前,先实现一个获取字符串长度的函数;
/* * @ String */ void __declspec(naked) asmstrlen() { _asm { push ebp; mov ebp, esp; sub esp, 0x4; mov dword ptr [ebp - 0x4], 0; jmp short _begin; _loop: mov eax, [ebp + 0x8]; // eax = String add eax, 0x1; // eax = String + 1 mov [ebp + 0x8], eax; // String++ mov ecx, [ebp - 0x4]; // ecx = i add ecx, 0x1; // ecx += 1 mov [ebp - 0x4], ecx; // i++ _begin: mov ecx, [ebp + 8]; // ecx = String movsx edx, byte ptr [ecx]; // edx = *String cmp edx, 0; je _end; jmp _loop; _end: mov eax, [ebp - 0x4]; // eax = len mov esp, ebp; pop ebp; ret 0x4; } }
4、接下来,就要编写通过HASH获取API地址的函数了
/* * @1 BaseAddr * @2 HASH */ void __declspec(naked) GetHASHAPIAddr() { _asm { push ebp; mov ebp, esp; sub esp, 0x14; push esi; push edi; mov eax, [ebp + 8]; // BaseAddr mov [ebp - 0x4], eax; mov eax, [eax + 0x3C]; // e_lfanew add eax, [ebp - 0x4]; // NTStartVA mov eax, [eax + 0x78]; // ExportStartRVA add eax, [ebp - 0x4]; // ExportStart_VA mov ebx, [eax + 0x1C]; // EATRVA add ebx, [ebp - 0x4]; // EAT mov [ebp - 0x8], ebx; // [ebp - 0x8] -> EAT mov ebx, [eax + 0x20]; // ENTRVA add ebx, [ebp - 0x4]; // ENT mov [ebp - 0xC], ebx; // [ebp - 0xC] -> ENT mov ebx, [eax + 0x24]; // EOTRVA add ebx, [ebp - 0x4]; // EOT mov [ebp - 0x10], ebx; // [ebp - 0x10] -> EOT xor ebx, ebx; mov eax, [eax + 0x18]; // NumOfNames mov [ebp - 0x14], eax; cld; _ENT_FIND: mov esi, [ebp - 0xC]; // ENTStartVA mov esi, [esi + 4 * ebx]; // ENTContentRVA add esi, [ebp - 0x4]; // ENTContentVA push esi; push esi; call asmstrlen; pop esi; push eax; push esi; call ApiHash; mov edi, [ebp + 0xC]; // HASH cmp eax, edi; je _ENT_OK; inc ebx; mov eax, [ebp - 0x14]; dec eax; mov [ebp - 0x14], eax; cmp eax, 0; jg _ENT_FIND; jmp _ENT_END; _ENT_OK: mov ecx, [ebp - 0x10]; // EOTStartVA mov ecx, [ecx + 2 * ebx]; and ecx, 0xFFFF; mov esi, [ebp - 0x8]; // EATStartVA mov eax, [esi + 4 * ecx]; // EAT Address RVA add eax, [ebp - 0x4]; // EAT Address VA _ENT_END: pop edi; pop esi; mov esp, ebp; pop ebp; ret 0x8; } }
我们只需要事先准备好需要的API的HASH值,就可以了,下面让我们来测试下;
5、测试
int main(int argc, char** argv) { DWORD LoadLibAddr = 0; _asm { call GetKernelBase; push 0x28182EF6; // LoadLibrayA HASH push eax; call GetHASHAPIAddr; mov LoadLibAddr, eax; } printf("LoadLibrary[0x%X] ", LoadLibAddr); getchar(); return 0; }
我们在调试器中输入这个地址:
可以看到,获取到了这个函数的地址;
获取有同学会说,这个是在kernel32.dll里的,其他dll里的函数也可以吗;当然了,看我们的GetHashAPIAddr参数就知道了;
来代码吧,搞一个MessageBox的函数,这个是在user32.dll里的,见代码,运行后会弹框,证明就成功了;
int main(int argc, char** argv) { char srcDll[] = "user32.dll"; DWORD LoadLibAddr = 0; _asm { call GetKernelBase; push 0x28182EF6; // LoadLibrayA HASH push eax; call GetHASHAPIAddr; mov LoadLibAddr, eax; push esi; mov esi, eax; lea eax, srcDll; push eax; call esi; push 0x564B6854; // MessageBoxA HASH push eax; call GetHASHAPIAddr; push 0; push 0; push 0; push 0; call eax; } printf("LoadLibraryA[0x%X] ", LoadLibAddr); getchar(); return 0; }
至此,我们的API算是都准备好了,通过实现HASH算法,我们去掉了占用体积过大的API字符串,瘦身的目的达到了;
在后续的文章中,笔者将带领大家一起分析ShellCode中的截断问题,敬请期待;