x64 内存访问监视器





5.00/5 (10投票s)
本文介绍了如何自动捕获特定内存区域的内存访问(读/写)并将这些更改记录到文件中。
引言
内存访问监视器实现为一个注入到目标进程中的 DLL。我扩展了我上一篇文章中描述的工具的命令行界面,https://codeproject.org.cn/Articles/1266083/x64-API-Hooker-plus-Disassembler,以注入和弹出我们的 DLL。我将包含现有的源代码(经过一些错误修复;我想知道它现在是如何工作的……)以及监视器 DLL 的源代码。DLL 本身也是 64 位的,但通过一些小的修改也可以变成 32 位的。
Using the Code
我们将使用向量化异常处理程序来捕获我们的读/写访问冲突。我们可以使用 AddVectoredExceptionHandler
函数添加一个进程范围的异常处理程序。
PVOID WINAPI AddVectoredExceptionHandler(
_In_ ULONG FirstHandler,
_In_ PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
第一个参数决定了多个异常处理程序被调用的顺序。如果我们即将监视的进程已经注册了自己的异常处理程序,那么将此参数设置为 TRUE
可能会很重要,这样我们就可以捕获我们的读/写异常并处理它们,而无需将它们传递给此处理程序,否则该处理程序可能会变得恼火并直接调用 TerminateProcess
。
向量化异常处理程序是进程范围的,并且适用于进程中的所有线程,因此我们需要同步多个线程之间的执行,以确保我们的监视器不会中断。MSDN 建议不要在处理程序中使用同步对象或分配内存,请参阅 这里的Remarks,所以我决定从Wikipedia实现一个简单的自旋锁(稍后您将看到代码)。
要监视的内存区域由以下 struct
表示。
struct MONITOR_ENTRY
{
UCHAR *Start; // start address of region
DWORD Size; // size of region
FILE *File; // each region has associated file to which we write memory read/writes
int Counter; // r/w access counter
};
当我们开始监视时,我们将保护更改为仅 PAGE_EXECUTE
,因此如果给定的区域包含代码,则允许执行。我们注册一个异常处理程序,当进程尝试读取或写入该内存区域时将被调用。异常处理程序具有以下原型。
LONG NTAPI Handler(EXCEPTION_POINTERS *ExceptionInfo);
以及 EXCEPTION_POINTERS
结构。
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
ContextRecord
存储异常发生时的线程上下文,而 ExceptionRecord
存储异常信息。我们可以修改线程上下文结构(例如,Rax
寄存器的值),因此当 handler 返回时,Windows 将在线程继续执行之前更新上下文。要信号化异常已被处理并继续执行,我们从 handler 返回 EXCEPTION_CONTINUE_EXECUTION
,但当我们对异常不感兴趣时,我们应该返回 EXCEPTION_CONTINUE_SEARCH
(例如,对于应该由我们的进程处理的异常)。
当发生读/写尝试时,我们将捕获 EXCEPTION_ACCESS_VIOLATION
(异常代码存储在 ExceptionInfo->ExceptionRecord->ExceptionCode
中)异常。为了处理它,我们将需要。
- 导致异常的指令地址。
- 无法访问数据的地址。
- 访问类型(读/写)。
第一个参数从线程上下文结构(ExceptionInfo->ContextRecord->Rip
)中检索,第二个参数存储在 ExceptionInfo->ExceptionRecord->ExceptionInformation[1]
中,访问类型存储在 ExceptionInfo->ExceptionRecord->ExceptionInformation[0]
中。有关更多详细信息,请参阅 此链接。我们将执行的操作如下。
- 获取锁。
- 挂起所有其他线程(因为我们无法动态更改保护,以防某个线程在我们的区域内执行代码)。
- 将区域的保护更改为
PAGE_READWRITE
,以便我们可以读取导致访问冲突的指令的字节。 - 将此指令复制到某个缓冲区(以防使用 rip 相对寻址,我们将需要对其进行一些修改,同时保留其副作用)。
- 在刚刚复制的指令后添加无效指令操作码(
UD2
)指令。 - 修改指令指针,使其指向我们的缓冲区。
- 继续执行(不释放锁)。
线程将在我们的缓冲区内继续执行,将执行我们复制的指令,然后尝试执行 UD2
指令。这将触发另一个异常 EXCEPTION_ILLEGAL_INSTRUCTION
。现在我们的操作是。
- 将区域的保护更改回
PAGE_EXECUTE
。 - 修改指令指针,使其指向原始导致访问冲突的指令之后的指令。
- 恢复所有其他线程。
- 释放锁。
- 继续执行。
我们需要做一个澄清:像 jmp qword ptr [rax]
这样的控制转移指令可以不用读权限执行,尽管它们隐式引用内存。
现在让我们看看我们的 DLL 监视器的实际代码。我们有 DllMain
来捕获目标进程线程的创建和终止。
extern BOOL g_Update;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
g_Update = TRUE;
break;
case DLL_THREAD_DETACH:
g_Update = TRUE;
break;
case DLL_PROCESS_DETACH:
break;
default:
break;
}
return TRUE;
}
处理指令字节的辅助函数。
/*
UD2
*/
UCHAR g_InvalidOpcode[] = { 0x0F, 0x0B };
/*
pop r64
*/
UCHAR g_RegisterRestore[] = { 0x48, 0x58 };
/*
push r64
push low dword // sign extended to 64bit before push
mov dword ptr [rsp + 4], high dword
pop r64
*/
UCHAR g_RegisterOverride[] = { 0x48, 0x50,
0x68, 0x00, 0x00, 0x00, 0x00,
0xc7, 0x44, 0x24, 0x04, 0x00, 0x00, 0x00, 0x00,
0x48, 0x58 };
#define REGISTER_OVERRIDE_SIZE sizeof(g_RegisterOverride)
#define REGISTER_RESTORE_SIZE sizeof(g_RegisterRestore)
#define INVALID_OPCODE_SIZE sizeof(g_InvalidOpcode)
void GenerateInvalidOpcode(UCHAR *Bytes)
{
memcpy(Bytes, g_InvalidOpcode, INVALID_OPCODE_SIZE);
}
void GenerateRegisterOverride(DWORD Register, DWORD64 Value, UCHAR *OverBytes)
{
memcpy(OverBytes, g_RegisterOverride, REGISTER_OVERRIDE_SIZE);
OverBytes[1] += Register;
*((INT32*)(OverBytes + 3)) = Value;
*((INT32*)(OverBytes + 11)) = Value >> 32;
OverBytes[16] += Register;
}
void GenerateRegisterRestore(DWORD Register, UCHAR *RestBytes)
{
memcpy(RestBytes, g_RegisterRestore, REGISTER_RESTORE_SIZE);
RestBytes[1] += Register;
}
void GenerateTrampoline(UCHAR *Ptr, UCHAR *Bytes, DWORD Size,
bool rip, int index, DWORD *pTrampSize)
{
DWORD64 Address;
DWORD TrampSize;
INT32 Offset;
UCHAR Rex, Lock, Prefix, Prefix0F;
UCHAR Opcode;
UCHAR Modrm;
DWORD AddrReg;
DWORD Reg;
DWORD i, j, pi;
i = 0;
j = 0;
if (rip)
{
if (Bytes[i] == 0xF0)
{
Lock = Bytes[i];
++i;
}
else Lock = 0;
if ((Bytes[i] == 0x66) || (Bytes[i] == 0xF2) || (Bytes[i] == 0xF3))
{
Prefix = Bytes[i];
++i;
}
else Prefix = 0;
if ((Bytes[i] >= 0x40) && (Bytes[i] <= 0x4F))
{
Rex = Bytes[i];
++i;
}
else Rex = 0;
if (Bytes[i] == 0x0F)
{
Prefix0F = Bytes[i];
++i;
}
else Prefix0F = 0;
Opcode = Bytes[i];
++i;
Modrm = Bytes[i];
++i;
Offset = *((INT32*)&Bytes[i]);
i += sizeof(Offset);
pi = Size - i;
i += pi;
TrampSize = REGISTER_OVERRIDE_SIZE + (i - sizeof(Offset)) + REGISTER_RESTORE_SIZE;
if ((Ptr + TrampSize + INVALID_OPCODE_SIZE) > (Ptr + BUFFER_SIZE))
{
fprintf(g_Entry[index].File, "buffer overflow\n");
TerminateProcess(GetCurrentProcess(), 0);
}
Address = (DWORD64)(Bytes + Size + Offset);
Reg = (Modrm & 0x38) >> 3;
// exclude: 0, 4, 5
// 0, 1, 2, 5, 6 ++
// 3, 7, 4 --
// 0: 1
// 1: 2
// 2: 3
// 3: 2
// 4: 3
// 5: 6
// 6: 7
// 7: 6
AddrReg = ((Reg == 7) || (Reg == 3) || (Reg == 4)) ? (Reg - 1) : (Reg + 1);
GenerateRegisterOverride(AddrReg, Address, &Ptr[j]);
j += REGISTER_OVERRIDE_SIZE;
if (Lock)
{
Ptr[j] = Lock;
++j;
}
if (Prefix)
{
Ptr[j] = Prefix;
++j;
}
if (Rex)
{
Ptr[j] = Rex;
++j;
}
if (Prefix0F)
{
Ptr[j] = Prefix0F;
++j;
}
Ptr[j] = Opcode;
++j;
Ptr[j] = AddrReg | (Reg << 3);
++j;
memcpy(&Ptr[j], &Bytes[i - pi], pi);
j += pi;
GenerateRegisterRestore(AddrReg, &Ptr[j]);
j += REGISTER_RESTORE_SIZE;
}
else
{
TrampSize = Size;
if ((Ptr + TrampSize + INVALID_OPCODE_SIZE) > (Ptr + BUFFER_SIZE))
{
fprintf(g_Entry[index].File, "buffer overflow\n");
TerminateProcess(GetCurrentProcess(), 0);
}
memcpy(&Ptr[j], &Bytes[i], Size);
j += Size;
}
GenerateInvalidOpcode(&Ptr[j]);
*pTrampSize = TrampSize;
}
更新、挂起和恢复线程的辅助函数。
DWORD g_ThreadId[100];
DWORD g_ThreadIdCount;
HANDLE g_ThreadHandle[100];
DWORD g_ThreadHandleCount;
void UpdateThreads()
{
HANDLE hThreadSnap;
THREADENTRY32 te32;
hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
te32.dwSize = sizeof(THREADENTRY32);
Thread32First(hThreadSnap, &te32);
g_ThreadIdCount = 0;
do
{
if ((te32.th32OwnerProcessID == GetCurrentProcessId()) &&
(te32.th32ThreadID != GetCurrentThreadId()))
{
if (g_ThreadIdCount == ARRAYSIZE(g_ThreadId))
{
fprintf(g_File, "Array for thread ids is too small\n");
TerminateProcess(GetCurrentProcess(), 0);
}
g_ThreadId[g_ThreadIdCount] = te32.th32ThreadID;
++g_ThreadIdCount;
}
} while (Thread32Next(hThreadSnap, &te32));
CloseHandle(hThreadSnap);
fprintf(g_File, "thread count updated: %d\n\n", g_ThreadIdCount);
fflush(g_File);
}
void SuspendThreads()
{
g_ThreadHandleCount = 0;
for (int i = 0; i < g_ThreadIdCount; ++i)
{
if (g_ThreadId[i] != GetCurrentThreadId())
{
g_ThreadHandle[g_ThreadHandleCount] =
OpenThread(THREAD_ALL_ACCESS, FALSE, g_ThreadId[i]);
SuspendThread(g_ThreadHandle[g_ThreadHandleCount]);
++g_ThreadHandleCount;
}
}
if (g_ThreadHandleCount) Sleep(THREAD_DELAY); // wait, SuspendThread is asynchronous
}
void ResumeThreads()
{
for (int i = 0; i < g_ThreadHandleCount; ++i)
{
ResumeThread(g_ThreadHandle[i]);
CloseHandle(g_ThreadHandle[i]);
}
if (g_ThreadHandleCount) Sleep(THREAD_DELAY); // wait, ResumeThread is asynchronous
}
自旋锁在 ASM
中实现。
PUBLIC spin_lock
PUBLIC spin_unlock
.data
locked dd 0
.code
spin_lock PROC
mov eax, 1
xchg eax, [locked]
test eax, eax
jnz spin_lock
ret
spin_lock ENDP
spin_unlock PROC
xor eax, eax
xchg eax, [locked]
ret
spin_unlock ENDP
END
并从 C
调用。
extern "C"
{
void spin_lock();
void spin_unlock();
}
用于保存内存范围、地址等信息的全局变量。
MONITOR_ENTRY g_Entry[100];
DWORD g_EntryCount;
FILE *g_File;
DWORD g_index;
PVOID g_Handler;
UCHAR *g_NextInstructionAddress;
UCHAR *g_InvalidOpcodeAddress;
UCHAR *g_DataAddress;
UCHAR *g_Buffer;
DWORD g_Access;
DWORD g_TicksBegin;
BOOL g_Stopped;
BOOL g_Update;
用于启动监视器的导出函数。内存范围由包含模块名称的 string
数组构成。
__declspec(dllexport) void StartMonitor()
{
DWORD OldProtect;
IMAGE_NT_HEADERS64 *Headers;
read_spec(L"data.bin");
char* Modules[] = { "{this}" };
char Buffer[MAX_PATH];
char *ModuleName;
int i;
for (i = 0; (i < ARRAYSIZE(Modules)) && (i < ARRAYSIZE(g_Entry)); ++i)
{
if (!strcmp(Modules[i], "{this}")) ModuleName = NULL;
else ModuleName = Modules[i];
g_Entry[i].Start = (UCHAR*)GetModuleHandleA(ModuleName);
if (!ModuleName)
{
GetModuleFileNameA((HMODULE)g_Entry[i].Start, Buffer, sizeof(Buffer));
ModuleName = Buffer + strlen(Buffer) - 1;
while (*ModuleName != '\\') --ModuleName;
++ModuleName;
}
else
{
strcpy(Buffer, ModuleName);
ModuleName = Buffer;
}
strcat(ModuleName, ".txt");
g_Entry[i].File = fopen(ModuleName, "w");
if (!g_Entry[i].File) TerminateProcess(GetCurrentProcess(), 0);
Headers = (IMAGE_NT_HEADERS64*)((UCHAR*)g_Entry[i].Start +
((IMAGE_DOS_HEADER*)g_Entry[i].Start)->e_lfanew);
g_Entry[i].Size = Headers->OptionalHeader.SizeOfImage;
if (!VirtualProtect(g_Entry[i].Start, g_Entry[i].Size, PAGE_EXECUTE, &OldProtect))
{
fprintf(g_Entry[i].File, "VirtualProtect\n");
TerminateProcess(GetCurrentProcess(), 0);
}
g_Entry[i].Counter = 0;
}
g_EntryCount = i;
g_Stopped = FALSE;
g_File = fopen("default.txt", "w");
if (!g_File) TerminateProcess(GetCurrentProcess(), 0);
fprintf(g_File, "StartMonitor : %d\n\n", GetCurrentThreadId());
fflush(g_File);
g_Buffer = (UCHAR*)VirtualAlloc(NULL, BUFFER_SIZE, MEM_RESERVE |
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!g_Buffer)
{
fprintf(g_File, "VirtualAlloc\n");
TerminateProcess(GetCurrentProcess(), 0);
}
g_TicksBegin = GetTickCount();
g_Handler = AddVectoredExceptionHandler(TRUE, Handler);
if (!g_Handler)
{
fprintf(g_File, "AddVectoredExceptionHandler\n");
TerminateProcess(GetCurrentProcess(), 0);
}
}
用于停止监视器的导出函数。我们执行的操作。
- 获取锁。
- 挂起所有其他线程(因为我们无法动态更改保护,以防某个线程在我们的区域内执行代码)。
- 将保护更改为
PAGE_EXECUTE_READWRITE
。 - 恢复所有其他线程。
- 释放锁。
- 删除我们的异常处理程序并进行清理。
__declspec(dllexport) void StopMonitor()
{
spin_lock();
UpdateThreads();
SuspendThreads();
DWORD OldProtect;
for (int i = 0; i < g_EntryCount; ++i)
{
if (!VirtualProtect(g_Entry[i].Start, g_Entry[i].Size,
PAGE_EXECUTE_READWRITE, &OldProtect))
{
fprintf(g_File, "VirtualProtect\n");
TerminateProcess(GetCurrentProcess(), 0);
}
}
g_Stopped = TRUE;
ResumeThreads();
spin_unlock();
RemoveVectoredExceptionHandler(g_Handler);
Sleep(THREAD_DELAY * 5); // wait, windows is asynchronous :-)
for (int i = 0; i < g_EntryCount; ++i)
{
fclose(g_Entry[i].File);
}
free_spec();
VirtualFree(g_Buffer, 0, MEM_RELEASE);
fprintf(g_File, "StopMonitor : %d, %d\n\n", GetCurrentThreadId(),
GetTickCount() - g_TicksBegin);
fclose(g_File);
}
以及处理程序本身。请注意,fprintf
函数可以替换为写入某个缓冲区然后当缓冲区满时刷新到磁盘的函数。此外,我们仅出于娱乐目的处理 MSVC_EXCEPTION
,它在我们的内存监视器中没有任何作用。
LONG NTAPI Handler(EXCEPTION_POINTERS *ExceptionInfo)
{
Buffer code_buf;
Instruction inst;
UCHAR *InstAddress, *DataAddress;
DWORD InstSize, TrampSize, ExcCode, OldProtect, i, Access;
ExcCode = ExceptionInfo->ExceptionRecord->ExceptionCode;
if (ExcCode == EXCEPTION_ACCESS_VIOLATION)
{
InstAddress = (UCHAR*)ExceptionInfo->ContextRecord->Rip;
Access = ExceptionInfo->ExceptionRecord->ExceptionInformation[0];
DataAddress = (UCHAR*)ExceptionInfo->ExceptionRecord->ExceptionInformation[1];
for (i = 0; i < g_EntryCount; ++i)
{
if ((DataAddress >= (UCHAR*)g_Entry[i].Start) &&
(DataAddress < ((UCHAR*)g_Entry[i].Start + g_Entry[i].Size)))
{
spin_lock();
if (g_Stopped)
{
spin_unlock();
return EXCEPTION_CONTINUE_EXECUTION;
}
if (Access == 0) fprintf(g_Entry[i].File, "Access: READ\n");
else if (Access == 1) fprintf(g_Entry[i].File, "Access: WRITE\n");
else
{
fprintf(g_Entry[i].File, "Access: EXECUTE\n");
TerminateProcess(GetCurrentProcess(), 0);
}
fprintf(g_Entry[i].File, "Counter: %d\n", g_Entry[i].Counter);
++(g_Entry[i].Counter);
fprintf(g_Entry[i].File, "Thread Id: %d\n", GetCurrentThreadId());
fprintf(g_Entry[i].File, "Instruction Address: %p\n", InstAddress);
fprintf(g_Entry[i].File, "Data Address: %p\n", DataAddress);
if (g_Update)
{
UpdateThreads();
g_Update = FALSE;
}
SuspendThreads();
if (!VirtualProtect(g_Entry[i].Start, g_Entry[i].Size,
PAGE_READWRITE, &OldProtect))
{
fprintf(g_Entry[i].File, "VirtualProtect\n");
TerminateProcess(GetCurrentProcess(), 0);
}
if (Access == 1) fprintf(g_Entry[i].File, "Data Before: ");
else fprintf(g_Entry[i].File, "Data: ");
for (int j = 0; j < VAR_SIZE; ++j)
{
fprintf(g_Entry[i].File, "%02hhX ", DataAddress[j]);
}
fprintf(g_Entry[i].File, "\n");
fflush(g_Entry[i].File);
c_MakeBuffer(InstAddress, 100, (Encoding)0, &code_buf);
inst_set_params(&inst, MODE_64, C_TRUE, &code_buf, NULL,
SHOW_ADDRESS | SHOW_LOWER | SHOW_PSEUDO);
if (!decode(&inst))
{
fprintf(g_Entry[i].File, "decode\n");
TerminateProcess(GetCurrentProcess(), 0);
}
InstSize = code_buf.i;
GenerateTrampoline(g_Buffer, InstAddress, InstSize, inst.rip, i, &TrampSize);
GenerateInvalidOpcode(g_Buffer + TrampSize);
ExceptionInfo->ContextRecord->Rip = (DWORD64)g_Buffer;
g_NextInstructionAddress = InstAddress + InstSize;
g_InvalidOpcodeAddress = g_Buffer + TrampSize;
g_DataAddress = DataAddress;
g_Access = Access;
g_index = i;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
else if (ExcCode == EXCEPTION_ILLEGAL_INSTRUCTION)
{
if (ExceptionInfo->ContextRecord->Rip == (DWORD64)g_InvalidOpcodeAddress)
{
i = g_index;
DataAddress = g_DataAddress;
Access = g_Access;
if (Access == 1)
{
fprintf(g_Entry[i].File, "Data After: ");
for (int j = 0; j < VAR_SIZE; ++j)
{
fprintf(g_Entry[i].File, "%02hhX ", DataAddress[j]);
}
fprintf(g_Entry[i].File, "\n");
}
fprintf(g_Entry[i].File, "\n");
fflush(g_Entry[i].File);
if (!VirtualProtect(g_Entry[i].Start, g_Entry[i].Size, PAGE_EXECUTE, &OldProtect))
{
fprintf(g_Entry[i].File, "VirtualProtect\n");
TerminateProcess(GetCurrentProcess(), 0);
}
ExceptionInfo->ContextRecord->Rip = (DWORD64)g_NextInstructionAddress;
ResumeThreads();
spin_unlock();
return EXCEPTION_CONTINUE_EXECUTION;
}
}
else if (ExcCode == MSVC_EXCEPTION)
{
THREADNAME_INFO *info =
(THREADNAME_INFO*)ExceptionInfo->ExceptionRecord->ExceptionInformation;
fprintf(g_File, "Thread Exception: %x %d %p\n",
ExcCode, GetCurrentThreadId(), ExceptionInfo->ContextRecord->Rip);
if (info->szName) fprintf(g_File, "Name: %s\n", info->szName);
fprintf(g_File, "Id: %d\n\n", info->dwThreadID);
fflush(g_File);
return EXCEPTION_CONTINUE_SEARCH;
}
fprintf(g_File, "Skip Exception: %x %d %p\n\n", ExcCode,
GetCurrentThreadId(), ExceptionInfo->ContextRecord->Rip);
fflush(g_File);
return EXCEPTION_CONTINUE_SEARCH;
}
可以看到,我们有一个默认文件,每个内存区域都有一个文件。默认文件的内容可能看起来像。
以及某个内存范围的文件内容可能看起来像。
要启动监视器,我们将使用传递给我们的工具的以下命令。
inject Monitor.dll
add kernel32.dll export AddVectoredExceptionHandler
addh Handlers.dll : AddVectoredExceptionHandlerHandler to kernel32.dll :
AddVectoredExceptionHandler
wait async
要停止监视器,我们将使用。
eject-stop
remove kernel32.dll : AddVectoredExceptionHandler
基本上就是这样!谢谢您的阅读。