动态获取API的地址

原理

现在虽然大部分Win32程序都使用ExitProcess函数来终止执行,但是其实用ret指令也是可以的。我们的应用程序的主程序可以被看成是一个被Windows调用的子程序。当父进程要创建一个子进程时,它会调用Kernel32.dll中的CreateProcess函数,CreateProcess函数在完成装载应用程序后,就会把一个返回地址用push压入栈,这个返回地址是子进程用ret来结束函数的时候要返回到的地方,返回到这后父进程会用ExitThread来结束这个线程,如果这个线程是子进程的最后一个线程的话,父进程就会再调用ExitProcess函数来结束子进程。重要的是:子进程返回到的地址,也就是父进程创建完子进程后压入栈的那个地址,是处于Kernel32.dll中的。所以,我们可以在程序中一开始就获取堆栈里的内容,这样我们就得到了一个处于Kernel32.dll中的地址,然后我们再顺着这个地址往低地址查询,直到找到PE文件头,那么我们就可以确定Kernel32.dll的起始地址了。得到了它的地址,我们可以根据函数名,通过数据目录找到导出表的地址,然后再遍历查找到GetProAddress函数的地址,有了这个函数,我们就可以轻松得到其他函数的地址了,当然我们还要得到LoadLibrary函数的地址,这样我们就可以得到其他动态链接库的基址了。

要注意的是,PE文件被装入内存时是按照64KB对齐的,所以Kernel32.dll的基址肯定也在某一个大小为64KB的页的开始部位,所以我们只要按照这个大小去找,就能找到Kernel32.dll的基址。

获取Kernel32.dll基址和GetProcAddress函数的入口地址的功能的代码放在一个函数中了,代码如下(参数_dwKernelRet就是程序一开始从SP处取得的值):

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;错误Handler
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_SEHHandler		proc	C _lpExceptionRecord,_lpSEH,\
						_lpContext,_lpDispatcherContext
						
				pushad
				mov		esi,_lpExceptionRecord
				mov		edi,_lpContext
				assume	esi:ptr EXCEPTION_RECORD,edi: ptr CONTEXT
				mov		eax,_lpSEH
				push	[eax + 0ch]			;恢复ebp
				pop		[edi].regEbp		;
				push	[eax + 8]
				pop		[edi].regEip		;设置IP指向发生中断后要执行的代码的位置
				push	eax
				pop		[edi].regEsp		;恢复ESP
				assume	esi:nothing,edi:nothing
				popad
				mov		eax,ExceptionContinueExecution
				ret
				
_SEHHandler		endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;在内存中扫描Kernel32.dll的基址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetKernelBase	proc	_dwKernelRet
				local	@dwReturn
				
				pushad
				mov		@dwReturn,0
;***************************************************************************************************
;重定位
;***************************************************************************************************
				call	@F
				@@:
				pop		ebx
				sub		ebx,offset @B		;获得偏移
;****************************************************************************************************
;创建用于错误处理的SEH结构
;****************************************************************************************************
				assume	fs:nothing
				push	ebp
				lea		eax,[ebx + offset _PageError]		;修正代码位置
				push	eax
				lea		eax,[ebx + offset _SEHHandler]		;中断处理函数的位置
				push	eax
				push	fs:[0]
				mov		fs:[0],esp
;*******************************************************************************************************
;查找Kernel32.dll的基地址
;*******************************************************************************************************
				mov		edi,_dwKernelRet
				and		edi,0ffff0000h						;取高位
				.while	TRUE
						.if		word ptr [edi] == IMAGE_DOS_SIGNATURE			;查看第一个字段,判断是否是DOS文件头
								mov		esi,edi
								add		esi,[esi + 003ch]						;加上指向PE文件头的偏移
								.if	word ptr [esi] == IMAGE_NT_SIGNATURE		;判断是否是PE头
										mov		@dwReturn,edi					;
										.break
								.endif
						.endif
						_PageError:
						sub		edi,010000h										;向上寻找,一直找到Kernel32.dll的基地址为止(PE文件被装入内存时按照64KB对齐的)
						.break	.if	edi < 070000000h
				.endw
				pop		fs:[0]							;恢复原来的SEH链
				add		esp,0ch
				popad	
				mov		eax,@dwReturn
				ret
				
_GetKernelBase	endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;从内存中模块的导出表中获取某个API的入口地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetApi			proc	_hModule,_lpszApi
				local	@dwReturn,@dwStringLength
				
				pushad
				mov		@dwReturn,0
;***************************************************************************************************************
;重定位
;***************************************************************************************************************
				call	@F
				@@:
				pop		ebx
				sub		ebx,offset @B					;获取偏移
;****************************************************************************************************************
;创建用于错误处理的SEH结构
;****************************************************************************************************************
				assume	fs:nothing
				push	ebp
				lea		eax,[ebx + offset _Error]		;修正代码位置
				push	eax
				lea		eax,[ebx + offset _SEHHandler]	;中断处理函数
				push	eax
				push	fs:[0]
				mov		fs:[0],esp
;*****************************************************************************************************************
;计算API字符串的长度(带尾部的0)
;*****************************************************************************************************************
				mov		edi,_lpszApi
				mov		ecx,-1
				xor		al,al
				cld
				repnz	scasb							;于指定区域扫描制定字符,区域由ES:DI及CX指定。字符由AL指定。如果找到ES:DI指向该字符,ZF=1,否则,ZF=0 
				mov		ecx,edi
				sub		ecx,_lpszApi					;减去字符串基地址得到字符串长度
				mov		@dwStringLength,ecx
;******************************************************************************************************************
;从PE文件头的数据目录获取导出表的位置
;******************************************************************************************************************
				mov		esi,_hModule
				add		esi,[esi + 3ch]
				assume	esi:ptr IMAGE_NT_HEADERS
				mov		esi,[esi].OptionalHeader.DataDirectory.VirtualAddress	;获取导出表的RVA
				add		esi,_hModule
				assume	esi:ptr IMAGE_EXPORT_DIRECTORY
;*******************************************************************************************************************
;查找符合条件名称的导出函数名
;*******************************************************************************************************************
				mov		ebx,[esi].AddressOfNames
				add		ebx,_hModule
				xor		edx,edx
				.repeat
						push	esi
						mov		edi,[ebx]
						add		edi,_hModule
						mov		esi,_lpszApi
						mov		ecx,@dwStringLength
						repz	cmpsb					;比较esi和edi指向的内容,如果相同就将EDI指向后面一个单位继续比较,单位为一个字节,ECX中存放要比较的字符长度,存在不相同的就停止并设置zf标志位为0
						.if		ZERO?
								pop		esi
								jmp		@F
						.endif
						pop		esi
						add		ebx,4
						inc		edx
				.until	edx >= [esi].NumberOfNames
				jmp		_Error
@@:
;*********************************************************************************************************************
;API名称索引-->序号索引-->地址索引
;*********************************************************************************************************************
				sub		ebx,[esi].AddressOfNames
				sub		ebx,_hModule
				shr		ebx,1
				add		ebx,[esi].AddressOfNameOrdinals		
				add		ebx,_hModule
				movzx	eax,word ptr [ebx]					;获得序号索引
				shl		eax,2
				add		eax,[esi].AddressOfFunctions
				add		eax,_hModule						;获得地址
;************************************************************************************************************************
;从地址表得到导出函数的位置
;************************************************************************************************************************
				mov		eax,[eax]					;得到指向函数地址的RVA
				add		eax,_hModule
				mov		@dwReturn,eax
_Error:
				pop		fs:[0]						;恢复原来的SEH链
				add		esp,0ch
				assume	esi:nothing
				popad
				mov		eax,@dwReturn
				ret
				
_GetApi			endp

在查找Kernel32.dll基址的时候,我们将参数(也就是一个位于Kernel32.dll中的一个地址)的低位清0,因为我们要按照64KB的大小来寻找。

当我们找到Kernel32.dll基址后,就可以通过这个基址定位到PE文件头中的IMAGE_OPTIONAL_HRADER32结构的数据目录表的基址,然后再通过数据目录找到导出表的位置,最后再通过函数名找到函数在导出函数序号表中的索引,然后获取函数的序号,最后获取到函数的真正入口地址。

可以写一个程序来验证一下:
 

				.386
				.model	flat,stdcall
				option	casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include			windows.inc 
_ProtoGetProcAddress	typedef proto	:dword, :dword
_ProtoLoadLibrary		typedef	proto	:dword
_ProtoMessageBox		typedef	proto	:dword, :dword, :dword, :dword
_ApiGetProcAddress		typedef	ptr		_ProtoGetProcAddress
_ApiLoadLibrary			typedef	ptr		_ProtoLoadLibrary
_ApiMessageBox			typedef	ptr		_ProtoMessageBox
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
				.data?
hDllKernel32	dd		?
hDllUser32		dd		?
_GetProcAddress	_ApiGetProcAddress		?
_LoadLibrary	_ApiLoadLibrary			?
_MessageBox		_ApiMessageBox			?

				.const
szLoadLibrary	db		'LoadLibraryA',0
szGetProcAddress db		'GetProcAddress',0
szUser32		db		'user32',0
szMessageBox	db		'MessageBoxA',0
szCaption		db		'A MessageBox !',0
szText			db		'Hello, Win32 ASM !',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
				.code
include			_GetKernel.asm
start:
;******************************************************************************************************************
;从堆栈中的Ret地址找到Kernel32.dll的基地址,并在Kernel32.dll
;的导出表中查找GetProcAddress函数的入口地址
;******************************************************************************************************************
				invoke	_GetKernelBase,[esp]				;此时ESP所指的地址在Kernel32.dll中
				.if		eax
						mov		hDllKernel32,eax
						invoke	_GetApi,hDllKernel32,addr szGetProcAddress			;得到GetProcAddress函数的入口地址
						mov		_GetProcAddress,eax
				.endif
;******************************************************************************************************************
;用得到的GetProcAddress函数得到LoadLibrary函数地址并装入其他DLL
;******************************************************************************************************************
				.if		_GetProcAddress
						invoke	_GetProcAddress,hDllKernel32,addr szLoadLibrary
						mov		_LoadLibrary,eax
						.if		eax
								invoke	_LoadLibrary,addr szUser32
								mov		hDllUser32,eax
								invoke	_GetProcAddress,hDllUser32,addr szMessageBox
								mov		_MessageBox,eax
						.endif
				.endif
				.if		_MessageBox
						invoke	_MessageBox,NULL,offset szText,offset szCaption,MB_OK
				.endif
				ret
			
end				start
						
				

程序先调用 _GetKernelBase函数获取kernel32.dll的基址,然后再调用_GetApi获取GetProcAddress函数的入口地址,然后通过GetProcAddress函数我们可以获得LoadLibrary函数的地址,这样我们就可以轻松得到我们想要的函数和DLL的地址了,当然我们要方便一点用这些获取的函数的话,要像这个程序一样在最前面要声明函数原型。

程序编译链接后运行:

可以正确显示窗口,那么证明这样做是可以的。我们可以再用之前的查看导入表的程序查看这个程序:

 可以看到这个函数里面没有任何导入函数,全都是我们自己导入的。

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值