20145319 《网络渗透》免考—API拦截技术
概述
- 本次实验在window环境下进行,主要通过编写hook代码和注入程序,将我们的hook代码通过dll文件的形式注入到目标中,拦截其系统函数使其跳转到我们编写好函数上,以此来达到我们的目的
- 我将给大家介绍目前的三种api hook技术(着重前两种)
- inline hook(内联hook)
- IAT hook(导入表Hook)
- windows消息hook
- 主要知识如下
- 远程注入
- Pe文件结构(主要是导入表相关结构)
- c++编程
实验内容
- API拦截技术,顾名思义,就是通过给API函数挂上钩子,拦截,控制相应API函数的调用,从而改变API执行结果的一种技术,在实际中API HOOK实现起来有两个难点,一是如何实现将自己的恶意代码注入到其他进程中,二则是如何给API函数挂钩
注入技术
-
HKLM/Software/Microsoft/Windows NT/CurrentVersion/Windows/AppInit_DLLs
修该注册表键值,添加相应dll,那么程序在加载user32.dll文件的时候同时也会加载相应dll,但是相对,对于不调用user32.dll的程序,则无法实现注入 - 使用
函数CreateRemoteThread
创建远程线程,远程线程中可以执行任意代码,通过代码重定位技术就可以实现我们远程dll注入 - 当然注入技术还有很多种,但是在这里我们主要研究API HOOK技术,就不再详细介绍了
API HOOK(内联HOOK)
hook技术,最核心的功能就是拦截原本信息,转而执行我们想要系统执行的部分,最直接的手段呢,就是直接修改二进制代码为Jmp指令,直接跳转到我们指定的地址来达到目的
如何拦截API函数?只需要在二进制文件中找到相应API函数的地址,将该函数的头几个字节修改成JMP指令,跳转到我们想执行的函数地址,执行完毕之后,再次执行原API函数被修改的字节,并跳转到原API地址,完成原本函数流程,由于这种方法是直接在程序流程中嵌入jmp指令来改变流程的,所以把它叫做内联hook(Inline Hook)
首先,我们要弄清楚的是jmp指令的长度,即我们需要修改多少个字节,使用ollydbg打开任意一个程序,随便将其中的一条指令修改成Jmp指令
从修改结果来看,jmp指令二进制形式为E9,长度为5个字节
-
那么,我们函数的流程应该如下
- 找到目标api函数的地址,保存其地址和前五个字节
- 明确hook函数的地址,构造jmp指令
- 执行Hook函数
- 将保存的api函数前五个字节写入原地址中
- 跳转到原API函数,完成原本的函数流程
为了实现我们的目的,我们选择将相应功能的代码写成dll文件,这样更方便我们进行操作
-
首先我们构造一个类来管理我们的函数以及相应信息(本来是想直接写函数的,后来发现需要保存原函数的二进制指令,保存原函数地址,单纯通过函数实在是太麻烦了,不由得不感叹面对对象这种方法还是方便啊)
#include"windows.h" class CILHOOK
{
public:
CILHOOK(); //构造
~CILHOOK(); //解钩 BOOL Hook(LPSTR pszModuleName,
LPSTR pszFuncName,
PROC pfnHookFunc); //HOOK函数 VOID UnHook(); //取消HOOK函数 BOOL ReHook(); //重新进行HOOK,回到正常函数流程 private:
PROC m_pfnOrig; //函数原地址
BYTE m_bOldBytes[5]; //函数原前五个字节代码
BYTE m_bNewBytes[5]; //jmp指令构造
}; -
编写class中的hook函数,unhook函数(取下钩子),rehook(重新挂钩),其中的根本原理上面已经提到过了,就是读取其中前五个字节,相应地址,将其记录下来并改写成Jmp指令,最后再将其还原,回到原流程中
#include "windows.h"
#include "hookClass.h" CILHOOK::CILHOOK()
{
m_pfnOrig = NULL;
ZeroMemory(m_bOldBytes, 5);
ZeroMemory(m_bNewBytes, 5);
} CILHOOK::~CILHOOK()
{ UnHook(); //脱钩 m_pfnOrig = NULL;
ZeroMemory(m_bOldBytes, 5);
ZeroMemory(m_bNewBytes, 5);
} BOOL CILHOOK::Hook(LPSTR pszModuleName,LPSTR pszFuncName,PROC pfnHookFunc)/*对指定模块的函数进行挂钩,参数分别是模块名称,函数名称,钩子函数名*/
{
BOOL bRet = FALSE; //获取指定模块中函数的地址
m_pfnOrig = (PROC)GetProcAddress(GetModuleHandle(pszModuleName), pszFuncName); if (m_pfnOrig != NULL)
{
DWORD dwNum = 0;
//将原来的数据存起来
ReadProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bOldBytes, 5, &dwNum); //构造jmp指令
m_bNewBytes[0] = '\xe9'; //jmp Opcode
//pfnHookFunc是HOOK后的目标地址
//m_pfnOrig是原来的地址
//5是指令长度
*(DWORD *)(m_bNewBytes + 1) = (DWORD)pfnHookFunc - (DWORD)m_pfnOrig - 5; //将构造好的地址写入该地址处
WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bNewBytes, 5, &dwNum); bRet = TRUE;
}
return bRet;
} VOID CILHOOK::UnHook() /*取消挂钩*/
{
if (m_pfnOrig != 0)
{
DWORD dwNum = 0;
//将原来的内容写回hook地址
WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bOldBytes, 5, &dwNum);
}
} BOOL CILHOOK::ReHook() /*重新挂钩*/
{
BOOL bRet = FALSE; if (m_pfnOrig != 0)
{
DWORD dwNum = 0;
//写入挂钩地址
WriteProcessMemory(GetCurrentProcess(), m_pfnOrig, m_bNewBytes, 5, &dwNum);
bRet = TRUE;
}
return bRet;
} -
至此,我们的基本功能都已经完成了,只需要在dllmain中定义具体的钩子函数,已经将钩子挂到相应的函数上即可(这里,我们选择了对创建进程的函数creatProcessW进行挂钩,当dll文件被加载时,完成挂钩,并弹出对话框提示)当我们创建进程时,系统会弹出内容为进程路径的对话框提示,如果我们选择否,则进程创建失败
#include "windows.h"
#include "hookClass.h" CILHOOK CreateProcessHook; BOOL WINAPI MyCreateProcessW(LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation)
{ BOOL bRet = FALSE; //弹出被创建的进程名
if (MessageBoxW(NULL, lpApplicationName, lpCommandLine, MB_YESNO) == IDYES)
{ //自己调用函数前先去掉钩子,否则会进入死循环
CreateProcessHook.UnHook(); bRet = CreateProcessW(lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation); CreateProcessHook.ReHook(); }
else{
MessageBox(NULL, "您启动的程序被拦截", "提示", MB_OK);
} return bRet; } BOOL APIENTRY DllMain(HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
//HOOK CreateProcessW()函数
CreateProcessHook.Hook("kernel32.dll", "CreateProcessW", (PROC)MyCreateProcessW);
MessageBox(NULL, TEXT("hello"), TEXT("helloWindow"), MB_OK);
break;
}
case DLL_PROCESS_DETACH:
{
CreateProcessHook.UnHook();
MessageBox(NULL, TEXT("goodbye"), TEXT("goodbyeWindow"), MB_OK);
break;
}
}
return TRUE;
} 下面以qq为例,打开对话框,当我们想给对方发送文件时,就会弹出对话框
运行注入程序
程序被拦截
当我们选择是,则能继续正常进行文件发送
这个本来是想注入到windows资源管理器中,但是最后不知道是因为windows有自身的防护机制还是自己权限不够所以没有办法得到相应进程的权限完成注入,自己也搜到了一些提权的资料,说实话在这方面了解不多,代码也不是太懂,不过反正提权好像是失败了,查资料又有说xp之后就没有debug权限了,所以这个问题至今还是没弄清楚
-
提权代码:
int EnableDebugPrivilege(LPTSTR name)
{
HANDLE token;
TOKEN_PRIVILEGES tp;
//打开进程令牌环
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token))
{
printf("open process token error!\n");
return 0;
}
//获得进程本地唯一ID
LUID luid;
if (!LookupPrivilegeValue(NULL, name, &luid))
{
printf( "lookup privilege value error!\n");
return 0;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
tp.Privileges[0].Luid = luid;
//调整进程权限
if (!AdjustTokenPrivileges(token, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
{
printf("adjust token privilege error!\n");
return 0;
}
return 1;
}
API HOOK(导入表HOOK)
通过修改PE拓展头中的导入地址表(IAT)中函数对应的地址,将原API函数地址修改为HOOK函数地址
这种hook技术就要求对于pe文件结构有一定的了解
导入表和导入地址表都存在于pe可选文件头中
IMAGE_OPTONAL_HEADER结构
中,是IMAGE_DATA_DIRECTORY结构
的数组-
导入表具体结构如下
Typedef struct _IMAGE_IMPORT_DESCRIPTOR{ Union{ DWORD Charatorics ;
DWORD OriginalFirstThunk ; } ; DWORD TimeDataStamp ;
DWORD ForwarderChain ;
DWORD Name ;
DWORD FirstThunk ;
} IMAGE_IMPORT_DESCRIPTOR ; -
其中有三个字段比较重要(加载后)
- OriginFirstThunk:该字段指向了导入表的RVA,是一个IMAGE_THUNK_DATA的结构体
- name:加载的dll文件名
- FirstThunk:指向导入地址表(IAT)的RVA,是一个IMAGE_THUNK_DATA的结构体
-
IMAGE_THUNK_DATA结构体如下
Typedef struct _IMAGE_THUNK_DATA{ Union { BYTE ForwarderString ;
WORD Function ;
DWORD Ordinal ;
PIMAGE_IMPORT_BY_NAME AddressOfData ;
} u1 ;
} IMAGE_THUNK_DATA32 ; 这个虽然说是结构体,但其实也可以说只是一个联合体,其作用是决定了函数以函数序号导入,还是以函数名导入,这里对我们影响不大
导入表整体结构如下图:
-
在了解了Pe文件的相关结构之后,我想我们思路已经非常清晰了
- 一个PE文件映像,从偏移位置0开始就是Dos Header,在Dos头中通过指针e_lfanew跳转带PE文件头
- 通过PE文件头可以得到
IMAGE_DATA_DIRECTORY结构
,而数据目录的第二个元素就是存储的导入表的信息 - 通过导入表的信息我们可以定位到导入表,其结构为
IMAGE_IMPORT_DESCRIPTOR
,再通过遍历所有的IMAGE_IMPORT_DESCRIPTOR结构
可以得到所有的DLL文件名,通过遍历每个结构的FirstThunk
可以得到每个函数的地址,通过遍历每个结构的OriginalFirstThunk
所指向的IMAGE_IMPORT_BY_NAME可以得到每个函数的导出函数名 - 我们要做的就是通过修改其中的地址和导出函数名即为我们自己的函数即可完成Hook
替换Windows消息处理函数实现Hook
- 这种方法主要是通过函数SetWindowLong来替换原来的消息处理函数,但是这个函数和具体技术我自己目前也不是很懂,所以就只做这个大概的介绍了