hook,想来很多小菜和我一样,看到这个词就觉得激动,如果有一天我也学会了hook,我一定会成为大神。回忆起来,一年前我刚刚
熟悉论坛的时候,也是这样的心情。今天这篇文章,专为祭奠这个想法。这篇文章介绍iat hook,也就是修改导入表实现函数调用的hook
(运行时hook本进程)。注:iat(import address table,导入函数地址表,在这里说明一下:导入表是image import table,导入函数地
址表是 image import address table,这是两个不同的pe文件中的表)
参考文章:http://www.xfocus.net/articles/200403/681.html
要实现iat hook,首先得清楚iat和导入表的结构。pe文件中,不管是在硬盘上的文件,还是内存中的映像,导入表和iat都会存在,
而且在硬盘上和在映像中的组织方式相同(但是整个pe文件在硬盘上和在内存中的组织方式有很大不同)。pe文件的结构在这
篇文章中不会讨论太多,但是理解
这篇文章需要pe文件结构的知识,请在了解pe文件结构的情况下阅读这篇文章,如果还不了解,可以参考这本书:windows pe权威指南,
写的非常详尽,特别是pe文件头和各种表。
pe文件中的导入表的起始是一组叫做image_import_descriptor的结构体,每一个结构体20个字节,对应于一个动态链接库(也许关于
这一点我是错的),以一个全0的结构体结束。用汇编定义如下:
IMAGE_IMPORT_DESCRIPTOR STRUCT
union
Characteristics dd ?
OriginalFirstThunk dd ?
ends
TimeDateStamp dd ?
ForwarderChain dd ?
Name1 dd ? ;我们需要关注这一个成员
FirstThunk dd ? ;还有这一个成员
IMAGE_IMPORT_DESCRIPTOR ENDS
这里我们只关心在内存中他们会指向哪里,而不关心在硬盘上的文件,所以我们的hook是在运行时完成的。上面的结构体中,Name1是一
个rva(relative virtual address,相对虚拟地址),也就是说它是一个相对于imagebase(模块基地址)的一个偏移,模块基地址可以用
GetModuleHandle(ModuleName)来获取,如果ModuleName我们传入NULL,可以获取这个函数在被执行时所在的模块。Name1指向的是什么呢?
我们前面说过,每一个IMAGE_IMPORT_DESCRIPTOR对应于一个动态链接库(广义),这个Name1指向的就是这个动态链接库的名字(以0结尾
什么的就不需要我说明了)。
FirstThunk也是一个rva,它指向Name1对应的动态链接库的所有的被当前程序导入了的函数,实际上它指向的地方就位于iat中,可以
说导入表和iat是你中有我,我中有你。那么iat到底是怎么组织的呢?参考下面:
链接库A导入函数1的地址
链接库A导入函数2的地址
00000000
链接库B导入函数1的地址
链接库B导入函数2的地址
链接库B导入函数3的地址
00000000
。。。。。。。
iat是分成一块一块的,每一块对应于一个被程序导入的动态链接库,每一块都是一个接一个的地址,最后以双字的0表示这一个动态
链接库的函数地址已经全部存储好了。实际上,在硬盘上iat的组织方式不是这样的,但是我们只关心在内存中它是怎样组织的。
了解了上面的知识后,我们就可以看看当我们在程序中,当调用一个函数的时候到底发生了什么。
比如下面这句:int pid=GetCurrentProcessId();我们要分解的就是GetCurrentProcessId到底是怎么完成的。
GetCurrentProcessId()位于kernle32.dll中,如果我们在程序中调用了这个函数,编译链接器就知道,我们的程序在运行的时候需要
知道GetCurrentProcessId在内存的哪个位置,于是就在pe文件中的导入表中加入一个image import descriptor,结构中的Name1指向一个
字符串:kernel32.dll,这样,pe加载器在将我们的程序加载到内存中时,它就知道,我们的程序需要调用kernel32.dll中的函数,它就
把kernel32.dll映射到我们的进程的空间中(要深入说明这个很复杂,我们可以简单的理解为,我们的程序的地址空间中已经把整个的kernel
32.dll映射了)。虽然这个需要的dll已经映射好了,但是我们要的函数的地址放在哪里呢?这里就是iat起作用的地方了,仔细看上面的
iat的组织方式,我们假设链接库A就是kernel32.dll,那pe加载器在加载的时候,就把GetCurrentProcessId的地址放到上面那个结构中
“链接库A导入函数1的地址”那个地方。
OK,我们假设需要的kernel32.dll已经映射好了,我们要用的函数的地址也找到了并且放到了前面说的那个地方,程序中调用这个函数
的时候到底怎么跳转的呢?
如果反汇编的话,它就变成下面这个模样:(地址我随意写的,大概这样)
0x00400078 call 0x00403456
.
.
.
.
0x00403456 jmp [0x00401234]
而在0x00401234里面放的是这个东西:0x7c8099b0,那上面的jmp就跳到x07c8099b0开始执行。
再看看上面这个流程,在0x00400078这个地方,有一个call指令,这个call指令就是代替源程序中的GetCurrentProcessId()这一句。
call到0x00403456后,发现是一个jmp指令,jmp到的地方是0x00401234这个地址中存放的一个地址,也就在是先到0x00401234中取出一个
双字值X,然后跳转到X处开始执行。这个流程有点拐弯抹角,可以仔细看看。
重要的地方在0x00401234,这个地方存放的就是GetCurrentProcessId()的地址,而0x00401234自己在的地方就是iat中,就是前面的
“链接库A导入函数1的地址”在的地方。
理解了上面的call流程,我们就可以想想,如果我们把0x00401234这个地址里面的东西换掉,它不是原来是0x7c8099b0吗,也就是
GetCurrentProcessId()函数在内存中的位置吗,我们把它换成0x00400000,或者随便什么东西,0x00000000什么的,如果我们的程序中,
在后面调用GetCurrentProcessId()的时候,它就跑到我们写的那个地方去了。
上面就是理论,下面看看实践。
#include <iostream>
#include <string>
#include <windows.h>
#include <Dbghelp.h>
using namespace std;
typedef int (*pgetiat)(PVOID,BOOLEAN,USHORT,PULONG);
int test_function()
{
MessageBox(NULL,"hook_entry function","hook",MB_OK);
return 0;
}
class iat_hook
{
private:
const char *module;
const char *procname;
PROC *newaddr;
PROC pfnHookAPIAddr;
pgetiat getiat;
HMODULE h;
public:
iat_hook();
iat_hook(const char *m,const char *p,PROC *n);
~iat_hook();
//~iat_hook(const char *);原来destructor不可以重载啊,为什么呢。。。
int hook();
};
int iat_hook::hook()
{
ULONG ulSize;
PIMAGE_IMPORT_DESCRIPTOR pimportdescriptor =
(PIMAGE_IMPORT_DESCRIPTOR)getiat(
GetModuleHandle(NULL),
TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT,
&ulSize
);
while (pimportdescriptor->Name)
{
//cout << (char *)((PBYTE) GetModuleHandle(NULL) + pimportdescriptor->Name) << endl;
PSTR pszModName = (PSTR)((PBYTE) GetModuleHandle(NULL) + pimportdescriptor->Name);
if (stricmp(pszModName, module) == 0)
break;
pimportdescriptor++;
}
PIMAGE_THUNK_DATA pThunk =
(PIMAGE_THUNK_DATA)((PBYTE) GetModuleHandle(NULL) + pimportdescriptor->FirstThunk);
while (pThunk->u1.Function)
{
PROC* ppfn = (PROC*) &pThunk->u1.Function;
BOOL bFound = (*ppfn == pfnHookAPIAddr);
if (bFound)
{
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(
ppfn,
&mbi,
sizeof(MEMORY_BASIC_INFORMATION)
);
VirtualProtect(
mbi.BaseAddress,
mbi.RegionSize,
PAGE_READWRITE,
&mbi.Protect
);
*ppfn = *newaddr;
DWORD dwOldProtect;
VirtualProtect(
mbi.BaseAddress,
mbi.RegionSize,
mbi.Protect,
&dwOldProtect
);
break;
}
pThunk++;
}
}
iat_hook::iat_hook()
{
cout << "you provied wrong parameters!" << endl;
ExitProcess(NULL);
}
iat_hook::iat_hook(const char *m,const char *p,PROC *n)
{
module =m;
procname = p;
newaddr = n;
//cout << "debug : " << hex << (int)*newaddr << endl; //done
HMODULE hdbghelp=LoadLibrary("dbghelp.dll");
getiat = (pgetiat) GetProcAddress (hdbghelp,"ImageDirectoryEntryToData");
if(getiat == NULL)
{
cout << "getprocaddress error" << endl;
ExitProcess(NULL);
}
//cout << "debug: " << hex << (int)getiat << endl; //done
h = GetModuleHandle(module);
pfnHookAPIAddr = GetProcAddress (h,procname);
//cout << "debug: " << hex << (int)pfnHookAPIAddr << endl; //done
}
iat_hook::~iat_hook()
{
cout << "yes you are right,this is destructor" << endl;
}
int main()
{
PROC stub = (PROC )&test_function;
iat_hook it("KERNEL32.dll","GetCurrentProcessId",&stub);
it.hook();
cout << "current pid : " << GetCurrentProcessId() << endl;
return 0;
}
我用的C++写的。首先可以看到一个test_function,也就是测试用的函数。然后是定义了一个iat_hook类,这个类在初始化的时候
需要提供我们要hook的函数所在的模块的名字M,函数的名字N,和一个函数指针P。我们的类中有一个重要的接口,就是hook(),调用它
就会把M中的函数N重定向到P,也就是如果调用函数N,执行的却是P所指向的函数。
我并不准备把程序拿来一行一行的讲,程序是拿来读的,读不懂可以慢慢读。我只说说需要注意的地方:
1.我写程序时本来想直接使用ImageDirectoryEntryToData()的,但是我用的g++编译器老是告诉我找不到函数,我也懒得去添加什么
头文件什么的,就用的动态加载,在程序运行的时候去找,并且我把名字改了,叫getiat,其实这个api函数还有非常多的功能,具体到这里
去参考:http://msdn.microsoft.com/en-us/library/windows/desktop/ms680148(v=vs.85).aspx
2.我们要修改的内存一般是不可写的,于是需要先用VirtualProtect()函数修改内存页面参数,等我们把要写的东西写好了再给改回
来。
3.我在这篇文章的开头给出了参考文献,那篇文章中在iat hook那一节给出的程序示例是有问题的,它老是该用程序的基地址的地方
用kernel32.dll的基地址,我改了过来。如果有人发现是由于我的理解有误,请及时指正。
这篇文章放在论坛上也许是有些令大神见笑了。我也是刚刚学会,现在的我还记得什么都不懂的痛苦和学习的艰辛,等我熟练使用后
忘记了那份痛苦,应该也没有激情来写这么一篇hook入门文章,所以趁着记忆,写下这篇文章。(我还记得我刚刚学会写shellcode的时候
也是激动不已的,可惜写过各种shellcode后,想下笔写shellcode的介绍和教程,却是感觉下不了笔,头脑中的想法就是,这么简单的东西
放上去不是引人笑话么。所以趁着我在hook方面仍是一个小菜,将更多的还在门外的同学带进小菜行列吧)
最后,既然有了一个hook的方法,希望能有高人能介绍一些hook后比较有趣的api,还有注射代码到其他进程的方法,如果能提供论坛
里一两个讨论这个的帖子的地址就更好了。俺要拿hook这把剑到处去戳一戳。
书短意长,不尽欲言,且慰同菜。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)