| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 7562 人关注过本帖
标题:病毒技术三:获取kernel32.dll内的API地址
只看楼主 加入收藏
AXRZ
Rank: 2
等 级:论坛游民
威 望:5
帖 子:48
专家分:84
注 册:2016-3-23
收藏
 问题点数:0 回复次数:4 
病毒技术三:获取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编辑过]

搜索更多相关主题的帖子: 编译器 字符串 技术 空间 
2016-05-29 02:23
hu9jj
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
来 自:红土地
等 级:贵宾
威 望:400
帖 子:11759
专家分:43407
注 册:2006-5-13
收藏
得分:0 
不错的学习参考资料

活到老,学到老!http://www.(该域名已经被ISP盗卖了)E-mail:hu-jj@
2016-05-29 14:23
wmf2014
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
等 级:贵宾
威 望:216
帖 子:2039
专家分:11273
注 册:2014-12-6
收藏
得分:0 
世上无难事,只要肯登攀!
另一种说法:
世上无难事,只怕有心人!
尽管已经没有这方面的兴趣,但还是要顶下楼主!

能编个毛线衣吗?
2016-05-29 18:52
AXRZ
Rank: 2
等 级:论坛游民
威 望:5
帖 子:48
专家分:84
注 册:2016-3-23
收藏
得分:0 
回复 3楼 wmf2014, hu9jj
谢谢二位的支持,最近工作忙,更新的会比较慢
2016-05-30 10:20
AXRZ
Rank: 2
等 级:论坛游民
威 望:5
帖 子:48
专家分:84
注 册:2016-3-23
收藏
得分:0 
顺带一提,下一张将会是病毒的初始化:将前面的三章结合在一起并简单提一提代码加密
2016-05-30 10:22
快速回复:病毒技术三:获取kernel32.dll内的API地址
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.016904 second(s), 8 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved