4. hash扫描获得api函数地址
今天这篇文章也是病毒编写中很重要的一篇文章,“hash扫描获得api函数地址”,通过它我们的病毒就可以在宿主程序中调用相应平台的库函数。呼,说白一点就是实现一个自己的GetProcAddress函数,然后通过上节课的所讲解的思路获得Kernel32基地址,然后来搜索api函数地址。
这篇文章我分为2个栏目:
1. 搜索获得api函数地址的实现。
2. hash算法搜索获得api函数地址的实现。
;;;;写文章比写代码累多了, 希望辛劳的成果能使大家有所收获,也不枉我此套专题。。。。。。。
;-------------------------------------------------
1. 搜索获得api函数地址的实现:
Windows和之前的dos最大的一个特色就是采用了动态链接库, 这样我们省了很多的内存。我们可以想象一下如果我们的程序都采用静态库的话,那么我们的所有程序所占内存是相当庞大的,而
Windows采用了动态链接库(这样保证了物理内存仅有一份此动态链接库的copy),这些动态链接库分别提供给我们用户了各种各样的编程接口来实现相应的功能,当我们用户程序调用它时必须包含相应的
库文件、函数名称,这样我们的PE LOADER在加载时才会将相应的动态链接库加载我们的进程的内存空间(实际上windows是通过分页机制将这些DLL的物理内存地址指向我们程序虚拟空间的页表中,这样
我们共享的是同一物理内存)。
那么我们上面介绍了,windows通过动态链接库提供给我们用户了各种各样的编程接口函数,那么一般的动态链接库的后缀是“.dll”,当然其他的后缀也可以,例如“.exe”, “.sys”,只不
过它们不常用罢了。因为加载器判断的不仅仅是文件后缀名,还有我们的文件结构。
所以一般我们调用某个动态库的函数,我们必须增加这个动态库的导入表结构,这样我们的windows 加载器才会把这个动态库加载到我们的内存空间,并修正导入表结构的导入函数地址,以便
我们的程序能正常的调用函数。那么这个动态链接库是如何输出函数来供我们的用户程序调用呢?它实际上是采用输出表结构来描述本dll需要导出哪些函数来供其他的程序调用,这样其他的用户程序才
能正常的调用此动态链接库的输出函数。
那么关键的点我们已经知道了,那就是动态链接库是采用输出表结构来描述导出函数。那么如果我们调用某动态链接库的输出函数,首先我们肯定是要查找到这些输出函数的地址?在那里查找
?我们肯定是在输出表结构。好明白这点后,我们来看输出表结构。
struct IMAGE_EXPORT_DIRECTORY
Characteristics dd ? ;未使用
TimeDateStamp dd ? ;文件生成时间
MajorVersion dw ? ;主版本号,一般为0
MinorVersion dw ? ;次版本号,一般为0
nName dd ? ;模块的真实名称
nBase dd ? ;基数, 加上序数就是函数地址数组的索引值
NumberOfFunctions dd ? ;AddressOfFunctions阵列的元素个数
NumberOfNames dd ? ;AddressOfNames阵列的元素个数
AddressOfFunctions dd ? ;指向函数地址数组
AddressOfNames dd ? ;函数名字的指针地址
AddressOfNameOrdinals dd ? ;指向输出序列号数组
ends
为了更好的理解输出表结构,我们采用fasm编写一段自己定义输出表结构的dll代码。
format PE GUI 4.0 DLL
include 'win32ax.inc'
entry __DllEntry
.text
;++
;
; BOOL
; DllMain(
; IN HINSTANCE hDllHandle,
; IN DWORD nReason,
; IN LPVOID Reserved
; )
;
; Routine Description:
;
; 测试文件是否是PE文件格式。
;
; Arguments:
;
; (esp) - return address
;
; Data (esp+4) - hDllHandle
; (esp+8) - nReason
; (esp+12)- Reserved
;
; Return Value:
;
; eax = TRUE, initialization succeeds; eax = FALSE, initialization fails。
;
;--
__DllEntry:
xor eax, eax
inc eax
ret 4*3
__MyMessageBox:
xor eax, eax
push eax
@pushsz 'Dll'
@pushsz '一个dll自定导出表结构例子'
push eax
call [MessageBox]
ret
.idata
section '.edata' export data readable
__IMAGE_EXPORT_DIRECTORY:
dd 0, 0, 0, rva szName, 0, 1, 1
dd rva Address_Tab
dd rva FuncName_Tab
dd rva Ordinals_Tab
;dll name
szName db 'Msg.dll', 0
;
Address_Tab:
dd rva __MyMessageBox ;取__MyMessageBox过程 rva地址
FuncName_Tab:
dd rva ($+4) ; ($ + 4) ptr "MyMessageBox"
db 'MyMessageBox', 0
Ordinals_Tab:
dw 0
.fixups
;++
OK,以上这段代码是实现自己定义输出表结构来实现输出__MyMessageBox过程的动态链接库,我们可以测试下看我们定义的输出表结构是否成功。再写一段过程,代码如下,然后运行,看我们DLL
指定的输出函数过程是否正常运行。
invoke LoadLibrary, 'Msg.dll'
invoke GetProcAddress, eax, 'MyMessageBox'
call eax
ret
我们可以看到首先调用LoadLibray函数来将我们的Msg.dll加载到我们进程的内存空间中,获取Msg.dll加载后的内存地址后,调用GetProcAddress来获得MyMessageBox函数的地址。我们上节课
程已经学习了如何查找kernel32.dll的基地址,那么我们只要实现一个GetProcAddress函数就可以轻松的获取kernel32.dll中的函数地址了。好,接下来我们的重点放在如何实现GetProcAddress函数上
。
;--
我们看刚刚上面我们自己定义输出表结构的动态链接库代码,看我定义输出表结构,我们来模拟下GetProcAddress的思路。
举个例子:
Ordinals_Tab:
dw 0 ;索引 0
dw 1 ;索引 1
Address_Tab:
rva __MyMessageBox ;对应 索引 0
rva __MyMessageBox2 ;对应 索引 1
nBase = 0
由于Ordinals_Tab 序号表中的序号值,对应的是Address_Tab的索引。
那么我们搜索函数的时候,只需要获得对应函数的在“Ordinals_Tab中的序号值”,然后通过(序号值 + 索引基数)就可以在Address_Tab中进行索引查找,得到的就是对应函数的RVA地址。简单
说就是
mov eax, [Address_Tab + ((Ordinals_Tab中的序号值 + 索引基数)*4)]
那么,如何取得对应函数的在“Ordinals_Tab中的序号值”?
这里我们可以在循环匹配函数名称的时候,如果没有匹配成功则将Ordinals_Tab的地址+2 (+2是因为,Ordinals_Tab表中的成员是dw类型,每次+2则表示指向下一个函数的Ordinals)。如果一旦
匹配成功,则直接读取Ordinals_Tab地址中的序号值,然后乘以4, +Address_Tab来读取函数的RVA地址,了解了思路是不是很简单?
那好,就让我们来实现代码考验下你是否到底明白了?
编写代码如下:
;++
;
; int
; GetApi(
; IN HINSTANCE hModule,
; IN char * lpApiString,
; )
;
; Routine Description:
;
; 获取指定函数的内存地址
;
; Arguments:
;
; (esp) - return address
;
; Data (esp+4) - hDllHandle
; (esp+8) - lpApiString
; Return Value:
;
; eax -> Function Mem Address。
;
;--
GetApi:
pop edx
pop eax ;hModule
pop ecx ;lpApiString
push edx
pushad
mov ebx, eax ;hModule ebx
mov edi, ecx ;lpApiString edi
xor al, al
.Scasb:
scasb
jnz .Scasb
dec edi
sub edi, ecx
xchg edi, ecx ; edi = lpApiString, ecx = ApiLen
mov eax, [ebx+3ch]
mov esi, [ebx+eax+78h] ;Get Export Rva
lea esi, [esi+ebx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
lodsd
xchg eax, edx ; edx = NumberOfNames
lodsd
push eax ; [esp] = AddressOfFunctions
lodsd
xchg eax, ebp
lodsd
xchg eax, ebp ; ebp = AddressOfNameOrdinals, eax = AddressOfNames
add eax, ebx
mov [esp+4*6], edi ;临时存储
mov [esp+4*5], ecx ;临时存储
.LoopScas:
dec edx
jz .Ret
mov esi, [eax+edx*4]
add esi, ebx
repz cmpsb
jz .GetAddr
mov edi, [esp+4*6]
mov ecx, [esp+4*5]
jmp .LoopScas
.GetAddr:
shl edx, 1
add ebp, edx
movzx eax, word [ebp+ebx]
shl eax, 2
add eax, [esp]
mov eax, [ebx+eax]
add eax, ebx
.Ret:
pop ecx
mov [esp+4*7], eax
popad
ret
分析:
代码其余部分很好理解,我们重点来看这里。
.LoopScas:
dec edx
jz .Ret
mov esi, [eax+edx*4]
add esi, ebx
repz cmpsb
jz .GetAddr
mov edi, [esp+4*6]
mov ecx, [esp+4*5]
jmp .LoopScas
.GetAddr:
shl edx, 1
add ebp, edx
movzx eax, word [ebp+ebx]
shl eax, 2
add eax, [esp]
mov eax, [ebx+eax]
add eax, ebx
我们之前的思路说:
在循环匹配的时候, 如果失败则将Ordinals_Tab的地址+2。但这里我们采用的是从后往前循环并通过NumberOfNames来作为索引来取AddressOfNames成员,所以我们就不能用Ordinals_Tab的地址
+2了(快点快点发挥你的想象力,自己实现个Ordinals_Tab的地址+2思路的函数)。
不过我们循环的时候是取得NumberOfNames来作为循环条件,这个成员表示的是AddressOfNames的元素个数。所以我们循环匹配函数的时候通过NumberOfNames - 1,如果匹配成功的话此时的
[NumberOfNames*2], 将是Ordinals_Tab的索引值。
然后我们通过 mov eax, [Ordinals_Tab + (索引值 *2)]来获得 AddressOfFunctions的索引值。然后通过 mov eax, [AddressOfFunctions + (索引值*4)]来获得函数地址。
思路就是这样,我想大家目前更多的事情应该去思考。 o(∩_∩)o... 祝你好运!
;---------------------------------------------------------------------
2. hash算法搜索获得api函数地址的实现
紧接着我们要讲解到的是hash算法搜索获得api函数地址。如上面的代码,我们一般要获得一个函数的地址,通常采用的是明文,例如定义一个api函数字符串"MessageBoxA",然后在
GetProcAddress函数中一个字节一个字节进行比较。这样弊端很多,例如如果我们定义一个杀毒软件比较敏感的api函数字符串,那么可能就会增加杀毒软件对我们的程序的判定值,而且定义这些字符串
还有一个弊端是占用的字节数较大。我们想想如何我们的api函数字符串通过算法将它定义成一个4字节的值,然后在GetProcAddress中把AddressOfNames表中的每个地址指向的api字符串通过我们的算法
压缩成4字节值后,与我们之前定义的4字节值进行判断,如果匹配成功则读取函数地址。
废话就不多说,我们来看一种rol 3移位算法,这个算法是每次将目的地址循环向左移动3位,然后将低字节与源字符串的每个字节进行异或。算法很精巧方便。代码如下:
;++
;
; int
; GetRolHash(
; IN char * lpApiString
; )
;
; Routine Description:
;
; 计算ApiString Hash值
;
; Arguments:
;
; (esp) - return address
;
; Data (esp+4) - lpApiString
;
; Return Value:
;
; eax = Hash String
;
;--
GetRolHash:
pop ecx
pop eax
push ecx
push esi
xor edx, edx
xchg eax, esi
cld
.Next:
lodsb
test al, al
jz .Ret
rol edx, 3
xor dl, al
jmp .Next
.Ret:
xchg eax, edx
pop esi
ret
还有很多方便小巧的算法,例如ROR 13等算法。我比较喜欢ROL 3, 所以推荐这个。那么我们接下来,我们来实现个匹配hash字符串的GetProcAddress,其实它和我们上面的基本一样,只不过它
将函数名表的字符串通过我们的算法过程获得hash值后与我们之前定义的hash值进行匹配,匹配成功则获得对应函数的地址。
过程如下:
;++
;
; int
; GetApi(
; IN HINSTANCE hModule,
; IN int iHashApi,
; )
;
; Routine Description:
;
; 获取指定函数的内存地址
;
; Arguments:
;
; (esp) - return address
;
; Data (esp+4) - hDllHandle
; (esp+8) - iHashApi
; Return Value:
;
; eax -> Function Mem Address。
;
;--
GetApi:
pop edx
pop eax ;hModule
pop ecx ;lpApiString
push edx
pushad
mov ebx, eax ;hModule ebx
mov edi, ecx ;iHashApi edi
mov eax, [ebx+3ch]
mov esi, [ebx+eax+78h] ;Get Export Rva
lea esi, [esi+ebx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
lodsd
xchg eax, edx ; edx = NumberOfNames
lodsd
push eax ; [esp] = AddressOfFunctions
lodsd
xchg eax, ebp
lodsd
xchg eax, ebp ; ebp = AddressOfNameOrdinals, eax = AddressOfNames
add eax, ebx
xchg eax, esi ; esi = AddressOfNames
.LoopScas:
dec edx
jz .Ret
lodsd
add eax, ebx
push edx
;计算hash字符串
push eax
call GetRolHash
pop edx
cmp eax, edi
jz .GetAddr
add ebp, 2
jmp .LoopScas
.GetAddr:
movzx eax, word [ebp+ebx]
shl eax, 2
add eax, [esp]
mov eax, [ebx+eax]
add eax, ebx
.Ret:
pop ecx
mov [esp+4*7], eax
popad
ret
OK,为了验证没有问题。我们来写一段简单的验证过程。。
format PE GUI 4.0
include 'win32ax.inc'
entry __Entry
.text
__Entry:
call GetKrnlBase3
push 0016EF74Bh ; Hash WinExec
push eax
call GetApi
push SW_SHOW
@pushsz "cmd.exe"
call eax
ret
;++
;
; int
; GetApi(
; IN HINSTANCE hModule,
; IN int iHashApi,
; )
;
; Routine Description:
;
; 获取指定函数的内存地址
;
; Arguments:
;
; (esp) - return address
;
; Data (esp+4) - hDllHandle
; (esp+8) - iHashApi
; Return Value:
;
; eax -> Function Mem Address。
;
;--
GetApi:
pop edx
pop eax ;hModule
pop ecx ;lpApiString
push edx
pushad
mov ebx, eax ;hModule ebx
mov edi, ecx ;iHashApi edi
mov eax, [ebx+3ch]
mov esi, [ebx+eax+78h] ;Get Export Rva
lea esi, [esi+ebx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
lodsd
xchg eax, edx ; edx = NumberOfNames
lodsd
push eax ; [esp] = AddressOfFunctions
lodsd
xchg eax, ebp
lodsd
xchg eax, ebp ; ebp = AddressOfNameOrdinals, eax = AddressOfNames
add eax, ebx
xchg eax, esi ; esi = AddressOfNames
.LoopScas:
dec edx
jz .Ret
lodsd
add eax, ebx
push edx
;计算hash字符串
push eax
call GetRolHash
pop edx
cmp eax, edi
jz .GetAddr
add ebp, 2
jmp .LoopScas
.GetAddr:
movzx eax, word [ebp+ebx]
shl eax, 2
add eax, [esp]
mov eax, [ebx+eax]
add eax, ebx
.Ret:
pop ecx
mov [esp+4*7], eax
popad
ret
;++
;
; int
; GetKrnlBase3(
; void
; )
;
; Routine Description:
;
; 获得kernel32基地址
;
; Arguments:
;
; (esp) - return address
;
;
; Return Value:
;
; eax = krnl32 base
;
;--
GetKrnlBase3:
mov eax, [fs:30h]
mov eax, [eax+0ch]
mov eax, [eax+1ch]
mov eax, [eax]
mov eax, [eax+8h]
ret
;++
;
; int
; GetRolHash(
; IN char * lpApiString
; )
;
; Routine Description:
;
; 计算ApiString Hash值
;
; Arguments:
;
; (esp) - return address
;
; Data (esp+4) - lpApiString
;
; Return Value:
;
; eax = Hash String
;
;--
GetRolHash:
pop ecx
pop eax
push ecx
push esi
xor edx, edx
xchg eax, esi
cld
.Next:
lodsb
test al, al
jz .Ret
rol edx, 3
xor dl, al
jmp .Next
.Ret:
xchg eax, edx
pop esi
ret
;运行后,程序运行一个cmd窗口,然后退出线程。。
例子:
push 0016EF74Bh ; Hash WinExec
push eax
call GetApi
这段例子代码我采用直接压入对应的函数字符串的hash值(如 WinExec 0016EF74Bh),其实我们利用宏完全可以做到在预编译阶段进行hash计算,这个就留到下下节课来讲解吧,为了大家方便,给大家发
布一个Hash Api String计算器。
如下附件图。。
好了,今天这篇文章就到这里了。大家再见。。总算今天下午把这篇文章给赶出来了。已经快接近0点,大家接着观看吧。。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。