题目要求:
设计和实现一个软件,其功能如下:
1、显示所有的进程列表;
2、选中一个进程,显示该进程的所有IAT中的函数;
3、选中一个IAT函数,实现Hooking和 inline Hooking,在Hooking的函数中显示”组号:姓名”。
显示所有进程列表
- 函数CreateToolhelp32Snapshot用于获取指定进程的快照或者所有进程的快照。并且返回快照的句柄。
- 函数Process32First用于获取进程快照中的第一个进程的信息,并且将这个进程的信息保存在一个PROCESSENTRY32的结构体中。在这个结构体中保存着进程的ID号和名称。
- 函数Process32Next用于获取快照中下一个进程的信息。我们循环调用这个函数,那么可以遍历进程快照中的所有进程。
源程序如下:
#include <windows.h>
#include<iostream>
#include <tlhelp32.h>
#include <stdio.h>
using namespace std;
DWORD GetProcessId(char*myprocess)//枚举进程函数
{
DWORD Pid = -1;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
//创建系统快照
PROCESSENTRY32 lPrs;
ZeroMemory(&lPrs, sizeof(lPrs));
lPrs.dwSize = sizeof(lPrs);
char *targetFile = myprocess;
bool flag = false;
if (Process32First(hSnap, &lPrs)) {//取得系统快照里第一个进程信息
//printf("name\t\t\tpid\t\tsize\n");
printf("%-50s%u\t\t%u\n", lPrs.szExeFile, lPrs.th32ProcessID, lPrs.dwSize);
if (strstr(targetFile, lPrs.szExeFile))//判断进程信息是否是explorer.exe
{
Pid = lPrs.th32ProcessID;
flag = true;
}
}
while (1)
{
ZeroMemory(&lPrs, sizeof(lPrs));
lPrs.dwSize = (&lPrs, sizeof(lPrs));
if (!Process32Next(hSnap, &lPrs))//继续枚举进程信息
{
break;
}
if (strstr(targetFile, lPrs.szExeFile))//判断进程信息是否是explorer.exe
{
Pid = lPrs.th32ProcessID;
flag = true;
}
printf("%-50s%u\t\t%u\n", lPrs.szExeFile, lPrs.th32ProcessID, lPrs.dwSize);
}
if(flag)
return Pid;
else return -1;
}
int main() {
printf("start.....\n");
char myprocess[50] = "cloudmusic.exe";
DWORD myPid = GetProcessId(myprocess);
printf("%u\n", myPid);
getchar();
return 0;
}
显示进程所有的IAT函数
首先获得模块的句柄:
// 取得主模块的模块句柄(即进程模块基地址)
HMODULE hModule = ::GetModuleHandleA(NULL);
然后把进程基址赋给pDosHeader,即起始基址就是PE的IMAGE_DOS_HEADER
IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hModule;
然后定位到PE header
基址hMod加上IMAGE_DOS_HEADER结构的e_lfanew成员到达IMAGE_NT_HEADERS
NT文件头的前4字节是文件签名("PE00" 字符串),然后是20字节的IMAGE_FILE_HEADER结。即到达IMAGE_OPTIONAL_HEADER结构的地址,获取了一个指向IMAGE_OPTIONAL_HEADER结构体的指针。
IMAGE_OPTIONAL_HEADER* pOpNtHeader = (IMAGE_OPTIONAL_HEADER*)((BYTE*)hModule + pDosHeader->e_lfanew + 24);
定位导入表:
通过IMAGE_OPTIONAL_HEADER结构中的DataDirectory结构数组中的第二个成员中的VirturalAddress字段定位到IMAGE_IMPORT_DESCRIPTOR结构的起始地址即获得导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的指针(导入表首地址)
IMAGE_IMPORT_DESCRIPTOR* pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)hModule + pOpNtHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
通过两重循环输出IAT函数,外层函数是对模块循环,内层循环对一个模块里面的所有函数进行循环输出:
while (pImportDesc->FirstThunk)
{
char* pszDllName = (char*)((BYTE*)hModule + pImportDesc->Name);
printf("模块名称:%s\n", pszDllName);
DWORD n = 0;
//一个IMAGE_THUNK_DATA就是一个导入函数
IMAGE_THUNK_DATA* pThunk = (IMAGE_THUNK_DATA*)((BYTE*)hModule + pImportDesc->OriginalFirstThunk);
while (pThunk->u1.Function)
{
//取得函数名称
char* pszFuncName = (char*)((BYTE*)hModule+pThunk->u1.AddressOfData+2); //函数名前面有两个..
printf("function name:%-25s, ", pszFuncName);
//取得函数地址
PDWORD lpAddr = (DWORD*)((BYTE*)hModule + pImportDesc->FirstThunk) + n; //从第一个函数的地址,以后每次+4字节
printf("addrss:%X\n", lpAddr);
n++; //每次增加一个DWORD
pThunk++;
}
printf("\n");
pImportDesc++;
}
实现Inline hooking
Inline hooking是一种拦截目标函数调用的方法,主要用于抗病毒软件,沙箱和恶意软件。 一般的想法是将函数重定向到我们自己的函数,以便在函数执行之前和/或之后执行处理。 这可能包括:检查参数,匀场,记录,欺骗返回的数据和过滤呼叫。 Rootkit倾向于使用钩子来修改从系统调用返回的数据,以隐藏其存在,而安全软件则使用它们来防止/监视潜在的恶意操作。
hooking通过直接修改目标函数(内联修改)中的代码来放置,通常通过用跳转覆盖前几个字节; 这允许在函数进行任何处理之前重定向执行。 大多数引擎引擎使用32位相对跳转,占用5个字节的空间。
如何实现:
将使用一个基于trampoline的钩子,它允许我们截取功能,同时仍然可以调用原件。这个钩子由3部分组成:
- Hook - 一个5字节的相对跳转,写入目标函数以挂接它,跳转将从挂钩函数跳转到我们的代码。
- proxy这是我们指定的函数(或代码),钩子放在目标函数上将跳转到。
- trampoline用于绕过钩子,所以我们可以正常地调用挂钩功能。
使用trampoline的原因是:
假设我们要钩住MessageBoxA,从代理功能中打印出参数,然后显示消息框:为了显示消息框,我们需要调用MessageBoxA(它们重定向到我们的代理函数,这反过来又调用MessageBoxA)。 显然从我们的代理函数中调用MessageBoxA将导致无限递归,并且程序由于堆栈溢出而最终崩溃。
我们可以简单地从代理函数中取消MessageBoxA,调用它,然后重新挂接它; 但是如果多个线程同时调用MessageBoxA,这将导致竞争条件,并可能导致程序崩溃。
相反,我们可以做的是存储MessageBoxA的前5个字节(这些被我们的钩子覆盖),然后当我们需要调用非挂钩的MessageBoxA时,我们可以执行存储的前5个字节,然后是5个字节的跳转 MessageBoxA(直接挂钩)。
只要前5个字节不是相对指令,它们可以在任何地方执行。
在这个例子中,函数的前5个字节组成了3个指令mov edi,edi; push ebp; mov ebp,esp,但是,例如,如果第一个指令是10个字节长,我们只存储5个字节trampoline将执行一半的指令,导致程序爆炸。 为了解决这个问题,我们必须使用反汇编来获取每个指令的长度。 最好的情况是前n个指令总共5个字节,最糟糕的情况是如果第一个指令是4个字节,而第二个指令是16个(x86指令的最大长度),则必须存储20个字节(4 + 16),这意味着trampoline的大小必须是25个字节(空间最多可达20个字节的指令,5个字节跳回挂钩的功能)。重要的是,返回跳转必须跳转到挂接的函数n个字节,其中n是我们存储在trampoline中的指令。
代码编写
首先,我们需要定义代理函数。我们把hooking函数重定向。对于这个例子,我们只需要在显示消息框之前打印出参数。
int WINAPI NewMessageBoxA(HWND hWnd, LPCSTR lpText, LPCTSTR lpCaption, UINT uType)
{
printf("MessageBoxA called!ntitle: %sntext: %snn", lpCaption, lpText);
return OldMessageBoxA(hWnd, lpText, lpCaption, uType);
}
int WINAPI NewMessageBoxW(HWND hWnd, LPWSTR lpText, LPCTSTR lpCaption, UINT uType)
{
printf("MessageBoxW called!ntitle: %wsntext: %wsnn", lpCaption, lpText);
return OldMessageBoxW(hWnd, lpText, lpCaption, uType);
}
OldMessageBox只是一个typedef,它将指向25个字节的可执行内存,该挂钩功能将存储trampoline。
typedef int (WINAPI *TdefOldMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCTSTR lpCaption, UINT uType);
typedef int (WINAPI *TdefOldMessageBoxW)(HWND hWnd, LPWSTR lpText, LPCTSTR lpCaption, UINT uType);
TdefOldMessageBoxA OldMessageBoxA = (TdefOldMessageBoxA)VirtualAlloc(NULL, 25, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
TdefOldMessageBoxW OldMessageBoxW = (TdefOldMessageBoxW)VirtualAlloc(NULL, 25, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
现在对于hooking函数,我们将具有以下参数:
- name - 挂钩功能的名称。
- dll - 目标函数所在的dll。
- proxy - 指向代理函数的指针(NewMessageBox)。
- original - 指向25字节的可执行内存的指针,存储trampoline。
- length - 指向一个变量的指针,该变量接收存储在trampoline中的指令值。
在hooking函数内部,我们将获取目标函数的地址,然后使用Hacker Dissasembler Engine(HDE32)来拆分每个指令并获取长度,直到我们有5个或更多个字节值得整个指令(hde32_disasm返回长度的第一个参数指向的指令)。
LPVOID FunctionAddress;
DWORD TrampolineLength = 0;
FunctionAddress = GetProcAddress(GetModuleHandleA(dll), name);
if(!FunctionAddress)
return FALSE;
//拆分每个指令的长度,直到我们有5个以上的字节值
while(TrampolineLength < 5)
{
LPVOID InstPointer = (LPVOID)((DWORD)FunctionAddress + TrampolineLength);
TrampolineLength += hde32_disasm(InstPointer, &disam);
}
为了构建实际的trampoline,我们首先将目标函数中的TrampolineLength字节复制到trampoline缓冲区(传递给参数“original”中的函数),然后我们将复制的字节用n字节附加到目标函数中,n是Trampoline的长度。
相对跳转是距离跳转结束的距离,即:(destination - (source + 5))。 跳跃的来源将是trampoline地址+trampoline长度,目的地将是hooking函数+trampoline长度。
DWORD src = ((DWORD)FunctionAddress + TrampolineLength);
DWORD dst = ((DWORD)original + TrampolineLength + 5);
BYTE jump[5] = {0xE9, 0x00, 0x00, 0x00, 0x00};
//将n个字节从目标函数存储到trampoline中
memcpy(original, FunctionAddress, TrampolineLength);
//设置跳转的第二个字节(偏移量),跳转到我们想要的位置
*(DWORD *)(jump+1) = src - dst;
//将跳转复制到trampoline的末端
memcpy((LPVOID)((DWORD)original+TrampolineLength), jump, 5);
在我们可以编写跳转函数之前,我们需要确保内存是可写的(通常不可以),我们通过使用VirtualProtect将保护设置为PAGE_EXECUTE_READWRITE来实现。
//确保该功能是可写的
DWORD OriginalProtection;
if(!VirtualProtect(FunctionAddress, 8, PAGE_EXECUTE_READWRITE, &OriginalProtection))
return FALSE;
要摆放hooking,我们需要做的就是创建一个从目标函数跳转到代理的跳转,然后我们可以用它覆盖目标的前5个字节。 为了避免在写入跳转时调用函数的任何风险,我们必须一次性写完。而atomic functions只能用于基础2(2,4,8,16等)的大小; 我们的跳转是5个字节,我们可以复制的最接近的大小是8,所以我们必须制作一个自定义函数SafeMemcpyPadded,它将源缓冲区用来从目的地的字节缓冲到8个字节,这样最后3个字节保持不变 复制后。
cmpxchg8b将edx:eax中保存的8个字节与目标进行比较,如果它们相等,则复制ecx:ebx中保存的8个字节,我们将edx:eax设置为目标字节,以使副本始终发生。
//构建并写hooking
*(DWORD *)(jump+1) = (DWORD)proxy - (DWORD)FunctionAddress - 5;
SafeMemcpyPadded(FunctionAddress, Jump, 5);
void SafeMemcpyPadded(LPVOID destination, LPVOID source, DWORD size)
{
BYTE SourceBuffer[8];
if(size > 8)
return;
//使用来自目的地的字节来填充源缓冲区
memcpy(SourceBuffer, destination, 8);
memcpy(SourceBuffer, source, size);
__asm
{
lea esi, SourceBuffer;
mov edi, destination;
mov eax, [edi];
mov edx, [edi+4];
mov ebx, [esi];
mov ecx, [esi+4];
lock cmpxchg8b[edi];
}
}
现在要做的就是恢复页面保护。 刷新指令缓存,并将length参数设置为Trampoline的长度。
//恢复页面保护
VirtualProtect(FunctionAddress, 8, OriginalProtection, &OriginalProtection);
//清楚CPU指令缓存
FlushInstructionCache(GetCurrentProcess(), FunctionAddress, TrampolineLength);
*length = TrampolineLength;
return TRUE;
hooking功能可以这样简单地调用。
DWORD length;
HookFunction("user32.dll", "MessageBoxA", &NewMessageBoxA, OldMessageBoxA, &length);
通过从OldMessageBox(Trampoline)将length字节复制到hooking函数来完成脱钩。