【文章标题】: HookApi中学习PE文件格式(一)
【文章作者】: petnt
【作者邮箱】: petnt@sohu.com
【使用工具】: MASM32\OLLYDBG
【操作平台】: XP SP2
【作者声明】: 本文既不是讲解如何HOOK API的文章,也不是详细介绍PE格式的文章。只是初学PE格式和MASM编程的一点体会,希望和我一样的菜鸟们能从中有所收获。高手们如能从中指点一二,小菜将在此不胜感激。
--------------------------------------------------------------------------------
【详细过程】:
本文实现的是Hook已运行中程序的API,我们并不讨论这样做有没有实用价值。我们注重的应该是学习的过程,特别是对于我们这些初学的小菜们。其实自己动手写一个小程序,远比你看书看上十遍要刻骨铭心的多。
思路:
1、找到系统中指定的进程。
2、搜取进程的导入表,找到指定的API。
3、修改指定API的FirstThunK(此方法只适用于不加壳且常规调用API的应用程序)
4、写操作指令到目标进程空间。(如果加入的代码要用到API的话,那将是一个比较有趣的问题。)
好了,有了大体思路,我们就开始实现之路吧。ACTION!
一、找到系统中指定的进程。
这好像和Hook API或PE文件格式没有一点关系,实现起来也比较简单。我们只需记住和熟悉几个API就可以了,下面请他们隆重登场:CreateToolhelp32Snapshot、Process32First、Process32Next。(怎么没有掌声?原来他们并不是明星!)
CreateToolhelp32Snapshot能获取系统现有进程的快照,快照的句柄返回在Eax中。
Process32First获取快照中的第一个进程有关信息,信息返回在一个结构中。
Process32Next用来循环获取进程信息。
(以上函数具体用法请参考msdn)
有了这3个函数,我们就可以找到我们要找的目标进程了。我们用一个函数来实现他吧!
ffGetProcHandle proc lpAppName ;lpAppName为指向目标进程的文件名的指针
local hSnapShot
local stProcess:PROCESSENTRY32
local dwReturn
invoke RtlZeroMemory,addr stProcess,sizeof stProcess ;申请一块内存,用来存放系统进程快照
mov stProcess.dwSize,sizeof stProcess ;注意这个结构必须先填写 dwSize项
invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0
mov hSnapShot,eax ;呵呵,这里应该考虑eax=0的情况。
invoke Process32First,hSnapShot,addr stProcess
.while eax
invoke ffStringCmp,addr stProcess.szExeFile,lpAppName
.if eax
jmp @Find
.endif
invoke Process32Next,hSnapShot,addr stProcess
.endw
mov eax,0 ;如果没发现我们的目标就老老实实的上报情况吧
mov dwReturn,eax
jmp @Ret
@Find:
mov eax,stProcess.th32ProcessID
invoke OpenProcess ,PROCESS_ALL_ACCESS,0,eax ;如果发现了,我们就以所有权限打开他
mov dwReturn,eax ;eax中就是我们要操作的进程的句柄了,这里也应该考虑eax=0的情况
@Ret:
invoke CloseHandle,hSnapShot
mov eax,dwReturn
ret
ffGetProcHandle endp
其中的ffStringCmp是我自己写的字符串比较函数。说起来惭愧,因为初学asm,有都不知道怎么比较两个字符串,想了半天不知道怎么解决,只好硬着头皮写了一个。现在想来好像api中就有这样的函数。下面也把它贴出来,希望高手予以指点。
ffStringCmp proc uses esi edi ,lpstring1,lpstring2
mov esi,lpstring1
xor ecx,ecx
.repeat
lodsb
inc ecx
.until !al ;因为不知道字符串的size,所以这里还煞费了我一番苦心
mov esi,lpstring1
mov edi,lpstring2
repz cmpsb
jnz @No
mov eax,1
jmp @Ret
@No:
mov eax,0
@Ret:
ret
ffStringCmp endp
好了,第一部分就算完成了。
二、搜取进程的导入表,找到指定的API。
我们有了指定进程的句柄,下面就要拿这个句柄开刀了。谁来主刀呢,下面有请:WriteProcessMemory和ReadProcessMemory兄弟俩!(来点掌声鼓励,这两个人还是比较有名气的)
看名字就知道这兄弟俩是用来读、写指定进程的地址空间数据的。手术刀有了,下面我们来了解一下我们的目标的内部结构吧:
一般情况下,PE文件总能很顺利的被载入到默认的位置上。PE的文件头部分是按原样搬进内存的,所以我们可以从文件头下手找到我们感兴趣的部分。介绍PE头格式的文章很多,都介绍得比较详细。可是其中我们感兴趣的部分并不多。仔细分析完PE格式,你就会发现其实他并没有表面上看上去那么复杂。好了,废话少说,PE头中包含的我们感兴趣的部分有:节的数目、大小、每个节的RVA、输入表的RVA、输出表的RVA等等,我们总能够在固定的位置上发现这些我们感兴趣的东西。好像又要跑题,我们现在感兴趣的地方是输入表,所以我们的矛头要快速的指向他。
如果一个程序被顺利的载入到了默认地址,那么我们就可以在这个地址上发现这个文件的PE头。(大多数情况下,这个地址是00400000,同样这个值我们可以在头中发现他。)PE头以一个标志性的“MZ”开头,在从文件头开始的偏移为3ch处,藏着另一个重要的偏移,这个偏移指向的是另一个标志——“PE”。如果发现这两个标志我们都对上了,我们就有99%的把握说,我们发现了载入内存中文件头。
从“PE”开始偏移80h处,藏着我们今天要挨刀的主角,输入表的RVA。顺着这个RVA,我们就可以发现我们的主角。我们有必要隆重的介绍一下我们的主角:
我们的主角由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,这个结构有5个成员:OriginalFirstThunk 、TimeDateStamp 、ForwarderChain 、Name1、FirstThunk。简而言之,每个结构对应了一个导入库的信息。这个程序用到了多少DLL库,就有多少个IMAGE_IMPORT_DESCRIPTOR结构与之相对应。这一系列的结构以一个全零的IMAGE_IMPORT_DESCRIPTOR结构结束。
OriginalFirstThunk 、FirstThunk 同样是一个RVA,两个RVA又分别指向了一系列的双字结构。这一系列的双字结构同样以一个全零的双字作为结构的结束。一个DLL中导入了多少函数,就会有多少个这样的双字结构与之对应。文件中的OriginalFirstThunk 、FirstThunk指向的位置存放的仍然是个RVA(如果导入函数是按照名称导入的话),他们又同时指向了一个结构,这个结构存放的就是导入函数的序号和函数名。载入内存中的FirstThunk所指向的位置存放的就是导入函数的实际地址组成的序列。这个序列与OriginalFirstThunk最终所指向的函数名序列相对应。
我们的任务,就是在所有的IMAGE_IMPORT_DESCRIPTOR结构中搜索OriginalFirstThunk所指向的函数名,如果发现了我们要找的函数,再找到与之对应的FirstThunk所指向的地址,我们就找到了我们要动手术的地方了。
好了,同样以一个函数实现我们的目标。
.data
DOS_HEADER equ 00400000h ;我们估且认为是这个默认值,其实为了安全起见,我们应该在PE头是把这个值读出来。
.code
ffFindApi proc hProcess , lpApiName;传来的参数为目标进程的句柄和指向API名字的指针
local BufferW:WORD
local lpPEheader:DWORD
local BufferDW:DWORD
local Import_VirtualAddress:DWORD
local Import_Size:DWORD
local szBuffer[512]:byte
local szName[64]:byte
local Import_FirstThunk:DWORD
local Import_OriginalFirstThunk:DWORD
invoke ReadProcessMemory,hProcess,DOS_HEADER,addr BufferW,2,NULL
.if BufferW!='ZM' ;比较这个位置上放的是不是著名的“MZ”
jmp @Error
.endif
invoke ReadProcessMemory,hProcess,DOS_HEADER+3ch,addr BufferW,2,NULL
.if !eax ;读取偏移3ch处的值,这个值是个偏移
jmp @Error ;这个偏移处应该放着另一个著名的标志。
.endif
movzx eax,BufferW
add eax,DOS_HEADER
mov lpPEheader,eax
invoke ReadProcessMemory,hProcess,lpPEheader,addr BufferW,2,NULL
.if BufferW!='EP' ;看看是不是著名的“PE”
jmp @Error
.endif
mov edx,lpPEheader
add edx,80h ;如果一切顺利,这里存放的一个双字就是我们要找的输入表
invoke ReadProcessMemory,hProcess,edx,addr BufferDW,4,NULL ;的偏移,紧接着的一个双字
.if !eax ;是输入表的大小
jmp @Error
.endif
mov eax,BufferDW
mov Import_VirtualAddress,eax
mov edx,lpPEheader
add edx,84h ;这个位置存放的就是输入表的大小
invoke ReadProcessMemory,hProcess,edx,addr BufferDW,4,NULL
.if !eax
jmp @Error
.endif
mov eax,BufferDW
mov Import_Size,eax
mov edx,Import_VirtualAddress
add edx,DOS_HEADER
invoke ReadProcessMemory,hProcess,edx,addr szBuffer,Import_Size,NULL
.if !eax ;将输入表读入我们预留的位置,以便于我们操作(千万别大于512),
jmp @Error ;因为我们给他留的地方就那么大,再多就住不下啦。:)
.endif
lea edi,szBuffer
assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
.while [edi].OriginalFirstThunk || [edi].TimeDateStamp || [edi].ForwarderChain || [edi].Name1 || [edi].FirstThunk
.if [edi].OriginalFirstThunk!=0
mov edx,[edi].OriginalFirstThunk
add edx,DOS_HEADER
mov Import_OriginalFirstThunk,edx
mov edx,[edi].FirstThunk
add edx,DOS_HEADER
mov Import_FirstThunk,edx
mov edx,[edi].OriginalFirstThunk
add edx,DOS_HEADER
invoke ReadProcessMemory,hProcess,edx,addr BufferDW,4,NULL
.if !eax
jmp @Error
.endif
.if BufferDW & IMAGE_ORDINAL_FLAG32 ;看是不是以序号导出的,是的话我们什么
.else ;也不做,因为我们不关心他
.while BufferDW
mov edx,BufferDW
add edx,2 ;因为前两个字节代表的是函数序号,我们关心的
add edx,DOS_HEADER ;是紧随其后的函数名
invoke ReadProcessMemory,hProcess,edx,addr szName,64,NULL
.if !eax ;名字大于64同样会住不下:),希望能够他们住。
jmp @Error
.endif
invoke ffStringCmp, addr szName ,lpApiName
;比较是否和我们要找的名字相同
.if eax
mov eax,Import_FirstThunk
jmp @Ret ;如果找到就返回这个函数地址在内存的RVA
.endif
assume ebx:nothing ;没找到就读取序列中的下一个继续比较
mov eax,Import_FirstThunk
add eax,4
mov Import_FirstThunk,eax
mov eax,Import_OriginalFirstThunk
add eax,4
mov Import_OriginalFirstThunk,eax
invoke ReadProcessMemory,hProcess,Import_OriginalFirstThunk,addr BufferDW,4,NULL
.if !eax
jmp @Error
.endif
.endw
.endif
.endif
add edi,sizeof IMAGE_IMPORT_DESCRIPTOR ;还没找到就找另一个DLL
.endw
@Error:
mov eax,0
@Ret:
assume ebx:nothing
assume edi:nothing
ret
ffFindApi endp
至此,第二任务完成。
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!(未完,待续)
[课程]Linux pwn 探索篇!