HOOK技术

HOOK技术_第1张图片

本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如有错漏,欢迎留言交流指正

HOOK

  • 什么是HOOK?
    • 用自己写的函数替换目标的函数,以后系统调用目标函数的时候就会调用我们自己的函数,就可以在自己的函数去监控程序执行的逻辑,进行参数的检查过滤等,进一步拦截、修改、放行。
  • HOOK发生的在什么地方 ?磁盘?内存?
    • HOOK是发生在内存中,只在此次系统运行期间有效,一旦系统或者程序重启,就需要重新HOOK
    • 有可能在磁盘中函数替换,不叫HOOK,而是叫感染
      • 比如电脑中了病毒,入口点就会被修改了
  • 对抗HOOK或者感染最有效的方法是:校验文件的签名(比如Hash值)

HOOK函数分三步:

  1. 找到目标函数地址
    • 不同的函数采用不同的方法
    • 比如SSDT表的函数,可以通过计算定位
    • 对于导出的函数,可以通过MmGetSysRoutineAddress()来获取函数地址
    • 对于未导出的函数(没有头文件,没有导出符号供我们调用,即无法调用这个函数),只能用windbg+微软的符号得到函数的特征码,在内核中暴力搜索特征码,找到函数的地址
  2. 实现一个新函数替换目标函数
    • SSDT HOOK中,新函数的签名返回值参数个数参数类型必须要和目标函数保持一致
    • INLINE HOOK中,新函数的签名返回值参数个数参数类型不一定需要和目标函数保持一致
  3. 恢复目标函数
    • 不需要监控了,或者驱动卸载了,就需要把恢复目标函数,让系统恢复正常,如果不恢复,比如驱动卸载了,系统访问目标函数的地址的时候,这个地址此时就是无效内存,系统就会蓝屏

R3 HOOK

DLL的原理

  • 动态链接库DLL (Dynamic Link Library),是一个包含可由多个程序同时使用的代码和数据的库, 被所有引用它的进程共有。
  • .dll是windows的动态链接库,对应的Linux上的动态链接库是.so
  • 经常使用的代码,独立出来放在DLL文件中,动态链接进程序,防止内存占用过大。
    • 静态链接库会被链接到exe程序中去,占空间比较大。
    • 动态链接库dll中代码和数据不会被链接到exe程序中去的,所以dll文件在内存中只有一份,多个exe程序调用dll的代码和数据的,其实是调用一个映射到dll中对应代码和数据地址。
    • 发布的时候,如果使用动态链接库,就需要把dll和exe一起发布,如果缺失dll,exe程序无法运行。
  • DLL占的究竟是谁的空间?
    • win32系统保证内存中只有一份DLL。
    • 通过文件映射实现:DLL首先被调入WIN32系统的全局堆,然后映射到调用这个DLL的进程地址空间
    • 多个进程调用时,每个进程都会收到DLL的一份映像可看作自己的代码
    • 进程使用的内存空间并不会随着dll的增大而增大(比如1个10M的DLL并不会让调用它的任何进程多占据10M的内存空间);
      • 程序调用LoadLibrary来加载dll,但并不会使进程使用内存大小发生明显变化,会增加一点点,是因为管理该dll用到的数据结构导致的,但可能不会增加很多。
      • DLL被加载之后系统的使用页面文件明显变大,而在任务管理器中并没有发觉哪个进程的内存使用明显增大了
  • DLL目的:共享代码节约内存。但是,DLL在安全领域的应用超过了此范畴
    • DLL的入口函数DllMain,可以在入口函数加入一些操作,达到安全防护或者恶意操作。

DLL的开发与使用

DLL的开发(DLL工程)
DllMain
  • 常见的EXEDLLOCXSYSCOM都是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;
}
DLL工程编译后:文件种类
  • .LIB文件:引入库文件包含被DLL导出的函数的名称位置
  • .DLL文件:DLL包含实际的函数数据
  • .EXE程序使用LIB文件链接到所需要使用的DLL文件。DLL库中的函数和数据并不复制到可执行文件中的
    • EXE文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行时,再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。
DLL的使用(MFC测试工程)
  • 1.lib+dll+头文件(隐式链接)
    • 要有lib文件和头文件,要手上有整个dll工程文件(是自己的项目),如果是调用别人的dll的话,别人一般只发布dll
    • 1.将dlllib文件拷贝到(MFC测试工程)的TestDl和TesetDell/Debug下
    • 2.在在MFC测试工程中包含lib的头文件和lib文件
    • 3.在在MFC测试工程中调用dll中的导出函数
/// 在MFC测试工程中
#include "libname.h" ///< 把dll工程的libname.h包含进来
#pragma comment(lib,"libname.lib") ///< 把dll工程的libname.lib包含进来
func(); ///< 在MFC测试工程调用dll文件的导出函数
/// 编译用lib文件,发行的时候用DLL文件。
  • 2.只有DLL文件(显示链接)
    • 比如ntdll.dIl,自己手上不可能有微软的源代码
    • 关句柄例子
///在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*的指针
  • 3.静态库(不是DLL范畴了)
#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来丢弃该消息,并阻正该消息的传递。
  • eg:notepad编辑txt文件
    • 正常情况下:按键盘输入字符123456,按键产生按键消息传给notepad,notepad收到按键消息输出字符123456,才能看到字符123456
    • 往系统注册低级键盘钩子后:按键盘输入字符123456,按键产生按键消息传给notepad之前会被钩子先截获,钩子截获之后,
      • 1.通过直接返回TRUE来丢弃该消息,并阻正该消息的传递。notepad不会收到按键消息,就不会显示字符123456
      • 2.如果想要该消息继续传递,那么它必须调用另外经个SDK中的APl函数CallNextHookEx来传递它。notepad收到按键消息输出字符123456
      • 3.也可以修改键盘的消息为111111,notepad收到修改后的消息输出字符111111,这是腾讯QQ密码框保护的原理
      • notepad收到按键消息输出字符123456,才能看到字符123456
在DLL中应用全局钩子:键盘和鼠标
  • 普通非低级鼠标键盘钩子(需要注入)
#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
}

  • 低级鼠标键盘钩子(无需dll注入)
// 低级键盘钩子
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登陆界面-qq密码框会触发保护,只要光标停留在qq的密码框,就随机返回一些乱码来保护密码。但qq只防御钩子,可以通过驱动(串口)来监控,可以绕过保护
  • 原理
    • 在系统中,每个进程都可以注册低级键盘钩子,这些键盘钩子被组织到链表上,注册越晚的键盘钩子越早执行越早拿到按键消息。QQ注册了三个钩子,优先级是WH_DEBUG>WH_KEYBOARD_LL>WH_MOUSE_LL
    • QQ启动一个线程,定时(1s)UNHOOK和HOOK上面3个钩子,保证自己最后被安装,自然就是先取得正确的按键信息;然后阻止这个消息继续传递下来给别人就算传下去交给别人,也要修改按键信息,传一个或多个错误的下去
  • 预防被下钩子
    • 应用层的SetWindowsHookExW对应在ShadowSSDT的NtUserSetWindowsHookEx,HOOKNtUserSetWindowsHookEx来监控哪个进程在下哪个钩子,拦截然后弹窗,提示用户

@todo 在主防中加入钩子的防护

DLL注入和DLL劫持

DLL注入
共享节
  • 目的:不同进程之间传递数据
  • A进程想获取B进程的密码,把dll注入B进程中,把密码拿到后放在dll的共享节里面,A进程通过共享节把密码拿到。
//创建一个新的节,将全局变里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是看不到指针指向地址的内容的),但通过共享节就可以实现通信(通过共享节中定义的变量)
  • 测试demo
/**
*  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放到目标进程,让目标进程加载dll(即将LoadLibrary(dllpath)强制插入到目标进程中),在目标进程执行DLLMain,从而获得在目标进程执行任意代码的机会。
  • DLL注入目的 :注入到某个进程中,可以执行某段恶意代码,窃取密码,提权、进行HOOK等
DLL注入的方式
  1. 通过dll文件注入
  • 把代码封装到dll的DLLMain里面,把dll文件注入到目标进程中去
  • 思路
    • 1.OpenProcess()打开目标进程(普通进程可以这样打开,但系统中特权进程打不开,需要通过驱动(DipatchIoctrl)去掉关键进程EPROCESS里的PslsProtectedProcess标志位和关闭DLL签名策略)
    • 2.获取待注入的DLL路径,分配一块目标进程内的内存(VirualAllocEx()),将路径拷贝到该内存中,即为执行LoadLibrary(dllpath)做准备
    • 3.获取kernel32.dll中的LoadLibraryA地址(因为kernel32.dll是在全局堆中,所以LoadLibraryA地址是可以在目标进程中运行的)
    • 4.调用CreateRemoteThread,在目标进程中执行loadlibrary + DLL的动作(LoadLibrary(dllpath)),即在目标进程中加载dll文件
    • 5.DLL中的DLLMain执行(比如hook、拷贝一些目标进程敏感数据、提权(如果目标进程权限比较高,注入的执行代码将获得和目标进程一样的权限,相当于提权了))
    • 6.释放分配的自标进程中的内存了
    • 7.获取kernel32.dll中的FreeLibrary地址
    • 8.调用CreateRemoteThread,在目标进程中执行FreeLibrary +DLL的动作
  • Debug权限与进程打开
/// 打开目标进程可能会因为权限不够高导致失败,所以在打开目标进程之前先提高权限,比如为当前进程添加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);

  • demo
//-----------------------------------------------
// 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;
}
  • 防御思路:
    • hook OpenProcessVirtualAllocExWriteProcessMemoryCreateRemoteThread在内核层对应的函数,下规则来检查参数标记,来防范dll注入。
  1. 内存加载注入
  • 思路:MemLoadLibrary2以PE文件(不一定是dll文件,也可以是数据包通过网络传输,绕开杀毒软件的文件过滤)加载到内存中,然后进行修复,再注入到目标进程中,隐藏性更好
    • MemLoadLibrary2负责解析PE,构建内存节(文件节对齐和内存节对齐方式不一致),重定位修复(地址是dll加载基地址的相对偏移,dll注入到内存中,基地址会发生改变,所有需要用重定位表来修复),导入表,IAT表初始化,对该DLL使用的其它DLL使用LdrLoadDll来加载其他dll,最后执行dllmain
  • 步骤:
    • 将用于memload的代码(MemLoadLibrary2)保存为文件(shellcode)
    • 将DLL文件读入内存
    • 将保存负责解析和加载DLL的shellcode (MemLoadLibrary2)的文件读入内存
    • 通过WriteProcessMemory把上面的代码(MemloadLibrary2)和数据DLL以及对应的API)写入目标进程
    • CreateRemoteThread让上述代码和数据执行:MemLoadLibrary2(dll)
  • Demo
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();
}
  1. 驱动注入
  • Github:blackbone https://github.com/DarthTon/Blackbone
  • Demo驱动DLL注入源码.zip
  • DemoinjectAllThe Things-master zip

参考资料:
[翻译]多种DLL注入技术原理介绍http://bbs.pediy.com/thread-220405.htm
Ring3注入总结及编程实现【有码】:https://bbs.pediy.com/thread-217722.htm

DLL劫持
  • 概念:程序调用LoadLibrary(ExX的时候,使用了相对路径进行加载对应的DLL,Windows会按照特定的顺序去搜索一目录,攻击者就可以构造一个同名伪造的DLL被加载(内部函数再指向真的DLL,但之前会执行恶意代码〉,执行特定的代码或者提权(UAC) 。
  • 默认搜索目录顺序:
    • 程序所在目录
    • 加载DLL时所在的当前目录
    • 系统目录SYSTEM32,在x64下Sysem32是存放64位程序的dll,SysWOW64才是存放32位程序的dll
    • WINDOWS目录
    • PATH环境变量中的目录
  • 扩展名+DLL劫持:
    可以在同自录下放该扩展名文件与伪造DLL,打开这件,对应的DLL即被加载,造成DLL劫持。
  • 微软的安全措施:
  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\safedllsearchmode注册表这个值设置之后,会调整dll搜索目录顺序:
    • 程序所在目录
    • 系统目录SYSTEM32
    • WINDOWS目录
    • 加载DLL时所在的当前目录
    • PATH环境变量中的自录
  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs注册表中标注的dll
    • 规定必须由SYSTEM32目录加载的DLL,其他地方的同名dll都不起作用了
    • 但ExcludeFromKnownDls可以排除部分DLL由SYSTEM32加载,但需重启系统
  • 防御dll劫持:
    • LoadLibrary()使用绝对路径
    • 白名单、签名验证
    • SetDliDirectory()把当前目录从搜索范围内去掉,打KB2533623补丁的系统上才可以
  • 杀毒软件防御思路
    • 全盘监控dll文件的状态(全盘监控太耗性能,所以还是在编程的时候加强防护意识更好),一旦发现有可疑的dll释放,将dll与云样本库匹配或者不在白名单上,如果匹配上,就把可疑的dll删除。

R3 HOOK

  • 不同于内核层的SSDT HOOK只需要把底层这些函数替换,对所有进程都会生效。R3 HOOK基于dll注入来实现的.
    • R3的进程之间是相互隔离的(内存空间的私有的),如果只替换某个函数,只在该进程有效,其他进程不受影响,所以需要自己开发一个dll,注入到目标进程中,然后再目标进程中把目标函数替换。
  • 可选引擎:找到目标原函数地址,把自己的函数和原目标原函数替换这些工作都可以由hook引擎来完成
    • nthookengine:开源免费
    • mhook:开源免费
    • Detour:微软官方发布的引擎,现在免费了
MHOOK
#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); ///< 恢复
NTHookEngine
/// 替换
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"));

R3跨进程HOOK思路
  • 总体流程:

    • 一个DLL文件(实现自己的API),调用HOOKengine来负责HOOK需要的API(主要工作在这里面)
    • 一个Hooker(即所谓的监控软件)来负责把DII文件注入(InjectDl)到目标进程
    • 一个Hookee程序,正常的程序,Hookee被Hooker获得其PID并成功打开,被Hooker DLL注入。
  • R3为什么需要DLL注入才能HOOK其它进程?

    • R3进程是私有地址空间,在自己进程HOOK了APl,无法影响其它进程中的API。所以,必须把HOOK代码通过DLL注入的方式进入目标进程。才能影响目标进程的行为。DLL注入不可缺少。
  • 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;
}
  • hooker
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();
}
  • hookee
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);
    }
}

SYSENTER

  • API应用层进入内核层的必经之路,替换的目标函数是KiFastCallEntry
  • 因为API从内核层进入内核层,都需要先通过KiFastCallEntry然后在经过SSDT表,所以完全可以HOOKKiFastCallEntry来替换SSDT HOOK
  • 比如x64上,API从内核层进入内核层调用的是syscall,在x64上进行SYSENTER HOOK的时候就是替换syscall对应的函数
  • 通过rdmsrwrmsr拿到KiFastCallEntry的地址

SSDT

  • SSDT表中存放函数地址
  • x86
    • 没问题,直接hook
  • x64
    • SSDT表没有导出,直接找,找不到的
    • x64引入了Patchguard技术(不允许修改内核,SSDT表是存储在ntoskrnl.exe(内核文件)中),hook SSDT表的时候就在该内核,操作系统隔几分钟扫描一次内核,验证签名的时候就会发现内核被修改了,触发蓝屏自我保护

函数ZW与NT区别:

  • Ntdll.dll中:ZW与NT完全一样
  • 在Ntoskrnl.exe中:
    • NT函数是存放在SSDT表中的,用来响应应用态的请求或者响应内核态Zw函数的请求,即无论走应用态路径还是内核态路径都是调用NT函数
    • Zw*->Nt*(Zw函数会调用Nt),Nt函数更底层,既然Nt函数更底层,内核态驱动可不可以直接调用NT函数呢?不能!
    • 因为Zw函数会把kthread中的PreviousMode设置为KernelMode,然后再调用Nt函数,因此再Nt函数中就不会进行参数检查。
    • 而如果直接调用Nt函数的话,必须程序员自己将PreviousMode设置为KernelMode(修改的过程很麻烦的,因为kthread是未导出的,要硬编码偏移来定位PreviousMode,才能修改),否则PreviousMode很可能仍然是UserMode,这样的话,Nt函数就会认为对它的调用来自用户态,从而做一些检查(probe内存,发现驱动传的是内核态内存,但PreviousMode很仍然是UserMode),这时就会调用失败会蓝屏,防止越权。
    • 所以在内核态,还是老老实实调用Zw函数,Zw函数要注意什么?(不要接受应用层内存,参见内核驱动漏洞与攻击预防的第4条b项

找到在SSDT表中目标函数的地址

  • 在x86下,SSDT表是导出的,即可以把它当作一个全局变量来用,直接访问SSDT表就可以得到SSDT表首地址(指针数组首地址),只需要知道索引就可以得到目标函数的地址了。如何获取到目标函数在SSDT表中索引值呢?
    • 思路一:解析Ntdll.dll的PE结构(Nt函数在SSDT表有一份,在Nrdll.dll也有一份,而且编号都一样的,即Ntdll.dll是PE文件,里面存了一个和SSDT表一样的表),通过目标函数的名字,找到目标函数的索引值
    • 思路二:研究Nt函数和与之对应的Zw函数之间的关系
    • 对反汇编 ZwReadFile函数反汇编得:
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
  • 拿到NtreadFile函数在NtReadFile在SSDT表的索引值
	/// 这条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
	}
}

优缺点

  • 兼容性
    • x86下,SSDT表是导出的,任何人都可以HOOK,有多个人HOOK的时候,如果有人卸载HOOK之后,忘记恢复目标函数。或者HOOK之后就把拦截的操作都拒绝了,没有给后来者机会。系统的兼容性无法保证。
    • HOOK不是微软官方支持(在WDK里面找不到HOOK demo),到后面在x64上引入Patchguard技术就是为了防止修改内核
  • 可卸载性
    • API—> SSDT函数-> Irp包-> 驱动(驱动分层,上层->下层,Irp包在下层的时候又可能会被挂起(没有返回))如果钩子卸载后,一些阻塞的函数才返回,此时钩子函数已经不在内存,系统就会蓝屏
    • 解决办法:为Hook的目标函数设置引用计数(加锁或者原子操作,如果引用计数不为0,即该函数还没有返回,下层Irp被挂起),而且Unhook需要通过DeviceloControl来进行(不要DriverUnload里面,而是单独构造一个控制码,调用RemoveHook)

支持多核的函数替换

  • 使用CR0关闭打开写保护在多核上是有蓝屏几率的
    • CR0写保护的汇编代码在多核环境下,同时运行者A,B线程,可能线程A关闭写保护,接着线程B打开了写保护,接着线程A修改SSDT表的目标函数地址就会触发蓝屏(写保护下SSDT表不可以被修改)?
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);

一个稳定的基本框架demo

  • @todo demo
  • 查看HOOK之后的SSDT表
  • x nt!kes*des*table*
  • dd addr
  • dds addr L length

进程创建监控(防止恶意进程创建)

在进程创建之前拦截,否则程序执行了,执行了部分恶意代码,已经达到目的了,此时再阻止它就没有意义了。

  • HOOK
    • 在xp上进程创建的过程:CreateProcessW->CreateProcesslnternalW→NtCreateProcessEx→NtCreateSection
    • 在VISTA以上系统,进程创建的过程:CreateProcessWe->CreateProcessInternalW→NtCreateUserProcess→NtCreateSection
      -所以可以通过hookNtCreateSection来监控进程的创建,统一使用一套代码即可。
  • 文件过滤
    • NtCreateSection有专属的Irp,IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION
    • 所以也可以文件过滤来拦截Irp监控进程的创建
      Data->lopb->Parameters.AcquireForSectionSynchronization.PageProtection == PAGE_EXECUTE
  • 回调函数
    • 也可以通过PsSetCreateProcessNotifyRoutineEx注册回调函数进行监控

@todo demo

进程保护(防止自己被别人干掉)

1.应用层杀进程调用的是TerminateProcess
  • 方法1:在内核走的是NtTerminateProcess,所以可以hook 这个函数NtTerminateProcess防止我们的进程被杀
  • 方法2:也可以hook NtOpenProcess防止别人打开进程
  • 方法3:还可以通过ObRegisterCallbacks注册回调保护进程
  • demo @todo
2.通过结束任务来杀进程(任务管理器)
  • 方法1:窗口标题不为空的程序,都会在任务列表中显示
    • 可以设置标题为空:SetWindowText(KT(""));来避免进程被杀
  • 方法2:结束任务:使用SendMessageTimeoutW发送WM_CLOSE消息,并设定超时时间为500ms,超过500ms走EndTask
    • 过滤WM CLOSE消息,来避免被杀
/// 应用层:在虚函数PreTranslateMessage里
BOOL CXXXDlg::PreTranslateMessage(MSG* pMsg)
{
	if( pMsg->message == WM_CLOSE)//过滤掉WM_CLOSE
		return TRUE;
}

驱动加载监控(防止别人进入内核)

进入内核方式:
  • 1.StartService()→NtLoadDriver->DriverEntry
    Hook_NtLoadDriver
    @todo demo
  • 存在问题:用INSTDRV.exe加载驱动时,进程路径的获取不准确。
  • 原因是:INSTDRV.exe通过RPC通信,让server.exe来加载驱动
  • 解决方法:INSTDRV.exe和services.exe是通过以下函数进行通信的,HOOK这两个函数知道正在加载的驱动的进程的PID,通过PID拿到进程的全路径
    • XP:NtRequestWaitReplyPort
    • WIN7:NtAlpcSendWaitReceivePort

    总结一把,较为精确判断SCM加载-看竺变全论坛.png

  • 2.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");
  • HOOK_NtSetSystemInformation
NTSTATUS NTAPI HOOK_NtSetSystemlnformation(
	IN ULONG SystemInformationClass,
	INOUT PVOID SystemInformation,
	IN ULONG SystemlnformationLength
)
if(SystemlnformationClass == SystemLoadAndCallmage) || (SystemlnformationClass == SystemLoadlmage))
	...

  • 3.回调: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

锁主页的思路

  • 流氓软件的最爱
  • 主页是存放载注册表里的,浏览器打开主页其实就是在查注册表
  • 思路1:对注册表查询下手
    • ZwOpenKey //打开注册表键
    • ZrQueryValuaKey //查询注册表键值
    • ZvSetValuaKey //设孟注册表键值
    • ZwClose //关闭句柄
  • 思路2:修改命令行参数
    • firefox.exe www.google.com
    • 应用层:注入浏览器HOOK getcommandlineW, peb->ProcessParameters->CommandLine.Buffer()
    • 内核层:利用PsSetCreateProcessNotifVRoutine通过callback函数带来的信息附加到进程中获取PEB的CommandLine.Buffer
  • 思路3:通过网络协议修改
    • 首页的网站解析的时候会经过DNS查询
    • TDI获取浏览器进程名,NDIS不能获取进程名修改IP进行dns过滤,目的端口是53,udp协议
      • TDI不能改数据包,NDIS不能获取进程名,所以需要配合,TDI与NDIS通过MDL共享内存通信,TDI获得NDIS建立的共享内存地址
  • 360:系统调用最后99%会到KiFastSystemCall,过了这个就进内核层执行真正功能,Win32API只是个内核层和用户层之间的代理,于是360就在KiFastSystemCall上对注册表查闻的下手,查询主页健值就返回锁定的主页
  • 金山:直接Hook用户层API,查询主页的注册表就值时直接种回锁定的主页
  • 管家:也是Hook用户层API。不过是Hook了进程创建进程的API。检测到是浏览器,就在浏资器的参数里加上锁定的主页。于是浏览器启动时就打开了我的主页
  • 还有乱七八糟的:
    • Hook Winsock的网络访问函数,检测到是浏览器第一次访问就改参数(改成发向我们主页)
    • Hosts文件,把别的主页重定向过来
    • 开驱动。Hook SSDT,把注册表访问函数改成我们自己的,检测倒访问主页键值直接返回我们的主页
  • 效果排列:
    Host文件>Hook Winsock>Hook进程创建API>Hook用户层注册表查询API>Hook KiFastSystemCall>Hook SSDT
  • 查杀对易程度:
    Host文件

ShadowSSDT HOOK

ShadowSSDT HOOK的作用
  • 窗口保护
    HOOK技术_第2张图片
  • 安全输入
    • NtUserSendInput(模拟按键)
    • NtUserGetAsyncKeyState(获取键盘按键状态)
    • NtUserOpenDesktop(打开安全桌面)
    • NtUserTranslate Message 虚假按键还原成真实的按
    • NtUsersetWindowsHookEx保护键盘钩子
  • 反截屏
    • NtGdiBitBlt
    • NtGdiStretchBlt
SHADOW表地址的获取
  • 首地址
    HOOK技术_第3张图片
    • 使用的硬编码,根据相同版本下与SSDT地址存在的偏移获取的SSDTSHADOW的首地址
  • Index
    • 和SSDT表一样,需要硬编码
  • 替换目标函数的地址(StartHook)
    • CSRSS进程。在SSDT HOOK是在DriverEntry中调用StartHook来进行HOOK的(能这样做是因为,DriverEntry处在system的进程上下文,而在system中载入了Ntosknl.exeSSDT表保存在Ntosknl.exe中),所以就可以在DriverEntry里访问到SSDT表),system进程并没有载入win32k.sys,所以,要访问shadowssdt表,必须KeStackAttachProces到一个有GUI线程的进程中,而csrss.exe就是这样的一个合适的进程(常驻进程,管理Windows图形相关任务)
    • 句柄表中Portobject(type21) \\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
);

INLINE

原理

  • SSTD HOOK的思路不同,而且不像SSDT HOOK只能HOOK SSDT表,而是SSDT表的函数内核中函数应用层的函数都可以使用INLINE HOOK来进行函数替换
    HOOK技术_第4张图片
  • 替换的指令长度>=5Byte(jmp xxxx 1+4Byte)
  • 替换的位置可以任意位置,具有一定的隐秘性
  • MyFn(外围函数)->Fn(处理函数)
  • 反汇编引擎
    • xde
    • BeaEngine(x32/x64)
    • libdasm
  • 直接的jmp分3
    • Short Jump(短跳转)
      机器码EB rel8只能跳转到256字节的范围内
    • Near Jump(近跳转)
      机器码E9 rel16/32可跳至同一个段的范围内的地址
    • Far Jump(远跳转)
      机器码EA ptr 16:16/32可跳至任意地址使用48位/32位全指针

Inline hook的步骤

  • 1.获得要inline hook的函数在内存中的地址
    • 如何找到函数的首地址?
    • 未导出函数:暴力搜索
    • 导出函数:直接引用或者MmGetSystemRoutineAddress
  • 2.实现T_MyFunc()与MyFunc函数,在T_MyFunc()函数中将参数压栈并调用MyFunC函数处理。
/// 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之后,调用保存的指令,跳回去。

SwapContext demo

@todo

  • 只能在xp上跑,因为SwapContext和它相关成员的取得都是基于xp的硬编码的,换到其他系统就需要重新调整硬编码了
  • 找到指令A,把指令A备份起来,紧接着指令A备份的位置设置一个JMP指令跳回去到指令A的后面
  • 构造一个jmp指令跳到外围处理函数,并将该jmp指令覆盖到原来的指令A,
  • 构造一个处理函数,处理完之后,返回到外围处理函数中,执行另一条jmp指令跳转到指令A的备份开始执行

IRP

  • 驱动的分发函数的地址是存放在pDriverObject->MajorFunction[]中的

IDT

  • IDT表(中断描述符表)中存一组中断服务函数的地址
  • 通过sidt拿到函数地址
 __asm 
	 sidt idt_info

OBJECT

  • OBJECT→OBJECT_HEADER→OBJECT_TYPE→OBJECT_TYPE_INITIALIZER
  • 涉及很多硬编码,代码质量不高

你可能感兴趣的:(windows)