======================================================
注:本文源代码点此下载
======================================================
原文地址:http://blog.csdn.net/wts/archive/2008/08/08/2786042.aspx
从网上看到《delphi api hook完全说明》这篇文章,基本上都是大家转来转去,原文出处我已经找不到了。
这篇文章写的很不错,但最后部分“permutefunction 的终极版本”描述的不太清楚,完全按照该文章代码执行,是不行的。
可能是作者故意这样做的?本文最后提供修正后的下载地址。
原文如下:
一、关于api hook
1.什么是api hook不知道大家是否还记得,在dos
系统中编程,经常会采取截取中断向量的技术:我们可以设置新的中断服务程序,当系统其他的程序调用这个中断时,就让它先调用我们自己设置的新的中断服务程
序,然后再调用原来的中断服务程序,这样就能够获得非凡的控制权。许多优秀的软件和大多数dos 病毒程序都采用了这个方法。
在windows 中,我们也可以采取类似技术。当系统调用某个api 函数时,就会先进入我们自己的函数,然后再调用原来的api
函数,这样,我们的程序就可以取得更多的控制权,我们就可对windows
系统中的任意一个函数调用进行动态拦截、跟踪、修改和恢复,就可让windows
系统中的任意一个函数按我们的设想工作。这种技术有许多名称,比如“陷阱技术”、“重入技术”等,不过我认为还是api hook
最贴切。原因嘛,等一下你看编程就明白了。
这样重要的技术,大家已经都知道了吧?哈哈,知道的都不说,不知道的呢,你就自己慢慢去摸索吧。偶尔有一两篇文章见于报端,不是藏头露尾,就是已经
过时了。还有的即使把原理都告诉你了,但就是不说它调用了哪些函数。要源代码?行,拿钱来。有人说了,这种技术用来编病毒最合适,所以..(因为菜刀可以
杀人,所以菜刀已被禁止使用了)。而实际呢,你看看使用了这种技术的国产软件就知道了:金山词霸、东方快车、richwin、东方词圣..在这里,我感觉
有必要简单说说金山词霸的工作原理。
2.金山词霸的工作原理大家都用过金山词霸吧?当你把光标指向一个单词,词霸就会自动弹出一个窗口并把单词的意思翻译出来。这究竟是怎么做出来的呢?我在这里简单说明一下。
( 1 ) 安装鼠标钩子。
( 2 )一旦光标在屏幕上移动,系统就会调用鼠标钩子,词霸通过鼠标钩子能够获得光标的坐标( x , y ) ,并安装textout()、exttextout()等api 函数钩子。
(3)词霸向光标下的窗口发重画消息,负责绘制该点的应用程序在收到wm_paint 消息后,就可能调用textout()、exttextout()等函数重绘字体。
(4)调用的函数将被钩子函数拦截,词霸就能够截获该次调用,从应用程序的数据段中将“文字”指针的内容取出,并作翻译处理。
(5)退出跟踪程序,返回到鼠标钩子,解除对textout()、exttextout()等api 函数的跟踪。
(6)完成了一次“屏幕抓字”。
这里的关键有两点:安装鼠标钩子和api 钩子。安装鼠标钩子非常简单,而api 钩子正是取词的核心代码。
3.关于delphi
事实上,随着互联网的普及,许多秘密都已不再是秘密,api hook 也一样。在网上,你已经可以找到这样的免费源代码,但是大部分可能已经过时,而且这些源代码大都是基于vc++的。如果你想找到用delphi 编写的源代码,那么,你还是读一读我的文章吧。
delphi 是编程工具史上的一个里程碑式的作品。如果你在使用它,我向你表示祝贺。如果你没有使用它,你也没有什么损失。网上关于几种语言谁好谁坏都吵得天翻地覆的了,我不想增加新仇也不想算算旧恨。每种语言都有它的优缺点,每个人都有自己选择的权利嘛!
不过,用delphi 编写api hook 有几处“陷阱”。我想,除了介绍api hook 以外,这也是为什么我要写这篇文章的一个原因吧!
4.哪些人可以读这篇文章
当然,读这篇文章并没有什么限制。但是你最好已经懂得鼠标钩子的制作过程,手边有msdn
那就再好不过了。我认为,只要你是windows
的程序员,就一定要有msdn。原因?有一套就明白啦。如果你懂得pe文件结构,那就更好了。在这篇文章里,我给出了所有的源代码(还不到2 0
0行)。如果你想修改程序,最好用softice。
5.关于我的程序
本文中的程序在windows me 的操作环境下,使用delphi5.0 编程调试通过。无论是商用还是个人使用,你都可以随意使用和修改本文中的程序,并且不需要在程序中加注我的个人信息。
二、用delphi 编写api hook
1.改写api 函数
为了使我们改写的代码正确运行,我们的函数必须和要改写的api
函数具有同样形式的形参。在我的程序中,我拦截了messageboxa 和messageboxw
两个函数。所以我这样定义自己的函数:function
myboxa(hwn:hwnd;lptext:pchar;lpcapion:pchar;utype:cardinal):integer;stdcall;function
myboxw(hwn:hwnd;lptext:pchar;lpcapion:pchar;utype:cardinal):integer;stdcall;
注意到我使用了stdcall 关键字,这是为了我的函数的形参的进出栈顺序与我们要拦截的函数一致。我们知道,为了系统的安全,win32
并不允许直接改写内存中的代码段。所以,有人想了好多种方法绕过系统的保护。实际上,win32
为了我们能安全地改写内存中的代码,提供了一个函数:writeprocessmemory。有许多人曾经告诉我,writeprocessmemory
也不能用来改写,不过我一直使用得很好。也许用它产生了一些bug,只是我并不知道罢了,所以还请这方面的专家指正。在pe文件中,当你呼叫另一模块中的
函数(例如user32.dll中的getmessage),编译器编译出来的call指令并不会把控制权直接传给dll
中的函数,而是传给一个jmp dwordptr [xxxxxxxx]指令,[xxxxxxxx]内含该函数的真正地址(函数进入点)。为了得到a
p i 函数的地址,我们可以这样:address:=@messageboxa。如上文所说的,我们得到的仅仅是一个跳转指令,后面紧接着的才是
messageboxa真正的开始代码的地址(具体可以查阅pe
文件的资料)。在下面的程序中我自定义了一个结构(叫记录?我习惯了,改不过口来),注意,这里使用了packed 关键字:
timportcode = packed record
jumpinstruction : word; // 应该是$25ff,jump 指令
addressofpointertofunction: ppointer;// 真正的开始地址
end;
pimportcode = ^timportcode;
其中,ppointer = ^pointer ;
用以下函数返回函数的真正地址:
function truefunctionaddress(func: pointer): pointer;
var
code: pimportcode;
begin
result:= func;
if func = nil then exit;
try
code := func;
if (code.jumpinstruction = $25ff) then begin
result := code.addressofpointertofunction^;
end;
except
result := nil;
end;
end;
这样,只要用我们的函数的地址替代它就可以了。替换函数:
procedure permutefunction(oldfunc:ppointer; newfunc:pointer);
var
written: dword;
begin
writeprocessmemory(getcurrentprocess, oldfunc, @newfunc, 4, written);
end;
你新建一个unit apihook,把上面的函数和结构写进去并保存下来。
2.第一个程序
新建一个application try1,主form 的单元名称不妨叫tryunit1。把上面的unit apihook 加进来。再新建一个unitmess,添加如下代码:
unit mess;
interface
uses
windows, messages, sysutils, classes, apihook;
procedure api_hookup;
procedure un_api_hook;
var
funcmessageboxa, funcmessageboxw: pimportcode;
implementation
type
tmessagea = function(hwn: hwnd; lptext: pchar; lpcapion: pchar; utype: cardinal):
integer; stdcall;
tmessagew = function(hwn: hwnd; lptext: pwidechar; lpcapion: pwidechar;
utype: cardinal): integer; stdcall;
var
oldmessageboxa: tmessagea;
oldmessageboxw: tmessagew;
function myboxa(hwn:hwnd;lptext:pchar;lpcapion:pchar;utype:cardinal): integer; stdcall;
begin
result := oldmessageboxa(hwn, 'succes hook a !', lpcapion, utype);
end;
function myboxw(hwn:hwnd;lptext:pwidechar;lpcapion:pwidechar;utype:cardinal):
integer; stdcall;
begin
result := oldmessageboxw(hwn, '成功挂上w!', lpcapion, utype);
end;
procedure api_hookup;
begin
if @oldmessageboxa = nil then
@oldmessageboxa := truefunctionaddress(@messageboxa);
if @oldmessageboxw = nil then
@oldmessageboxw := truefunctionaddress(@messageboxw);
permutefunction(funcmessageboxa.addressofpointertofunction, @myboxa);
permutefunction(funcmessageboxw.addressofpointertofunction, @myboxw);
end;
procedure un_api_hook;
begin
if @oldmessageboxatrydll 加载
--> 运行api_hookup,为trydll
挂上mybox。即使我们按下了button1,其他的进程加载了trydll,也仅仅是运行api_hookup,为trydll 挂上mybox
而已,而其他的进程(包括try2 本身)并没有挂上mybox。不信,你可以在trydll 中加上一个启动mybox
的函数,测试一下。例如,你可以在t r y d l l 的s t o p h o o k 函数中的unhookwindowshookex
语句前,加上一句:messageboxw(0,'messageboxw','这是测试dll是否加载了mybox',mb_ok);你可以看到弹出窗
口中的信息是“成功挂上w!”而不是“messageboxw”。并且由于我们的trydll
加载时就启动了api_hookup,卸载时才运行un_api_hook,所以不论你是否按下button1
和button3,并且不论你按下了几次,每次你按下button3
都会得到“成功挂上w!”的信息。看来,真正的麻烦才刚刚开始。实际上,我们刚才的工作都是有用的。我们剩下的工作就是改进unit apihook
中的truefunctionaddress 函数而已。想不到吧?为了能让其他的进程挂上mybox,我们必须了解一下pe文件的格式。
3.pe文件格式
分配表、页模式、虚拟内存、内存映射..够写一本书的了吧?我这儿只想简单地说两句。pe文件格式是windows
9x以上版本和windows nt操作系统中广泛采用的32 位可执行文件格式。与16 位的ne
格式不同的是,如果在内存中建立module(module这一术语通常用来表示已装入内存的可执行文件或dll的代码、数据及资源,除了程序中直接用到
的代码和数据外,一个module也指用于判定代码及数据在内存中位置的支撑数据结构),则这个module
中的代码、数据、输入表、输出表以及其他有用的数据结构等使用的内存都放在一个连续的内存块中。编程人员只要知道装载程序文件映像到内存后的地址,即可通
过映像后面的各种指针找到module中的所有内容。具体来说,pe格式中的许多项是以rva(相对虚拟地址)方式指定的,rva就是某个项相对于文件映
像地址的偏移。例如,装载程序将一个pe文件装入到虚拟地址空间,从0x10000开始的内存中,如果pe文件中某个表在映像中的起始地址是
0x10800,那么该表的rva就是0x800。将rva转化为可用的指针,只要将rva的值加上module的基地址即可。基地址是指装入到内存中的
exe或dll程序的开始地址,它是windows编程中的一个重要概念。为了方便起见,win32将module的基地址作为module的实例句柄
(instance handle)。在win32 中,你可以直接调用getmodulehandle取得指向dll的指针,通过该指针访问该dll
module的内容。
pe文件格式可执行文件共有五部分组成:ms-dos首部、pe
首部、信息块表、信息块、辅助信息。ms-dos首部是一个极小的dos 程序,一般是为了显示像“thisprogram cannot be
run in ms-dos mode”这类的信息。在delphi 中,其定义在pimagedosheader
结构中。该首部还给出了pe首部结构的起始地址,_lfanew字段就是真正pe 首部的相对偏移。pe 首部在delphi
中定义为pimagentheaders结构,该结构是由一个双字的标志项和两个子结构构成:
signature : dword;
fileheader : timagefileheader;
optionalheader : timageoptionalheader;
标
志项是为了说明该可执行文件是“pe\o\o”、“ne”还是“le”。timagefileheader包含了编译器产生的coff
obj信息。timageoptionalheader包含有堆栈初始大小、块表大小、数据目录(data
directory)及其他一些重要信息。你并不需要知道timageoptionalheader的所有字段。最重要的两个字段是i m a g e
b a s e 和s u b s y s t e m 。其中还有一个重要的字段——image_data_directory
datadirectory[image_numberof_directory_entries]。数组一开始的元素内含可执行文件重要部位的rva及
大小。数组的第一个元素代表exported function table(如果有的话)的地址和大小,第二个元素代表imported
functiontable的地址和大小,依此类推。在我们的程序里,用它得到rva:
rva := nt^.optionalheader.datadirectory[image_directory_entry_import].virtualaddress;
这样,我们也得到了exported function table 和imported function table。这里有必要谈一谈importedfunction table。
exe/dll
在被加载内存之前,存放在pe 文件的imported
table(或称为.idata)中的信息是给加载器用来决定函数地址并修补它们,以便完成image 用的。而在被加载之后, .idata
内含的是指针,指向exe/dll的输入函数。.idata section(imported
table)是以一个image_import_descriptor数组开始。在pe文件中联结(implicitly link)的dlls
都会在此有一个对应的image_import_descriptor
结构。最后一个image_import_descriptor结构的内容全部为null,以此作为结束符号。
image_import_descriptor的格式描述如下:
dword
characteristics/originalfirstthunk:这是一个偏移值(一个rva),对应于一个输入函数。dword
timedatestamp:这是文件的产生时刻。通常此栏为0 。然而微软的b i n d
程序可以将此image_import_descriptor所对应的dll的产生时刻写到这里来。
dword
forwarderchain:这个字段关系到所谓的forwarding(转交),意味着一个dll
函数在参考(呼叫、利用)另一个dll。这个字段内含一个索引,指向firstthunk数组。被这个索引所指定的函数就是一个转交函数。dword
name:这是一个rva,指向一个以null为结束字符的ascii字符串,内含imported
dll的名称。pimage_thunk_data firstthunk:这是一个r v a , 指向一个d w o r d
s(image_thunk_data)数组。大部分情况下,该d w o r d 被解释为一个指向image_import_by_name
结构的指针。然而,以函数序号(而非函数名称)输入,也是有可能的。image_import_descriptor结构中,最重要的部分是i m p
o r t e d d l l 的名称以及两个image_thunk_data dwords 数组。每一个image_thunk_data
dwords对应一个输入函数。在exe 文件中,两个数组(分别由characteristics 和firstthunk
栏位指向)平行存在,并且都以null 为结束符号。第一个数组(由characteristics
指向)从不被修改,有时候它又被称为hint-name table。第二个数组(由firstthunk
指向)则被加载器改写。载入器一一检阅每一个image_thunk_data,
并找出它所记录的函数的地址,然后把位址写入image_thunk_data这个dword 之中。
我们在改写api hook
那一节谈过,对dll 函数的呼叫会导致一个jmp dword
ptr[xxxxxxxx]指令。[xxxxxxxx]事实上参考到firstthunk数组中的一个元素。由于这个image_thunk_data
数组内容已被加载器改写为输入函数的地址,所以它又被称为importedaddress table(iat)。
我希望大家能仔细地研究一下
msdn里关于这方面的文章。你天天编程都和pe文件打交道,不了解可不行哟。我感觉说得太多了。我之所以说了这么多,特别是
对image_import_descriptor结构,因为delphi好像并没有定义image_import_descriptor结构,也许是我
没有找到。在我的程序里,自定义了这个结构,为了找到这个结构都快把我累疯了。你也找找吧,如果找到了可要告诉我哦。
type
pimage_import_descriptor = ^
image_import_descriptor;
image_import_descriptor = packed record
originalfirstthunk : dword;
timedatestamp : dword;
forwarderchain : dword;
name : dword;
firstthunk : dword;
end;
注意,这里也使用了packed 关键字。packed record相当于c 语言中的structure(知道为什么我把packedrecord 称作结构了吧)。
还
记得我们前面提到的getmodulehandle吗?delphi的帮助里是这么说的:“parameters lpmodulename
pointsto a null-terminated string that names a win32 module(either a
.dll or .exe file)..if this parameter isnull, getmodulehandle returns a
handle of the fileused to create the calling
process.”如果我们把getmodulehandle的参数设为null,哈哈,一切都有了(你有我有全都有)!
为了保证正确地拦截,我
们必须穷举p e 文件中的image_import_descriptor 数组,看是否有我们的m o d u l e
(例如user32.dll)。如是,则穷举了image_thunk_data,看是否引入了我们需拦截的函数。在我的程序里, 我穷举了p e
文件中的image_import_descriptor
数组,并穷举image_thunk_data,和我们拦截的函数的真正地址比较,如是,则替换它。这样做的好处是我们不必知道我们拦截的函数是从
user32.dll、gdi32.dll还是从kernel32.dll中引入的。
唉,说多了让人心烦,也许你早就全知道啦,实在不行,聪明的你读读代码也就全明白了。还是看看关键的截获代码吧,这是permutefunction 的终极版本,只需用它代替原版本,程序就全部完成了。关键的代码只有10行哦。
4.关键的代码
(* ------------------------------------------- *)
(* permutefunction功能 :用 newfunc替代 oldfunc *)
(* windows me + delphi 5.0 *)
(* ------------------------------------------ *)
unit apihook;
interface
uses
windows, classes ;
type
pimage_import_entry = ^image_import_entry;
image_import_entry = record
characteristics : dword;
timedatestamp : dword;
majorversion : word;
minorversion : word;
name : dword;
lookuptable : dword;
end;
function truefunctionaddress(code: pointer): pointer;
function permutefunction(oldfunc, newfunc: pointer):
integer;
implementation
type
timportcode = packed record
jumpinstruction: word;
addressofpointertofunction: ^pointer;
end;
pimportcode = ^timportcode;
function truefunctionaddress(code: pointer): pointer;
var func: pimportcode;
begin
result := code;
if code = nil then exit;
try
func := code;
if (func.jumpinstruction=$25ff) then begin
result := func.addressofpointertofunction^;
end;
except
result := nil;
end;
end;
function permutefunction(oldfunc, newfunc: pointer):
integer;
var isdone: tlist;
function permuteaddrinmodule(hmodule: thandle; oldfunc,
newfunc: pointer): integer;
var
dos : pimagedosheader;
nt : pimagentheaders;
importdesc : pimage_import_entry;
rva : dword;
func : ^pointer;
dll : string;
f : pointer;
written : dword;
begin
result := 0;
dos := pointer(hmodule);
if isdone.indexof(dos) >= 0 then exit;
isdone.add(dos);
oldfunc := truefunctionaddress(oldfunc);
if isbadreadptr(dos,sizeof(timagedosheader)) then exit;
if dos.e_magic then exit;
nt := pointer(integer(dos) + dos._lfanew);
rva := nt^.optionalheader.datadirectory
[image_directory_entry_import].virtualaddress;
if rva = 0 then exit;
importdesc := pointer(integer(dos)+rva);
while(importdesc^.name0) do
begin
dll := pchar(integer(dos) + importdesc^.name);
permuteaddrinmodule(getmodulehandle(pchar(dll)),
oldfunc,newfunc);
func := pointer(integer(dos) + importdesc.lookuptable);
while func^ nil do
begin
f := truefunctionaddress(func^);
if f = oldfunc then
begin
writeprocessmemory(getcurrentprocess,func,
@newfunc,4,written);
if written > 0 then inc(result);
end;
inc(func);
end;
inc(importdesc);
end;
end;
begin
isdone := tlist.create;
try
result := permuteaddrinmodule(getmodulehandle(nil),
oldfunc,newfunc);
finally
isdone.free;
end;
end;
end.
修正后的代码下载地址:http://download.csdn.net/source/575736
另外按照这种方式进行hook,当程序退出的时候,总是会导致资源管理器异常(explorer.exe),一直困扰我,不知道该怎么解决。
有朋友知道解决办法的话,还请告知,非常感谢。
======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/