挂钩Windows API.docx
《挂钩Windows API.docx》由会员分享,可在线阅读,更多相关《挂钩Windows API.docx(41页珍藏版)》请在冰豆网上搜索。
挂钩WindowsAPI
===========================[挂钩WindowsAPI]==================
SoBeIt
Author:
Holy_Father
Version:
1.1english
Date:
6.10.2002
=====[1.内容]=============================================
1.内容
2.介绍
3.挂钩方法
3.1运行前挂钩
3.2运行时挂钩
3.2.1使用IAT挂钩本进程
3.2.2改写入口点挂钩本进程
3.2.3保存原始函数
3.2.4挂钩其它进程
3.2.4.1DLL注入
3.2.4.2独立的代码
3.2.4.3原始修改
4.结束语
=====[2.介绍]====================================================
这篇文章是有关在OSWindows下挂钩API函数的方法。
所有例子都在基于NT技术的Windo
ws版本NT4.0及以上有效(WindowsNT4.0,Windows2000,WindowsXP)。
可能在其它Wind
ows系统也会有效。
你应该比较熟悉Windows下的进程、汇编器、PE文件结构和一些API函数,才能明白这篇
文章里的内容。
这里使用"HookingAPI"这个术语表示对API的完全修改。
当调用被挂钩的API时,我们的
代码能立刻被执行。
我将写下完全的挂钩过程。
=====[3.挂钩方法]==============================================
一般来说我们的目的是用我们的代码取代一些函数里的代码。
这些问题有时可以在进程
运行前解决。
这些大多数时候可以用我们运行的用户级进程来完成,目的可以是修改程序的
行为。
举个例子应用程序的破解,比方说有些程序会在启动时需要原光盘,我们想要不用光
盘就启动它。
如果我们修改获取驱动类型的函数我们就可以让程序从硬盘启动。
当我们挂钩系统进程时(比如说服务)这些不可能做到或者我们不打算这么做,或者在这
个例子里我们不知道哪个进程才是目标。
这时我们就要用到动态挂钩(在运行时挂钩)的技术
。
使用的例子有rootkit或者病毒里的反杀毒软件的技术。
=====[3.1运行前挂钩]===========================================
这里修改我们想要修改函数来自的物理模块(大多数时候是.exe或.dll)。
在这里我们至
少有3种可能的做法。
第一种可能是找到函数的入口点然后重写它的代码。
这会因为函数的大小而受限制,但
我们能动态加载其它一些模块(APILoadLibrary),所以应该足够了。
内核函数(kernel32.dll)是通用的因为Windows中每个进程都有这个模块的拷贝。
另一个
好处是如果我们知道哪些模块在某版本中会修改,我们可以在一些API如LoadLibraryA中使用
直接的指针。
这是因为kernel模块在内存中地址在相同Windows版本中是固定的。
我们同样也
能用动态加载的模块的作用。
在这里它的初始化部分在加载进内存后立刻就运行。
在新模块
的初始化部分我们不受限制。
第二种可能是在模块中被代替的函数只是原函数的扩展。
然后我们选择要么修改开始的
5个字节为跳转指令或者改写IAT。
如果改为跳转指令,那么将会改变指令执行流程转为执行
我们的代码。
如果调用了IAT记录被修改的函数,我们的代码能在调用结束后被执行。
但模块
的扩展没那么容易,因为我们必须注意DLL首部。
下一个是修改整个模块。
这意味着我们创建自己的模块版本,它能够加载原始的模块并
调用原始的函数,当然我们对这个不感兴趣,但重要的函数都是被更新的。
这种方法对于有
的模块过大有几百个导出函数的很不方便。
=====[3.2运行时挂钩]==========================================
在运行前挂钩通常都非常特殊,并且是在内部面向具体的应用程序(或模块)。
如果我们
更换了kernel32.dll或ntdll.dll里的函数(只在NT操作系统里),我们就能完美地做到在所有
将要运行的进程中替换这个函数。
但说来容易做起来却非常难,因为我们不但得考虑精确性
和需要编写比较完善的新函数或新模块,但主要问题是只有将要运行的进程才能被挂钩(要挂
钩所有进程只能重启电脑)。
另一个问题是如何进入这些文件,因为NT操作系统保护了它们。
比较好的解决方法在进程正在运行时挂钩。
这需要更多的有关知识,但最后的结果相当不错
。
在运行中挂钩只对能够写入它们的内存的进程能成功。
为了能写入它自己我们使用API函数
WriteProcessMemory。
现在我们开始运行中挂钩我们的进程。
=====[3.2.1使用IAT挂钩本进程]===================================
这里有很多种可能性。
首先介绍如何用改写IAT挂钩函数的方法。
接下来这张图描述了P
E文件的结构:
+-------------------------------+-offset0
|MSDOS标志("MZ")和DOS块|
+-------------------------------+
|PE标志("PE")|
+-------------------------------+
|.text|-模块代码
|程序代码|
||
+-------------------------------+
|.data|-已初始化的(全局静态)数据
|已初始化的数据|
||
+-------------------------------+
|.idata|-导入函数的信息和数据
|导入表|
||
+-------------------------------+
|.edata|-导出函数的信息和数据
|导出表|
||
+-------------------------------+
|调试符号|
+-------------------------------+
这里对我们比较重要的是.idata部分的导入地址表(IAT)。
这个部分包含了导入的相关信
息和导入函数的地址。
有一点很重要的是我们必须知道PE文件是如何创建的。
当在编程语言
里间接调用任意API(这意味着我们是用函数的名字来调用它,而不是用它的地址),编译器并
不直接把调用连接到模块,而是用jmp指令连接调用到IAT,IAT在系统把进程调入内存时时会
由进程载入器填满。
这就是我们可以在两个不同版本的Windows里使用相同的二进制代码的原
因,虽然模块可能会加载到不同的地址。
进程载入器会在程序代码里调用所使用的IAT里填入
直接跳转的jmp指令。
所以我们能在IAT里找到我们想要挂钩的指定函数,我们就能很容易改
变那里的jmp指令并重定向代码到我们的地址。
完成之后每次调用都会执行我们的代码了。
这
种方法的缺点是经常有很多函数要被挂钩(比方说如果我们要在搜索文件的API中改变程序的
行为我们就得修改函数FindFirstFile和FindNextFile,但我们要知道这些函数都有ANSI和W
IDE版本,所以我们不得不修改FindFirstFileA、FindFirstFileW、FindNextFileA和FileNe
xtFileW的IAT地址。
但还有其它类似的函数如FindFirstFileExA和它的WIDE版本FindFirstF
ileExW,也都是由前面提到的函数调用的。
我们知道FindFirstFileW调用FindFirstFileExW
,但这是直接调用,而不是使用IAT。
再比如说ShellAPI的函数SHGetDesktopFolder也会直接
调用FindFirstFilwW或FindFirstFileExW)。
如果我们能获得它们所有,结果就会很完美。
我们通过使用imagehlp.dll里的ImageDirectoryEntryToData来很容易地找到IAT。
PVOIDImageDirectoryEntryToData(
INLPVOIDBase,
INBOOLEANMappedAsImage,
INUSHORTDirectoryEntry,
OUTPULONGSize
);
在这里Base参数可以用我们程序的Instance(Instance通过调用GetModuleHandle获得):
hInstance=GetModuleHandleA(NULL);
DirectoryEntry我们可以使用恒量IMAGE_DIRECTORY_ENTRY_IMPORT。
#defineIMAGE_DIRECTORY_ENTRY_IMPORT1
函数的结果是指向第一个IAT记录指针。
IAT的所有记录是由IMAGE_IMPORT_DESCRIPTOR定
义的结构。
所以函数结果是指向IMAGE_IMPORT_DESCRIPTOR的指针。
typedefstruct_IMAGE_THUNK_DATA{
union{
PBYTEForwarderString;
PDWORDFunction;
DWORDOrdinal;
PIMAGE_IMPORT_BY_NAMEAddressOfData;
};
}IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;
typedefstruct_IMAGE_IMPORT_DESCRIPTOR{
union{
DWORDCharacteristics;
PIMAGE_THUNK_DATAOriginalFirstThunk;
};
DWORDTimeDateStamp;
DWORDForwarderChain;
DWORDName;
PIMAGE_THUNK_DATAFirstThunk;
}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
IMAGE_IMPORT_DESCRIPTOR里的Name成员变量是模块名字的指针。
如果我们想要挂钩某个
函数比如是来自kernel32.dll我们就在导入表里找属于名字kernel32.dll的描述符号。
我们
先调用ImageDirectoryEntryToData然后找到名字是"kernel32.dll"的描述符号(可能不只一
个描述符号是这个名字),最后我们在这个模块的记录里所有函数的列表里找到我们想要的函
数(函数地址通过GetProcAddress函数获得)。
如果我们找到了就必须用VirtualProtect函数
来改变内存页面的保护属性,然后就可以在内存中的这些部分写入代码了。
在改写了地址之
后我们要把保护属性改回来。
在调用VirtualProtect之前我们还要先知道有关页面的信息,
这通过VirtualQuery来实现。
我们可以加入一些测试以防某些函数会失败(比方说如果第一次
调用VirtualProctect就失败了,我们就没办法继续)。
PCSTRpszHookModName="kernel32.dll",pszSleepName="Sleep";
HMODULEhKernel=GetModuleHandle(pszHookModName);
PROCpfnNew=(PROC)0x12345678,//这里存放新地址
pfnHookAPIAddr=GetProcAddress(hKernel,pszSleepName);
ULONGulSize;
PIMAGE_IMPORT_DESCRIPTORpImportDesc=
(PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(
hKernel,
TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT,
&ulSize
);
while(pImportDesc->Name)
{
PSTRpszModName=(PSTR)((PBYTE)hKernel+pImportDesc->Name);
if(stricmp(pszModName,pszHookModName)==0)
break;
pImportDesc++;
}
PIMAGE_THUNK_DATApThunk=
(PIMAGE_THUNK_DATA)((PBYTE)hKernel+pImportDesc->FirstThunk);
while(pThunk->u1.Function)
{
PROC*ppfn=(PROC*)&pThunk->u1.Function;
BOOLbFound=(*ppfn==pfnHookAPIAddr);
if(bFound)
{
MEMORY_BASIC_INFORMATIONmbi;
VirtualQuery(
ppfn,
&mbi,
sizeof(MEMORY_BASIC_INFORMATION)
);
VirtualProtect(
mbi.BaseAddress,
mbi.RegionSize,
PAGE_READWRITE,
&mbi.Protect)
)
*ppfn=*pfnNew;
DWORDdwOldProtect;
VirtualProtect(
mbi.BaseAddress,
mbi.RegionSize,
mbi.Protect,
&dwOldProtect
);
break;
}
pThunk++;
}
调用Sleep(1000)的结果如例子所示:
00407BD8:
68E8030000push0000003E8h
00407BDD:
E812FAFFFFcallSleep
Sleep:
;这是跳转到IAT里的地址
004075F4:
FF25BCA14000jmpdwordptr[00040A1BCh]
原始表:
0040A1BC:
7967E87700000000
新表:
0040A1BC:
7856341200000000
所以最后会跳转到0x12345678。
=====[3.2.2改写入口点挂钩本进程]==================
改写函数入口点开始的一些字节这种方法相当简单。
就象改变IAT里的地址一样,我们也
要先修改页面属性。
在这里对我们想要挂钩的函数是一开始的5个字节。
为了之后的使用我们
用动态分配MEMORY_BASIC_INFORMATION结构。
函数的起始地址也是用GetProcAddress来获得
。
我们在这个地址里插入指向我们代码的跳转指令。
接下来程序调用Sleep(5000)(所以它会
等待5秒钟),然后Sleep函数被挂钩并重定向到new_sleep,最后它再次调用Sleep(5000)。
因
为新的函数new_sleep什么都不做并直接返回,所以整个程序只需要5秒钟而不是10秒种。
.386p
.modelflat,stdcall
includeliblib\kernel32.lib
SleepPROTO:
DWORD
GetModuleHandleAPROTO:
DWORD
GetProcAddressPROTO:
DWORD,:
DWORD
VirtualQueryPROTO:
DWORD,:
DWORD,:
DWORD
VirtualProtectPROTO:
DWORD,:
DWORD,:
DWORD,:
DWORD
VirtualAllocPROTO:
DWORD,:
DWORD,:
DWORD,:
DWORD
VirtualFreePROTO:
DWORD,:
DWORD,:
DWORD
FlushInstructionCachePROTO:
DWORD,:
DWORD,:
DWORD
GetCurrentProcessPROTO
ExitProcessPROTO:
DWORD
.data
kernel_namedb"kernel32.dll",0
sleep_namedb"Sleep",0
old_protectdd?
MEMORY_BASIC_INFORMATION_SIZEequ28
PAGE_READWRITEdd000000004h
PAGE_EXECUTE_READWRITEdd000000040h
MEM_COMMITdd000001000h
MEM_RELEASEdd000008000h
.code
start:
push5000
callSleep
do_hook:
pushoffsetkernel_name
callGetModuleHandleA
pushoffsetsleep_name
pusheax
callGetProcAddress
movedi,eax;最后获得Sleep地址
pushPAGE_READWRITE
pushMEM_COMMIT
pushMEMORY_BASIC_INFORMATION_SIZE
push0
callVirtualAlloc
testeax,eax
jzdo_sleep
movesi,eax;为MBI结构分配内存
pushMEMORY_BASIC_INFORMATION_SIZE
pushesi
pushedi
callVirtualQuery;内存页的信息
testeax,eax
jzfree_mem
callGetCurrentProcess
push5
pushedi
pusheax
callFlushInstructionCache;只是为了确定一下:
)
leaeax,[esi+014h]
pusheax
pushPAGE_EXECUTE_READWRITE
leaeax,[esi+00Ch]
push[eax]
push[esi]
callVirtualProtect;我们要修改保护属性,这样才能够写入代码
testeax,eax
jzfree_mem
movbyteptr[edi],0E9h;写入跳转指令
moveax,offsetnew_sleep
subeax,edi
subeax,5
incedi
stosd;这里是跳转地址
pushoffsetold_protect
leaeax,[esi+014h]
push[eax]
leaeax,[esi+00Ch]
push[eax]
push[esi]
callVirtualProtect;恢复页保护属性
free_mem:
pushMEM_RELEASE
push0
pushesi
callVirtualFree;释放内存
do_sleep:
push5000
callSleep
push0
callExitProcess
new_sleep:
ret004h
endstart
第二次调用Sleep的结果是这样:
004010A4:
6888130000push000001388h
004010A9:
E80A000000callSleep
Sleep:
;这里是跳转到IAT里的地址
004010B8:
FF2514204000jmpdwordptr[000402014h]
tabulka:
00402014:
7967E8776C7DE877
Kernel32.Sleep:
77E86779:
E937A95788jmp0004010B5h
new_sleep:
004010B5:
C20400ret004h
=====[3.2.3保存原始函数]=====================================
更多时候我们需要的不仅仅是挂钩函数。
比方说也许我们并不想取代给定的函数而只是
想检查一下它的结果,或者也许我们只是想在函数被使用特定的参数来调用时才取代原函数
。
比较好的例子有前面提过的通过取代FindXXXFile函数来完成隐藏文件。
所以如果我们想要
隐藏指定的文件并且不想被注意的话,就得对其它所有文件只调用没有被修改过的原始函数
。
这对使用修改IAT的方法时是很简单的,为调用原始函数我们可以用GetProcAddress获得它
的原始地址,然后直接调用。
但修改入口点的方法就会有问题,因为修改了函数入口点的5个
字节,使我们破坏了原函数。
所以我们必须保存开始的那些指令。
这将用到以下的技术。
我们知道我们要修改开始的5个字节但不知道里面包含多少条指令以及指令的长度。
我们
得为开始那些指令保留足够的内存空间。
16个字节应该足够了,因为函数开始时通常没有多
长的指令,很可能根本就用不到16个字节。
整个被保留的内存用0x90(0x90=nop)来填满。
下
一个5个字节预留给将在之后填入的跳转指令。
old_hook:
db090h,090h,090h,090h,090h,090h,090h,090h
db090h,090h,090h,090h,090h,090h,090h,090h
db0E9h,000h,000h,000h,000h
现在我们已准备好拷贝开始的指令。
为获得指令长度的代码相当麻烦,这就是我们得使
用已完成的引擎的原因。
它是由Z0MBiE写的。
传入参数是我们要获得长度的指令的地址。
输
出参数在eax里。
;LDE32,Length-DisassemblerEngine,32-bit,(x)1999-2000Z0MBiE
;specialeditionforREVERTtool
;version1.05
C_MEM1equ0001h;|
C_MEM2equ0002h;|maybeusedsimultaneously
C_MEM4equ0004h;|
C_DATA1equ0100h;|
C_DATA2equ0200h;|maybeusedsimultaneously
C_DATA4equ0400h;|
C_67equ0010h;usedwithC