xxxx(六):吊打CRC32检测的无痕hook
1、hook方式有多种,这里做了一个系统性的总结对比,如下:
https://www.cnblogs.com/theseventhson/p/14324562.html 之前这里做了接受消息的hook,用的就是最初级的hook方式: jmp到我们自己的处理逻辑。上面也分析了,这种方式缺点非常明显;最牛逼的神级hook:VT读写分离前面已经介绍过了,今天继续介绍高级的hook方式:硬件断点;
2、现代软件开发,尤其是大型软件开发,绝对不可能一步到位,开发期间肯定存在各种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,回调函数会不停被执行,导致卡死 }
效果展示:自己在手机上用filehelper发消息,准确接收到并展示;
公众号的消息长这样:url地址清晰可见:http://mp.weixin.qq.com/s?__biz=MjM5MDgwMzc4MA==&mid=2654877578&idx=1&sn=1aae7e4dd03c8edcb97dce95bbccbb96&chksm=bd75a5a18a022cb73c63a95e2d808a1ae4e3e0ca0db6d2735595c83459f52f55b06edc9bd4e4&scene=0&xtrack=1#rd
还有诸如其他的语音、红包、图片、转发的公众号文章、发到群里的地理位置等;群消息还能带上发消息的人(isWid字段);消息的格式各种各样,啥都有,这次真的开眼界了!有个字段叫cdnthumburl,好长一串,全是数字,从名字看,貌似和CDN有关(这是个视频,应该是从最近的CDN节点下载),后续空了继续研究这些字段的来历和作用!
注意:末尾的时间是我自己添加的,xxxx原始的消息并没有这个!
Edit本身没有排版功能,遇到公众号、语音、视频等消息,看起来很乱,建议复制到notepad看,自动分割和排版,效果好很多:
用x32dbg打开看:原程序的机器码完好无损,完全看不出被改过!
之前调试时人为下了软件断点,这里完全看不到硬件断点!
参考:
1、https://bbs.pediy.com/thread-173853.htm 白话windows之四 异常处理机制(VEH、SEH、TopLevelEH...)