本文内容引述至《windows核心编程》
DLL注入
Dll注入除了使用前文《windows注入》中所推荐的三种方法
- 修改注册表
- Hook
- RemoteThread
还有其他的注入方式
木马注入DLL
木马注入常见的黑客手法,把预知常见的进程必然载入的DLL库同名替换。
Dll库除了名字要相同,还有原来DLL中导出的所有符号,而且随着替换的Dll版本发生变化,更改了导入表甚至是导入地址,那么这样的适配工作就会很麻烦。
将DLL作为调试器注入
系统载入一个被调试程序的时候,会在被调试程序的地址空间准备完毕后,但在调试程序的主线程尚未开始执行任何代码之前,自动通知调试器。这时候,调试器可以将一些代码注入到被调试程序的地址空间中。让调试程序的主线程执行代码。
这样的方式要求我们对被调试线程的CONTEXT结构进程操作,也意味着我们必须编写与CPU相关代码。
注入代码
使用CreateProcess注入代码
如果注入代码的进程是由我们的进程生成的,那么就可以用这种方法。这种方法允许我们修改子进程的状态,又不影响它的执行。
- 让进程生成一个被挂起的子进程;
- 从.exe模块的头文件中取得主进程的起始内存地址;
- 将位于该内存地址处的机器指令保存起来;
- 强制将一些手工编写的机器指令写入到该内存地址处,指令应该调用LoadLibrary来载入一个DLL;
- 让子进程的主线程恢复运行,执行编写的指令;
- 把保存起来的原始指令恢复到起始地址处;
- 让进程从起始地址继续执行
这个方法在6,7步时有难度,因为必须修改正在执行的代码。
好处:首先,在应用开始前得到地址空间;其次,非常容易对应用程序和注入的Dll进行调试;最后,方法同样适用于控制台应程序和GUI应用程序。
API拦截例子
简单地注入DLL并不能为我们提供足够的信息。我们常常想要知道某个进程中的线程具体是怎么调用各种函数的,还相对windows函数进行修改。
通过覆盖代码拦截API
覆盖代码实现API的拦截是比较简单可控的方式
- 在内存中对要拦截的函数(比如Kernel32.dll中的ExitProcess)进行定位,从而得到它的内存地址;
- 把这个函数其实的几个字节保存到内存中;
- 用CPU的一条JUMP指令来覆盖这个函数起始的几个字节,这条JUMP指令用来跳转到我们的替代函数的内存地址。
- 当线程调用被拦截函数的时候,跳转指令实际上会调转到我们的替代函数中;
- 为了撤销对函数的拦截,需要把2中保存到字节放回被拦截函数其实的地址中;
- 继续调用被拦截函数,让该函数执行它的正常处理;
- 当原来的函数返回时,我们再次执行第2步和第3步,这样我们的替换函数将来还会被调用到
缺点
由于系统升级,x86、x64、IA-64以及其他CPU的JUMP指令各不相同,必须手动编写机器指令。
修改模块的导入段拦截API
知识背景
一个模块的导入段包含一组DLL,为了让模块能够运行,这些DLL是必须的。此外,导入段还包含一个符号表,其中列出了该模块从各DLL中导入的符号。当该模块调用一个导入函数的时候,线程实际上会先从模块的导入表中得到相应的导入函数地址,然后再跳转到那个地址。
具体实现
因此,为了拦截一个特定的函数,可以通过修改它在模块的导入段的地址。
void CAPIHook::ReplaceIATEntryInOneMod(PCSTR pszCalleeModName, PROC pfnCurrent, PROC pfnNew, HMODULE hmodCaller) {
ULONG ulSize;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL;
__try {
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(
hmodeCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
}
__except {
}
if (pImportDesc == NULL)
return;
// 找到导入描述符,包含对被调用者功能的引用
for (; pImageDes->Name; pImportDesc++) {
PSTR pszModName = (PSTR)((PBYTE)hmodCaller + pImportDesc->Name);
if (lstrcmpiA(pszModName, pszCalleeModName) == 0) {
// 获取导入函数表IAT
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)
((PBYTE)hmodCaller + pImportDesc->FirstThunk);
// 替换函数
for (; pThunk->u1.Function; pThunk++) {
// 获取函数地址
PROC* ppfn = (PROC*)&pThunk->u1.Function;
// 目标函数
BOOL bFound = (*ppfn == pfnCurrent);
if (bFound) {
if (!WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL) && (ERROR_NOACCESS == GetLastError())) {
DWORD dwOldProtect;
if (VirtualProtect(ppfn, sizeof(pfnNew), PAGE_WRITECOPY, &dwOldProtect)) {
WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL);
VirtualProtect(ppfn, sizeof(pfnNew), dwOldProtect, &dwOldProtect);
}
}
return;
}
}
}
}
}
对于上述替换函数的调用,这里取例如下
一个名为Database.exe的模块,调用了Kernel32.dll中的ExitProcess函数,但是我们希望在调用DbExtend.dll模块中的MyExitProcess函数。
PROC pfnOrig = GetProcAddress(GetModulehandle("Kernel32"),"ExitProcess");
HMODULE hmodCaller = GetModuleHandle("Database.exe");
ReplaceIATEntryInOneMod(
"Kernel32.dll", // 包含目标函数的模块
pfnOrig, // 调用函数地址
MyExitProcess, // 替换函数地址
hmodCaller); // 调用地址
ReplaceIATEntryInOneMod函数,第一件事就是调用ImageIDrectoryEntryToData并传入IMAGE_DIRECTORY_ENTRY_IMPORT,其目的是为了对hmodCaller的导入段进行定位,如果返回为NULL,说明没有导入段。函数ImageIDrectoryEntryToData是由ImageHlp.dll提供的。
如果有导入段,函数返回导入段地址,实际上是个PIMAGE_IMPORT_DESCRIPTOR类型的指针,然后从导入段中遍历查找的符号,导入段都是ANSI格式的。
如果定位到目标符号引用,获得一个PIMAGE_THUNK_DATA 结构组成的数组,其中包含于导入符号有关的信息。有时候编译器可能会生成多个导入段,所以遍历没有退出。
获取导入段数组后,遍历查找一个与符号当前地址相匹配的地址。
如果找到了地址,函数调用WriteProcessMemory将地址修改为替代函数的地址。如果发生错误,使用VirutalProtect修改页面保护属性,修改完指针后,用VirtualProtect恢复页面保护属性。
这里ReplaceIATEntryInOneMod函数修改的函数调用来至于同一个模块,但是地址空间中的另一个DLL可能也会调用ExitProcess,如果Database.exe之外的模块试图调用ExitProcess,那么它会成功调用到kernel32中的ExitProcess函数。
存在的问题
考虑LoadLibrary,GetProcAddress函数对ReplaceIATEntryInOneMod的影响
问题1
LoadLibrary函数如果在ReplaceIATEntryInOneMod之后被调用,或者windows会首先载入这些静态链接的DLL,而不给我们机会去更新他们的导入地址表中与ExitProcess中有关的部分。
解决办法
ReplaceIATEntryInOneMod变成ReplaceIATEntryInOneMods,这样新的隐式载入的模块也能得到更新。
问题2
调用函数直接通过GetProcAddress取得调用函数的地址
typedef int (WINAPI *PFNEXITPORCESS)(UINT uExitCode);
PFNEXITPROCESS pfnExitProcess = (PFNEXITPORCESS)GetProcAddress(GetModuleHandle("Kernel32"), "ExitProcess");
pfnExitProcess(0);
解决办法
对GetProcAddress函数拦截,并返回相应的替换函数地址。