【文章标题】: 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
至此,第二任务完成。
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!(未完,待续)

【文章标题】: 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