Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

基于hvpp框架实现多EPT hooks

hvpp项目中给出了使用EPT实现无痕hook的示例, 但是编写的VT驱动实际上是特化处理了, 假定只需要实现单一EPT hook并且只有让Guest到虚假页的需求的话可以使用项目给出的VT驱动中定义的数据结构, 它只储存一个待hook的执行页和一个读写时映射到的虚假页, 但是实际应用时不可能只有这些需求, 所以这里完善一下框架实现一个多EPT hooks

数据结构层设计

为了实现多EPT hooks, VT驱动层势必要储存更多来自Guest请求hook的页地址, 这里为了方便后续禁用或取消掉一个hook, 我采用了hashmap作为高层结构管理要hook的页和虚假页地址.

但是驱动层是无异常的, 而C++ STL中的几乎所有数据结构都存在抛出异常而不处理异常的问题, 所以需要自己手动实现一个hashmap类型

hvpp_hashmap_t

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#pragma once
#include <ntddk.h> // for ExAllocatePoolWithTag, ExFreePoolWithTag
#include <cstdint>
#include <cstddef>
#include <type_traits>

template<typename Key, typename Value>
class hvpp_hashmap_t
{
public:
using key_t = Key;

struct Entry {
key_t key;
Value value;
bool used;
};

// capacity must be power-of-two for simpler mod
hvpp_hashmap_t(size_t capacity = 1024, ULONG PoolTag = 'kHpV') noexcept :
m_capacity(capacity),
m_pool_tag(PoolTag)
{
if ((m_capacity & (m_capacity - 1)) != 0) {
size_t v = 1;
while (v < m_capacity) v <<= 1;
m_capacity = v;
}

size_t bytes = sizeof(Entry) * m_capacity;
m_entries = static_cast<Entry*>(ExAllocatePoolWithTag(NonPagedPoolNx, bytes, m_pool_tag));
if (m_entries) {
RtlZeroMemory(m_entries, bytes);
for (size_t i = 0; i < m_capacity; ++i) {
m_entries[i].used = false;
m_entries[i].key = 0;
}
}
}

~hvpp_hashmap_t() noexcept
{
if (m_entries) {
ExFreePoolWithTag(m_entries, m_pool_tag);
m_entries = nullptr;
}
}

// non-copyable
hvpp_hashmap_t(const hvpp_hashmap_t&) noexcept = delete;
hvpp_hashmap_t& operator=(const hvpp_hashmap_t&) noexcept = delete;

// insert or update; returns true on success
bool insert(key_t key, const Value& value) noexcept
{
if (!m_entries) return false;
size_t idx = hash_key(key) & (m_capacity - 1);
for (size_t i = 0; i < m_capacity; ++i) {
size_t j = (idx + i) & (m_capacity - 1);
if (!m_entries[j].used) {
m_entries[j].used = true;
m_entries[j].key = key;
m_entries[j].value = value;
return true;
}
if (m_entries[j].used && m_entries[j].key == key) {
// update existing
m_entries[j].value = value;
return true;
}
}
return false; // table full
}

// find pointer to value or nullptr
Value* find(key_t key) noexcept
{
if (!m_entries) return nullptr;
size_t idx = hash_key(key) & (m_capacity - 1);
for (size_t i = 0; i < m_capacity; ++i) {
size_t j = (idx + i) & (m_capacity - 1);
if (!m_entries[j].used) return nullptr;
if (m_entries[j].used && m_entries[j].key == key) {
return &m_entries[j].value;
}
}
return nullptr;
}

// alternatively use [] to find or insert default
inline Value& operator[](const key_t& key) noexcept
{
Value* it = this->find(key);
if (it) return *it;

Value default_value{};
bool ok = this->insert(key, default_value);
if (!ok) {
return *it; // insertion failed, return nullptr
}
// find must now succeed
Value* it2 = this->find(key);
hvpp_assert(it2);
return *it2;
}

// erase key, return true if existed
bool erase(key_t key) noexcept
{
if (!m_entries) return false;
size_t idx = hash_key(key) & (m_capacity - 1);
for (size_t i = 0; i < m_capacity; ++i) {
size_t j = (idx + i) & (m_capacity - 1);
if (!m_entries[j].used) return false;
if (m_entries[j].used && m_entries[j].key == key) {
// remove and re-insert cluster following deletion to keep probe chain consistent
m_entries[j].used = false;
m_entries[j].key = nullptr;
// reinsert cluster
size_t k = (j + 1) & (m_capacity - 1);
while (m_entries[k].used) {
Entry tmp = m_entries[k];
m_entries[k].used = false;
m_entries[k].key = nullptr;
insert(tmp.key, tmp.value);
k = (k + 1) & (m_capacity - 1);
}
return true;
}
}
return false;
}

private:
static inline size_t hash_key(key_t k) noexcept
{
// simple pointer hash
uintptr_t v = static_cast<uintptr_t>(k);
// mix bits (64->32 mix)
v ^= (v >> 33);
v *= 0xff51afd7ed558ccdULL;
v ^= (v >> 33);
v *= 0xc4ceb9fe1a85ec53ULL;
v ^= (v >> 33);
return static_cast<size_t>(v);
}

private:
Entry* m_entries = nullptr;
size_t m_capacity;
ULONG m_pool_tag;
};

接下来就是设计具体储存页信息的数据结构了, 一开始的想法是只储存3个数据: 要hook的页(Original page), 虚假页(Fake page), 访问权限(Access type). 这样一来就可以只通过vmcall的寄存器来传入所有需要的参数了, 在创建hook时将Original page映射到自身的物理地址, 并将权限设置为Access type, 在处理EPT violation时再将映射修改到Fake page并将访问权限设置为~(Fake page).

但是这样的设计有一个明显缺陷, 那就是必须指定Original page的访问权限, 并且无法实现类似分别将读和写映射到不同的Fake page上, 遂放弃.

然后考虑了一下在实现这个想法时候的另一个比较理想的数据结构, 即传入每一种Access type对应的页, 在创建hook时随意将Original page映射到某个权限对应的页上并设置该权限, 在处理EPT violation时根据访问类型来进行映射. 这样可以比较完美地实现各种EPT hook需求, 唯一的问题是这样一来需要传入4个参数(Original page, Read page, Write page, Excute page), 无法在vmcall时直接使用寄存器传入, 于是使用框架中vcpu的guest_read_memory方法来读取Guest内存中存放的参数.

ept_hook_info

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
class ept_hook_info {
//
// Contains information about a single EPT hook:
// - original_page: The original page that is being hooked.
// - read_page: The page to use when read access is attempted.
// - write_page: The page to use when write access is attempted.
// - execute_page: The page to use when execute access is attempted.
//
public:
pa_t original_page;
pa_t read_page;
pa_t write_page;
pa_t execute_page;
bool enabled;

ept_hook_info() noexcept : \
original_page(0), read_page(0), write_page(0), execute_page(0), enabled(false) {};
ept_hook_info(pa_t op, pa_t rp, pa_t wp, pa_t ep, bool on=true) noexcept : \
original_page(op), read_page(rp), write_page(wp), execute_page(ep), enabled(on) {};

inline void init(pa_t op, pa_t rp, pa_t wp, pa_t ep, bool on) noexcept {
this->original_page = op;
this->read_page = rp;
this->write_page = wp;
this->execute_page = ep;
this->enabled = on;
}

inline ept_hook_info& operator=(const ept_hook_info other) noexcept{
this->init(other.original_page, other.read_page, other.write_page, other.execute_page, other.enabled);
return *this;
}
};

至此两个实现多EPT hooks依赖的重要数据结构就设计出来了, 接下来就是对hook逻辑的完善

Hook 逻辑层设计

现在vcpu需要储存的数据就是刚刚设计的hashmap了, 这里选择使用Original page的GPA作为key索引hook信息:

1
2
3
4
5
6
7
struct per_vcpu_data
{
ept_t ept;
hvpp_hashmap_t<uint64_t, ept_hook_info> ept_hooks;

per_vcpu_data() noexcept : ept_hooks(hvpp_hashmap_t<uint64_t, ept_hook_info>(256, 'Hmap')) { };
};

接下来是创建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
case 0xc1:
{
cr3_guard _{ vp.guest_cr3() };

auto p_hook_info = vp.context().rdx_as_pointer;
struct ept_hook_info {
void* original_page;
void* read_page;
void* write_page;
void* execute_page;
} hook_info{};
vp.guest_read_memory(
p_hook_info,
&hook_info,
sizeof(ept_hook_info)
);

pa_t original_page = pa_t::from_va(hook_info.original_page),
read_page = pa_t::from_va(hook_info.read_page);

data.ept_hooks[original_page.value()].init(
original_page,
read_page,
pa_t::from_va(hook_info.write_page),
pa_t::from_va(hook_info.execute_page),
true
);

hvpp_trace("vmcall (EPT hook) original page: 0x%p", hook_info.original_page);

vp.ept().split_2mb_to_4kb(original_page & ept_pd_t::mask, original_page & ept_pd_t::mask);
vp.ept().map_4kb(original_page, read_page, epte_t::access_type::read);

vmx::invept_single_context(vp.ept().ept_pointer());
break;
}

这里选择创建时就把Original page映射到读到的Fake page, 要为了代码美观也可以把它映射到自己然后设置为无权限(

删除hook的逻辑, 删除hook要求传入创建hook时使用的Original page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case 0xc2:
{
cr3_guard _{ vp.guest_cr3() };

auto original_page = pa_t::from_va(vp.context().rdx_as_pointer);
auto entry = data.ept_hooks[original_page.value()];

hvpp_trace("vmcall (EPT unhook)");

//
// Merge the 4kb pages back to the original 2MB large page.
// Note that this will also automatically set the access
// rights to read_write_execute.
//

vp.ept().join_4kb_to_2mb(entry.original_page & ept_pd_t::mask, entry.original_page & ept_pd_t::mask);

//
// We've changed EPT structure - mappings derived from EPT
// need to be invalidated.
//
vmx::invept_single_context(vp.ept().ept_pointer());
break;
}

最后是处理EPT violation的逻辑:

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
void vmexit_custom_handler::handle_ept_violation(vcpu_t& vp) noexcept
{
auto exit_qualification = vp.exit_qualification().ept_violation;
auto guest_pa = vp.exit_guest_physical_address();
auto guest_va = vp.exit_guest_linear_address();
auto& data = user_data(vp);

auto guest_pa_aligned = PAGE_ALIGN(guest_pa.value());
auto entry = data.ept_hooks[reinterpret_cast<uint64_t>(guest_pa_aligned)];

if (!entry.enabled) {
hvpp_trace("EPT violation on non-hooked page PA: 0x%p", guest_pa.value());
base_type::handle_ept_violation(vp);
return;
}

if (exit_qualification.data_read)
{
hvpp_trace("data_read LA: 0x%p PA: 0x%p", guest_va.value(), guest_pa.value());
vp.ept().map_4kb(
entry.original_page,
entry.read_page,
epte_t::access_type::read
);
}
else if(exit_qualification.data_write){
hvpp_trace("data_write LA: 0x%p PA: 0x%p", guest_va.value(), guest_pa.value());
vp.ept().map_4kb(
entry.original_page,
entry.write_page,
epte_t::access_type::write
);
}
else {
hvpp_trace("data_excute LA: 0x%p PA: 0x%p", guest_va.value(), guest_pa.value());
vp.ept().map_4kb(
entry.original_page,
entry.execute_page,
epte_t::access_type::execute
);
}

vp.suppress_rip_adjust();
}

测试代码

为了测试多EPT hooks的效果, 这里选择在原项目给出的EPT hook测试基础上在删除对ZwClose的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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#include <cstdio>
#include <cstdint>

#include <windows.h>

#include "ia32/asm.h"
#include "lib/mp.h"
#include "detours/detours.h"
#include "udis86/udis86.h"

#include "../hvpp/hvpp/lib/ioctl.h"

using ioctl_enable_io_debugbreak_t = ioctl_read_write_t<1, sizeof(uint16_t)>;

#define PAGE_SIZE 4096
#define PAGE_ALIGN(Va) ((PVOID)((ULONG_PTR)(Va) & ~(PAGE_SIZE - 1)))

static int HookCallCount = 0;

using pfnZwClose = NTSTATUS(*)(_In_ HANDLE Handle);
PVOID KUserSharedData = (PVOID)0x7FFE0000;

DECLSPEC_NOINLINE
NTSTATUS
Hook_ZwClose(
_In_ HANDLE Handle
)
{
HookCallCount += 1;
return 0;
}

auto Hook = [](void** OriginalFunction, void* HookedFunction) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(OriginalFunction, HookedFunction);
DetourTransactionCommit();
};

auto Unhook = [](void** OriginalFunction, void* HookedFunction) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(OriginalFunction, HookedFunction);
DetourTransactionCommit();
};

typedef struct ept_hook_info {
void* original_page;
void* read_page;
void* write_page;
void* execute_page;
} hook_info, *phook_info;

auto Hide = [](phook_info EPTinfo) {
struct CONTEXT { phook_info EPTinfo; } Context{ EPTinfo };
ForEachLogicalCore([](void* ContextPtr) {
CONTEXT* Context = (CONTEXT*)ContextPtr;
ia32_asm_vmx_vmcall(0xc1, (uint64_t)Context->EPTinfo, 0, 0);
}, &Context);
};

auto Unhide = [](void* OriginalPage) {
struct CONTEXT { void* OriginalPage; } Context{ OriginalPage };
ForEachLogicalCore([](void* ContextPtr) {
CONTEXT* Context = (CONTEXT*)ContextPtr;
ia32_asm_vmx_vmcall(0xc2, (uint64_t)Context->OriginalPage, 0, 0);
}, &Context);
};

auto Disassemble = [](void* Address) {
ud_t u;
ud_init(&u);

ud_set_input_buffer(&u, (uint8_t*)Address, 16);
ud_set_mode(&u, 64);
ud_set_syntax(&u, UD_SYN_INTEL);

while (ud_disassemble(&u))
{
printf("\t%s\n", ud_insn_asm(&u));
}
};

void TestHook() {
phook_info epthook1 = (phook_info)malloc(sizeof(hook_info));
phook_info epthook2 = (phook_info)malloc(sizeof(hook_info));
pfnZwClose ZwCloseFn = (pfnZwClose)GetProcAddress(LoadLibraryA("ntdll.dll"), "ZwClose");

PVOID OriginalFunction = (PVOID)ZwCloseFn;
PVOID OriginalFunctionAligned = PAGE_ALIGN(OriginalFunction);

PVOID OriginalFunctionBackup = malloc(PAGE_SIZE * 2);
PVOID OriginalFunctionBackupAligned = PAGE_ALIGN((ULONG_PTR)OriginalFunctionBackup + PAGE_SIZE);
memcpy(OriginalFunctionBackupAligned, OriginalFunctionAligned, PAGE_SIZE);

{
epthook1->original_page = OriginalFunctionAligned;
epthook1->read_page = OriginalFunctionBackupAligned;
epthook1->write_page = OriginalFunctionBackupAligned;
epthook1->execute_page = OriginalFunctionAligned;
}

PVOID HookedFunction = (PVOID)&Hook_ZwClose;

printf("OriginalFunction : 0x%p\n", OriginalFunction);
printf("OriginalFunctionAligned : 0x%p\n", OriginalFunctionAligned);
printf("OriginalPage : 0x%p\n", OriginalFunctionBackup);
printf("OriginalPageAligned : 0x%p\n", OriginalFunctionBackupAligned);
printf("HookedFunction : 0x%p\n", HookedFunction);
printf("\n");

VirtualLock(OriginalFunctionAligned, PAGE_SIZE);
VirtualLock(OriginalFunctionBackupAligned, PAGE_SIZE);

printf("Original:\n");
Disassemble(ZwCloseFn);
ZwCloseFn(NULL);
printf("HookCallCount = %i (expected: 0)\n\n", HookCallCount);
printf("\n");

printf("Hook:\n");
Hook(&OriginalFunction, HookedFunction);
Disassemble(ZwCloseFn);
ZwCloseFn(NULL);
printf("HookCallCount = %i (expected: 1)\n\n", HookCallCount);
printf("\n");

printf("Hide:\n");
Hide(epthook1);
Disassemble(ZwCloseFn);
ZwCloseFn(NULL);
printf("HookCallCount = %i (expected: 2)\n\n", HookCallCount);


printf("======== Multiple EPT hook test ========\n");
printf("Before hook: KD_DEBUGGER_ENABLED = %u\n", ((volatile PBYTE)KUserSharedData)[0x2d4]);

PVOID KUserShareDataBackup = malloc(2 * PAGE_SIZE);
PVOID KUserShareDataBackupAligned = PAGE_ALIGN((ULONG_PTR)KUserShareDataBackup + PAGE_SIZE);
memcpy(KUserShareDataBackupAligned, KUserSharedData, PAGE_SIZE);

{
epthook2->original_page = KUserSharedData;
epthook2->read_page = KUserShareDataBackupAligned;
epthook2->write_page = KUserShareDataBackupAligned;
epthook2->execute_page = KUserShareDataBackupAligned;
}

VirtualLock(KUserSharedData, PAGE_SIZE);
VirtualLock(KUserShareDataBackupAligned, PAGE_SIZE);
((volatile PBYTE)KUserShareDataBackupAligned)[0x2d4] = 0; // KD_DEBUGGER_ENABLED = FALSE
Hide(epthook2);
printf("After hook: KD_DEBUGGER_ENABLED = %u\n", ((volatile PBYTE)KUserSharedData)[0x2d4]);

Unhide(KUserSharedData);
printf("Unhide : KD_DEBUGGER_ENABLED = %u\n", ((volatile PBYTE)KUserSharedData)[0x2d4]);
VirtualUnlock(KUserSharedData, PAGE_SIZE);
VirtualUnlock(KUserShareDataBackupAligned, PAGE_SIZE);
free(KUserShareDataBackup);
printf("======== end of Multiple EPT hook test ========\n\n");


printf("Unhide:\n");
Unhide(OriginalFunctionAligned);
Disassemble(ZwCloseFn);
ZwCloseFn(NULL);
printf("HookCallCount = %i (expected: 3)\n\n", HookCallCount);
printf("\n");

printf("Unhook:\n");
Unhook(&OriginalFunction, HookedFunction);
Disassemble(ZwCloseFn);
ZwCloseFn(NULL);
printf("HookCallCount = %i (expected: 3)\n\n", HookCallCount);
printf("\n");

VirtualUnlock(OriginalFunctionAligned, PAGE_SIZE);
VirtualUnlock(OriginalFunctionBackupAligned, PAGE_SIZE);
free(OriginalFunctionBackup);
}

int main() {
TestHook();

system("pause");
return 0;
}

执行结果:

image-20251030163937065