病毒技术三:获取kernel32.dll内的API地址
正如上一章所说的,有了LoadLibrary()和GetProcAddress()系统函数之后我们就不需要大费周章的去找位于其他DLL内的API了。下面是寻找API的子过程,这个子过程适用于所有DLL,意味着它也可以去寻找其他DLL内的API,只需要把基址和导出表地址改一改就行了(DLL必须被加载过,意味着它必须存在于该程序的地址空间中):;编译器:NASM
;此函数用于寻找任何DLL中的API(注意:因为没有加保护措施,所以如果试图去寻找不存在的API将会出现因越界而访问非法区域的错误)
;此函数堆栈清理由调用方完成
;参数: ESI = API字符串偏移地址, [ESP+8] = (DWORD)DLL Export Directory VA(导出表虚拟地址), [ESP+4] = (DWORD)DLL的基址
;返回: EAX=API的虚拟地址
;变动的寄存器: DF, EAX, EBX, ECX, EDX, ESI, EDI
GETFUNCTIONADDRESS:
CLD
MOV ECX,ESI
GFA0:
LODSB ;取得需寻找API字符串的长度(不算入空字符)
TEST AL,AL
JNZ GFA0
SUB ESI,ECX
DEC ESI
XCHG ECX,ESI ;需寻找API字符串的长度现在位于ECX中
MOV EDI,[ESP+8] ;做遍历ExportNamePtrTable(API名字符串导出表)的准备
MOV EDI,[EDI+20H] ;ExportNamePtrTableRVA位于ExportDirectory内的20H处
ADD EDI,[ESP+4] ;通过加上DLL基址将ExportNamePtrTable的RVA(相对虚拟地址)转换为实际虚拟地址
XCHG ESI,EDI
XOR EBX,EBX ;我们使用EBX来计数,用于后面在ExportOrdinalTable中找到API的序数(位于地址表中的第几个)
MOV EDX,EDI
GFA1: ;遍历ExportNamePtrTable内的字符串,逐个比较
INC EBX ;EBX存折这里最重要的数据:API在NameOrdinalTable(序数表)内的序数
LODSD ;这里取得的是DLL内某API字符串地址的RVA,下一条指令将其转变为VA(通过加上DLL基址)
ADD EAX,[ESP+4]
PUSH ESI ;保存必要的数据:字符串地址
MOV ESI,EAX
MOV EDI,EDX
PUSH ECX
REPE CMPSB ;串比较指令,若字符串完全相等则设置ZF标志位
POP ECX
POP ESI
JNE GFA1 ;若DLL内API表的字符串与我们向此函数请求的API字符串相同则移入下一步:找到API地址
DEC EBX ;因为之前INC EBX位于跳转前,所以会多出来1,这样做的目的仅仅是优化(不需要另一个条件跳转分支)
MOV ESI,[ESP+8] ;ESI = DLL ExportDirectory
MOV ESI,[ESI+24H] ;ESI = ExportOrdinalTableRVA(序数表:位于ExportDirectory的24H处)
ADD ESI,[ESP+4] ;将ExportOrdinalTableRVA转换为VA
MOVZX ESI,WORD [ESI+EBX*2] ;ESI = API地址表的序数(是地址表内的第几个地址),这里将EBX*2的原因是ExportOrdinalTable内的每个序数为WORD(一个WORD为2字节,所以乘2);所以如果此API为ExportNamePtrTable的第5个,那么在ExportOrdinalTable内的第10个WORD将会存着此API在ExportAddressTable内的序数
MOV EBX,[ESP+8] ;ESI = DLL ExportDirectory
MOV EBX,[EBX+1CH] ;ESI = ExportAddressTableRVA(地址表:位于ExportDirectory的1CH处)
ADD EBX,[ESP+4] ;将ExportAddressTableRVA转换为VA
MOV EBX,[EBX+ESI*4] ;ESI = API的RVA(相对DLL的偏移地址),这里将将ESI乘4的原因和上面类似,只不过是因为地址表里的一个地址为DWORD,大小为4字节,所以寻址时乘4
ADD EBX,[ESP+4] ;将API的RVA转换为VA(实际虚拟地址)
MOV EAX,EBX ;EAX = API的VA
RETN
MASM版本:
;编译器:MASM
;此函数用于寻找任何DLL中的API(注意:因为没有加保护措施,所以如果试图去寻找不存在的API将会出现因越界而访问非法区域的错误)
;参数: ESI = API字符串偏移地址, [ESP+8] = (DWORD)DLL Export Directory(导出表)的VA(虚拟地址), [ESP+4] = (DWORD)DLL的基址
;返回: EAX=API的虚拟地址
;变动的寄存器: DF, EAX, EBX, ECX, EDX, ESI, EDI
GETFUNCTIONADDRESS PROC
CLD
MOV ECX,ESI
GFA0:
LODSB ;取得需寻找API字符串的长度(不算入空字符)
TEST AL,AL
JNZ GFA0
SUB ESI,ECX
DEC ESI
XCHG ECX,ESI ;需寻找API字符串的长度现在位于ECX中
MOV EDI,[ESP+8] ;做遍历ExportNamePtrTable(API名字符串导出表)的准备
MOV EDI,[EDI+20H] ;ExportNamePtrTableRVA位于ExportDirectory内的20H处
ADD EDI,[ESP+4] ;通过加上DLL基址将ExportNamePtrTable的RVA(相对虚拟地址)转换为实际虚拟地址
XCHG ESI,EDI
XOR EBX,EBX ;我们使用EBX来计数,用于后面在ExportOrdinalTable中找到API的序数(位于地址表中的第几个)
MOV EDX,EDI
GFA1: ;遍历ExportNamePtrTable内的字符串,逐个比较
INC EBX ;EBX存折这里最重要的数据:API在NameOrdinalTable(序数表)内的序数
LODSD ;这里取得的是DLL内某API字符串地址的RVA,下一条指令将其转变为VA(通过加上DLL基址)
ADD EAX,[ESP+4]
PUSH ESI ;保存必要的数据:字符串地址
MOV ESI,EAX
MOV EDI,EDX
PUSH ECX
REPE CMPSB ;串比较指令,若字符串完全相等则设置ZF标志位
POP ECX
POP ESI
JNE GFA1 ;若DLL内API表的字符串与我们向此函数请求的API字符串相同则移入下一步:找到API地址
DEC EBX ;因为之前INC EBX位于跳转前,所以会多出来1,这样做的目的仅仅是优化(不需要另一个条件跳转分支)
MOV ESI,[ESP+8] ;ESI = DLL ExportDirectory
MOV ESI,[ESI+24H] ;ESI = ExportOrdinalTableRVA(序数表:位于ExportDirectory的24H处)
ADD ESI,[ESP+4] ;将ExportOrdinalTableRVA转换为VA
MOVZX ESI,WORD PTR [ESI+EBX*2] ;ESI = API地址表的序数(是地址表内的第几个地址),这里将EBX*2的原因是ExportOrdinalTable内的每个序数为WORD(一个WORD为2字节,所以乘2);所以如果此API为ExportNamePtrTable的第5个,那么在ExportOrdinalTable内的第10个WORD将会存着此API在ExportAddressTable内的序数
MOV EBX,[ESP+8] ;ESI = DLL ExportDirectory
MOV EBX,[EBX+1CH] ;ESI = ExportAddressTableRVA(地址表:位于ExportDirectory的1CH处)
ADD EBX,[ESP+4] ;将ExportAddressTableRVA转换为VA
MOV EBX,[EBX+ESI*4] ;ESI = API的RVA(相对DLL的偏移地址),这里将将ESI乘4的原因和上面类似,只不过是因为地址表里的一个地址为DWORD,大小为4字节,所以寻址时乘4
ADD EBX,[ESP+4] ;将API的RVA转换为VA(实际虚拟地址)
MOV EAX,EBX ;EAX = API的VA
RETN
GETFUNCTIONADDRESS ENDP
以上代码可以通过C语言代码验证:
unsigned int APIAddress = GetProcAddress(GetModuleHandleA("API所在的DLL名称"), "API名称");
--------获取Export Directory(导出表)的地址--------
位于DLL的PE头的78H偏移处为导出表的RVA,加上DLL基址即可取得导出表的VA,即前面的子过程需要的参数
可以使用如下代码来获取kernel32.dll导出表的地址:
CALL GETKERNEL32ADDR ;这是“病毒技术二”中提及的子过程,其获取kernel32.dll的基址
MOV ESI,EAX
ADD ESI,[ESI+3CH] ;ESI = PE头地址
MOV ESI,[ESI+78H] ;ESI = ExportDirecotry导出表的RVA
ADD ESI,EAX ;ESI = ExportDirectory导出表的VA,即我们需要的数据
;--------调用GETFUNCTIONADDRESS函数--------
;接着如上代码
PUSH ESI ;参数1:ExportDirectoryVA入栈
PUSH EAX ;参数2:DLL基址入栈
MOV ESI,OFFSET GetProcAddressAPIS ;参数3:字符串偏移--ESI = 字符串偏移,NASM语法中去除OFFSET
CALL GETFUNCTIONADDRESS
POP ECX ;堆栈清理
POP ECX ;堆栈清理
...
GetProcAddressAPIS: ;API字符串定义(需空字符结尾)
DB "GetProcAddress",0
--------DLL Export Directory(导出表)--------
导出表是DLL的重要机制,它表明了DLL向外开放的函数字符串(名字),以及此函数的地址,这里我们需要的几个数据也就是AddressOfNames, AddressOfNameOrdinals, AddressOfFunctions
导出表Export Directory的结构:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //offset 0x0
DWORD TimeDateStamp; //offset 0x4
WORD MajorVersion; //offset 0x8
WORD MinorVersion; //offset 0xa
DWORD Name; //offset 0xc
DWORD Base; //offset 0x10
DWORD NumberOfFunctions; //offset 0x14
DWORD NumberOfNames; //offset 0x18
DWORD AddressOfFunctions; //offset 0x1c
DWORD AddressOfNames; //offset 0x20
DWORD AddressOfNameOrdinals; //offset 0x24
}
顺提一下:前面的代码中我有能力加入保护机制(通过NumberOfFunctions和NumberOfNames比较计数器中的值检测有无越界),但是这样没有必要,因为在调试的时候就可以发现DLL中到底有无此API:有的话则返回地址,没有则会发生越界错误。所以这样做没有意义反而增大程序大小。
--------RVA(相对虚拟地址)和VA(虚拟地址)--------
前面代码中很重要的一点就是我们不断在进行RVA到VA的转换,现代操作系统由于引入了GDT全局描述符表机制,所以所有进程程的地址都是虚拟的(这里的虚拟地址由GDT引导到实际物理地址)
我们这里不需要过多注重虚拟地址,RVA和VA的区别其实就是OFFSET偏移和实际地址的区别,通过把RVA加上DLL基址,我们将会取得VA
前面的章节:
病毒技术二:获取kernel32.dll基址--http://bbs.bccn.net/viewthread.php?tid=464861&page=1&extra=page%3D1#pid2568288
病毒技术一:程序的重定位--http://bbs.bccn.net/thread-464424-1-1.html
[此贴子已经被作者于2016-5-30 22:22编辑过]