制作API HOOK
网上找了许多资料,也大致了解了一下原理。其实说白了也不是很困难。核心思想就是用你自己的处理过程,取代掉Windows的API处理。这非常类似于Java等的AOP。下面就是一些实现。
首先是API Hook的核心代码了,这个代码即是用于替换函数指针。
定义入口记录:
type
PIMAGE_IMPORT_ENTRY = ^TIMAGE_IMPORT_ENTRY;
TIMAGE_IMPORT_ENTRY = packed record
Characteristics: DWORD;
TimeDateStamp: DWORD;
MajorVersion: Word;
MinorVersion: Word;
Name: DWORD;
LookupTable: DWORD;
end;
这个结构的源头来自于MSDN,没有MSDN的话,也很难自己想到。当然了,如果对程序PE很有研究的话,也应该能凭经验写出这个记录来。
接下来,需要另一个记录,用于保存跳转指令和要跳转到的API函数。
type
PTIMPORT_CODE = ^TIMPORT_CODE;
TIMPORT_CODE = packed record
JmpPtr: Word;
PtrAdd: ^Pointer;
end;
其中JmpPtr保存的是导入表地址,在Delphi中可以用$25FF表示。这个值的来源,可以自己反汇编调试得到。在Call API的时候,跟踪即可,如下所示:
@@ PUSH 0
@@ MOV EAX,123
@@ CALL EAX
@@ POPFD
@@ POPAD
@@ FF25 5D108000 JMP DWORD PTR DS:[EBP]+4A
@@ NOP
注意红色的那句,将最前面那个导入表的地址,高低位互换即可。
接下来实现两个方法,一个是用于得到真实API的地址,另一个用于将自定义的函数替换掉API
function GetAPIAddress(ApiPtr: Pointer): Pointer;
begin
Result := ApiPtr;
if ApiPtr= nil then
exit;
try
if (PTIMPORT_CODE(ApiPtr).JmpPtr = $25FF) then
Result := PTIMPORT_CODE(ApiPtr).PtrAdd^;
except
Result := nil;
end;
end;
function SwapPtr(OldPtr, NewPtr: Pointer): Integer;
var
lstDosHead: TList;
function hkSwapPtr(h: Cardinal; OldPtr, NewPtr: Pointer): Integer;
var
DosHeader: PImageDosHeader;
NTHeader: PImageNTHeaders;
ImpEty: PIMAGE_IMPORT_ENTRY;
VAddr: DWORD;
Func: ^Pointer;
DLLName: string;
fOld: Pointer;
wBytes: DWORD;
begin
Result := 0;
DosHeader := Pointer(h);
if lstDosHead.IndexOf(DosHeader) >= 0 then
exit;
lstDosHead.Add(DosHeader);
OldPtr := GetAPIAddress(OldPtr);
if IsBadReadPtr(DosHeader, SizeOf(TImageDosHeader)) then
exit;
if DosHeader.e_magic <> IMAGE_DOS_SIGNATURE then
exit;
NTHeader := Pointer(Integer(DosHeader) + DosHeader._lfanew);
VAddr := NTHeader^.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
if VAddr = 0 then
exit;
ImpEty := Pointer(integer(DosHeader) + VAddr);
while ImpEty^.Name <> 0 do
begin
DLLName := PChar(Integer(DosHeader) + ImpEty^.Name);
hkSwapPtr(GetModuleHandle(PChar(DLLName)), OldPtr, NewPtr);
Func := Pointer(Integer(DosHeader) + ImpEty^.LookupTable);
while Func^ <> nil do
begin
fOld := GetAPIAddress(Func^);
if fOld = OldPtr then
begin
WriteProcessMemory(GetCurrentProcess, Func, @NewPtr, 4, wBytes);
if wBytes > 0 then
Inc(Result);
end;
Inc(Func);
end;
Inc(ImpEty);
end;
end;
begin
lstDosHead:= TList.Create;
try
Result := hkSwapPtr(GetModuleHandle(nil), OldPtr, newptr);
except
end;
lstDosHead.Free;
end;
这两步做好后,接下来的事表就很简单了,我尝试修改FindWindow这个API的处理。
type
TFindWindow = function (lpClassName, lpWindowName: PChar): HWND; stdcall;
TFindWindowA = function (lpClassName, lpWindowName: PAnsiChar): HWND; stdcall;
TFindWindowW =function (lpClassName, lpWindowName: PWideChar): HWND; stdcall;
var
OldFindWindow: TFindWindow;
OldFindWindowA: TFindWindowA;
OldFindWindowW: TFindWindowW;
function hkFindWindow(lpClassName, lpWindowName: PChar): HWND; stdcall;
begin
if lpWindowName = 'Form1' then
Result := 0
else
Result := OldFindWindow(lpClassName, lpWindowName);
end;
function hkFindWindowA(lpClassName, lpWindowName: PAnsiChar): HWND; stdcall;
begin
if lpWindowName = 'Form1' then
Result := 0
else
Result := OldFindWindowA(lpClassName, lpWindowName);
end;
function hkFindWindowW(lpClassName, lpWindowName: PWideChar): HWND; stdcall;
begin
if lpWindowName = 'Form1' then
Result := 0
else
Result := OldFindWindowW(lpClassName, lpWindowName);
end;
很明确了,接管的结果应该是,当调用FindWindow这个API时,如果lpWindowName这个参数是Form1,那么直接返回0。也就是说, 不想让别的程序能找到Form1。如果找的不是Form1,那么就把控制权再交还给原先的FindWindow,让它继续执行。
下面的代码是处理用自己的函数替换掉API函数。替换过后,API就被接管了。
procedure DoHook;
begin
if @OldFindWindow = nil then
@OldFindWindow := GetAPIAddress(@FindWindow);
if @OldFindWindowA = nil then
@OldFindWindowA := GetAPIAddress(@FindWindowA);
if @OldFindWindowW = nil then
@OldFindWindowW := GetAPIAddress(@FindWindowW);
SwapPtr(@OldFindWindow, @hkFindWindow);
SwapPtr(@OldFindWindowA, @hkFindWindowA);
SwapPtr(@OldFindWindowW, @hkFindWindowW);
end;
procedure DoUnHook;
begin
if @OldFindWindow <> nil then SwapPtr(@hkFindWindow, @OldFindWindow);
if @OldFindWindowA <> nil then SwapPtr(@hkFindWindowA, @OldFindWindowA);
if @OldFindWindowW <> nil then SwapPtr(@hkFindWindowW, @OldFindWindowW);
end;
前一个函数是用于替换API,当程序结束时,为了不影响系统的正常运作,应当把替换过的API再换回正常的。
替换API的工作完成后,我们需要将它挂到系统上,这样才能够在全局范围内替换掉API。如果不进行系统级的挂勾,那么对于API的替换,只作用于当前程序本身。
挂勾到系统也很简单,如下:
var
HookHandle: Cardinal;
function GetMsgProc(code: integer; removal: integer; msg: Pointer): Integer; stdcall;
begin
Result := 0;
end;
procedure DoHookSystem;
begin
HookHandle := SetWindowsHookEx(WH_GETMESSAGE, @GetMsgProc, HInstance, 0);
end;
procedure DoUnHookSystem;
begin
UnhookWindowsHookEx(HookHandle);
end;
这里借用了GetMsgProc这个回调函数,并直接返回0,使挂勾能持续运作。
与DoHook时一样,为了使程序退出后系统不受影响,也需要一个DoUnHookSystem方法。
到此为止,一个简单的API Hook就完成了。对于Windows API的编程有些时候的确是很麻烦,必要时还得借助汇编或是Spy++之类的工具来帮助完成任务。当然了,通过此例,也可以看到,API Hook其实并不是什么非常高深的技术,凭借着对PE的了解,加上手头有个MSDN,许多问题就能迎刃而解了
判断API函数是否被Hook
随后我们以此为基础,实现一个判断API 是否被Hook的程序
要判断API是否被Hook了,其实质相当的简单,就是判断一下指定的API地址是否原始的。
于是将原文中的GetAPIAddress()函数直接拿来用一下,就有了这个函数
其中原始的MessageBox函数是直接从user32.dll中取得的
对于已经被Hook的API函数,要判断就应当从原始的出处再获取一次函数指针
procedure TForm1.Button1Click(Sender: TObject);
var
orgMsgBox: Pointer;
h: Cardinal;
begin
h := LoadLibrary('user32.dll');
orgMsgBox := GetProcAddress(h, 'MessageBoxA');
if GetAPIAddress(@MessageBox) <> orgMsgBox then
// TODO: Hooked
else
// TODO: NOT Hooked
FreeLibrary(h);
end;
判断API是否被Hook通常用来实现防破解,是一项相当有用的技术