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...)
