【谢谢forgot的提醒,特此更正,这个GetProcAddress是一个病毒的自实现过程,不是sdk中真实的GetProcAddress。】
模拟GetProcAddress很早以前就有了,我发出来是觉得那个不细,毕竟这样的东西是给像我一样的菜鸟学习的,在我学习的过程中总是遇到很多笼统的教学帖或者普及帖,所以详细的分析了一下,也当做自己的练习。
分析的肯定有不妥之处,还请各位多多包含~
GetProcAddress:
代码:
00401931 /$ 55 push ebp 00401932 |. 8BEC mov ebp, esp 00401934 |. 81EC 0C010000 sub esp, 10C ; 分配10C大小的栈空间 0040193A |. 53 push ebx 0040193B |. 56 push esi 0040193C |. 57 push edi 0040193D |. 8B7D 08 mov edi, dword ptr [ebp+8] ; edi = arg1 00401940 |. 33DB xor ebx, ebx ; ebx = 0 00401942 |. 3BFB cmp edi, ebx ; 判断输入参数是否为0 00401944 |. 0F84 CA000000 je 00401A14 ; 如果参数为0,则跳转 0040194A |. 8B47 3C mov eax, dword ptr [edi+3C] ; 获取输入模块的IMAGE_NT_HEADERS结构偏移,并传入eax 0040194D |. 03C7 add eax, edi ; 用模块基址+IMAGE_NT_HEADERS的偏移 = IMAGE_NT_HEADERS的RVA 0040194F |. 8B70 78 mov esi, dword ptr [eax+78] ; IMAGE_NT_HEADERS偏移0x78处是IMAGE_DATA_DIRECTORY结构数组首地址,在这里取第一个目录项,也就是输出表的RVA 00401952 |. 8B40 7C mov eax, dword ptr [eax+7C] ; eax = 输出表的大小 00401955 |. 8945 F8 mov dword ptr [ebp-8], eax ; 把输出表大小传入局部变量 00401958 |. 8B45 0C mov eax, dword ptr [ebp+C] ; eax = 第二个参数内容,第二个参数是一个函数名字符串 0040195B |. 03F7 add esi, edi ; esi = 输出表在内存空间中的地址,定位输出表 0040195D |. 3D 00000100 cmp eax, 10000 ; 如果参数地址大于0x10000 ,则说明是按名称输出,否则这里要按序号输出 00401962 |. 73 1C jnb short 00401980 00401964 |. 8B4E 10 mov ecx, dword ptr [esi+10] ; ecx = IMAGE_EXPORT_DIRECTORY.Base 函数序号基数 00401967 |. 8B56 14 mov edx, dword ptr [esi+14] ; edx = IMAGE_EXPORT_DIRECTORY.NumberOfFunctions 输出表中函数数量 0040196A |. 03D1 add edx, ecx ; edx = 最大的函数序号 0040196C |. 3BC2 cmp eax, edx ; 对比参数中的序号,和本dll库中的最大的函数序号 0040196E |. 0F83 A0000000 jnb 00401A14 ; 如果大于本dll中的最大的函数序号,则返回错误 00401974 |. 3BC1 cmp eax, ecx ; 对比参数中的序号,和本dll库中的最小的函数序号 00401976 |. 0F82 98000000 jb 00401A14 ; 如果小于本dll中的最小的函数序号,则返回错误 0040197C |. 2BC1 sub eax, ecx ; eax = 函数的位序,表明参数序号代表的函数所在的位置 0040197E |. EB 47 jmp short 004019C7 00401980 |> 8B4E 20 mov ecx, dword ptr [esi+20] ; esi + 0x20 是IMAGE_EXPORT_DIRECTORY.AddressOfNames,即ecx = 输出名称表 00401983 |. 895D 08 mov dword ptr [ebp+8], ebx ; 参数清零 00401986 |. 03CF add ecx, edi ; ecx = 输出名称表在内存中的位置 00401988 |. 395E 18 cmp dword ptr [esi+18], ebx ; 判断IMAGE_EXPORT_DIRECTORY.AddressOfFunctions是否为0 0040198B |. 76 45 jbe short 004019D2 ; <= 0 则跳转 0040198D |. 894D FC mov dword ptr [ebp-4], ecx ; 把输出地址表的VA传给第一个局部变量 00401990 |. EB 03 jmp short 00401995 00401992 |> 8B45 0C /mov eax, dword ptr [ebp+C] ; 接下来是一个循环,在动态库输出表中查找指定的函数 00401995 |> 8B55 FC mov edx, dword ptr [ebp-4] ; edx = 输出名称表va 00401998 |. 8BCF |mov ecx, edi ; ecx = 输入的参数(HMODULE) 0040199A |. 030A |add ecx, dword ptr [edx] ; ecx = 输出名称表中的第一个函数名称的地址 0040199C |. 51 |push ecx 0040199D |. 50 |push eax 0040199E |. E8 2FFDFFFF |call 004016D2 ; 判断两字符串是否相等 004019A3 |. 59 |pop ecx 004019A4 |. 84C0 |test al, al 004019A6 |. 59 |pop ecx 004019A7 |. 74 11 |je short 004019BA ; 判断比较结果 004019A9 |. FF45 08 |inc dword ptr [ebp+8] 004019AC |. 8345 FC 04 |add dword ptr [ebp-4], 4 ; 跳到下一个函数 004019B0 |. 8B45 08 |mov eax, dword ptr [ebp+8] ; eax = 比较次数 004019B3 |. 3B46 18 |cmp eax, dword ptr [esi+18] ; 判断是否到达了输出表尾 004019B6 |.^ 72 DA \jb short 00401992 ; 如果没到尾,则继续循环查找函数 004019B8 |. EB 18 jmp short 004019D2 004019BA |> 8B46 24 mov eax, dword ptr [esi+24] ; eax = IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals,即导出序号表地址rva 004019BD |. 8B4D 08 mov ecx, dword ptr [ebp+8] ; ecx = 函数位置 004019C0 |. 8D0448 lea eax, dword ptr [eax+ecx*2] ; eax = 刚刚找到的函数在序号表中的序号word RVA 004019C3 |. 0FB70438 movzx eax, word ptr [eax+edi] ; eax = 函数序号 004019C7 |> 8B4E 1C mov ecx, dword ptr [esi+1C] ; ecx = IMAGE_EXPORT_DIRECTORY.AddressOfFunctions 函数地址表 004019CA |. 8D0481 lea eax, dword ptr [ecx+eax*4] ; eax = 函数的真正地址的RVA保存的地址 004019CD |. 8B1C38 mov ebx, dword ptr [eax+edi] ; ebx = 函数在内存中的真正地址的RVA 004019D0 |. 03DF add ebx, edi ; ebx = 函数在内存中的va 004019D2 |> 8B45 F8 mov eax, dword ptr [ebp-8] ; eax = 输出表大小 004019D5 |. 03C6 add eax, esi ; eax = 输出表尾 004019D7 |. 3BD8 cmp ebx, eax ; 判断函数地址和输出表尾 004019D9 |. 73 6F jnb short 00401A4A ; if (ebx > 输出表尾va || ebx < 输出表头va || ebx == 0) 则跳转 004019DB |. 3BDE cmp ebx, esi 004019DD |. 72 6B jb short 00401A4A 004019DF |. 85DB test ebx, ebx 004019E1 |. 74 67 je short 00401A4A 004019E3 |. 8A03 mov al, byte ptr [ebx] ; 函数的第一个byte放入al 004019E5 |. 8BF3 mov esi, ebx ; esi = 函数地址 004019E7 |> 84C0 /test al, al 004019E9 |. 74 29 |je short 00401A14 ; 如果内容为0则函数返回 004019EB |. 3C 2E |cmp al, 2E ; 如果是'.' 004019ED |. 74 06 |je short 004019F5 ; 如果其中有‘.’ ,则说明此函数来自于其他库 004019EF |. 8A46 01 |mov al, byte ptr [esi+1] 004019F2 |. 46 |inc esi 004019F3 |.^ EB F2 \jmp short 004019E7 004019F5 |> 80A5 F4FEFFFF>and byte ptr [ebp-10C], 0 ; ebp-10c位清零 004019FC |. 6A 40 push 40 004019FE |. 59 pop ecx ; ecx = 0x40 004019FF |. 33C0 xor eax, eax ; eax = 0 00401A01 |. 8DBD F5FEFFFF lea edi, dword ptr [ebp-10B] ; edi = buffer(ebp-10B) 00401A07 |. F3:AB rep stos dword ptr es:[edi] ; ebp-10b 缓冲区清0 共0x100个0 00401A09 |. 66:AB stos word ptr es:[edi] 00401A0B |. AA stos byte ptr es:[edi] ; 再补充3位的0 00401A0C |. 8BFE mov edi, esi ; edi = '.'的位置 00401A0E |. 2BFB sub edi, ebx ; 用‘.’的位置 - 函数首位置 则得到了动态库的名字长度 00401A10 |. 85FF test edi, edi ; 判断长度 00401A12 |. 7F 04 jg short 00401A18 ; 如果大于0,说明正确 00401A14 |> 33C0 xor eax, eax ; 如果<=0 则本函数返回0 00401A16 |. EB 34 jmp short 00401A4C ; 跳到结尾 00401A18 |> 57 push edi ; /dll名长度 00401A19 |. 8D85 F4FEFFFF lea eax, dword ptr [ebp-10C] ; | 00401A1F |. 53 push ebx ; |dll名地址 00401A20 |. 50 push eax ; |dest 00401A21 |. E8 DC380000 call <jmp.&MSVCRT.memcpy> ; \memcpy 00401A26 |. 80A43D F4FEFF>and byte ptr [ebp+edi-10C], 0 ; 清0 00401A2E |. 83C4 0C add esp, 0C 00401A31 |. 8D85 F4FEFFFF lea eax, dword ptr [ebp-10C] ; eax = dll名 00401A37 |. 50 push eax ; /加载动态库 00401A38 |. FF15 04604000 call dword ptr [<&kernel32.GetM>; \GetModuleHandleA 00401A3E |. 46 inc esi ; 跳过'.' 因为本库中如果有其他库的函数,保存形式为“Advapi32.RegQueryValueEx”,即跳过.后就是函数名的地址 00401A3F |. 56 push esi ; 压入函数名 00401A40 |. 50 push eax ; 压入模块名 00401A41 |. E8 EBFEFFFF call 00401931 ; 递归调用 00401A46 |. 59 pop ecx 00401A47 |. 8BD8 mov ebx, eax ; ebx = 函数地址 00401A49 |. 59 pop ecx 00401A4A |> 8BC3 mov eax, ebx ; eax = 函数地址,用于返回 00401A4C |> 5F pop edi 00401A4D |. 5E pop esi 00401A4E |. 5B pop ebx 00401A4F |. C9 leave 00401A50 \. C3 retn
分析之后总结的流程图:

还原C代码:
前面分析注释了很多代码,这个没写注释了。
MyGetProcAddress 中没有对第二个参数进行判断,我发现我看过的所有的自己实现的GetProcAddress都判断第二个参数是否为NULL,如果是NULL则函数返回,这块严格的讲是有问题的。因为如果dll的DEF中明确定义了函数的序号为0,则此时第二个参数传入的0就是有意义的。
代码:
FARPROC WINAPI MyGetProcAddress(HMODULE hModule, LPCSTR lpProcName) { PIMAGE_DOS_HEADER pDOSHeader; PIMAGE_NT_HEADERS pNTHeader; PIMAGE_EXPORT_DIRECTORY pExportDir; LONG peNtHeaderBaseOffset; DWORD dwImage_Export_Directory_RVA; DWORD dwImage_Export_Directory_Size; DWORD dwAddressOfNamesVA; DWORD dwProcOrdinalNumber; DWORD dwProcAddressVA; int count = 0; //计数 char dllName[_MAX_FNAME]; if (NULL == hModule) return 0; pDOSHeader = (PIMAGE_DOS_HEADER)hModule; peNtHeaderBaseOffset = pDOSHeader->e_lfanew; pNTHeader = (PIMAGE_NT_HEADERS)(hModule + peNtHeaderBaseOffset); dwImage_Export_Directory_RVA = pNTHeader->OptionalHeader.DataDirectory[0].VirtualAddress; dwImage_Export_Directory_Size = pNTHeader->OptionalHeader.DataDirectory[0].Size; pExportDir = (PIMAGE_EXPORT_DIRECTORY)(hModule + dwImage_Export_Directory_RVA); if ((DWORD)lpProcName > 0x10000) { if (pExportDir->AddressOfFunctions > 0) { dwAddressOfNamesVA = pExportDir->AddressOfNames + (DWORD)hModule; do { if (!strcmp(lpProcName, (const char*)*(DWORD*)((DWORD)hModule + dwAddressOfNamesVA))) goto _jmp; count++; dwAddressOfNamesVA += 4; } while ((DWORD)count < pExportDir->NumberOfNames); goto _whenend; _jmp: dwProcOrdinalNumber = *(DWORD*)((pExportDir->AddressOfNameOrdinals + count*2) + (DWORD)hModule); } } else {//如果第二个参数是函数序号 if ((unsigned long)lpProcName > pExportDir->Base + pExportDir->NumberOfFunctions || (unsigned long)lpProcName <pExportDir->Base) return 0; dwProcOrdinalNumber = (DWORD)lpProcName - pExportDir->Base; //函数位置 } dwProcAddressVA = *(DWORD*)((pExportDir->AddressOfFunctions + dwProcOrdinalNumber*4) + (DWORD)hModule) + (DWORD)hModule; _whenend: if (dwProcAddressVA >dwImage_Export_Directory_Size + dwImage_Export_Directory_RVA + (DWORD)hModule || dwProcAddressVA < (DWORD)hModule + dwImage_Export_Directory_RVA || dwProcAddressVA == 0) return (FARPROC)dwProcAddressVA; count = 0; DWORD dwProcAddr = dwProcAddressVA; byte bTmp = *(byte*)dwProcAddr; do { if (0 == bTmp) return 0; if (0x2E == bTmp) //ascii '.' break; bTmp = *(byte*)(dwProcAddr + 1); dwProcAddr++; } while (1); unsigned int libNameLength = dwProcAddr - dwProcAddressVA; if (libNameLength <= 0) return 0; memset(dllName, 0, _MAX_FNAME); memcpy(dllName, (const void*)dwProcAddressVA, libNameLength); HMODULE hDllModule = GetModuleHandleA(dllName); dwProcAddr++; return MyGetProcAddress(hDllModule, (LPCSTR)dwProcAddr); //递归 }