首页
论坛
课程
招聘
[原创]HookApi中学习PE文件格式(二)[原创]
2007-11-15 17:50 7375

[原创]HookApi中学习PE文件格式(二)[原创]

petnt 活跃值
12
2007-11-15 17:50
7375
【文章标题】: 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

[培训] 优秀毕业生寄语:恭喜id咸鱼炒白菜拿到远超3W月薪的offer,《安卓高级研修班》火热招生!!!

上传的附件:
收藏
点赞0
打赏
分享
最新回复 (7)
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lOOp 活跃值 2007-11-15 19:24
2
0
学习的一塌糊涂。。。。
雪    币: 7298
活跃值: 活跃值 (368)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
hxsoft 活跃值 2007-11-15 21:12
3
0
占座,第一篇才学完!
雪    币: 2355
活跃值: 活跃值 (258)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
tjszlqq 活跃值 1 2008-2-25 14:48
4
0
向楼主学习!收下慢慢看!
雪    币: 100
活跃值: 活跃值 (44)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
NWMonster 活跃值 1 2008-2-25 15:26
5
0
学习中………………谢谢分享。
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
wsyzyddd 活跃值 1 2008-2-28 01:56
6
0
谢谢LZ的HOOK教程
现在晚上2:00在看,晕死了。。
明天复习
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huanjian 活跃值 2008-3-1 08:30
7
0
偶也是学习的一踏糊涂了
雪    币: 200
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
厚皮文 活跃值 2008-3-2 00:22
8
0
偶也是学习的一踏糊涂了
游客
登录 | 注册 方可回帖
返回