HOOK技术学习笔记-其三

0x00 前言

之前写了CRC Check Bypass篇,以LUA脚本的方式过检测

而本文则以C++ HOOK的方式来过掉CRC检测,本文后续的所有代码均是为了写成DLL

之前那一篇用的CE自带的gtutorial-x86_64

本文依然使用CE自带的gtutorial,不过是gtutorial-i386

0x01 Go to do it !

浅析

具体思路和原理已经写在了CRC Check Bypass篇,这里便不再重复叙述。

由于由64位(x86_64)换成了32位(i386),地址也会有所变化,依然按照之前那篇的方式去寻找,找得三个扫描地址。

对于004332C2

可以看到此处代码将[eax+ecx*2]的值赋值给eax,从而进行后续的操作。

由此可以知道两条信息:

  1. [eax+ecx*2]即为需要校验的字节,即将被存放到eax中。
  2. eax+ecx*2(注意没有加[])即为存放该字节的地址。

接下来要做的就是“移花接木”,具体细节在CRC Check Bypass篇也有写,概括就是,复制全部代码段到一段自己申请的内存,让这个CRC扫描程序去扫描复制过去的代码段,然后自己就可以随意修改真正的代码段了。

但是问题的难点,也就是本文的重点也就来了:如何用C++去实现这些?

难点一 内联汇编如何编写?

可以在一个函数中采用__asm{}代码块的方式来编写汇编代码

基本格式为

1
2
3
4
5
static __declspec(naked) void foo() {
__asm {
//........
}
}

static关键字为了规避LNK2005的报错

naked关键字告诉编译器,以下的汇编不需要编译器再优化什么指令

难点二 相关参数如何传递?

接下来就是边界比较了,那么该如何将上边界、下边界、基址、复制的代码段的基址等传递进来?

我曾想过将函数写成static __declspec(naked) void foo(int param1, int param2)这种形式,但是这种形式的函数,在HOOK的时候,也是需要传参的,这样就极大的加大了HOOK框架的编写难度。

翻阅了许多现有的代码,我发现大部分人都采用了硬编码的方式,例如要push程序基址,因为32位程序基址几乎都是0x400000,他们可以在确认目标程序的基址是0x400000以后硬编码进去,会有类似于如下的代码

1
2
3
4
5
DWORD base = 0x400000
......省略一大堆
__asm{
push base
}

基址还好,基本是个固定值,但是如果涉及到程序大小,而我还想动态获取它,那该如何操作?

由于算是第一次接触内联汇编,也是不了解具体的优先顺序,我一直担心一个问题。

如果我创建一个全局变量temp,然后利用一个函数动态地在HOOK之前就给这个全局变量temp赋上相应的值,那么我的内联汇编中类似于push temp的这个语句,到底是push了动态赋值前的temp还是动态赋值后的temp?

经过实验,答案是后者!

这也就意味着,我可以大胆的将全局变量应用到内联汇编中,尽管在编写代码的时候它们还未被真正的赋值!

而且后续的测试证明,即便是封装成一个类,然后创建一个类对象,去传递这个对象的成员变量,也是可行的!

但是也一定要记住,在HOOK之前,一定保证全局变量被动态赋值了!!

优化点一 封装成类

涉及到这么变量,不妨封装成一个类,然后在构造函数中,执行各种初始化(动态赋值)操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//CRC_Bypass.h

#define JumpCall(frm, to) (int)( ((int)to - (int)frm) - 5)

#define CRC_addr_1_offset 0x332C2
#define CRC_addr_2_offset 0x33322
#define CRC_addr_3_offset 0x33382
#define CRC_addr_1_return_offset 0x332CC
#define CRC_addr_2_return_offset 0x3332C
#define CRC_addr_3_return_offset 0x3338C

class CRC_Bypass
{
public:
CRC_Bypass() {//构造函数
GetProcessImageBase(&(this->pProcessImageBase), &(this->dwProcessImageSize));
this->dwProcessImageBase = (DWORD)(this->pProcessImageBase);
this->dwProcessImageEnd = (this->dwProcessImageBase) + (this->dwProcessImageSize);
this->dwCRC_Bypass_return_1 = this->dwProcessImageBase + CRC_addr_1_return_offset;
this->dwCRC_Bypass_return_2 = this->dwProcessImageBase + CRC_addr_2_return_offset;
this->dwCRC_Bypass_return_3 = this->dwProcessImageBase + CRC_addr_3_return_offset;
this->dwCRC_Bypass_Address_1 = this->dwProcessImageBase + CRC_addr_1_offset;
this->dwCRC_Bypass_Address_2 = this->dwProcessImageBase + CRC_addr_2_offset;
this->dwCRC_Bypass_Address_3 = this->dwProcessImageBase + CRC_addr_3_offset;
CopyMemory2New(this->pProcessImageBase, this->dwProcessImageSize,&(this->pAddrOfCopy));
this->dwAddrOfCopy = (DWORD)(this->pAddrOfCopy);
}

PVOID pProcessImageBase;//程序基址
DWORD dwProcessImageBase;//程序基址
DWORD dwProcessImageSize;//程序代码段大小
DWORD dwProcessImageEnd;//程序代码段结束位置
PVOID pAddrOfCopy;//复制的代码段的基址
DWORD dwAddrOfCopy;//复制的代码段的基址

DWORD dwCRC_Bypass_return_1;//返回位置1
DWORD dwCRC_Bypass_return_2;//返回位置2
DWORD dwCRC_Bypass_return_3;//返回位置3

DWORD dwCRC_Bypass_Address_1;//Hook位置1
DWORD dwCRC_Bypass_Address_2;//Hook位置2
DWORD dwCRC_Bypass_Address_3;//Hook位置3

BYTE* CRC_Bypass_1_Original_Bytes;//保存被HOOK位置的原始字节码
BYTE* CRC_Bypass_2_Original_Bytes;//保存被HOOK位置的原始字节码
BYTE* CRC_Bypass_3_Original_Bytes;//保存被HOOK位置的原始字节码
private:
BOOL GetProcessImageBase(PVOID* pProcessImage, DWORD* dwProcessSize);//获取程序基址和代码段大小
BOOL CopyMemory2New(PVOID pProcessImage, DWORD dwProcessSize, PVOID* pAddrOfCopy);//执行复制内存的操作
};

难点三 内联汇编函数如何设计?

对于0x4332C2处,编写得到如下代码:其中test是上述封装的类的一个对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//为了避免“naked只能应用到非成员函数的定义”的报错,后续所有这类内联汇编函数均写在CRC_Bypass.cpp中
CRC_Bypass test;

static __declspec(naked) void CRC_Bypass_1() {
__asm {
push eax
lea eax, [eax + ecx * 2]
cmp eax, [test.pProcessImageBase]
jb originalcode1
cmp eax, [test.dwProcessImageEnd]
ja originalcode1
sub eax, [test.pProcessImageBase]
add eax, [test.pAddrOfCopy]
movzx eax, word ptr[eax]
jmp exit1
originalcode1 :
movzx eax, word ptr[eax + ecx * 2]
exit1 :
xor eax, [ebp - 0x10]
mov[ebp - 0x10], eax
pop eax
push[test.dwCRC_Bypass_return_1]
ret
}
}

首先,由于会用到eax寄存器,故需要保存一下原有的值,可使用

1
push eax

但是记得最后再进行一个pop eax

浅析中我们分析得知,

  1. [eax+ecx*2]即为需要校验的字节,即将被存放到eax中。
  2. eax+ecx*2(注意没有加[])即为存放该字节的地址。

第2点可以作为突破口

我们需要得到这个地址,显然可以使用lea指令,具体如下

1
lea eax, [eax + ecx * 2]

从而使得eax中存放的是当前正在扫描的地址

接下来比较这个地址是否超过上下界,如果越界均跳转到originalcode1标签处,然后执行程序原本的代码,不再进行“移花接木”。

1
2
3
4
cmp eax, [test.pProcessImageBase]
jb originalcode1
cmp eax, [test.dwProcessImageEnd]
ja originalcode1

接下来借助如下三个关系

当前语句在原始代码段中的偏移=当前地址(还在原始代码段中)-原始代码段的基址

语句在原始代码段中的偏移=语句在新代码段(复制过去的)中的偏移

当前语句在新代码段中的地址=新代码段的基址+偏移语句在原始代码段中的偏移

可以得当前地址-原始代码段基址+新代码段基址=目标扫描地址(我们想让程序去扫的那个地址)

1
2
sub eax, [test.pProcessImageBase]
add eax, [test.pAddrOfCopy]

然后由于程序原本后续的各种操作都是利用eax寄存器

而目前eax中是地址,地址里面存的是等待进行后续操作的值

故需要将eax中的值再传递给eax寄存器

1
movzx eax, word ptr[eax]

然后需要直接跳转到exit1标签处,以避免执行originalcode1标签处的原始代码。

1
jmp exit1

至此,我们完成了“移花接木”操作。

但是并没有完全结束,我们还需要收尾,由于我采用的是HOOK技术学习笔记-其二篇的HOOK小模板,固定至少需要修改8个字节

明显能看出来,这8个字节会占用3条指令,第三条指令虽然占了但是没占满。

所以可以得出来如下结论:

  1. 我们需要在内联汇编函数中,补全被占掉的指令,其中第一条已经在前面补完了,包括两种处理情况,第一种是地址在上下界之间,则“移花接木”然后补;第二种是地址越界了,直接补(originalcode1)。
  2. 补完以后要跳转回第四条指令,关于这个跳转,有很多办法,可以jmp,也可以push 地址/ret

而且,还记得前面的push eax吗,在收尾的时候,也要进行一次pop eax

综上所述,可以有

1
2
3
4
5
6
exit1 :
xor eax, [ebp - 0x10]
mov[ebp - 0x10], eax
pop eax
push[test.dwCRC_Bypass_return_1]
ret

至此,内联汇编函数设计完毕,其他两处均可以仿照这个思路来进行设计!

难点四 如何进行HOOK?

我采用的是HOOK技术学习笔记-其二篇的HOOK小模板,能够保证HOOK的原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//CRC_Bypass.cpp
//======================================================================================

void MakeMemoryWritable(unsigned long ulAddress, unsigned long ulSize, DWORD* OldProtect)
{
MEMORY_BASIC_INFORMATION* mbi = new MEMORY_BASIC_INFORMATION;
VirtualQuery((void*)ulAddress, mbi, ulSize);
*OldProtect = mbi->Protect;
if (mbi->Protect != PAGE_EXECUTE_READWRITE)
{
unsigned long* ulProtect = new unsigned long;
VirtualProtect((void*)ulAddress, ulSize, PAGE_EXECUTE_READWRITE, ulProtect);
delete ulProtect;
}
delete mbi;
}

//======================================================================================

void RestoreMemoryProtect(unsigned long ulAddress, unsigned long ulSize, DWORD OldProtect)
{
if (OldProtect != PAGE_EXECUTE_READWRITE) {
unsigned long* temp = new unsigned long;
VirtualProtect((void*)ulAddress, ulSize, OldProtect, temp);
delete temp;
}
}

//======================================================================================
/*
由于采用InterlockedExchange64方式,固定会占8字节
从HOOK的起始位置往后数8字节,不足的进行对齐,ulNops即需要对齐多少字节
这里并没有将多出来的碎片字节nop掉,而是直接返回到存在碎片字节的指令的下一条指令
*/
BOOL Hook(unsigned long ulAddress, void* Function, BYTE** pSavedArray, unsigned long ulNops)
{
__try{
DWORD OldProtect = 0;
MakeMemoryWritable(ulAddress, 8+ ulNops, &OldProtect);
*pSavedArray = (BYTE*)malloc(8+ ulNops);
if (*pSavedArray == NULL) {
return FALSE;
}
memcpy(*pSavedArray, (void*)ulAddress, 8+ ulNops);
BYTE bReplace[8] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
bReplace[0] = 0xE9;
*(DWORD*)(&(bReplace[1])) = JumpCall(ulAddress, Function);
LONG64 llReplace;
memcpy(&llReplace, bReplace, 8);
InterlockedExchange64((LONG64 volatile*)ulAddress, llReplace);
RestoreMemoryProtect(ulAddress, 8+ ulNops, OldProtect);
return TRUE;
}
__except (EXCEPTION_EXECUTE_HANDLER) { return FALSE; }
}

//======================================================================================
BOOL CRC_Bypass_Start() {
Hook(test.dwCRC_Bypass_Address_1, CRC_Bypass_1, &test.CRC_Bypass_1_Original_Bytes, 2);
Hook(test.dwCRC_Bypass_Address_2, CRC_Bypass_2, &test.CRC_Bypass_2_Original_Bytes, 2);
Hook(test.dwCRC_Bypass_Address_3, CRC_Bypass_3, &test.CRC_Bypass_3_Original_Bytes, 2);
return TRUE;
}

BOOL CRC_Bypass_End() {
memcpy((void*)(test.dwCRC_Bypass_Address_1), test.CRC_Bypass_1_Original_Bytes, _msize(test.CRC_Bypass_1_Original_Bytes));
memcpy((void*)(test.dwCRC_Bypass_Address_2), test.CRC_Bypass_2_Original_Bytes, _msize(test.CRC_Bypass_2_Original_Bytes));
memcpy((void*)(test.dwCRC_Bypass_Address_3), test.CRC_Bypass_3_Original_Bytes, _msize(test.CRC_Bypass_3_Original_Bytes));
return TRUE;
}
1
2
3
4
5
6
//main.h
#pragma once
#include<Windows.h>
#include<stdio.h>
#include<TlHelp32.h>
#include "CRC_Bypass.h"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "main.h"

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CRC_Bypass_Start();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
//CRC_Bypass_End(); 由于本文只是为了测试CRC_Bypass的C++ DLL形式的可行性,故对此做了注释,若取消注释,DLL注入后执行完CRC_Bypass_Start()便紧跟着执行CRC_Bypass_End(),导致效果不可视。当然正式项目肯定会对这块做一个处理的。
break;
}
return TRUE;
}

0x02 参考

内联汇编关于push的一个问题

C++ Inline ASM内联汇编详解

在__asm块中使用C或C++

借鉴的一个代码

-------------至此本文结束感谢您的阅读-------------
如果觉得这篇文章对您有用,请随意打赏。 (๑•⌄•๑) 您的支持将鼓励我继续创作!