本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如有错漏,欢迎留言交流指正
替换
目标的函数,以后系统调用目标函数的时候就会调用我们自己的函数,就可以在自己的函数去监控
程序执行的逻辑,进行参数的检查
,过滤
等,进一步拦截、修改、放行。内存
中,只在此次
系统运行期间有效,一旦系统或者程序重启
,就需要重新HOOK
磁盘
中函数替换,不叫HOOK,而是叫感染
入口点
就会被修改了校验
文件的签名
(比如Hash值)地址
不同
的函数采用不同
的方法计算定位
导出
的函数,可以通过MmGetSysRoutineAddress()
来获取函数地址未导出
的函数(没有头文件,没有导出符号
供我们调用,即无法调用这个函数),只能用windbg
+微软的符号
得到函数的特征码,在内核中暴力搜索
特征码,找到函数的地址替换
目标函数
SSDT HOOK
中,新函数的签名
、返回值
、参数个数
、参数类型
必须要和目标函数保持一致INLINE HOOK
中,新函数的签名
、返回值
、参数个数
、参数类型
不一定需要和目标函数保持一致恢复
目标函数
无效内存
,系统就会蓝屏
DLL
(Dynamic Link Library),是一个包含可由多个程序同时使用的代码和数据的库, 被所有引用它的进程共有。.dll
是windows的动态链接库,对应的Linux上的动态链接库是.so
独立
出来放在DLL文件中,动态链接进程序,防止内存占用过大。
静态链接库
会被链接到exe程序中去,占空间比较大。动态链接库
dll中代码和数据不会被链接到exe程序中去的,所以dll文件在内存中只有一份
,多个exe程序调用dll的代码和数据的,其实是调用一个映射到dll中对应代码和数据地址。一起发布
,如果缺失dll,exe程序无法运行。只有一份
DLL。文件映射
实现:DLL首先被调入WIN32系统的全局堆
,然后映射到调用这个DLL的进程地址空间
。LoadLibrary
来加载dll,但并不会使进程使用内存大小发生明显变化,会增加一点点,是因为管理该dll用到的数据结构
导致的,但可能不会增加很多。DLL目的
:共享代码节约内存。但是,DLL在安全领域的应用超过了此范畴
DLL
的入口函数DllMain
,可以在入口函数加入一些操作,达到安全防护或者恶意操作。EXE
、DLL
、OCX
、SYS
、COM
都是PE文件。
EXE
的入口函数是main
,可以双击执行,而SYS
的入口函数是DriverEntry
,不可以双击执行,需要相应的加载方法,同样地,DLL
的入口函数DllMain
,不可以双击执行,需要相应的加载方法,可以在入口函数加入一些操作// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: //进程加载
case DLL_THREAD_ATTACH: //线程加载
case DLL_THREAD_DETACH: //进程卸载
case DLL_PROCESS_DETACH: //线程卸载
break;
}
return TRUE;
}
#ifdef HELLODELL_EXPORTS ///< 但在DLL项目中找不到有定义HELLODELL_EXPORTS的代码,是因为这个宏是在项目属性-C/C++-命令行中定义了,
#define HELLODELL_API __declspec(dllexport) ///< 所以在这个项目中,HELLODELL_EXPORTS会作为导出
#else
#define HELLODELL_API __declspec(dllimport)
#endif
/// 新添一个导出函数2
HIDLL_API int fnMyfunc(void)
{
MessageBox(NULL, _T("hell dll"), _T("mydll"), MB_OK); ///< _T这个宏,需要包含tchar.h头文件,表示字符串类型随着项目不同而发生变化,unicode或者多字节
return 0;
}
.LIB
文件:引入库文件包含被DLL导出的函数的名称
和位置
.DLL
文件:DLL包含实际的函数
和数据
.EXE
程序使用LIB文件链接
到所需要使用的DLL文件。DLL库中的函数和数据并不复制到可执行文件中的
内存地址
,这样当一个或多个应用程序运行时
,再把程序代码和被调用的函数代码链接
起来,从而节省了内存资源。dll
和lib
文件拷贝到(MFC测试工程)的TestDl和TesetDell/Debug下头文件
和lib文件
调用
dll中的导出函数/// 在MFC测试工程中
#include "libname.h" ///< 把dll工程的libname.h包含进来
#pragma comment(lib,"libname.lib") ///< 把dll工程的libname.lib包含进来
func(); ///< 在MFC测试工程调用dll文件的导出函数
/// 编译用lib文件,发行的时候用DLL文件。
///在dll的头文件中使用
/// 由于C++编译的时候有命名粉碎机制(为了重载),在C++中使用C中的函数,需要用关键字extern "C" 告诉编译器,不改名
extern "C" HIDLL_API int fnFunc(void);
/// 在MFC项目中使用
HMODULE hMod = LoadLibrary(_T("hidll.dll")); ///< 把dll加载到内核空间,hMod是dll在内存中的起始地址,"hidll.dll"这里的dll是相对路径,会存在dll劫持漏洞。因为在LoadLibrary()有一个搜索顺序,优先从当前目录下搜索,如果看图软件有漏洞,放一张美女.jpg到一个文件夹,同时构造一个dll放在这个文件夹,当忍不住诱惑打开的美女.jpg时候,就会执行看图软件,然后会加载当前目录下的dll,执行恶意代码,成功劫持
if (hMod == NULL)
{
return;
}
FUNC func = (FUNC)GetProcAddress(hMod, "fnFunc"); ///< 通过函数名称拿到函数地址
if (func)
{
func();
}
FreeLibrary(hMod); ///< 释放掉hMod,hMod是一个viod*的指针
#pragma comment( lib, "libname.lib)
extern "C" void func();//或者HEADER
func();
回调函数
)是当指定的一些消息被系统中任何应用程序所处理时,这个钩子就被调用。
键盘钩子
的时候,敲的任何一个按键都会被记录SetWindowsHookEx
为这些消息注册钩子HHOOK WINAPI SetwindowsHookEx(
__in int idHook, //钩子类型
__in HOOKPROC lpfn, //钩子(回调函数)地址
__in HINSTANCE hMod, //包含lpfn的实例句柄,即包含钩子的dll在内存中的起始地址
__in DWORD dwThreadld); //线程ID,如果为0,则监控所有线程的全局钩子
//钩子的类型
WH_CALLWNDPROC and WH_CALLWNDPROCRET//监视SendMessage(),需要DLL注入
WH_CBT
WH_DEBUG //在系统调用其他Hook钩子例程之前,系统会调用它,可屏蔽其他钩子
WH_FOREGROUNDIDLE
wH_GETMESSAGE
WH_JOURNALPLAYBACKwHJOURNALRECORD
WH KEYBOARD_LL //低级键盘钩子,全局有效,无需注入,调试钩子屏蔽不了
WH_KEYBOARD
H_MOUSE_LL //低级鼠标钩子
WH_MOUSE
WH_MSGFILTER and WH_SYSMSGFILTER
WH_SHELL
WH_KEYBOARD
在系统处理后处理,注入式键盘挂钩(把包含钩子的dll注入到目标进程)
Ctrl+alt+del
系统会先处理掉,WH_KEYBOARD
没法截获。在应用程序调用GetMessage或者PeekMessaoe函数并且有键盘消息(换下或者释放)的时候会调用相应的函数进行处理。WH_KEYBOARD_LL
是在系统处理前处理的,只要有键盘输入事件的发生,它都会将键盘消息传给相应函数,容易引起挂起之类的问题,内部通过LowLevelHooksTimeout
控制超时。控制权
的钩子函数在完成对消息的处理后,如果想要该消息继续传递,那么它必须调用另外经个SDK中的APl函数CallNextHookEx来传递它。钩子函数也可以通过直接返回TRUE来丢弃该消息,并阻正该消息的传递。123456
,按键产生按键消息传给notepad,notepad收到按键消息输出字符123456
,才能看到字符123456
123456
,按键产生按键消息传给notepad之前会被钩子先截获,钩子截获之后,
123456
CallNextHookEx
来传递它。notepad收到按键消息输出字符123456
111111
,notepad收到修改后的消息输出字符111111
,这是腾讯QQ密码框保护的原理123456
,才能看到字符123456
#include
HOOK g_hMouse = NULL;
HOOK e_hKeyboard = HULL;
//创建一个新的节,将全局变里g_hWnd放入其中
#pragma data_seg("MySec")
HWND g_hWnd = NULL;
#pragma data_seg()
//设置刚创建的节为共享的节
#pragma comment(linker, "/SECTION:MySec,RWS")
// RWS:读/写/共享
// 共享段里的变量可供进程的多个实例访问。而普通全局变量,只对一个实例有效。
// 比如Notpad,开多几个notepad,每个notepad就是notepad.exe不同的实例,如果在notepad.exe中定义在共享段里的变量,在一个实例对共享节中的变量进行了加减,在另一个实例中就可以看到变化,
//即可以在多个实例之间进行通信。由于应用层中进程之间是隔离的(进程A通过指针的方式传递给进程B,进程B是看不到指针指向地址的内容的),但通过共享节就可以实现通信(通过共享节中定义的变量)
//鼠标钩子过程
LRESULT CALLBACK MouseFroc(
int nCode, //hook code
WPARAM wParam, //message identifier
LPARAM lParam //mouse coordinates
)
{
return 1; //屏蔽所有鼠标消息
}
//键盘钩子过程
LRESULT CALLBACK KeyboardFroc (
int code, //hook code
WPARAM wParam, //virtual-key code 键值
LPARAM lParam ///keystroke-message information
)
{
return 1; //屏蔽所有键盘消息
}
//安装鼠标钩子过程的函数
void SetHook(HWND hwnd) //参数是为了让dll获得调用进程的主窗口的句柄
{
g_hWnd = hwnd;
//hook所有进程的鼠标、键盘消息
g_house = SetWindowsHookEx(WH_MOUSE,MouseProc,GetModuleHendle("Hook,dll"),O);
g_hKeyboard = SetWindowsHookEx(WH_KEYBOARD,KeyboardProc,GetModuleHandle("Hook,dll"),0);
//Hook.h
/// 把SetHook()函数导出
extern"c" __declspec(dllexport) void SetHook (HWND hwnd);
// app .cpp
//需要把包含钩子的dll注入到目标进程
//导入函数
__decispec(dlimport) void SetHook (HND hwnd);
BO0L CHookTestDlg::OnInitDialogo()
{
int cxScreen,cyScreen;
cxScreen = GetSystemMetrics(SM_CXSCREEN);
cyScreen = GetSystemetrics (SM_CYSCREEN);
setWindowPos(&widTopMost,0, 0, cxScreen, cyScreen,SwP_SHDWWINDOW);
//调用DLL中的函数
setHook(m_hWnd); ///< 监控进程某个窗口的的键盘和鼠标的消息
return TRUE; //return TRUEunless you set the focus
}
// 低级键盘钩子
HHOOK g_Hook;
LRESULT CALLBACK LowLevelKeyboardProc(INT nCode,WPARAM WPararm,LPARAM lParam)
{
KBDLLHOOKSTRUCT *pkbhs = (KBDLLHOOKSTRUCT *)lRaram;
BOOL bControlKeyDown = 0;
switch(nCode)
{
// wParam:按键的状态(按下或弹起)WM_KEYDOWN、MKEYUP等
// lParam:指向KeyboardHookStruct结构的指针,该结构包含了按键的详细信息
// nCode:等于HC_ACTION时,wParam和lParam包含键盘信息
case HC_ACTION:
// Check to see if the CTRL key is pressed
// ControlKeyDown = GetAsyncKeyState(vK_CONTROL) >> == ((sizeof(SHORT)*8)-1); // 不仅可以判断按下ctrl,也可以判断按下大小写键等
// Disable CTRL+ESC
if (pkbhs->vkCode == Vk_ESCAPE && bControlKeyDown)
return 1; // 1.如果同时按下空格和ctrl键,直接返回TRUE来丢弃该消息,并阻正该消息的传递。
printf("%c",pkbhs->vkCode);
break;
}
//return CallNextHookEx(g_Hook, nCode , wParam, lParam); //2.消息继续往下传
return 1;
}
void ChookKeyboardllDlg::OnBn ClickedOk()
{
//TOD0:在此添加控件通知处理程序代码
g_Hook = (HHOOK)SetWindowsHookEx(WH_KEYBOARD_LL,(HOOKPROC)LowLevelKeyboardProc,GetModuleHandleW(0),0);
// CDialogEx: OnOK();
//低级鼠标钩
LRESULT CALLBACK LowLevelMouseProc(int nCode,WPARAMwParam,LPARAM IParam)
{
if(code == HC_ACTION)
{
if(wParam == WM_LBUTTONDOWN)
{
return 1; //禁用鼠标左键
}
return CallNextHookEx(0,nCode ,wParam,lParam);
}
}
SetWindowsHookExW(WH_MOUSE_LL,LowLevelMouseProc,GetModuleHandlew(o),0);
/*LPARAM typetypedef struct tagMSLLHOOKSTRUCT
{
POINT pt;
DWORD mouseData;
DWORD flags;
DWORD time;
uLONG_PTR dwExtralnfo;
}MSLLHOOKSTRUCT,*PMSLLHOOKSTRUCT,*LPMSLLHOOKSTRUCT;*/
绕过保护
。越早执行
,越早拿到按键消息
。QQ注册了三个钩子,优先级是WH_DEBUG>WH_KEYBOARD_LL>WH_MOUSE_LL正确
的按键信息;然后阻止这个消息继续传递下来给别人就算传下去交给别人,也要修改按键信息,传一个或多个错误
的下去SetWindowsHookExW
对应在ShadowSSDT的NtUserSetWindowsHookEx
,HOOKNtUserSetWindowsHookEx
来监控哪个进程在下哪个钩子,拦截然后弹窗,提示用户@todo 在主防中加入钩子的防护
目的
:不同进程之间传递数据//创建一个新的节,将全局变里g_hWnd放入其中
#pragma data_seg("MySec")
HWND g_hWnd = NULL;
#pragma data_seg()
//设置刚创建的节为共享的节
#pragma comment(linker, "/SECTION:MySec,RWS")
// RWS:读/写/共享
// 共享段里的变量可供进程的多个实例访问。而普通全局变量,只对一个实例有效。
// 比如Notpad,开多几个notepad,每个notepad就是notepad.exe不同的实例,如果在notepad.exe中定义在共享段里的变量,在一个实例对共享节中的变量进行了加减,在另一个实例中就可以看到变化,即可以在多个实例之间进行通信。由于应用层中进程之间是隔离的(进程A通过指针的方式传递给进程B,进程B是看不到指针指向地址的内容的),但通过共享节就可以实现通信(通过共享节中定义的变量)
/**
* Copyright (c) 2022, 源代码已同步到gitee(https://gitee.com/ciscco/system_secure_official_account/tree/master/R3_HOOK/3ShareSection)
* All rights reserved.
*
* @file ShareSection.cpp
* @version v0.1
* @author cisco(微信公众号:坚毅猿)
* @date 2022-03-27 22:52
*
* @brief
* @note
* @see
*/
// ShareSection.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include
#pragma data_seg("Shared") //创建名为Shared的数据段
int a = 0; //数据段Shared中的变量a,此处a必须进行初始化
#pragma data_seg()
int b = 0; //普通全局变量
#pragma comment(linker, "/SECTION:Shared,RWS") //为数据段Shared指定读,写及共享属性。
int main(int argc, char* argv[])
{
a++;
b++;
printf("a:%d, b:%d\n", a, b);
system("pause");
return 0;
}
DLL注入
:把开发好的dll放到目标进程,让目标进程加载dll(即将LoadLibrary(dllpath)
强制插入到目标进程中),在目标进程执行DLLMain
,从而获得在目标进程执行任意代码的机会。DLL注入目的
:注入到某个进程中,可以执行某段恶意代码,窃取密码,提权、进行HOOK等DLL注入的方式
通过dll文件注入
OpenProcess()
打开目标进程(普通进程可以这样打开,但系统中特权进程打不开,需要通过驱动(DipatchIoctrl)去掉关键进程EPROCESS里的PslsProtectedProcess
标志位和关闭DLL签名策略
)目标进程内
的内存(VirualAllocEx()
),将路径拷贝到该内存中,即为执行LoadLibrary(dllpath)
做准备kernel32.dll
中的LoadLibraryA
地址(因为kernel32.dll是在全局堆中,所以LoadLibraryA地址是可以在目标进程中运行的)CreateRemoteThread
,在目标进程中执行loadlibrary + DLL
的动作(LoadLibrary(dllpath)),即在目标进程中加载dll文件/// 打开目标进程可能会因为权限不够高导致失败,所以在打开目标进程之前先提高权限,比如为当前进程添加Debug权限
BOOL AddDebugPrivilege(void)
{
TOKEN_PRlVILEGEs tp;
LUID luid;
HANDLEhToken;
if(LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&luid)j
return FALSE,
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid=luid;tk
tp.Privileges[0].Attributes-SE_PRIVILEGE。ENABLED;if( !OpenProcessToken(GetCurrentProcess().
TOKEN_ADJUST_PRIVILLEGES,&hToken))
return FALSE;
}
if( !AdjustTokenPrivileges(hToken,FALSE,&tp,sizeof(TOKEN_PRIVILEGES)(PTOKEN PRIVILEGESNULL.(PDWORD)NULL))
{
return FALSE;
小
return TRUE;
}
1.打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD |
PROCESS_QUERY_INFORMATION |
PROCEss_vM_OPERATIONI |
PROCEss_vMRITE |
PROcESsVMREAD,FALSE,PID);
//-----------------------------------------------
// InjectDll
// Notice: Loads "LibSpy.dll" into the remote process
// (via CreateRemoteThread & LoadLibrary)
//
// Return value: 1 - success;
// 0 - failure;
//
int InjectDll(HANDLE hProcess)
{
HANDLE hThread;
char szLibPath[_MAX_PATH];
void* pLibRemote = 0; // the address (in the remote process) where
// szLibPath will be copied to;
DWORD hLibModule = 0; // base adress of loaded module (==HMODULE);
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
// Get full path of "LibSpy.dll"
if (!GetModuleFileName(hInst, szLibPath, _MAX_PATH)) ///< 获取LibSpy.exe的全路径
return false;
strcpy(strstr(szLibPath, ".exe"), ".dll"); ///< 改成dll的全路径
// 1. Allocate memory in the remote process for szLibPath
// 2. Write szLibPath to the allocated memory
/// sizeof(szLibPath)有问题,只计算指针的长度,结果是4Byte,虽然VirtualAllocEx()分配的内存是4K对齐,即使传入4,也是分配4K但下面还是会出问题
/// 改为strlen(szLibPath)修复,如果是字符串指针的化才会出问题,看错了,这里是字符串数组sizeof(szLibPath)没有问题
// pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath), MEM_COMMIT, PAGE_READWRITE );
pLibRemote = ::VirtualAllocEx(hProcess, NULL, strlen(szLibPath), MEM_COMMIT, PAGE_READWRITE);
if (pLibRemote == NULL)
return false;
/// sizeof(szLibPath)有问题,只计算指针的长度,结果是4Byte,只拷贝路径的前4Byte的数据
/// 改为strlen(szLibPath)修复,如果是字符串指针的化才会出问题,看错了,这里是字符串数组sizeof(szLibPath)没有问题
// ::WriteProcessMemory(hProcess, pLibRemote, (void*)szLibPath,sizeof(szLibPath),NULL);
::WriteProcessMemory(hProcess, pLibRemote, (void*)szLibPath, strlen(szLibPath), NULL);
// Load "LibSpy.dll" into the remote process
// (via CreateRemoteThread & LoadLibrary)
hThread = ::CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress(hKernel32, "LoadLibraryA"),
pLibRemote, 0, NULL); ///< 相当于执行LoadLiraryA(pLibRemote)
if (hThread == NULL)
goto JUMP;
::WaitForSingleObject(hThread, INFINITE);
// Get handle of loaded module
::GetExitCodeThread(hThread, &hLibModule);
::CloseHandle(hThread);
JUMP:
::VirtualFreeEx(hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE);
if (hLibModule == NULL)
return false;
// Unload "LibSpy.dll" from the remote process
// (via CreateRemoteThread & FreeLibrary)
hThread = ::CreateRemoteThread(hProcess,
NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress(hKernel32, "FreeLibrary"),
(void*)hLibModule,
0, NULL);
if (hThread == NULL) // failed to unload
return false;
::WaitForSingleObject(hThread, INFINITE);
::GetExitCodeThread(hThread, &hLibModule);
::CloseHandle(hThread);
// return value of remote FreeLibrary (=nonzero on success)
return hLibModule;
}
OpenProcess
、VirtualAllocEx
、WriteProcessMemory
、CreateRemoteThread
在内核层对应的函数,下规则来检查参数标记,来防范dll注入。内存加载注入
MemLoadLibrary2
以PE文件(不一定是dll文件,也可以是数据包通过网络传输,绕开杀毒软件的文件过滤)加载到内存中,然后进行修复,再注入到目标进程中,隐藏性更好
shellcode
(MemLoadLibrary2)的文件读入内存WriteProcessMemory
把上面的代码(MemloadLibrary2)和数据DLL以及对应的API)写入目标进程CreateRemoteThread
让上述代码和数据执行:MemLoadLibrary2(dll)int _tmain(int argc, _TCHAR* argv[])
{
std::cout << "Hello World!\n";
SetProcessPrivilege();//获取debug权限,提高权限是为了打开尽可能多的进程
SaveShellCode();//将MemLoadLibrary2保存为shellcode文件
MemLoad_Test1();//本进程测试MemLoadLibrary2加载并执行dll,会弹窗一次
Memload_inject_Test2(_ttoi(argv[1]));//将MemLoadLibrary2(dll)注入到目标进程,执行加载dll,弹窗一次
getchar();
}
驱动注入
参考资料:
[翻译]多种DLL注入技术原理介绍http://bbs.pediy.com/thread-220405.htm
Ring3注入总结及编程实现【有码】:https://bbs.pediy.com/thread-217722.htm
LoadLibrary
(ExX的时候,使用了相对路径进行加载对应的DLL,Windows会按照特定的顺序去搜索一目录,攻击者就可以构造一个同名伪造的DLL被加载(内部函数再指向真的DLL,但之前会执行恶意代码〉,执行特定的代码或者提权(UAC) 。加载DLL时所在的当前目录
Sysem32
是存放64位程序的dll,SysWOW64
才是存放32位程序的dllHKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\safedllsearchmode
注册表这个值设置之后,会调整dll搜索目录顺序:
加载DLL时所在的当前目录
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
注册表中标注的dll
绝对路径
#include mhook.h
typedef ULONG (WINAPl* _NtOpenProcess)(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK AccessMask
IN PVOID ObjectAttributes,
IN PCLIENT_ID Clientld );
/// 保存目标原函数的地址,如何知道应用层的api在哪个dll中?A:查Microsoft Docs
_NtOpenProcess TrueNtOpenProcess = (NtOpenProcess)GetProcAddress(GetModuleHandle(L"ntdll"), "NtOpenProcess");
/// 自己的hook函数,放行
ULONG WINAPI HookNtOpenProcess(OUT PHANDLE ProcessHlandle,
INACCESS MASK AccessMask
IN PVOID ObjectAttributes,INPCLIENT_ID Clientld)
{
return TrueNtOpenProcess(ProcessHandle,
AccessMask,
objectAttributes,
Clientld);
}
/// 在DLLMain中执行
Mhook_SetHook((PVOID*)&TrueNtOpenProcess,HookNtOpenProcess); ///< 替换
Mhook_Unhook((PVOID*)&TrueNtOpenProcess); ///< 恢复
/// 替换
BOOL (__cdecl *HookFunction)(ULONG_PTR OriginalFunction,ULONG_PTR HookFunction);
/// 恢复
VOID(__cdecl *UnhookFunction)(ULONG_PTR OriginalFunction);
/// 获取原来目标函数的地址,NTHookEngine并不是通过GetProcAddress()来拿到,而是维护了一个原目标函数-hook函数的表
ULONG_PTR(__cdecl *GetOriginalFunction)(ULONG_PTR HookEunction);
int WINAPI MyMessageBoxW( HWND hWnd,LPCWSTR lpText,
LPCWWSTR lpCaption, UINT uType,
WORD wLanguageld, DWORD dwMilliseconds)
{
int (WINAPI *pMessageBoxW)(HWND hWnd,LPCWsTR lpText,
LPCWSTR IpCaption,UINT uType,
WORD wLanguageld,DWORD dwMilliseconds);
pMessageBoxW = (int (WINAPI *)(HWND,LPCWSTR,LPCWSTR,UINT,WORD, DWORD))GetOriginalFunction((ULONG_PTR)MyMessageBoxW); ///< 获取原目标函数的地址
return pMessageBoxWV(hWnd,lpText,L"Hooked MessageBox",
uType, wLanguageld,dwMilliseconds); ///< 返回替换后的函数
}
/// 替换
HookFunction((ULONG_PTR)GetProcAddress(LoadLibrary(_T("User32.dll")),"MessageBoxTimeoutW"),
(ULONG_PTR)&MyMessageBoxW);
/// 恢复
UnhookFunction((ULONG_PTR)GetProcAddress(LoadLibrary(_T("User3 2.dll")),"MessageBoxTimeoutW"));
总体流程:
Hooker
(即所谓的监控软件)来负责把DII文件注入(InjectDl)到目标进程Hookee
程序,正常的程序,Hookee被Hooker获得其PID并成功打开,被Hooker DLL注入。R3为什么需要DLL注入才能HOOK其它进程?
DLLMain
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
HookIt();
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
//UnHook();
break;
}
return TRUE;
}
void ChookerDlg::OnBnClickedOk()
{
//LoadLibraryW(_T("hookdll.dll"));
AddDebugPrivilege(); ///< 把自己的进程添加Debug权限
UpdateData(TRUE); ///< 拿到输入的pid
//MessageBox(_T("Failed"));
HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD |
PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE |
PROCESS_VM_READ, FALSE, m_dwPid);
if (hProcess == NULL)
{
MessageBox(_T("Failed"));
return;
}
InjectDll(hProcess, _T("hookdll.dll")); ///< 把dll注入到pid对应的进程中去
//(CButton*)GetDlgItem(IDOK)->EnableWindow(FALSE);
//OnOK();
}
void CHookeeDlg::OnBnClickedOk()
{
// TODO: Add your control notification handler code here
MessageBox(_T("hi"), _T("hi"), MB_OK); ///< 如果被hooker.exe注入dll成功,dll中DLLMain会被执行,在里面进行函数替换,MessageBox替换成MyMessageBox,R3 Hook成功
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
if (CreateProcess(_T("C:\\WINDOWS\\system32\\cmd.exe"),
_T(""), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
}
KiFastCallEntry
KiFastCallEntry
然后在经过SSDT表,所以完全可以HOOKKiFastCallEntry
来替换SSDT HOOKsyscall
,在x64上进行SYSENTER HOOK的时候就是替换syscall对应的函数rdmsr
和wrmsr
拿到KiFastCallEntry
的地址Patchguard
技术(不允许修改内核,SSDT表是存储在ntoskrnl.exe
(内核文件)中),hook SSDT表的时候就在该内核,操作系统隔几分钟扫描一次内核,验证签名的时候就会发现内核被修改了,触发蓝屏自我保护Ntdll.dll
中:ZW与NT完全一样kthread
中的PreviousMode设置为KernelMode
,然后再调用Nt函数,因此再Nt函数中就不会进行参数检查。kthread
是未导出的,要硬编码偏移来定位PreviousMode,才能修改),否则PreviousMode很可能仍然是UserMode
,这样的话,Nt函数就会认为对它的调用来自用户态
,从而做一些检查(probe内存,发现驱动传的是内核态内存,但PreviousMode很仍然是UserMode
),这时就会调用失败会蓝屏,防止越权。内核驱动漏洞与攻击预防的第4条b项
)Uf nt!ZwReadFile
.text:00406508 move eax,0B7h ;观察可以发现0B7h有点特殊,是NtReadFile在SSDT表的索引值,但在x64上,NtReadFile在SSDT表的索引值不在第一条指令,而是在中间
.text:0040650D lea ead,[esp+FileHandle]
.text:00406511 pushf
.text:00406512 push 8
.text:00406514 call _KiSystemService
.text:00406519 retn 24h
.text:00406519_ZwReadFile@36 endp
/// 这条x86汇编指令占5个Byte,操作码mov eax是B8,后四个Byte是操作数 0000B7h无符号整数
/// ZwReadFile是一个函数指针,指向函数起始的首地址,先将函数指针转化成unsigned char *类型,此时指针移动的长度才是1Byte,即((usigned char *)ZwReadFile + 1)定位到四个字节的索引值的起始地址,然后把四个字节索引值读取出来,即以四个字节为单位,读取指针指向的值,即*(DWORD *)((usigned char *)ZwReadFile + 1)
DWORD index = *(DWORD *)((usigned char *)ZwReadFile + 1)
FuncAddr = KeServiceDesctiptortable + index * 4; ///< 指针运算,相当于KeServiceDesctiptortable[index * 4]这里面是存放着目标函数绝对地址
/// x64上,SSDT表存放的不是绝对地址,而是相对偏移,是相对KeServiceDescriptortable起始地址的偏移,即存放的是(目标函数的绝对地址-KeServiceDescriptortable起始地址)*16,即目标函数地址= KeServiceDesctiptortable + (KeServiceDesctiptortable[index * 4] / 16)
/// x64的SSDT为什么不直接存放绝对地址而是存放相对偏移,因为x64的地址是8个Byte,如果SSDT表存放8Byte的绝对地址,SSDT表就会变大,为了依然用4Byte表示8Byte地址,就采用存放相对偏移的办法,类似于实模式分段模型,但为什么*16呢,可能是为了和实模式分段模型中*16保持一致吧,也可能是访问速度快吧
FuncAddr = KeServiceDescriptortable + ((KeServiceDescriptorable + index * 4) >> 4)
Ntdll.dll
中的 API 都只不过是一个简单的包装函数而已,当 Kernel32.dll
中的 API 通过 Ntdll.dll
时,会完成参数的检查;再调用一个中断(int 2Eh
或者sysenter/syscall
指令),从而实现从 R3 进入 R0 ;并且将所要调用的服务号(也就是在 SSDT 数组中的索引值)存放到寄存器 EAX
中,并且将参数地址放到指定的寄存器(EDX)中,再将参数复制到内核地址空间中,再根据存放在 EAX 中的索引值来在 SSDT 数组中调用指定的服务/// 结构
#pragma pack(1)
typedef struct ServiceDescriptorEntry
{
unsigned int *ServiceTableBase; ///< SSDT表的首地址
unsigned int *ServiceCounterTableBase;
unsigned int NumberOfServices; ///< SSDT表表项的数目
unsigned char *ParamTableBase; ///< SSDT表表项(某个函数)参数的个数
}ServiceDescriptorTableEntry_t,*PServiceDescriptorTableEntry_t;
#pragma pack()
/// 导入
_declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable;
/// KeServiceDescriptorTable.ServiceTableBase可以之间使用了
/// 引用:用下面的宏来计算出某个Nt函数的地址(以及在SSDT表中的位置)_function是Nt函数对应的Zw函数
/// *(DWORD *)((usigned char *)_function + 1),计算目标函数的偏移,从而得到目标函数的地址
#define SYSTEMSERVICE(_function) KeserviceDescriptorTable.ServiceTableBase[*(PULONG)(PUCHAR)_function+1)]
/// 把宏进一步简化
#define SDT SYSTEMSERVICE SDT(ZwCreateSection) //NtCreateSection
/// 以NtCreateSection为例,实现一个新函数
/// 定义一个和NtCreateSection签名一样的函数指针类型
typedef NTSTATUS (*NTCREATESECTION)(
OUT PHANDLE SectionHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG PageAttributess,
IN ULONG SectionAttributes,
IN HANDLE FileHandle OPTIONAL);
/// 用这个函数指针类型定义一个函数指针,用来备份目标函数原来的地址,一是用于恢复函数,二是为了放行(让函数正常执行)
static NTCREATESECTION OldNtCreateSection;
/// 实现一个新函数用于替换目标函数,新的函数名字任意取,要求参数和返回值要一样
/// 通过这些参数可以分析这次该进程在操作什么东西,比如创建文件,读写文件,创建进程等
NTSTATUSNTAPI HOOK_NtCreateSection(
PHANDLE SectionHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttribufes
PLARGE_INTEGER SectionSize,
ULONG Protect,
ULONG Attributes,
HANDLE FileHandle)
{
/// 放行,要返回目标函数原来的函数地址,保证旧的正常的功能
/// 如果返回的是新函数的地址,就会造成重入,一直转圈圈HOOK_NtCreateSection->NtCreateSection->HOOK_NtCreateSection
return OldNtCreateSection(SectionHandle,
DesiredAccess,
ObjectAttributes,
SectionSize,
Protect,
Attributes,
FileHandle);
/// 如果要阻止
// return 拒绝的状态码
}
/// 替换目标函数
void StartlHook (void)
{
/// 获取未导出的服务函数索引号
__asm ///< x86可以嵌入汇编,X64不能这样做
{
push,eax ;备份eax,用eax只作为数据暂存容器
mov eax,CR0 ;CR0寄存器的第十六位存放的是对内核的写保护,1表示只能读不能写
and eax,OFFFEFFFFh ;要修改SSDT表,所以要关闭写保护
mov CRO,eax
pop eax ;恢复eax
}
/// window原子操作CAS,替换目标函数的在SSDT表中的地址,并保存目标函数原来的地址
OldNtCreateSection = (NTCREATESECTION)InterlockedExchanget((PLONG)
&SDT(ZwCreateSection), //必须是ZwCreateSection,而不能是NtCreateSection
(LONG)HOOK_NtCreateSection);
//关闭
__asm
{
push eax
mov eax,CR0
or eax, NOT OFFFEFFFFh ;恢复写保护
mov CR0,eax
pop eax
}
return ;
}
///
/// 恢复目标函数
void RemoveHook(void)
{
__asm
{
push eax
mov eax,CRO
and eax,OFFFEFFFFh
mov CRO, eax
pop eax
}
InterlockedExchange((PLONG)&SDT(ZwCreateSection),(LONG)OldNtCreateSection);
__asm
{
push eax
mov eax,CR0
or eax, NOT OFFFEFFFFh
mov CRO,eax
pop eax
}
}
Patchguard
技术就是为了防止修改内核NTSTATUS RtlSuperCopyMemory(
IN VOID UNALIGNED *Dst
IN CONST VOID UNALIGNED *Src,
IN ULONG Length)
{
/// MDL是一个对物理内存的描述,负责把虚拟内存映射到物理内存
/// 把目标地址锁死在内存中,防止切换出去
PMDL pmdl = loAllocateMdI(Dst,Length,0,0,NULL); ///< 分配mdl
if(pmdl=-NULL)
return STATUS_UNSUCCESSFUL;
MmBuildMdlForNonPagedPool(pmdl); ///< build mdl
unsigned int *Mapped = (unsigned int *)MmMapLockedPages(pmdl,KernelMode); ///< 锁住内存
if(!Mapped)i{
loFreeMdlf(pmdl);
return STATUS_UNSUCCESSFUL;
}
/// 拷贝之前,先把Irql级别提升到DpcLevel
KIRQL kirql = KeRaiselrqlToDpcLevel();
RtICopyMemory(Mapped,Src,Length);
KeLowerlrqlkirql(kirql);
MmUnnapLockedPagest(PVOID)Mapped,pmdl); ///< 锁住内存
IoFreeMdl(pmdl); ///< free mdl
return STATUS_SUCCESS;
}
/// 安装:
OldZwLoadDriver = SDT(ZwLoadDriver);
ULONG hookAddr = (uLONG) Hook_ZwLoadDriver;
RtlSuperCopyMemory(&SDT(ZwLoadDriver), &hookAddr , 4);
/// 卸载:
ULONG oldAddr = (ULONG)OldZwLoadDriver;
RtlSuperCopwMemorwi&SDT(ZwLoadDriver),&oldAddr , 4);
x nt!kes*des*table*
dd addr
dds addr L length
在进程创建之前拦截,否则程序执行了,执行了部分恶意代码,已经达到目的了,此时再阻止它就没有意义了。
NtCreateProcessEx
→NtCreateSectionNtCreateUserProcess
→NtCreateSectionNtCreateSection
来监控进程的创建,统一使用一套代码即可。NtCreateSection
有专属的Irp,IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION
PsSetCreateProcessNotifyRoutineEx
注册回调函数进行监控@todo demo
NtTerminateProcess
防止我们的进程被杀NtOpenProcess
防止别人打开进程ObRegisterCallbacks
注册回调保护进程不为空
的程序,都会在任务列表中显示
SetWindowText(KT(""));
来避免进程被杀/// 应用层:在虚函数PreTranslateMessage里
BOOL CXXXDlg::PreTranslateMessage(MSG* pMsg)
{
if( pMsg->message == WM_CLOSE)//过滤掉WM_CLOSE
return TRUE;
}
StartService()→NtLoadDriver->DriverEntry
总结一把,较为精确判断SCM加载-看竺变全论坛.png
NtSetSystemlnformation
Rt1InitRcodestring((&CregSAm.ModuleName),L"\\??\C:\\ MyModulDrv.sys"); / /加载的驱动就是这个
printf("%ws\n",GregsImage.ModuleName.Buffer);
if(NT_SUCCESS(ZwSetSystemInformation(SystemLoadAndCa11Inage,&GregsImage,sizeof(SYSTEM_LOAD_AND_CALL_IMAGE))))//加载进内核空间
printf("Driver Loaded.\n");
else printf("Driver not loaded.\n");
NTSTATUS NTAPI HOOK_NtSetSystemlnformation(
IN ULONG SystemInformationClass,
INOUT PVOID SystemInformation,
IN ULONG SystemlnformationLength
)
if(SystemlnformationClass == SystemLoadAndCallmage) || (SystemlnformationClass == SystemLoadlmage))
...
PsSetLoadlmageNotifyRoutine
/// 把DriverEntry改了
UCHAR fuck[] = "\xB8\x22\x00\xC0\xc3" VxCopyMemory(DriverEntry,fuck,sizeof(fuck));
///相当于
mov eax,0xc0000022 //b8 status_access_denied,eax保存的是函数的返回值,即status_access_denied返回给系统,驱动就会加载不起来
return //c3
@todo
firefox.exe www.google.com
共享内存
通信,TDI获得NDIS建立的共享内存地址360
:系统调用最后99%会到KiFastSystemCall
,过了这个就进内核层执行真正功能,Win32API
只是个内核层和用户层之间的代理,于是360就在KiFastSystemCall上对注册表查闻的下手,查询主页健值就返回锁定的主页金山
:直接Hook用户层API,查询主页的注册表就值时直接种回锁定的主页管家
:也是Hook用户层API。不过是Hook了进程创建进程的API。检测到是浏览器,就在浏资器的参数里加上锁定的主页。于是浏览器启动时就打开了我的主页效果排列
:查杀对易程度
:CSRSS
进程。在SSDT HOOK是在DriverEntry中调用StartHook来进行HOOK的(能这样做是因为,DriverEntry处在system的进程上下文,而在system中载入了Ntosknl.exe
(SSDT
表保存在Ntosknl.exe
中),所以就可以在DriverEntry里访问到SSDT表),system进程并没有载入win32k.sy
s,所以,要访问shadowssdt表,必须KeStackAttachProces到一个有GUI线程的进程中,而csrss.exe就是这样的一个合适的进程(常驻进程,管理Windows图形相关任务)\\Wndows\\Apiport
的PID即是csrss
进程@todo
BOOL(NTAPI *REAL_NtGdiStretchBlt)(
IN HDC hdcDst,
IN int xDst,
IN int yDst
IN int cxDst,
IN int cyDst,
IN HDChdcSrc,
IN int xSrc,
IN int ySrc,
IN int cxSrc,
IN int cySrc,
IN DWORD dwRop,
IN DWORD dwBackColor
);
BOOL(NTAPI *REAL_NtGdiBitBlt)
( IN HDC.hdcDst,
IN int X,
IN int y,
IN int cx,
IN int cy,
IN HDC hdcSro,
IN int xSrc,
IN int ySrc,
IN DWORD rop4,
IN DWORD crBackColor,
IN FLONG fl
);
SSTD HOOK
的思路不同,而且不像SSDT HOOK只能HOOK SSDT表
,而是SSDT表的函数
、内核中函数
、应用层的函数
都可以使用INLINE HOOK
来进行函数替换3
种
Short Jump
(短跳转)EB rel8
只能跳转到256字节的范围内Near Jump
(近跳转)E9 rel16/32
可跳至同一个段的范围内的地址Far Jump
(远跳转)EA ptr 16:16/32
可跳至任意地址使用48位/32位全指针MmGetSystemRoutineAddress
/// nakedcall表示裸函数,自己去实现传参和栈平衡代码
/// 如果不加nakedcall,编译器会生成一些传参压栈的代码,会干扰我们传参
void nakedcall T_MyFunc()
{
push xxx
push xxx
ret = MyFunc();
mov eax,ret;
ret
jmp B;
}
@todo
Naked call
编译器不会给这种函数增加初始化和清理代码,不能用return返回返回值,只能用插入汇编返回结果。
//naked 调用约定。用户自己清理堆栈。不能进行原型声明,否则错误。
__declspec(naked) int add(int a,int b)
{
__asm push ebp //必须加上两句修改栈帧,否则引用了错误的数据
__asm moy ebp, esp
__asm mov eax, a
__asm add eax, b2
__asm pop ebp
__asm ret
}
这个修饰是和__stdcall及cdecl结合使用的,前面是它和cdecl结合使用的代码,对于和stdcall结合的代码,则变成:
__declspec(naked) int_stdcall function(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret 8 //注意后面的8
}
cdecl/fastcall/stdcall/thiscall/nakedcall
扩展阅读(重要):Calling convention: http://gccfeli.cn/tag/naked-call
__declspec(naked) T_MyFunc(......)
__asm
{
mov edi, edi
push ebp
mov ebp,esp
// 参数压栈,传给MyFunc
push [ebp+0ch]
push [ebp+8]
call MyFunc
//获得结果,如果阻止就结束,否则放行并跳回原来的指令
cmp eax,1
jz end
mov eax,FuncAddress
add eax,5
jmp eax
end:
// 恢复栈
pop ebp
retn 8
}
3.HOOK。保存函数开头的指令到某个内存中,并在该内存中加JMP指令到开头指令的后面的指令。并将开头的指令替换为JMP T_MyEunc地址。
4.在T_MyFunC执行完MyFunc之后,调用保存的指令,跳回去。
@todo
pDriverObject->MajorFunction[]
中的sidt
拿到函数地址 __asm
sidt idt_info
OBJECT→OBJECT_HEADER→OBJECT_TYPE→OBJECT_TYPE_INITIALIZER