// 本文转自网络, 原始出处不明确.
// 转载目的: 学习 + 分享
一、关于 API Hook
1. 什么是 API Hook
不知道大家是否还记得,在 DOS系统中编程,经常会采取截取中断向量的技术:我们可以设置新的中断服务程序,当系统其他的程序调用这个中断时,就让它先调用我们自己设置的新的中断服务程序,然后再调用原来的中断服务程序,这样就能够获得非凡的控制权。许多优秀的软件和大多数DOS 病毒程序都采用了这个方法。
在 Windows 中,我们也可以采取类似技术。当系统调用某个 API
这样重要的技术,大家已经都知道了吧?哈哈,知道的都不说,不知道的呢,你就自己慢慢去摸索吧。偶尔有一两篇文章见于报端,不是藏头露尾,就是已经过时了。还有的即使把原理都告诉你了,但就是不说它调用了哪些函数。要源代码?行,拿钱来。有人说了,这种技术用来编病毒最合适,所以..(因为菜刀可以杀人,所以菜刀已被禁止使用了)。而实际呢,你看看使用了这种技术的国产软件就知道了:金山词霸、东方快车、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文件结构,那就更好了。在这篇文章里,我给出了所有的源代码(还不到 200 行)。如果你想修改程序,最好用 SoftIce。
5. 关于我的程序
本文中的程序在 Windows Me 的操作环境下,使用 Delphi5.0编程调试通过。无论是商用还是个人使用,你都可以随意使用和修改本文中的程序,并且不需要在程序中加注我的个人信息。
二、用 Delphi 编写 API Hook
1. 改写 API 函数
为了使我们改写的代码正确运行,我们的函数必须和要改写的 API 函数具有同样形式的形参。在我的程序中,我拦截了 MessageBoxA和 MessageBoxW 两个函数。所以我这样定义自己的函数:
view plaincopy to clipboardprint?
function MyBoxA(hwn: HWND; lpText: PAnsiChar; lpCapion: PAnsiChar;uType: Cardinal): Integer;stdcall;
function MyBoxW(hwn: HWND; lpText: PWideChar; lpCapion: PWideChar;uType: Cardinal): Integer; stdcall;
function MyBoxA(hwn: HWND; lpText: PAnsiChar; lpCapion: PAnsiChar;uType: Cardinal): Integer; stdcall;
function MyBoxW(hwn: HWND; lpText: PWideChar; lpCapion: PWideChar;uType: Cardinal): Integer; stdcall;
注意到我使用了 stdcall 关键字,这是为了我的函数的形参的进出栈顺序与我们要拦截的函数一致。我们知道,为了系统的安全,Win32并不允许直接改写内存中的代码段。所以,有人想了好多种方法绕过系统的保护。实际上,Win32为了我们能安全地改写内存中的代码,提供了一个函数:WriteProcessMemory。有许多人曾经告诉我,WriteProcessMemory也不能用来改写,不过我一直使用得很好。也许用它产生了一些 BUG,只是我并不知道罢了,所以还请这方面的专家指正。在 PE文件中,当你呼叫另一模块中的函数(例如 USER32.DLL 中的GetMessage),编译器编译出来的 CALL指令并不会把控制权直接传给 DLL 中的函数,而是传给一个 JMP DWORDPTR [XXXXXXXX] 指令,[XXXXXXXX]内含该函数的真正地址(函数进入点)。为了得到 API 函数的地址,我们可以这样:Address :=@MessageBoxA。如上文所说的,我们得到的仅仅是一个跳转指令,后面紧接着的才是 MessageBoxA真正的开始代码的地址(具体可以查阅 PE文件的资料)。在下面的程序中我自定义了一个结构(叫记录?我习惯了,改不过口来),注意,这里使用了 packed 关键字:
view plaincopy to clipboardprint?
TImportCode = packed record
end;
PImportCode = ^TImportCode;
TImportCode = packed record
end;
PImportCode = ^TImportCode;
其中,PPointer = ^Pointer ;
用以下函数返回函数的真正地址:
view plaincopy to clipboardprint?
function TrueFunctionAddress(func: Pointer):Pointer;
var
begin
end;
function TrueFunctionAddress(func: Pointer): Pointer;
var
begin
end;
这样,只要用我们的函数的地址替代它就可以了。替换函数:
view plaincopy to clipboardprint?
procedure PermuteFunction(OldFunc: PPOinter; NewFunc:Pointer);
var
begin
end;
procedure PermuteFunction(OldFunc: PPOinter; NewFunc:Pointer);
var
begin
end;
你新建一个 Unit APIHook,把上面的函数和结构写进去并保存下来。
2. 第一个程序
新建一个 Application TRY1,主 Form 的单元名称不妨叫 TRYUnit1。把上面的 Unit APIHook加进来。再新建一个 UnitMESS,添加如下代码:
view plaincopy to clipboardprint?
unit mess;
interface
uses
procedure API_Hookup;
procedure Un_API_Hook;
var
implementation
type
var
function MyBoxA(hwn: HWND; lpText: PAnsiChar; lpCapion: PAnsiChar;uType: Cardinal): Integer;stdcall;
begin
end;
function MyBoxw(hwn: HWND; lpText: PWideChar; lpCapion: PWideChar;uType: Cardinal): Integer;stdcall;
begin
end;
procedure API_Hookup;
begin
end;
procedure Un_API_hook;
begin
end;
initialization
FuncMessageboxA :=@MessageBoxA;
FuncMessageboxW :=@MessageBoxW;
end.
unit mess;
interface
uses
procedure API_Hookup;
procedure Un_API_Hook;
var
implementation
type
var
function MyBoxA(hwn: HWND; lpText: PAnsiChar; lpCapion: PAnsiChar;uType: Cardinal): Integer; stdcall;
begin
end;
function MyBoxw(hwn: HWND; lpText: PWideChar; lpCapion: PWideChar;uType: Cardinal): Integer; stdcall;
begin
end;
procedure API_Hookup;
begin
end;
procedure Un_API_hook;
begin
end;
initialization
FuncMessageboxA := @MessageBoxA;
FuncMessageboxW := @MessageBoxW;
end.
在主窗体上添加三个按钮,添加 Onclick 代码,如下:
view plaincopy to clipboardprint?
procedure TForm1.Button1Click(Sender:TObject);
begin
end;
procedure TForm1.Button3Click(Sender:TObject);
begin
end;
procedure TForm1.Button2Click(Sender:TObject);
begin
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
end;
记得要保存,在后面我们还要使用它们。编译一下,
运行..啊哈,成功了!且慢,别高兴得太早。如果现在新建一个 Application TestTry,在 Form上添加一个按钮,OnClick 事
件如下:
view plaincopy to clipboardprint?
procedure TForm1.Button1Click(Sender:TObject);
begin
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
end;
先运行 TRY1,再运行 TestTry。结果呢?原来,API Hook 仅仅在 TRY1中挂上了,并没有在所有的系统进程中挂上。
你也许听说过,必须把我们的函数和 API_Hookup、Un_API_Hook放在一个动态链接库里(这是对的)。那么,你就试试吧。如果你这样做了,也并不能在所有的系统进程中挂上。原因很简单,我们仅仅改变了进程指向API 函数的指针,系统中其他的进程还是各管各的。
3. Hook Hook Hook Hook Hook Hook..
我们想想做鼠标钩子时的做法:用 SetWindowsHookEx挂上鼠标钩子,当其他的进程发出鼠标消息时,我们的程序就会拦截到并作出响应。我们还可以用 UnhookWindowsHookEx解除鼠标钩子。我们也必须为我们的函数挂上钩子。不过,鼠标有各种消息响应其他进程,我们的进程有什么消息呢?如果没有消息又怎样响应其他进程呢?即使我们自定义了消息,其他的进程又怎样“懂得”我们的消息呢?真像走入了绝境。
不用怕,至少我们有两种方法。一是完全模仿 SetWindowsHookEx,编制自己的MySetWindowsHookEx,我看过大多数的 API Hook程序里都用了这个方法。其实,我们不必舍近而求远,我们完全可以继续使用SetWindowsHookEx,因为系统还为我们提供了一个函数:GetMsgProc。在 Delphi 中输入GetMsgProc,然后光标停在上面,按 F1 键。怎么样,Delphi 帮助里讲得够清楚的吧?好了,我们的消息也有了。
想一想吧:我们在动态链接库中挂上 WH_GETMESSAGE 消息钩子,当其他的进程发出 WH_GETMESSAGE消息时,就会加载我们的动态链接库,如果在我们的 DLL 加载时自动运行 API_Hook,不就可以让其他的进程挂上我们的 APIHook 吗?
4. 第二个程序
说干就干。新建一个动态链接库 Library TryDLL,把原来的 Unit APIHook 和 Unit MESS加进来。Library TryDLL 的代码如下:
view plaincopy to clipboardprint?
uses
{$R *.RES}
function GetMsgProc(Code: Integer; Removal: Integer; Msg: Pointer):Integer; stdcall;
begin
end;
var HookHandle: THandle;
procedure StartHook;stdcall;
begin
end;
procedure StopHook;stdcall;
begin
end;
exports
begin
end.
// 为了卸载时能解除钩子, 在 Unit MESS单元最后加上一句:
finalization
end.
uses
{$R *.RES}
function GetMsgProc(Code: Integer; Removal: Integer; Msg: Pointer):Integer; stdcall;
begin
end;
var HookHandle: THandle;
procedure StartHook; stdcall;
begin
end;
procedure StopHook; stdcall;
begin
end;
exports
begin
end.
// 为了卸载时能解除钩子, 在 Unit MESS 单元最后加上一句:
finalization
end.
当然,别忘了对 mess 做相应修改。编译好后别忘了存盘。新建 Application TRY2 程序,主 Form 的单元名称不妨叫TRYUnit2,在 Form1 上添加三个 Button,并声明:
view plaincopy to clipboardprint?
procedure StartHook; stdcall; external'TRYDLL.DLL';
procedure StopHook; stdcall; external'TRYDLL.DLL';
procedure StartHook; stdcall; external 'TRYDLL.DLL';
procedure StopHook; stdcall; external 'TRYDLL.DLL';
三个 Button 的 OnClick 代码如下:
view plaincopy to clipboardprint?
procedure TForm1.Button1Click(Sender:TObject);
begin
end;
procedure TForm1.Button2Click(Sender:TObject);
begin
end;
procedure TForm1.Button3Click(Sender:TObject);
begin
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
end;
编译好后运行吧。这次倒好,无论你怎样测试,都得不到钩上的信息。整理一下我们的工作吧:TRY2 运行--> TryDLL 加载 --> 运行 API_Hookup,为TryDLL 挂上 MyBox。即使我们按下了 Button1,其他的进程加载了 TryDLL,也仅仅是运行 API_Hookup,为TryDLL 挂上 MyBox 而已,而其他的进程(包括 TRY2 本身)并没有挂上 MyBox。不信,你可以在 TryDLL中加上一个启动 MyBox 的函数,测试一下。例如,你可以在 TryDLL 的 StopHook 函数中的UnhookWindowsHookEx 语句前,加上一句:MessageBoxW(0, 'MessageBoxW','这是测试DLL是否加载了MyBox', MB_OK);你可以看到弹出窗口中的信息是“成功挂上W!”而不是“MessageBoxW”。并且由于我们的 TryDLL 加载时就启动了API_Hookup,卸载时才运行 Un_API_Hook,所以不论你是否按下 Button1 和Button3,并且不论你按下了几次,每次你按下 Button3都会得到“成功挂上W!”的信息。看来,真正的麻烦才刚刚开始。实际上,我们刚才的工作都是有用的。我们剩下的工作就是改进 UnitAPIHook 中的 TrueFunctionAddress 函数而已。想不到吧?为了能让其他的进程挂上 MyBox,我们必须了解一下PE 文件的格式。
联系客服