【文章标题】: HookApi中学习PE文件格式(二)
【文章作者】: petnt
【作者邮箱】: petnt@sohu.com
【使用工具】: MASM32\OLLYDBG
【操作平台】: XP SP2
【作者声明】: 本文既不是讲解如何HOOK API的文章,也不是详细介绍PE格式的文章。只是初学PE格式和MASM编程的一点体会,希望和我一样的菜鸟们能从中有所收获。高手们如能从中指点一二,小菜将在此不胜感激。
--------------------------------------------------------------------------------
【详细过程】:
由于第一次发帖,所以不知道应该发在哪个版块下面,在此感谢版主的提醒与照顾。下面继续我们的HOOK之旅:
三、修改指定API的FirstThunK。
此方法只适用于不加壳且常规调用API的应用程序。程序如果加了壳,我们看到的输入表是外壳所用的输入表,即使被保护的程序用了相同的API,用下面的方法也HOOK不到他;如果我们的目标程序是自己亲自动手加载的DLL,同样我们在输入表中也看不到他所用到的API。还好我们的目的并不是研究怎么去HOOK各种情况下的API(实际上我也没那个水平:)),我们只是按照我们的思路去实现我们的目标。
在第二部分,我们已经得到了要HOOK的API的入口地址的RVA。如果我们把这个RVA处的地址写上我们的程序入口地址,那么程序要用到这个API的时候,就会被骗到我们程序里。为了显示我们骗术高明,我们还需要把真正的地址保存,这样可能保证程序在被我们骗走转晕之后,我们再把他送上回家之路。(虽然看上去好像很高明,实际上地球人都知道这种骗术。)
还有一点要说明,前面两个部分中我们肆无忌惮的读写目标进程,并不代表目标进程真的会任意的让我们去读写。或许,前面的成功只是代表我们的幸运。下面我们可能就再没有那么幸运了,因为输入表被载入内存之后,这个位置将只是可读。(这是在我几次失败后才发现的,看来菜是要付出代价的。:))为了我们的目标,不得不三顾茅庐,请出下面这位神仙:VirtualProtectEx。或许你可能会说,我有N种办法让输入表所在节变得可写。可我只是想在目标进程中小动手脚,而不想改变原文件,不要笑小菜我执著。哈哈,话又多了。言归正传,总之这个函数可以悄悄的让我们想改变的地方变得可写。
好了,我们把我们的想法变为现实吧。(下面只是代码片断,在第四部分我会做更详细的解释)
;改变我们要改写的位置的保护属性
invoke VirtualProtectEx,hProcess,dwRva,4,PAGE_EXECUTE_READWRITE,addr BufferDW
.if !eax ;dwRva是我们第二部分得到的RVA
jmp @Error
.endif
;将我们的代码入口写入指定位置,至于为什么要这样,将在下一部分解释。在这里,我们只需要
mov ecx,BaseAddress ;知道原理就可以了
add ecx,offset @NewEntry
sub ecx,offset APPEND_CODE
mov NewEntry,ecx
invoke WriteProcessMemory,hProcess, dwRva,addr NewEntry,4,NULL
第三部分的功能,在理论上我们已经实现了。
四、写操作指令到目标进程空间。
我们把程序骗到我们的代码中干些什么呢?可能有人想到了一些坏主意,算了,我们不能太黑(哈哈,其实太黑我也不会),就简单的显示一个MessageBox吧,用来提示我们的代码运行了。(总要有点成果嘛!)
如果加入的代码要用到API的话,那将是一个比较有趣的问题。我们可以在我们自己的程序中任意调用系统API,系统会根据我们调用把一切都安排好,就像输入表并不需要我们亲手打造一样。现在的问题是,我们的代码要在目标进程中执行。系统事先并不知道我们要调用API,就像包吃包住变成食宿自理,所有一切都得要自己动手了,哪怕我们只是想简单显示一个MessageBox。
还好,系统给了我们两个重要的API,LoadLibrary 和 GetProcAddress。这两个函数可以加载任意一个DLL并获取其中函数的地址。呵呵,或许我们看到了希望。但新的问题又出现,这两个函数本身就是API,我们又怎么获取他们的地址呢?
这两个函数位于Kernel32.dll中,这个dll我们因该面熟(对于我来说,也仅仅是面熟),因为我们经常看见他。看到他我们好像又看到了希望,因为他好像是所有应用程序都要用到的一个“常委”。(似乎windows是通过他来运行应用程序的,我不敢确定,因为我无法证实)为了测试我们的想法,我们可以写个小程序来测试他。
.386
.model flat,stdcall
option casemap:none
.data
.code
start:
ret
end start
编译并用OD载入他,点击M查看内存,哈哈,看到了我们熟悉的身影。
既然我们可以肯定他会被载入,下面的任务就是我们如何来定位他了。这方面的文章很多,实现方法也很多。甚至我们可以不惜麻烦一页一页地去寻找他,我们总有办法来发现他。发现它后我们可以在输出表中定为我们需要的函数。然而,这些工作让我们这些小菜来做,好像有点繁琐。能不能有其他捷径呢?
前面我们提到了“常委”一词,既然是常委,windows会不会给点特殊待遇呢?我们多载入几个程序看看,kernel32.dll总是被载到7C800000,再去查看一下他的默认载入值,恰好是7C800000。(我的系统是这样的,不知道其他的系统会不会是这样)我们是不是可以认为他总是在默认的载入地址载入呢?这好像是一个要承担风险的问题。
从另一个角度出发,我们可以很方便的获得我们自己进程中的LoadLibrary 和 GetProcAddress的入口地址,这对于我们获得目标进程的这两个函数的地址有没有帮助呢?我们可以大胆的设想,我们的程序肯定会和目标进程在同一个windows下运行,所以我们的程序和目标进程会用相同的kernel32.dll,即使他们不被载入到默认的载入地址,那么相同函数入口相对于k32文件头的偏移是不是相同呢?(这是我的设想,呵呵,小菜的想法总是很多,但却无法得到理论上的证实。)
由此,可以根据我们的想法写出一个获取目标进程API入口地址的函数:
.data
szKernel32 db 'Kernel32.dll',0
.code
ffGetK32ApiHandle proc dwKernelBase , lpApiName ;参数分别为目标进程k32的载入地址和
;指向目标API名称字符串的指针
local dwK32Base
invoke GetModuleHandle,addr szKernel32 ;获取本进程k32的载入地址
.if eax
mov dwK32Base,eax
invoke GetProcAddress,dwK32Base,lpApiName ;获取目标API在本进程的入口地址
.if eax
mov ebx,dwKernelBase ;根据我们的设想转换成目标进程的入口地址
sub ebx,dwK32Base
add eax,ebx
.endif
.endif
ret
ffGetK32ApiHandle endp
好了,如果一切顺利(哈哈,小菜我总是喜欢这样说,但确实我无法规避上面的代码所带来的风险),我们得到了目标进程中的LoadLibrary 和 GetProcAddress的入口地址。有了这两个函数,我们就可以去找MessageBox函数的入口地址了(没想到这么一个小API会给我们带来这么的麻烦)。
没想到我们又遇到了麻烦,要用API,我们无可避免地要用到变量。我们用普通方法读写变量的代码,被放到目标进程之后,变量的地址将无法得到确认。怎样才能正确的访问到变量呢?呵呵,小菜的想法别人肯定也想过了,所以一番查书和google之后,我们看到了这样的指令组合:
call @F
@@:
pop ebx
sub ebx,offset @B
这个之后所得的ebx,将是我们正确访问到变量的基础。好了,问题解决得差不多了,我们来写我们的代码吧:
;准备注入目标程序的代码
APPEND_CODE equ this byte
szUser32 db 'user32',0
szMessageBox db 'MessageBoxA',0
szCaptionXW db '询问',0
szText db '您所HOOK的API正要被运行,要他正常运行吗?',0
hDllUser32 dd ?
hMessageBox dd ?
@NewEntry:
call @F
@@:
pop ebx
sub ebx,offset @B
lea eax,[ebx+szUser32] ;下面的代码用来获取User32.dll基址
push eax
db 0e8h ;我们再也无法正常使用我们熟悉的invoke 了 ,参数都要自己push了
@LoadLibrary: ;0e8h是call的编码
dd ? ;这里写入的值应该是:想要CALL到的实际地址-@LOADLIBRARY处地址-4
mov [ebx+hDllUser32],eax
lea eax,[ebx+szMessageBox] ;下面的代码用来获取MessageBox入口
push eax
push [ebx+hDllUser32]
db 0e8h
@GetProcAddress:
dd ?
mov [ebx+hMessageBox],eax ;下面准备调用MessageBox,这里要注意push的顺序
mov ecx ,MB_YESNO ;我就在这里出过错。(别笑我菜)
push ecx
lea eax,[ebx+szCaptionXW]
push eax
lea ecx,[ebx+szText]
push ecx
push 0
call [ebx+hMessageBox] ;终于可以call我们的“小”MessageBox了
.if eax ==IDNO ;如果不让我们hook的函数运行而直接返回,就要考虑到堆栈平衡了
pop ecx ;这个是API调用后要返回的地址,我们应该保存
add esp,10h ;这个10h应该根据情况改变,我所hook的函数有四个参数,所以
push ecx ;这里是10h,一个参数应该是04h,以此类推
ret
.endif
@ToOldEntry:
db 68h ;这里之所以用了push ret组合,是因为我不知道jmp后面的地址该如何算,可是我在
@dwOldEntry: ;用到Call的时候不得不面对了这个问题
dd ? ;用来填入原来的入口地址
db 0c3h
APPEND_CODE_END equ this byte
;注入代码到此结束
;下面这个函数用来向目标进程注入上面的代码
.data
szLoadLibrary db 'LoadLibraryA',0
szGetProcAddress db 'GetProcAddress',0
.code
ffAddCode proc hProcess,dwRva ;参数为目标进程的句柄 和 我们在第二部分所得到的RVA
local BufferDW:DWORD
local RetEntry:DWORD
local lpLoadLibrary:DWORD
local lpGetProcAddress:DWORD
local BaseAddress:DWORD
local NewEntry:DWORD
;读取并保存 目标api的入口地址
invoke ReadProcessMemory,hProcess,dwRva,addr BufferDW,4,NULL
.if !eax
jmp @Error
.endif
mov eax,BufferDW
mov RetEntry,eax
;用我们有风险的代码获取两个重要函数的地址,其中的7c800000h最好从k32种读出
invoke ffGetK32ApiHandle,7c800000h,addr szLoadLibrary
mov lpLoadLibrary,eax
invoke ffGetK32ApiHandle,7c800000h,addr szGetProcAddress
mov lpGetProcAddress,eax
;在目标进程申请空间,准备写入代码
invoke VirtualAllocEx ,hProcess,NULL,offset APPEND_CODE_END-offset APPEND_CODE,\
MEM_COMMIT,PAGE_EXECUTE_READWRITE
.if !eax
jmp @Error
.endif
mov BaseAddress,eax
;把执行代码写入申请空间
invoke WriteProcessMemory,hProcess,BaseAddress,offset APPEND_CODE,\
offset APPEND_CODE_END-offset APPEND_CODE,NULL
;将LOADLIBRARY的地址写入指定位置
mov ecx,BaseAddress ;call后面的双字实际上是一个偏移量
add ecx,offset @LoadLibrary ;我们必须经过换算才能call到正确
sub ecx,offset APPEND_CODE ;的位置 下同
sub lpLoadLibrary,ecx
mov edx,lpLoadLibrary
sub edx,4
mov lpLoadLibrary,edx
invoke WriteProcessMemory,hProcess, ecx, addr lpLoadLibrary,4,NULL
;将GetProcAddress的地址写入指定位置
mov ecx,BaseAddress
add ecx,offset @GetProcAddress
sub ecx,offset APPEND_CODE
sub lpGetProcAddress,ecx
mov edx,lpGetProcAddress
sub edx,4
mov lpGetProcAddress,edx
invoke WriteProcessMemory,hProcess, ecx, addr lpGetProcAddress,4,NULL
;改变我们要改写的位置的保护属性
invoke VirtualProtectEx,hProcess,dwRva,4,PAGE_EXECUTE_READWRITE,addr BufferDW
.if !eax
jmp @Error
.endif
;将我们的代码入口写入指定位置
mov ecx,BaseAddress ;新入口=申请内存的基址+原计划入口与注入代码开
add ecx,offset @NewEntry ;头处的偏移,之所以称为原计划是指我们的程序
sub ecx,offset APPEND_CODE ;编译好之后入口和开头所对应的地址,下同
mov NewEntry,ecx
invoke WriteProcessMemory,hProcess, dwRva,addr NewEntry,4,NULL
;将返回入口写入指定位置
mov ecx,BaseAddress
add ecx,offset @dwOldEntry
sub ecx,offset APPEND_CODE
invoke WriteProcessMemory,hProcess, ecx, addr RetEntry,4,NULL
jmp @Ret
@Error:
mov eax,0
@Ret:
ret
ffAddCode endp
至此,我们所有的功能函数都已经实现了。我们用一个主函数将他们组织起来:
.386
.model flat,stdcall
option casemap:none
include e:\masm32\include\windows.inc
include e:\masm32\include\user32.inc
includelib e:\masm32\lib\user32.lib
include e:\masm32\include\kernel32.inc
includelib e:\masm32\lib\kernel32.lib
.const
szExe db 'Ini.exe',0 ;这应该根据情况指定
szCaption db '提示',0
szSuccess db '我找到并Hook了指定的API!',0
szFailed db '没有发现指定进程或打开进程失败!',0
szApiName db 'WritePrivateProfileStringA',0 ;这是我试验时hook的函数
szNoApi db '没有发现进程中引用指定的API!',0
.data?
hProcess2 dd ?
.code
include GetProcHandle.asm
include FindApi.asm
include GetK32ApiHandle.asm
include Code.asm
start:
invoke ffGetProcHandle,addr szExe
.if eax
mov hProcess2,eax
invoke ffFindApi,eax,addr szApiName
.if eax
;invoke MessageBox,NULL,offset szSuccess,offset szCaption,MB_OK
invoke ffAddCode,hProcess2,eax
.if eax
invoke MessageBox,NULL,offset szSuccess,offset szCaption,MB_OK
.endif
.else
invoke MessageBox,NULL,offset szNoApi,offset szCaption,MB_OK
.endif
.else
invoke MessageBox,NULL,offset szFailed,offset szCaption,MB_OK
.endif
invoke ExitProcess,NULL
end start
编译连接之后,先运行我们目标程序,再运行我们的程序,一切正常。我们期待的“小”MessageBox也顺利弹出了。(说来简单,其实真是不容易啊!) :)
其实,想法和现实之间是存在很多问题的。只要我们多动手才能发现并解决这些问题,这才是我们这些小菜们提高水平的捷径。在此感谢论坛给我们提供交流的机会,同时感谢罗云彬和他的《Windows环境下32位汇编语言程序设计(第2版)》,是这本书把我带进了汇编世界。
注:因为程序用到了WriteProcessMemory,所以有些防火墙可能要出来说话。请放心使用,因为我还没有能力写病毒或木马。另所附目标程序为 罗云彬 编写的例程,再次感谢。
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2007年11月15日 17:28:40
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
上传的附件: