hook翻译过来是拦截的意思, 我们很多时候也叫钩子,其实是很形象的.
hook有什么作用呢?
1.当代码执行到某行时,获取寄存器值和内存里的值,进行调试分析,例如hook明文包.
2.当代码执行到某行时,插入想执行的代码.例如迅雷拦截发包函数.
3.当代码执行到某行时,修改寄存器,达到某些篡改目的.
好,那我们来学习第一种hook.
原理:
直接在想hook的地址修改汇编代码,改成跳转到我们自己的子程序里,执行完毕再跳转回来,继续执行.
原理看起来非常简单,但是我们要解决几个问题.
第一个问题,由于是直接修改汇编代码,破坏的代码是一定要还原的,否则会对原来的程序产生影响.
第二个问题,我们的子程序执行代码会修改寄存器,跳转回来以后会对原程序产生影响(故意篡改可以忽略),
所以寄存器也是需要保存和还原的.
第三个问题,跳转到我们的子程序,二进制值是怎么计算的.
(这里x86为例子,结尾给出x64代码例子,因为本身没多少差别)
当然位置随意选择,没有什么限制,但是如果你是一个新手,
尽量不要去选择改变堆栈的代码,因为这样很可能在你需要用到堆栈计算的时候,会给你增加一定的计算量.
同时要注意,我们选择的代码长度必须大于等于5字节.
因为我们要把他修改成jmp xxxx 或则call xxxx 这都是需要5字节的.
选择的代码可以是一条或则是多条,只要大于等于5字节就可以了.
好,这里我们随便选择一条如下:
8B93 94090000 mov edx, dword ptr [ebx+994]
不是堆栈操作代码,6字节满足大于等于5字节,这就可以了
8B93 94090000 mov edx, dword ptr [ebx+994]
把这句代码,无论是二进制 还是汇编指令都保存起来.
原因很简单,我们破坏了要还原,不能对源程序产生影响,谁污染谁治理嘛.
多余的字节修改成nop,不留垃圾字节,否则会导致垃圾字节和下面的代码形成代码混淆
我们改E8 XXXX 和E9 XXXX都可以,拿E8举例子
主要是XXXX怎么计算出来的
XXXX = 跳转到的地址 - 开始跳转的地址
但是这里面开始跳转的地址 并不等于 hook地址哦,原因非常简单,E8 XXXX没执行的时候是不知道要跳转的,
真正的跳转开始地址是执行完了5字节的E8 xxxx才开始的.
所以 开始跳转的地址 = hook地址 + 5
所以 XXXX = 跳转到的地址 - hook地址 - 5;
代码如下:
void HXSYDialog::OnBnClickedButton17()
{
DWORD Hook子程序指针 = (DWORD)明文包HOOKcall;
DWORD 跳转值=Hook子程序指针-Hook地址-5;
*(BYTE*)Hook地址= 0xE8;
*(DWORD*)(Hook地址 + 1) = 跳转值;
}
1.先了解下流程再写代码
pushad====我们说不能破坏寄存器,那么先保存,如果篡改为目的的话 就不要加保存寄存器了
做我们想做的事
POPad ====还原寄存器
还原之前破坏的代码(之前保存的就有用了)
jmp 或则 retn回去
比如我们只想加2句nop
子程序里汇编代码如下:
看一下我们的代码实现
__declspec(naked) void 明文包HOOKcall() { __asm { pushad//保存寄存器 } // 任鸟飞逆向 想做的事 加到这里 // 这里我们 不想加什么就空着了 __asm { popad//还原寄存器 mov edx, dword ptr [ebx+994]//还原破坏的代码 retn } }
这样一个简单的Hook 就完美完成了.
当然我们还要增加一个卸载的功能,不需要或则退出的时候要恢复hook
void HXSYDialog::OnBnClickedButton18() { *(BYTE*)Hook地址 = 0x8B;//8B93 94090000 mov edx, dword ptr [ebx+994] *(DWORD*)(Hook地址 + 1) = 0x99493; *(BYTE*)(Hook地址 + 5) = 0; }
下面再发一个X64的例子,用就lostark吧
和x86比较呢,主要就2个区别
第一, hook的代码长度不同
第二,X64不让直接内联汇编,我们要用.asm写,当然你也可以用inter编译器
#pragma pack(1)
typedef struct _Thook修改代码
{
WORD movabs_rax;
UINT64 jmpAddr;
WORD pushraxret;
}Thook修改代码;
#pragma pack()
void Call_Hook明文包()
{
extern QWORD 明文包Hook地址;
g_hook地址 = 明文包Hook地址;
g_gobackaddr = (QWORD)g_hook地址 + 14;
Thook修改代码 A = { 0xB848,(UINT64)&hookCall,0xC350 };// 把游戏代码修改成为跳转到 asm
memcpy_s((LPVOID)g_hook地址, 12, &A, 12);
}
void Call_还原明文包()
{
// 0 | 48:8B4F 40 | mov rcx, qword ptr ds : [rdi + 40] |
// 0 | 48 : 8B01 | mov rax, qword ptr ds : [rcx] | rcx : "^$"
// 0 | 4C : 8BCA | mov r9, rdx |
// 0 | 45 : 0FB7C0 | movzx r8d, r8w
BYTE old_CallCode[14] = { 0x48,0x8B,0x4F,0x40,0x48,0x8B,0x01,0x4C,0x8B,0xCA,0x45,0x0F,0xB7,0xC0 };//====可能改变
DWORD nSize = sizeof(old_CallCode);
memcpy_s((LPVOID)g_hook地址, nSize, &old_CallCode, nSize);
}
这里面为了让大家明白,只要能跳转过去都可以用了 push + ret的方式 和jmp 以及call其实是没有区别的
操作一下看看,是不是一样呢?
具体的.asm 文件汇编的编写方式,因为也是比较长的一段, 不会的同学,请跳到任鸟飞逆向的X64汇编学习章节
有的程序用上面的方法直接hook成功了,
但是有的程序我们发现hook以后没有反应,HOOK的位置代码没有被修改.
如果是我们HOOK代码错误的原因,应该是被修改了, 然后程序崩溃,这样一个顺序才符合逻辑.
但是现在是没有修改成功,那么说明应该是写内存出的错误。
我们来谈谈 VirtualProtect 这个函数 .
先来了解内存属性,内存属性包括Read、Write、Execute的组合,即可读、可写、可执行。
由于很多时候我们需要hook,而代码段很可能页面属性是不可写,所以直接hook 是失败的.
简单点说就是hook地址的属性是不允许写入.那当然成功不了.
那么我们只要用VirtualProtect 函数修改页面属性为PAGE_EXECUTE_READWRITE就可以了.
代码如下:
void HXSYDialog::OnBnClickedButton17()
{
DWORD Hook子程序指针 = (DWORD)明文包HOOKcall;
DWORD 跳转值=Hook子程序指针-Hook地址-5;
DWORD old=0;
VirtualProtect((PVOID)Hook地址,100,PAGE_EXECUTE_READWRITE,&old);
//任鸟飞逆向:
*(BYTE*)Hook地址= 0xE8;
*(DWORD*)(Hook地址 + 1) = 跳转值;
VirtualProtect((PVOID)Hook地址,100,old,&old);
}
void HXSYDialog::OnBnClickedButton18()
{
DWORD old=0;
VirtualProtect((PVOID)Hook地址,100,PAGE_EXECUTE_READWRITE,&old);
*(BYTE*)Hook地址 = 0x8B;//8B93 94090000 mov edx, dword ptr [ebx+994]
*(DWORD*)(Hook地址 + 1) = 0x99493;
*(BYTE*)(Hook地址 + 5) = 0;
VirtualProtect((PVOID)Hook地址,100,old,&old);
}
这样就可以正常hook 和Unhook了
一般软件或则游戏中,代码段的页面属性为不可写的时候,
我们可以调用VirtualProtect 或则 VirtualProtectEx 修改页面属性,就可以修改和hook代码段了.
但是有的时候我们发现VirtualProtect函数怎么调用都一直失败.
查看参数也没有错误,那么就有一种可能,该函数被Hook被检测了.
所以我们要,绕过VirtualProtect & VirtualProtectEx 检测 修改代码段.
首先先来了解下该函数到内核的执行流程
kernel32.VirtualProtect -> kernelbase.VirtualProtect -> ntdll.NtProtectVirtualMemory ->进内核
我们来观察下X86 和X64 中的代码流程,如下:
X86(KDXY为例)
kernel32.VirtualProtect
759504C0 > 8BFF mov edi, edi
759504C2 55 push ebp
759504C3 8BEC mov ebp, esp
759504C5 5D pop ebp
759504C6 - FF25 90139B75 jmp dword ptr [<&api-ms-win-core-memory-l1-1-0.VirtualProtect>] ; KernelBa.VirtualProtect
调用
kernelBase.VirtualProtect
76514DB0 > 8BFF mov edi, edi
76514DB2 55 push ebp
76514DB3 8BEC mov ebp, esp
76514DB5 51 push ecx
76514DB6 51 push ecx
76514DB7 8B45 0C mov eax, dword ptr [ebp+C]
76514DBA 56 push esi
76514DBB FF75 14 push dword ptr [ebp+14]
76514DBE 8945 FC mov dword ptr [ebp-4], eax
76514DC1 FF75 10 push dword ptr [ebp+10]
76514DC4 8B45 08 mov eax, dword ptr [ebp+8]
76514DC7 8945 F8 mov dword ptr [ebp-8], eax
76514DCA 8D45 FC lea eax, dword ptr [ebp-4]
76514DCD 50 push eax
76514DCE 8D45 F8 lea eax, dword ptr [ebp-8]
76514DD1 50 push eax
76514DD2 6A FF push -1
76514DD4 FF15 48175D76 call dword ptr [<&ntdll.NtProtectVirtualMemory>] ; ntdll.ZwProtectVirtualMemory
76514DDA 8BF0 mov esi, eax
76514DDC 85F6 test esi, esi
76514DDE 0F88 24F60200 js 76544408
76514DE4 33C0 xor eax, eax
76514DE6 40 inc eax
76514DE7 5E pop esi
76514DE8 C9 leave
76514DE9 C2 1000 retn 10
调用
ntdll.NtProtectVirtualMemory
77912ED0 > B8 50000000 mov eax, 50
77912ED5 BA B0899277 mov edx, 779289B0
77912EDA FFD2 call edx
77912EDC C2 1400 retn 14
77912EDF 90 nop
然后就进内核了
以上步骤换一个X86游戏或则软件也是一样的.
X64(TD为例)
kernel32.VirtualProtect
00007FFAA741BC7 | 48:FF25 D15B0600 | jmp qword ptr ds:[<&VirtualProtect>] |
kernelBase.VirtualProtect
00007FFAA5D84DA | 48:8BC4 | mov rax,rsp |
00007FFAA5D84DA | 48:8958 18 | mov qword ptr ds:[rax+0x18],rbx |
00007FFAA5D84DA | 55 | push rbp |
00007FFAA5D84DA | 56 | push rsi |
00007FFAA5D84DA | 57 | push rdi |
00007FFAA5D84DA | 48:83EC 30 | sub rsp,0x30 |
00007FFAA5D84DA | 49:8BF1 | mov rsi,r9 |
00007FFAA5D84DB | 4C:8948 D8 | mov qword ptr ds:[rax-0x28],r9 |
00007FFAA5D84DB | 45:8BC8 | mov r9d,r8d |
00007FFAA5D84DB | 48:8950 08 | mov qword ptr ds:[rax+0x8],rdx |
00007FFAA5D84DB | 41:8BE8 | mov ebp,r8d |
00007FFAA5D84DB | 48:8948 10 | mov qword ptr ds:[rax+0x10],rcx |
00007FFAA5D84DC | 4C:8D40 08 | lea r8,qword ptr ds:[rax+0x8] |
00007FFAA5D84DC | 48:83C9 FF | or rcx,0xFFFFFFFFFFFFFFFF |
00007FFAA5D84DC | 48:8D50 10 | lea rdx,qword ptr ds:[rax+0x10] |
00007FFAA5D84DC | 48:FF15 72EE1500 | call qword ptr ds:[<&NtProtectVirtualMemory> |
00007FFAA5D84DD | 0F1F4400 00 | nop dword ptr ds:[rax+rax],eax |
00007FFAA5D84DD | 33DB | xor ebx,ebx |
00007FFAA5D84DD | 8BF8 | mov edi,eax |
00007FFAA5D84DD | 85C0 | test eax,eax |
00007FFAA5D84DE | 0F88 81FD0400 | js kernelbase.7FFAA5DD4B68 |
00007FFAA5D84DE | BB 01000000 | mov ebx,0x1 |
00007FFAA5D84DE | 8BC3 | mov eax,ebx |
00007FFAA5D84DE | 48:8B5C24 60 | mov rbx,qword ptr ss:[rsp+0x60] |
00007FFAA5D84DF | 48:83C4 30 | add rsp,0x30 |
00007FFAA5D84DF | 5F | pop rdi |
00007FFAA5D84DF | 5E | pop rsi |
00007FFAA5D84DF | 5D | pop rbp |
00007FFAA5D84DF | C3 | ret |
ntdll.NtProtectVirtualMemory
00007FFAA862D93 | 4C:8BD1 | mov r10,rcx |
00007FFAA862D93 | B8 50000000 | mov eax,0x50 | 50:'P'
00007FFAA862D93 | F60425 0803FE7F 01 | test byte ptr ds:[0x7FFE0308],0x1 |
00007FFAA862D94 | 75 03 | jne ntdll.7FFAA862D945 |
00007FFAA862D94 | 0F05 | syscall |
00007FFAA862D94 | C3 | ret |
00007FFAA862D94 | CD 2E | int 0x2E |
00007FFAA862D94 | C3 | ret |
00007FFAA862D94 | 0F1F8400 00000000 | nop dword ptr ds:[rax+rax],eax |
以上都是X86 和X64 正常情况下的代码
我们到天堂W 里看一眼
发现如下:
kernel32.VirtualProtect
00007FFAA741BC7 | 48:FF25 D15B0600 | jmp qword ptr ds:[<&VirtualProtect>] |
kernelBase.VirtualProtect
00007FFAA5D84DA | E9 69A2F2FF | jmp 0x7FFAA5CAF00E |====破坏了前7字节
00007FFAA5D84DA | 58 | pop rax |
00007FFAA5D84DA | 1855 56 | sbb byte ptr ss:[rbp+0x56],dl |
00007FFAA5D84DA | 57 | push rdi |
00007FFAA5D84DA | 48:83EC 30 | sub rsp,0x30 |
00007FFAA5D84DA | 49:8BF1 | mov rsi,r9 |
00007FFAA5D84DB | 4C:8948 D8 | mov qword ptr ds:[rax-0x28],r9 |
00007FFAA5D84DB | 45:8BC8 | mov r9d,r8d |
00007FFAA5D84DB | 48:8950 08 | mov qword ptr ds:[rax+0x8],rdx |
00007FFAA5D84DB | 41:8BE8 | mov ebp,r8d |
00007FFAA5D84DB | 48:8948 10 | mov qword ptr ds:[rax+0x10],rcx |
00007FFAA5D84DC | 4C:8D40 08 | lea r8,qword ptr ds:[rax+0x8] |
00007FFAA5D84DC | 48:83C9 FF | or rcx,0xFFFFFFFFFFFFFFFF |
00007FFAA5D84DC | 48:8D50 10 | lea rdx,qword ptr ds:[rax+0x10] |
00007FFAA5D84DC | 48:FF15 72EE1500 | call qword ptr ds:[<&NtProtectVirtualMemory> |
00007FFAA5D84DD | 0F1F4400 00 | nop dword ptr ds:[rax+rax],eax |
00007FFAA5D84DD | 33DB | xor ebx,ebx |
00007FFAA5D84DD | 8BF8 | mov edi,eax |
00007FFAA5D84DD | 85C0 | test eax,eax |
00007FFAA5D84DE | 0F88 81FD0400 | js kernelbase.7FFAA5DD4B68 |
00007FFAA5D84DE | BB 01000000 | mov ebx,0x1 |
00007FFAA5D84DE | 8BC3 | mov eax,ebx |
00007FFAA5D84DE | 48:8B5C24 60 | mov rbx,qword ptr ss:[rsp+0x60] |
00007FFAA5D84DF | 48:83C4 30 | add rsp,0x30 |
00007FFAA5D84DF | 5F | pop rdi |
00007FFAA5D84DF | 5E | pop rsi |
00007FFAA5D84DF | 5D | pop rbp |
00007FFAA5D84DF | C3 | ret |
ntdll.NtProtectVirtualMemory
00007FFAA862D93 | E9 1F17F6FF | jmp 0x7FFAA858F054 | 头部====破坏了8字节
00007FFAA862D93 | 0000 | add byte ptr ds:[rax],al |
00007FFAA862D93 | 00F6 | add dh,dh |
00007FFAA862D93 | 04 25 | add al,0x25 |
00007FFAA862D93 | 0803 | or byte ptr ds:[rbx],al |
00007FFAA862D93 | FE | ??? |
00007FFAA862D93 | 7F 01 | jg ntdll.7FFAA862D941 |
00007FFAA862D94 | 75 03 | jne ntdll.7FFAA862D945 |
00007FFAA862D94 | 0F05 | syscall |====进内核
00007FFAA862D94 | C3 | ret |
00007FFAA862D94 | CD 2E | int 0x2E |
int 2E中断处理程序把EAX里的值作为查找表中的索引,去找到最终的目标函数。这个表就是系统服务表SST.
00007FFAA862D94 | C3 | ret |
00007FFAA862D94 | 0F1F8400 00000000 | nop dword ptr ds:[rax+rax],eax |
int 2E中断处理程序把EAX里的值作为查找表中的索引,去找到最终的目标函数。这个表就是系统服务表SST.
00007FFAA862D94 | C3 | ret |
00007FFAA862D94 | 0F1F8400 00000000 | nop dword ptr ds:[rax+rax],eax |
我们发现了 函数头部都有hook
这种执行VirtualProtecrt就是必然失败的
因为天堂W的 kernelBase.VirtualProtect 和 ntdll.NtProtectVirtualMemory 头部被hook了
那么怎么办呢?
还原hook,修改回正常函数头
但是有的游戏可能需要过掉游戏的重复hook,什么是重复hook呢?
就是你修改回来,他还会改回去
甚至可能还有CRC效验,这就相当于处理了一个麻烦,又增加了一个麻烦,不是很推荐.
因为会被重复写入, 我们对函数头部下访问断点,抓他重复写入或则说crc的代码
断到访问的位置( 和抓CRC 访问一个道理)
00000000458AB886 | BA 08000000 | mov edx,0x8 |
00000000458AB88B | 48:8B0CC1 | mov rcx,qword ptr ds:[rcx+rax*8] |
00000000458AB88F | FF15 3BDA0200 | call qword ptr ds:[<&IsBadReadPtr>] |
第一次访问 判断指针是否可读
00000000458AB895 | 85C0 | test eax,eax |
00000000458AB897 | 0F85 F7050000 | jne npggnt64.458ABE94 |
00000000458AB89D | 48:63BC24 4413000 | movsxd rdi,dword ptr ss:[rsp+0x1344] |
00000000458AB8A5 | 48:8D15 A4FB0300 | lea rdx,qword ptr ds:[0x458EB450] |
00000000458AB8AC | 48:638C24 4413000 | movsxd rcx,dword ptr ss:[rsp+0x1344] |
00000000458AB8B4 | 48:8D05 25FA0300 | lea rax,qword ptr ds:[<&CreateProcessInternalW>] |
00000000458AB8BB | 48:8B04C8 | mov rax,qword ptr ds:[rax+rcx*8] |
00000000458AB8BF | 48:8B00 | mov rax,qword ptr ds:[rax] |
第二次访问 rax == hook的5字节
00000000458AB8C2 | 48:3904FA | cmp qword ptr ds:[rdx+rdi*8],rax |
和 hook 表里进行比较
00000000458AB8C6 | 0F84 C8050000 | je npggnt64.458ABE94 |
第一种过检测方法 改hook表里的值
00000000458AB8CC | 48:C78424 4813000 | mov qword ptr ss:[rsp+0x1348],0x0 |
第二种 改跳转je 直接改成jmp
00000000458AB8D8 | 48:638C24 4413000 | movsxd rcx,dword ptr ss:[rsp+0x1344] |
00000000458AB8E0 | 48:8D05 F9F90300 | lea rax,qword ptr ds:[<&CreateProcessInternalW>] |
00000000458AB8E7 | 48:8B04C8 | mov rax,qword ptr ds:[rax+rcx*8] |
断到代码以后,我们分析出了他的效验
无论是直接修改效验跳转还是修改hook表都是可以达到过检测的效果的.
虽然我不推荐这样过,但是不耽误你们掌握这个知识哦.
重写ntdll.NtProtectVirtualMemory 函数
咱们直接自己重写一份.不用他的就完事了, 不用他的就相当于绕过他的检测,得到一些启示吗?启示就是用到的函数都可以自己重写一份,对吗?
1. 进入内核函数 原封不动照抄过来就可以,ntdll.NtProtectVirtualMemory 5个参数.
X86
DWORD g_dwSsdt = (DWORD)GetModuleHandleA("ntdll.dll") + 0x84FC0;
DWORD g_addr = (DWORD)GetModuleHandleA("任鸟飞逆向.dll") + 0x2F162;//hook 地址
__declspec(naked) void NewNtProtectVirtualMemory(DWORD a,PVOID BaseAddress, ULONG ProtectSize, ULONG NewProtect, PULONG OldProtect)
{
__asm {
mov eax,0x50
mov edx,g_dwSsdt
call edx
ret 0x14
}
}
X64
extern "C" DWORD newVirtualProtect( QWORD a, QWORD** BaseAddress, QWORD* ProtectSize, QWORD NewProtect, DWORD* OldProtect);
newVirtualProtect proc
mov r10,rcx
mov eax,50h
syscall
ret
newVirtualProtect endp
2.直接调用即可
X86
void CTESTDIALOG::OnBnClickedButton1()
{
通用_输出调试信息2("任鸟飞逆向:内核地址:%X\r\n", g_dwSsdt);
通用_输出调试信息2("任鸟飞逆向:修改地址:%X\r\n", g_addr);
DWORD old = 0;
DWORD dwRet = 0;
PDWORD p4 = &old;//页面原属性
ULONG p3 = PAGE_WRITECOPY;
PVOID p11 = (PVOID)g_addr;
PVOID* p1 = &p11;
DWORD p22 = 0x20;
PULONG p2 = &p22;
__asm {
push p4
push p3
push p2
push p1
push 0xFFFFFFFF
mov eax, NewNtProtectVirtualMemory
call eax
mov dwRet,eax
}
通用_输出调试信息2("任鸟飞逆向:页面属性修改返回:%X\r\n", dwRet);
*(BYTE*)g_addr = 0xEB;
p4 = &old;
p3 = old;
p11 = (PVOID)g_addr;
p1 = &p11;
p22 = 0x20;
p2 = &p22;
__asm {
push p4
push p3
push p2
push p1
push 0xFFFFFFFF
mov eax, NewNtProtectVirtualMemory
call eax
mov dwRet, eax
}
}
X64
void CTestDialog::OnBnClickedButton4()
{
__try
{
QWORD addr = 0x00007FF7870024F7;
DWORD old = 0;
DWORD a = VirtualProtect((PVOID)addr, 100, PAGE_EXECUTE_READWRITE, &old);
通用_输出调试信息("修改页面属性结果: %d", a);
*(BYTE*)addr = 0xCC;
a = VirtualProtect((PVOID)addr, 100, old, &old);
通用_输出调试信息("修改页面属性结果: %d", a);
}
__except (1)
{
通用_输出调试信息("VirtualProtect异常");
}
}
void CTestDialog::OnBnClickedButton5()
{
__try
{
QWORD* addr = (QWORD*)0x00007FF7870024F7;
DWORD old = 0;
QWORD* p11 = addr;
QWORD** p1 = &p11;
QWORD p22 = 0x100;//这地方写DWORD 看看会有什么样的错误和坑,学习调试
QWORD* p2 = &p22;
DWORD a = newVirtualProtect( 0xFFFFFFFFFFFFFFFF,p1, p2, PAGE_EXECUTE_READWRITE, &old);
通用_输出调试信息("修改页面属性结果: %d", a);
*(BYTE*)addr = 0xCC;
a = newVirtualProtect(0xFFFFFFFFFFFFFFFF,p1, p2, old, &old);
通用_输出调试信息("修改页面属性结果: %d", a);
}
__except (1)
{
通用_输出调试信息("newVirtualProtect异常");
}
}
这样就可以了
上面第一种hook,大家可能发现对抗好多啊,有没有对抗少一点的hook方式,那就来第二种吧
硬断HOOK也有人叫无痕HOOK是利用VEH异常+硬件断点实现的.
所以我们要先了解异常机制 和 硬件断点,就很容易了.
异常分两种,第一种软件模拟产生的异常
在C++等一些高级语言中,在程序需要的时候可以主动抛出异常,这种高级语言抛出的异常就是模拟产生的异常,并不是真正的异常。
例如代码: throw 1;
第二种,CPU 发现的异常, 这是真正意义的异常,是CPU发现的.
流程是, CPU发现异常,然后记录异常信息,异常类型,异常发生的位置等等
把异常信息存放在异常信息结构体里
然后通过KiDispatchException函数分发异常,寻找异常处理函数,进行异常处理
例如以下代码,执行的时候,CPU指令发现异常, 查IDT 表 , 根据下表知道异常值0xC0000094L 除0异常
执行中断处理函数,执行 0号异常
int 任鸟飞 = 100;
int x = 0;
int y =任鸟飞/x;
CPU发现异常
VEH先处理,处理不了给SEH,再处理不了给UEH,任何一个位置处理完毕传递给VCH
VEH是是第一个处理异常的函数.
通过以下函数我们可以设置VEH回调函数,也就是说我们接管VEH异常(这是后面代码需要用到的)
AddVectoredExceptionHandler(TRUE, VectorExceptionHandler);//参数1调用的顺序,参数2回调
回调函数的两个返回值:
EXCEPTION_CONTINUE_EXECUTION(-1) :继续执行
EXCEPTION_CONTINUE_SEARCH(0) :继续搜索
当VEH没有处理了异常,就会给SEH处理
我们常用的__try __excpet 就是SEH异常处理
__try{} 括号里的是保护代码体
__excpet(1) 小括号里的是过滤表达式
__except()
//EXCEPTION_EXECUTE_HANDLER 执行except中的代码,不再回去执行了 1
//EXCEPTION_CONTINUE_SEARCH 我处理不了,别人处理 0
//EXCEPTION_CONTINUE_EXECUTION 我已经处理完了,可以继续执行了 -1
__except(){} 里面的是异常处理块
未处理异常的最终处理,这个函数只能有一个,被保存在全局变量中
,是整个异常处理中的最后一环
LONG WINAPI TopLevelHandle(_EXCEPTION_POINTERS* ExceptionInfo)
{
//奔溃前的提醒工作
MessageBox(0, L"程序即将崩溃", L"提示", 0);
return EXCEPTION_CONTINUE_SEARCH;
}
// 通过一个函数可以直接的安装 UEH
SetUnhandledExceptionFilter(TopLevelExceptionFilter);
只会在异常被处理的情况下最后调用,VEH 不会对异常进行处理
LONG WINAPI VectoredContinueHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
// VEH 不会对异常进行处理,调用的时机和异常处理的情况有关
printf("VCH: ExceptionCode: %X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
returnEXCEPTION_CONTINUE_SEARCH;
}
了解了异常,的全部流程 我们来看怎么下硬件断点
调试寄存器DRX
硬件断点不依赖被调试程序,而是依赖于CPU中的调试寄存器。
调试寄存器有8个,分别为Dr0到Dr7,可以直接被读写操作
调试寄存器有什么作用:
1.设置发生断点的地址
2.设置断点的长度(1,2,4个字节,但是执行断点只能是1)
3.设置在调试异常产生的地址执行的操作
4.设置断点是否可用
5.在调试异常产生时,调试条件是否是可用
DR0,DR1,DR2,DR3 4个地址是用来设置断点地址的,所以用户最多能够设置4个硬件断点.
Dr4和Dr5是保留的。
DR7是调试控制寄存器
0-7 位设置断点
16-17,20-21,24-25,28-29位 断点类型 0执行断点 1 写入断点 10读写断点
18-19,22-23,26-27,30-31位 断点长度 0 1字节 1 2字节 10 8字节 11 4字节
具体想了解可以设置尝试效果
设置硬件断点实现hook的流程
1.找到我们想hook的线程(一条或则是多条)
2.得到线程上下文(通俗点就是线程中寄存器,调试器寄存器,指针,堆栈指针等各种信息)
每个线程上下文是独立的
3.修改DR0-DR7 然后设置线程上下文
这样程序执行到我们下断点的位置就会抛异常了
4.当然抛异常,我们要异常处理函数去处理,也就是我们上面说的VEH
所以我们要设置一个异常处理函数,接收异常并且处理异常
5.这样全部完成以后,CPU执行指令,执行到我们下断的地方,
发现指令地址和DR0-DR3中的某一个地址相同,就回抛异常,异常到我们的异常处理函数
我们就相当于hook到信息了,然后做想做的事,之后再把EIP改回去正常执行代码即可
根据以上步骤我们来编写代码
首先写 一个无痕hook的类
头文件
cpp
1.设置线程上下文之前要先暂停线程,设置完毕再恢复线程
否则可能访问冲突
2.修改drx之前 一定要GetThreadContext,虽然有的游戏不需要,毕竟我们需要通用
3.要筛选线程,并不是所有线程都是我们需要hook的
我们拿天堂W举例子
找个位置hook 明文包
明文包位置如下:
我们hook的代码
mov rcx,rax
当执行到这里断下的时候本条代码并没有被执行
而是抛出异常到回调函数
所以异常处理 只需要2句代码
第一句 还原mov rcx,rax
第二句 修改RIP 指向下一条代码
有同学说 , 不用还原代码 不修改RIP 让他直接执行不行吗?
答案是不行的,这里已经有异常了,如果不修改RIP 就变成死循环了
好我们直接创建一个无痕_hook类直接调用安装
1.需要几个断点,写几个地址
2.DR7需要一个断点写1 二个写5 三个写0x15 四个写0x55 当然也可以一直写0x55
回调函数
1.处理完毕的异常 要返回去继续运行, 没有处理的异常继续搜索 注意返回值不一样
2.还原被hook的代码 然后RIP跳到下一条
3.有的同学说 调试输出信息会产生错误, 那是因为这个函数里面我们用了OutputDebugString,他也是抛异常的方式输出的,所以放到异常处理函数中 会产生递归,解决方法非常简单, 我们把自己DLL的线程排除在hook之外即可,你hook自己的界面线程干嘛 - -
代码完成我们直接到游戏中进行走路,hook成功,抓到封包了.
这种hook其实和第一种hook效果上没有什么区别.
唯一优点是,只用hook 1字节就可以了,不用选位置.
原理很简单,我们把要hook的位置,第一个字节改成CC,代码执行到这里,抛异常,我们在异常处理函数中做处理,还原代码,再跳转到一条代码执行即可.
对应的异常是
异常信息->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT//断点异常
这里面简单示例,直接hook send头部,上面的CC不是hook的哦,注意看48 是头部第一个字节
hook代码
hook以后的效果,第一个字节从48改成了CC
我们来看回调函数,和之前是一样的,就是修改下异常类型即可
这里面注意的是rsp 不用计算,因为他是保存和还原异常时的信息
本条代码是5字节 rip+5即可.
这样就hook成功了,抓到了 封包的信息
网上很多所谓的抓包工具 ,就只是hook send头部而已,没有任何作用
因为都是加密封包,我们是要分析到明文封包,再进行hook才是有意义的
内存断点的原理:内存断点的实现方式是将你欲下断地址的内存页属性改为PAGE_NOACCESS,这个属性会把当前内存页设为禁止任何形式的访问,如果进行访问会触发一个内存访问异常。
然后我们异常处理函数判断触发这个异常的位置是否跟你下断的地址相同
相同进行处理即可.
同时也可以达到hook的效果.
虽然内存断点的效率经常很不理想,但是因为仅仅是修改了一个内存属性,所以内存断点可以下数量非常多、单断点范围非常大。这是它的优势。
简单看下代码
PVOID 内存异常指针 = 0;
LONG NTAPI 内存异常处理(struct _EXCEPTION_POINTERS* 异常信息)
{
if (异常信息->ExceptionRecord->ExceptionCode == 0xC0000005)//内存断点异常
{
if ((UINT64)异常信息->ExceptionRecord->ExceptionAddress == (QWORD)send)//我们的异常
{
QWORD rcx = 异常信息->ContextRecord->Rcx;
QWORD rdx = 异常信息->ContextRecord->Rdx;
QWORD r8 = 异常信息->ContextRecord->R8;
QWORD r9 = 异常信息->ContextRecord->R9;
通用_输出调试信息("rcx:%llX rdx:%llX r8:%llX r9:%llX", rcx, rdx, r8, r9);
//还原处理
*(QWORD*)(异常信息->ContextRecord->Rsp + 8) = 异常信息->ContextRecord->Rbx;//mov qword ptr ss:[rsp+0x8],rbx |
异常信息->ContextRecord->Rip += 5;
DWORD old = 0;
VirtualProtect((PVOID)send, 2, PAGE_EXECUTE_READWRITE, &old);
return EXCEPTION_CONTINUE_EXECUTION;//返回去运行
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
void CTestDialog::OnBnClickedButton29()
{
内存异常指针 = AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)内存异常处理);
DWORD old = 0;
VirtualProtect((PVOID)send, 2, PAGE_NOACCESS, &old);
}
void CTestDialog::OnBnClickedButton30()
{
DWORD old = 0;
VirtualProtect((PVOID)send, 2, PAGE_EXECUTE_READWRITE, &old);
//任鸟飞逆向
RemoveVectoredExceptionHandler(内存异常指针);
}
好了,本期内容就到这里了
码字不易, 估计2万多字了吧,有点赞的朋友,现在这里谢过了.
如果有什么不懂可以找我探讨,同时视频里会有更多的实战和操作,相信可以让他彻底掌握所有种类的hook 和对抗,有兴趣可以到公众:任鸟飞逆向 进行交流学习。