hook方式有多种,这里做了一个系统性的总结对比,如下:
https://www.cnblogs.com/theseventhson/p/14324562.html 之前这里做了接受消息的hook,用的就是最初级的hook方式: jmp到我们自己的处理逻辑。上面也分析了,这种方式缺点非常明显;最牛逼的神级hook:VT读写分离前面已经介绍过了,今天继续介绍高级的hook方式:硬件断点;
现代软件开发,尤其是大型软件开发,绝对不可能一步到位,开发期间肯定存在各种bug。为了方便找到这些bug,软件上有专门的调试机制,比如在某行代码下软件断点,然后步过、步进等。这里软件断点本质就是在用户指定的地址改写成0xCC,也就是int 3指令,cpu执行到这里后就产生异常,然后由中断向量表的3号routine来处理这个异常。除了软件断点,x86架构的cpu也支持设置硬件断点,整个图示图下:
和硬件调试相关的寄存器一共有7个:DR0-DR3分别设置需要断下的地址,DR7可以控制DR0-DR3是否有效。如果需要启用这4个调试寄存器,DR7要设置为0b01010101,也就是L0\L1L2\L3都要为1;
正式介绍代码前,先介绍一个重要的结构体:PCONTEXT,如下:
typedef struct DECLSPEC_NOINITALL _CONTEXT {
//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
typedef CONTEXT *PCONTEXT;
这个结构体保存了常用的所有寄存器,OD、x32dbg、x64dbg等常见的调试器都用了这个结构体读取某个时间点时进程的寄存器,分析人员也可以直接在调试器的界面更改寄存器的值,非常方便。这些功能都是通过读写PCONTEXT结构体实现的。那么问题来了,怎么才能得到PCONTEXT结构体了?
PCONTEXT为调试而生,为了得到这个结构体,就要想办法产生异常;windwos操作系统专门针对异常的处理有一整套完整的机制,这里为了理解,简单介绍一下:windwos下3环进程运行时,如果遇到异常(比如除0),大致的处理顺序如下:
- 先看看有没有调试器(通过编译器运行exe也算),如果有,就发消息给调试器让其处理;
- 如果没有调试器,或则调试器没处理,进入进程自己的VEH继续处理。VEH本质是个双向链表,存储了异常的handler代码,此时windwos会挨个遍历这个链表执行这些handler(感觉原理和vmp很像,估计vmp借鉴了这里的思路)
- 如果VEH还没处理好,接着由线程继续处理。线程同样有个异常接管的链表,叫SEH;windows同样会遍历SEH来处理异常
- 如果SEH还没处理好,继续给线程的UEH传递,UEH只有一个处理函数了
- 如果UEH还没处理好,就回到进程的VCH处理;
基于windwos开发的应用数以万计,微软绝对不可能出厂时就考虑到所有的异常,其各种handler不太可能处理所有的异常,所以微软又开放了接口,让开发人员自定义异常的handler;对于开发人员来说,肯定是越靠前越好,所以这里选择VEH来添加自定义的handler(调试器是最先收到异常通知的,但外挂在正常使用时不太可能有调试的功能,除非开发人员自己单独开发调试器的功能,这样成本太高了)。windwos开放了一个API,叫AddVectoredExceptionHandler,可以给VEH添加用户自定义的异常处理handler,如下:
AddVectoredExceptionHandler(1, PvectoredExceptionHandler)
函数有两个参数:第一个参数如果不是0,那么自定义的handler最先执行;如果是0,那么自定义的handler最后执行。这里我们当然希望自己的handler最先执行了,所以设置成1;另一个参数就是自定义的handler了,这个函数的原型:
LONG PvectoredExceptionHandler(
_EXCEPTION_POINTERS *ExceptionInfo
)
{...}
继续追踪这个函数的参数,如下:
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
这里终于得到了我们想要的PCONTEXT;这个PCONTEXT只有在程序出异常时windwos才会在VEH暴露出来,开发人员才能进一步修改DR寄存器地值,所以这里要先人为产生软件异常(比如设置0xCC),让后由我们自定义的handler接管,得到PCONTEXT后就能愉快的修改DR寄存器组了;
由于各种原因,完整的代码就不展示了,这里展示核心的片段:
-
这里先自定义一个函数,然后给函数的开始地址设置0xCC,当CPU执行到这里时,产生异常,被我们自定的PvectoredExceptionHandler接管,同时暴露了PCONTEXT,这时再在openDbg函数设置硬件断点和开启硬件调试功能!
-
硬件断点只有4个,都存放在数组中,每个元素又封装了一层DbgPoint;
LONG _stdcall PvectoredExceptionHandler(PEXCEPTION_POINTERS val)
{
//CString wTxt;
//wTxt.Format(L"%X", val->ExceptionRecord->ExceptionCode);
//AfxMessageBox(wTxt);
unsigned _eip = val->ContextRecord->Eip;
if (val->ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT)//0x80000003是int 3
{
/*根据hook的地址,在链表中查找回调函数和返回地址*/
PHOOKPOINT point = htdHook2Ptr->Points.FindPoint((LPVOID)_eip);
if (point)
{
/*注意这里我们自定义回调函数的调用方法:
1、先执行point->GetHookBack2(),得到得到addPoint时设置的回调函数地址(赋值给了成员变量DestCall);
2、再执行DestCall(val->ContextRecord),这才是真正执行我们回调函数的地方
*/
if (point->GetHookBack2()(val->ContextRecord))//如果回调函数返回true,修复代码;
{
val->ContextRecord->Eip = (unsigned)point->CodeFix;
}
else
{
val->ContextRecord->Eip = (unsigned)point->AddressRet;//回调函数返回false,跳转到我们人为指定的地方
}
/*这个异常我已经搞定,源程序可以继续执行了*/
return EXCEPTION_CONTINUE_EXECUTION;
}
/*hook链表中没找到这个eip,说明不是我们自己的hook点,继续search异常的接管代码*/
else return EXCEPTION_CONTINUE_SEARCH;
}
if (val->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)//0x80000004是cpu异常
{
//
//AfxMessageBox(L"1");
auto point=htdHook2Ptr->DbgPoints.FindPoint((LPVOID)_eip);//看看当前地址是不是我们事先设置好的硬件断点
if (point)//hook的点是存在的,说明就是我们事先设置好的硬件断点,先执行我们的回调函数
{
//AfxMessageBox(L"2");
if (point->DestCall(val->ContextRecord))//这里就直接显式用回调函数了。回调函数返回true,需要继续回到hook下一行单步
{
//AfxMessageBox(L"3");
val->ContextRecord->Dr7 = 0; //这里取消所有的硬件断点,不仅仅是当前的
/*TF位设置成1,cpu进入单步调试模式;执行下一行指令时,同样会触发STATUS_SINGLE_STEP异常,会继续进入现在的这个if条件;
上面刚取消所有硬件断点,如果这里不设置单步模式,后续的硬件断点都会失效*/
val->ContextRecord->EFlags |= 0x100;
}
else //回调函数返回false,eip采用执行的地址(setHook的时候传入的),没必要再单步了,这时候可以告诉CPU,我已经把异常处理掉了,你可以继续!
{
return EXCEPTION_CONTINUE_EXECUTION;//
}
}
else//hook点不存在,大概率是上面设置了TF=1单步调试,但这些地址并不是我们设置的断点,所以不需要执行回调,直接继续设置硬件断点后继续执行
{
htdHook2Ptr->DbgPoints.OpenDbg(val->ContextRecord);//给当前的DR0-DR3分别设置4个point的断点(不一定是当前地址,而是我们setHook时指定的),执行到任何一个都能断下来
}
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;//既不是单步,也不是0xCC,说明异常和我们没关系,丢给OS继续处理
}
void ThreadTrap()
{
_asm //防止编译器把函数优化掉
{
mov eax,eax
mov eax,eax
mov eax,eax
}
}
bool InThread(HOOKREFS2)
{
htdHook2Ptr->UnHook((LPVOID)_EIP);
htdHook2Ptr->DbgPoints.OpenDbg(val);//启用硬件调试
return false;
}
/*设置线程劫持环境*/
void htdHook2::Init()
{
/*
1、对ThreadTrap函数设置0xCC,让其产生异常,然后被我们接管,从而得到context
2、InThread是回调函数,return是false,让eip继续指向ThreadTrap,这不成死循环了么?
3、所以回调函数InThread要恢复被挂钩的地方
4、这里设置0xCC的是自己的函数,不是目标进程函数,所以CRC32检测是无效的;
5、核心目的是进入InThread设置硬件断点(直接调用OpenDbg函数是不行的,必须人为制造异常后才行,因为需要PCONTEXT)
*/
SetHook(ThreadTrap, 1, InThread, ThreadTrap);
/*注意函数的调用方式:
1、让ThreadTrap执行,触发我们事先设置好的异常
2、如果直接ThreadTrap(),可能会被编译器优化成内联函数,也就是3行mov eax,eax直接放入Init函数,就不走函数调用了,避免push压栈的操作,效率更高
3、所以这里用汇编call显式调用
*/
DWORD dRet=(DWORD)ThreadTrap;
_asm call dRet;
}
/*
1、注册异常的接管函数。这个是构造函数,生成对象时自动调用了
*/
htdHook2::htdHook2()
{
htdHook2Ptr = this;
PPointLast = &Points;
AddVectoredExceptionHandler(1, PvectoredExceptionHandler);//完成了异常的接管
}
/*这里只破坏一个字节,就算被用pchunter类的ark工具检测到挂钩点,由于没有call地址,也不好被handler函数*/
void htdHook2::SetHook(LPVOID Address, uchar len, HOOKBACK2 hookBack,LPVOID AddressRet)
{
DWORD dOld;
DWORD dNew;
VirtualProtect(Address, 0x1, PAGE_EXECUTE_READWRITE, &dOld); //修改HookFactroy内存属性为可以执行
PPointLast = PPointLast->AddPonit(Address, AddressRet, hookBack, len);
char* code = (char*)(Address);
code[0] = 0xCC;
VirtualProtect(Address, 0x1, dOld, &dNew);
}
bool htdHook2::SetHook(LPVOID Address, HOOKBACK2 hookBack, LPVOID AddressRet)
{
return DbgPoints.AddHookPoint(Address, hookBack, AddressRet);
}
/*先恢复address原来的代码,再将现在的point从链表取出*/
void htdHook2::UnHook(LPVOID Address)
{
//卸载HOOK
PHOOKPOINT _point=Points.FindPoint(Address);
if (_point)
{
_point->Recover();
_point->BackPoint->NextPoint = _point->NextPoint;
if (_point->NextPoint)_point->NextPoint->BackPoint = _point->BackPoint;
delete _point;
}
}
这里设置context的各个关键寄存器的值。为了给调试寄存器设置值,需要先得到PCONTEXT
void DBGPOINT::OpenDbg(PCONTEXT _context)
{
_context->Dr0 = (DWORD)Point[0].Address;//即使address是0也不影响
_context->Dr1 = (DWORD)Point[1].Address;
_context->Dr2 = (DWORD)Point[2].Address;
_context->Dr3 = (DWORD)Point[3].Address;
_context->Dr7 = 0b01010101;//这才真正启用硬件断点
}
自定义的消息hook代码:由于并未破坏机器码,所以不需要到处跳转和修复,代码少了很多,逻辑也明晰了很多!拦截到的消息直接在DiologBox的Edit打印出来:
CString GetMsgByAddress(DWORD memAddress)
{
CString tmp;
DWORD msgLength = *(DWORD*)(memAddress + 4);//每个消息下面都有2个4byte的正数保存了这个字符串的长度
if (msgLength > 0) {
WCHAR* msg = new WCHAR[msgLength + 1]{ 0 };
wmemcpy_s(msg, msgLength + 1, (WCHAR*)(*(DWORD*)memAddress), msgLength + 1);
tmp = msg;
delete[]msg;
}
return tmp;
}
CWndMain* pCWndMain{};
bool hookMsg(HOOKREFS2) {
CTime time = CTime::GetCurrentTime();
CString strTime = time.Format(_T("%Y-%m-%d %H:%M:%S"));
DWORD** msgAddress = (DWORD**)(val->Esp);
CString wid = GetMsgByAddress(**msgAddress + 0x40);
CString fullmsg = GetMsgByAddress(**msgAddress + 0x68);
CString isWid = GetMsgByAddress(**msgAddress + 0x164);
CString md5 = GetMsgByAddress(**msgAddress + 0x178);
msg = wid + fullmsg + isWid + md5;
msg.Format(_T("\r\nwid=%s, msg=%s,isWid=%s, md5=%s, time=%s\r\n"), wid, fullmsg, isWid, md5, strTime);
pCWndMain->EDIT_SHOWMSG.SetSel(-1,-1);//FALSE表示会随光标位置改变而滚动滚动条
pCWndMain->EDIT_SHOWMSG.ReplaceSel(msg);
//pCWndMain ->EDIT_SHOWMSG.SetWindowTextW(msg);
//AfxMessageBox(msg);
return true;//如果返回false,dr7不会被置0,回调函数会不停被执行,导致卡死
}
用x32dbg打开看:原程序的机器码完好无损,完全看不出被改过!
之前调试时人为下了软件断点,这里完全看不到硬件断点!