首页
论坛
课程
招聘
[原创]HOOK学习笔记与心得
2014-10-27 18:27 30961

[原创]HOOK学习笔记与心得

2014-10-27 18:27
30961
前言
自己从高中那时对黑客比较感兴趣,那时候去过很多论坛学所谓的“黑客技术”。同时也经常在一些QQ群,论坛里混,但见效甚微。
因为我很喜欢玩游戏,上大学后,同学们都发疯似的玩一款游戏—三国杀,现在玩的人已经很少了,被某公司抄袭后垄断了。由于当时学业繁重,然后自己又爱面子,所以在想如何学业不落下的同时游戏等级也能比别人高,上网找到了一款三国杀的刷分软件。兴高采烈得打开准备刷分,用了一天之后就不能用了,30元一个月的收费标准,穷学生哪能舍得啊?于是想找人破解,但转念一想,谁会帮你破解呢?还是自己来吧~自己动手,丰衣足食。上网+图书馆疯狂找资料,花了一个周末,把这个软件KO了,当时是高兴死了。那种成就感和兴奋感恐怕也只有亲身试过的人才知道。很快就刷到了150级了,比整天玩游戏的同学等级还要高,在满足了我虚荣心的同时也带我走向了逆向之门,或许这就是一种缘分吧。
好了,背景就不多说了。今天和大家一起学习一下C++中的HOOK API技术。相信大家都知道这是逆向PJ中的一个特别重要也是实用的技术。(*^__^*)
一、        Hook介绍
钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理window消息或特定事件。
我们知道Windows系统API函数都是被封装到DLL中,在某个应用程序要调用一个API函数的时候,如果这个函数所在的DLL没有被加载到本进程中则加载它,然后保存当前环境(各个寄存器和函数调用完后的返回地址等)。接着程序会跳转到这个API函数的入口地址去执行此处的指令。由此看来,我们想在调用真正的API之前先调用我们的函数,那么可以修改这个API函数的入口处的代码,使他先跳转到我们的函数地址,然后在我们的函数最后再调用原来的API函数。
简单来说HOOK API 可以理解成对程序将要执行系统函数的一个拦截, 拦截后执行自己写的代码以达到完成某种特定的目的,再恢复程序继续执行,很多PJ中的Patch 机器码,盗号木马等都是用这个方法。
二、        Hook API实战
OK,既然我们需要实现一个这样的一个HOOK。当然需要两样东西,一是目标程序,一是我们的代码。
1.        程序:
新建一个dll工程文件目录如图:

我们自己新建一个Add.def文件,然后添加到工程中即可,如图我是添加到Source Files里。
跟exe有个main或者WinMain入口函数一样,DLL也有它自己的一个入口函数,就是DllMain。
我们打开dllmain.cpp  代码如下:
// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"

int WINAPI add(int a, int b)
{
	return a + b;
}

BOOL APIENTRY DllMain(HANDLE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
	)
{
	return TRUE;
}

DEF文件是模块定义文件,模块定义 (.def) 文件为链接器提供有关被链接程序的导出、属性及其他方面的信息。生成 DLL 时,.def 文件最有用。由于存在可代替模块定义语句使用的链接器选项,通常不需要 .def 文件。也可以将 __declspec(dllexport) 用作指定导出函数的手段。在链接器阶段可以使用 /DEF(指定模块定义文件)链接器选项调用 .def 文件。如果生成的 .exe 文件没有导出,使用 .def 文件将使输出文件较大并降低加载速度。
        在VC++中,生成DLL可以不使用.def文件。只需要在VC++的函数定义前要加__declspec(dllexport)修饰就可以了。但是使用__declspec(dllexport)和使用.def文件是有区别的。如果DLL是提供给VC++用户使用的,你只需要把编译DLL时产生的.lib提供给用户,它可以很轻松地调用你的DLL。但是如果你的DLL是供其他程序如VB、delphi,以及.NET用户使用的,那么会产生一个小麻烦。因为VC++对于__declspec(dllexport)声明的函数会进行名称转换,如下面的函数:
__declspec(dllexport) int __stdcallIsWinNT()
会转换为IsWinNT@0,这样你在VB中必须这样声明:
Declare Function IsWinNT Lib "my.dll" Alias "IsWinNT@0" () As Long
@的后面的数由于参数类型不同而可能不同。这显然不太方便。所以如果要想避免这种转换,就要使用.def文件方式。
EXPORTS后面的数可以不给,系统会自动分配一个数。对于VB、PB、Delphi用户,通常使用按名称进行调用的方式,这个数关系不大,但是对于使用.lib链接的VC程序来说,不是按名称进行调用,而是按照这个数进行调用的,所以最好给出。
        .def 文件中的第一条 LIBRARY 语句不是必须的,但LIBRARY 语句后面的 DLL 的名称必须正确,即与生成的动态链接库的名称必须匹配。此语句将 .def 文件标识为属于 DLL。链接器将此名称放到 DLL 的导入库中。
EXPORTS 语句列出名称,可能的话还会列出 DLL 导出函数的序号值。通过在函数名的后面加上 @ 符和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从 1 到 N,其中 N 是 DLL 导出函数的个数。
LIBRARY BTREE
EXPORTS
Insert @1
Delete @2
Member @3
Min @4
如果使用 MFC DLL 向导创建 MFC DLL,则向导将为您创建主干 .def 文件并将其自动添加到项目中。添加要导出到此文件的函数名。对于非 MFC DLL,必须亲自创建 .def 文件并将其添加到项目中。
如果导出 C++ 文件中的函数,必须将修饰名放到 .def 文件中,或者通过使用外部“C”定义具有标准 C 链接的导出函数。如果需要将修饰名放到 .def 文件中,则可以通过使用 DUMPBIN 工具或 /MAP 链接器选项来获取修饰名。请注意,编译器产生的修饰名是编译器特定的。如果将 Visual C++ 编译器产生的修饰名放到 .def 文件中,则链接到 DLL 的应用程序必须也是用相同版本的 Visual C++ 生成的,这样调用应用程序中的修饰名才能与 DLL 的 .def 文件中的导出名相匹配。
因此def文件代码如下:
LIBRARY  Add
DESCRIPTION "ADD LA"
EXPORTS
add  @1;

在如图位置可以检验是否导入了:

由此,一个简单的dll我们就完成了。
下面用MFC写一个程序来调用我们的dll。
还是新建一个工程,目录如下:

MFC的.Cpp中主要代码如下:
void CMFCApplication5Dlg::OnBnClickedButton1()
{
	// TODO: Add your control notification handler code here

		HINSTANCE hAddDll = NULL;
		typedef int (WINAPI*AddProc)(int a, int b);//函数原型定义  
		AddProc add;
		if (hAddDll == NULL)
		{
			hAddDll = ::LoadLibrary(_T("Win32DLL.dll"));//加载dll  
		}
		add = (AddProc)::GetProcAddress(hAddDll, "add");//获取函数add地址  

		int a = 123;
		int b = 456;
		int c = add(a, b); 
		CString tem;
		tem.Format(_T("%d+%d=%d"), a, b, c);
		AfxMessageBox(tem);
	
}

效果如图:出现这个说明你成功了

一个调用dll中加法函数的MFC程序我们就完成了,下面就需要自己写一个dll,让程序执行我们的代码。

新建一个MFC的 dll工程,工程名为Hook,然后我们在Hook.cpp文件里面编写的代码如下:
// Hook.cpp : Defines the initialization routines for the DLL.
#include "stdafx.h"
#include "Hook.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif
//变量定义  

#pragma data_seg("SHARED") //不同Instance共享的该变量   
static HHOOK  hhk = NULL; //鼠标钩子句柄  
static HINSTANCE hinst = NULL; //本dll的实例句柄 (hook.dll)  
#pragma data_seg()  
#pragma comment(linker, "/section:SHARED,rws")  
//以上的变量为共享  

CString temp; //用于显示错误的临时变量  
bool bHook = false; //是否Hook了函数  
bool m_bInjected = false; //是否对API进行了Hook  
BYTE OldCode[5]; //原程序API入口代码  
BYTE NewCode[5]; //新跳转的API代码 (jmp xxxx)  
typedef int (WINAPI*AddProc)(int a, int b);//add.dll中的add函数定义  
AddProc add; //add.dll中的add函数  
HANDLE hProcess = NULL; //所处进程的句柄  
FARPROC pfadd;  //指向add函数的远指针  
DWORD dwPid;  //所处进程ID  
//end of 变量定义  
// CHookApp

BEGIN_MESSAGE_MAP(CHookApp, CWinApp)
END_MESSAGE_MAP()

//鼠标钩子过程,什么事情也不做,目的是注入dll到程序中  
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
	return CallNextHookEx(hhk, nCode, wParam, lParam);
}
//开启钩子的函数  
void HookOn()
{
	ASSERT(hProcess != NULL);

	DWORD dwTemp = 0;
	DWORD dwOldProtect;

	//将内存保护模式改为可写,老模式保存入dwOldProtect  
	VirtualProtectEx(hProcess, pfadd, 5, PAGE_READWRITE, &dwOldProtect);
	//将所属进程中add()的前5个字节改为Jmp Myadd   
	WriteProcessMemory(hProcess, pfadd, NewCode, 5, 0);
	//将内存保护模式改回为dwOldProtect  
	VirtualProtectEx(hProcess, pfadd, 5, dwOldProtect, &dwTemp);

	bHook = true;
}
//关闭钩子的函数  
void HookOff()//将所属进程中add()的入口代码恢复  
{
	ASSERT(hProcess != NULL);

	DWORD dwTemp = 0;
	DWORD dwOldProtect;

	VirtualProtectEx(hProcess, pfadd, 5, PAGE_READWRITE, &dwOldProtect);
	WriteProcessMemory(hProcess, pfadd, OldCode, 5, 0);
	VirtualProtectEx(hProcess, pfadd, 5, dwOldProtect, &dwTemp);
	bHook = false;
}
//然后,写我们自己的Myadd()函数  
int WINAPI Myadd(int a, int b)
{
	//截获了对add()的调用,我们给a,b都加上一定的数
	a = a + 987;
	b = b + 654;

	HookOff();//关掉Myadd()钩子防止死循环  

	int ret;
	ret = add(a, b);

	HookOn();//开启Myadd()钩子  

	return ret;
}
//好,最重要的HOOK函数: 
void Inject()
{

	if (m_bInjected == false)
	{ //保证只调用1次  
		m_bInjected = true;

		//获取add.dll中的add()函数  
		HMODULE hmod = ::LoadLibrary(_T("Win32DLL.dll"));
		add = (AddProc)::GetProcAddress(hmod, "add");
		pfadd = (FARPROC)add;

		if (pfadd == NULL)
		{
			AfxMessageBox(L"cannot locate add()");
		}

		// 将add()中的入口代码保存入OldCode[]  
		_asm
		{
			lea edi, OldCode
				mov esi, pfadd
				cld
				movsd
				movsb
		}

		NewCode[0] = 0xe9;//实际上0xe9就相当于jmp指令  
		//获取Myadd()的相对地址  
		_asm
		{
			lea eax, Myadd
				mov ebx, pfadd
				sub eax, ebx
				sub eax, 5
				mov dword ptr[NewCode + 1], eax
		}
		//填充完毕,现在NewCode[]里的指令相当于Jmp Myadd  
		HookOn(); //可以开启钩子了  
	}
}


CHookApp::CHookApp()
{
	
}
CHookApp theapp;


//鼠标钩子安装函数:  
BOOL InstallHook()
{

	hhk = ::SetWindowsHookEx(WH_MOUSE, MouseProc, hinst, 0);

	return true;
}

//卸载鼠标钩子函数  
void UninstallHook()
{
	::UnhookWindowsHookEx(hhk);
}



//在dll实例化中获得一些参数  
BOOL CHookApp::InitInstance()
{
	CWinApp::InitInstance();

	//获得dll 实例,进程句柄  
	hinst = ::AfxGetInstanceHandle();
	DWORD dwPid = ::GetCurrentProcessId();
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwPid);
	//调用注射函数  
	Inject();
	return TRUE;
}

接着需要配置一下DEF文件:
; Hook.def : Declares the module parameters for the DLL.
LIBRARY "HOOK"
EXPORTS
InstallHook  
UninstallHook


最后HOOK成功的效果如图所示:

值得一提的是很多人都是改API开头的5个字节,但是现在很多杀毒软件用这样的方法检查API是否被HOOK,或其他病毒木马在你之后又改了前5个字节,这样就会互相覆盖,最后一个HOOK API的操作才是有效的。挂钩的方法很多,这里只是最基础的一种。希望大家多多补充。多多指正。
了解好以下的问题能够更好的提升自己的HOOK水平:
1.CPU指令长度问题,在32位系统里,一条JMP/CALL指令的长度是5个字节,因此你只有替换API里超过5个字节长度的机器码(或者替换几条指令长度加起来是5字节的指令),否则会影响被更改的小于5个字节的机器码后面的数条指令,甚至程序流程会被打乱,产生不可预料的后果;
2.参数问题,为了访问原API的参数,你要通过EBP或ESP来引用参数,因此你要非常清楚你的HOOK代码里此时的EBP/ESP的值是多少;
3.时机的问题,有些HOOK必须在API的开头,有些必须在API的尾部,比如HOOK CreateFilaA(),如果你在API尾部HOOK API,那么此时你就不能写文件,甚至不能访问文件;HOOK RECV(),如果你在API头HOOK,此时还没有收到数据,你就去查看RECV()的接收缓冲区,里面当然没有你想要的数据,必须等RECV()正常执行后,在RECV()的尾部HOOK,此时去查看RECV()的缓冲区,里面才有想要的数据;
4.上下文的问题,有些HOOK代码不能执行某些操作,否则会破坏原API的上下文,原API就失效了;
5.同步问题,在HOOK代码里尽量不使用全局变量,而使用局部变量,这样也是模块化程序的需要;
6.最后要注意的是,被替换的CPU指令的原有功能一定要在HOOK代码的某个地方模拟实现。

若有疏漏之处,欢迎各位大侠指正!
2014.10.27 6:30 pm

第五届安全开发者峰会(SDC 2021)10月23日上海召开!限时2.5折门票(含自助午餐1份)

上传的附件:
收藏
点赞0
打赏
分享
最新回复 (21)
雪    币: 55
活跃值: 活跃值 (19)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
LessonXK 活跃值 1 2014-10-27 20:04
2
0
沙发~多谢分享!
雪    币: 71
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mumaren 活跃值 2014-10-27 22:52
3
0
很基础,很通俗
雪    币: 264
活跃值: 活跃值 (14)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
byebing 活跃值 2014-10-28 22:09
4
0
顶~~~~~
雪    币: 202
活跃值: 活跃值 (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
albeta 活跃值 2014-10-28 22:30
5
0
入门级资料,不过值得支持了。
雪    币: 163
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
sinmon 活跃值 2014-10-28 23:35
6
0
Mark!
雪    币: 21
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
豆浆蟹蟹 活跃值 2014-11-5 13:10
7
0
再来个x64位下的hook呢
雪    币: 2555
活跃值: 活跃值 (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
湘火 活跃值 2014-11-5 19:52
8
0
好文章~~
雪    币: 10
活跃值: 活跃值 (16)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Carlton 活跃值 2014-11-5 20:09
9
0
写的挺好的 楼主加油!顺便问一下VS2012 不装Visual Assist X有自动提示么
雪    币: 1926
活跃值: 活跃值 (84)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
韩梦雅 活跃值 2015-1-12 14:39
10
0
瞬间感觉自己好弱啊,一个周末搞定了,我现在还没入门!!
雪    币: 188
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
JackJoker 活跃值 2015-1-12 22:15
11
0
不错,支持个
雪    币: 3701
活跃值: 活跃值 (438)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kxzpy 活跃值 2015-3-16 06:05
12
0
不错 支持
雪    币: 98
活跃值: 活跃值 (163)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
褐眼男子 活跃值 1 2015-3-17 10:47
13
0
标记一下
雪    币: 300
活跃值: 活跃值 (191)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
yeyeshun 活跃值 2 2015-3-17 14:31
14
0
hookon和hookoff不需要使用WriteProcessMemory,因为dll已经注入了目标进程,fadd的地址相当于本地地址,可以直接当作本地内存进行操作,比如*fadd=xxx 这样的,或者memcpy函数。
另外Inject里面也不需要内联汇编,一样的道理。
当然了并不是说你写的不对或者写的不好,而是交流一下其他的手法,操作本地内存可以更灵活
雪    币: 140
活跃值: 活跃值 (18)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
習生 活跃值 1 2015-10-9 15:04
15
0
学习了 谢谢
雪    币: 9707
活跃值: 活跃值 (430)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
pnccm 活跃值 2016-1-28 09:55
16
0
完全看不懂不知道要如何才能正确的学习hook方面的知识
雪    币: 6476
活跃值: 活跃值 (812)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xie风腾 活跃值 2017-12-23 17:17
17
0

楼主的复件也是牛了,还以为是源码
原来就是图
雪    币: 333
活跃值: 活跃值 (16)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
czcqq 活跃值 2 2017-12-23 19:39
18
0
楼主,你这HOOK程序,问题很严重啊!虽然能运行,但是你没看出有一个严重的问题么?你可以试试多线程频繁HOOK以及卸载HOOK的情况,你就会发现一个非常严重的后果!
雪    币: 333
活跃值: 活跃值 (16)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
czcqq 活跃值 2 2017-12-23 19:42
19
0
看你们学习HOOK的时候栽倒在我曾经栽倒的地方,真是不是滋味啊!
雪    币: 333
活跃值: 活跃值 (16)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
czcqq 活跃值 2 2017-12-23 19:51
20
0
yeyeshun hookon和hookoff不需要使用WriteProcessMemory,因为dll已经注入了目标进程,fadd的地址相当于本地地址,可以直接当作本地内存进行操作,比如*fadd=xxx 这样的,或 ...
这个倒是其次,你没有发现,它没有处理多线程竞争么?按照它这调用方法,在多线程同时HOOK的时候就会出现一个线程在调用函数的时候,另外一个线程有可能正在进行HOOK,这是很危险的,WriteProcessMemory并不是线程同步的,还有,它没有对线程调用HOOK的函数进行调用次数统计,这个统计的作用是保证DLL模块在退出时所有的HOOK调用已经完成,当HOOK进行调用时,这个统计就加一,HOOK调用完成时,统计减一,如果模块需要退出,就检查HOOK调用统计,如果统计不为0,就让模块进入等待,直到调用统计为0为止!如果HOOK调用未完成,而模块又退出了,那么等待HOOK目标的后果就只有崩溃了
雪    币: 333
活跃值: 活跃值 (16)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
czcqq 活跃值 2 2017-12-23 19:57
21
0
yeyeshun hookon和hookoff不需要使用WriteProcessMemory,因为dll已经注入了目标进程,fadd的地址相当于本地地址,可以直接当作本地内存进行操作,比如*fadd=xxx 这样的,或 ...
楼主,你的这个函数int  WINAPI  Myadd(int  a,  int  b)
{
        //截获了对add()的调用,我们给a,b都加上一定的数
        a  =  a  +  987;
        b  =  b  +  654;
 
        HookOff();//关掉Myadd()钩子防止死循环 
 
        int  ret;
        ret  =  add(a,  b);
 
        HookOn();//开启Myadd()钩子 
 
        return  ret;
}虽然可以防止钩子死循环  ,你这样调用却产生了一个多线程竞争的问题,要解决这个问题,你就不能这样关闭HOOK
雪    币: 300
活跃值: 活跃值 (191)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
yeyeshun 活跃值 2 2017-12-25 10:18
22
0
czcqq 楼主,你的这个函数int WINAPI Myadd(int a, int b) { //截获了对add()的调用,我们给a,b都加上一定的数 a = a + 987; ...
我思考了一下,发现我并不是楼主。。。
这些东西如果要写到完美的话还是很复杂的,我只是指出其中可以改进的一点而已。真正要使用的话,我选择minHook
游客
登录 | 注册 方可回帖
返回