当前位置: 首页 > 技术干货 > 浅谈进程隐藏技术

浅谈进程隐藏技术

发表于:2024-07-05 16:56 作者: hope 阅读数(4808人)

前言

在之前几篇文章已经学习了解了几种钩取的方法

● 浅谈调试模式钩取

● 浅谈热补丁

● 浅谈内联钩取原理与实现

● 导入地址表钩取技术

这篇文章就利用钩取方式完成进程隐藏的效果。

进程遍历方法

在实现进程隐藏时,首先需要明确遍历进程的方法。

CreateToolhelp32Snapshot

CreateToolhelp32Snapshot函数用于创建进程的镜像,当第二个参数为0时则是创建所有进程的镜像,那么就可以达到遍历所有进程的效果。

#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>

int main()
{
   //设置编码,便于后面能够输出中文
   setlocale(LC_ALL, "zh_CN.UTF-8");
   //创建进程镜像,参数0代表创建所有进程的镜像
   HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
   if (hSnapshot == INVALID_HANDLE_VALUE)
  {
       std::cout << "Create Error" << std::endl;
       exit(-1);
  }

   /*
   * typedef struct tagPROCESSENTRY32 {
   * DWORD dwSize;               进程信息结构体大小,首次调用之前必须初始化
   * DWORD cntUsage;              引用进程的次数,引用次数为0时,则进程结束
   * DWORD th32ProcessID;           进程的ID
   * ULONG_PTR th32DefaultHeapID;      进程默认堆的标识符,除工具使用对我们没用
   * DWORD th32ModuleID;                 进程模块的标识符
   * DWORD cntThreads;            进程启动的执行线程数
   * DWORD th32ParentProcessID;           父进程ID
   * LONG pcPriClassBase;          进程线程的基本优先级
   * DWORD dwFlags;              保留
   * TCHAR szExeFile[MAX_PATH];         进程的路径
   * } PROCESSENTRY32;
   * typedef PROCESSENTRY32 *PPROCESSENTRY32;
   */
   PROCESSENTRY32 pi;
   pi.dwSize = sizeof(PROCESSENTRY32);
   //取出第一个进程
   BOOL bRet = Process32First(hSnapshot, &pi);
   while (bRet)
  {
       wprintf(L"进程路径:%s\t进程号:%d\n", pi.szExeFile, pi.th32ProcessID);
       //取出下一个进程
       bRet = Process32Next(hSnapshot, &pi);
  }
}

EnumProcesses

EnumProcesses用于将所有进程号的收集。

#include <iostream>
#include <Windows.h>
#include <Psapi.h>

int main()
{
   setlocale(LC_ALL, "zh_CN.UTF-8");

   DWORD processes[1024], dwResult, size;
   unsigned int i;
//收集所有进程的进程号
   if (!EnumProcesses(processes, sizeof(processes), &dwResult))
  {
       std::cout << "Enum Error" << std::endl;
  }

   //进程数量
   size = dwResult / sizeof(DWORD);

   for (i = 0; i < size; i++)
  {
       //判断进程号是否为0
       if (processes[i] != 0)
      {
           //用于存储进程路径
           TCHAR szProcessName[MAX_PATH] = { 0 };
           //使用查询权限打开进程
           HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
               PROCESS_VM_READ,
               FALSE,
               processes[i]);

           if (hProcess != NULL)
          {
               HMODULE hMod;
               DWORD dwNeeded;
//收集该进程的所有模块句柄,第一个句柄则为文件路径
               if (EnumProcessModules(hProcess, &hMod, sizeof(hMod),
                   &dwNeeded))
              {
                   //根据句柄获取文件路径
                   GetModuleBaseName(hProcess, hMod, szProcessName,
                       sizeof(szProcessName) / sizeof(TCHAR));
              }
               wprintf(L"进程路径:%s\t进程号:%d\n", szProcessName, processes[i]);
          }
      }  
  }
}

ZwQuerySystemInfomation

ZwQuerySystemInfomation函数是CreateToolhelp32Snapshot函数与EnumProcesses函数底层调用的函数,也用于遍历进程信息。代码参考https://cloud.tencent.com/developer/article/1454933

#include <iostream>
#include <Windows.h>
#include <ntstatus.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")

//定义函数指针
typedef NTSTATUS(WINAPI* NTQUERYSYSTEMINFORMATION)(
IN      SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT   PVOID                    SystemInformation,
IN      ULONG                    SystemInformationLength,
OUT PULONG                   ReturnLength
);

int main()
{
   //设置编码
setlocale(LC_ALL, "zh_CN.UTF-8");
   //获取模块地址
HINSTANCE ntdll_dll = GetModuleHandle(L"ntdll.dll");
if (ntdll_dll == NULL) {
std::cout << "Get Module Error" << std::endl;
exit(-1);
}

NTQUERYSYSTEMINFORMATION ZwQuerySystemInformation = NULL;
//获取函数地址
ZwQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)GetProcAddress(ntdll_dll, "ZwQuerySystemInformation");
if (ZwQuerySystemInformation != NULL)
{
SYSTEM_BASIC_INFORMATION sbi = { 0 };
       //查询系统基本信息
NTSTATUS status = ZwQuerySystemInformation(SystemBasicInformation, (PVOID)&sbi, sizeof(sbi), NULL);
if (status == STATUS_SUCCESS)
{
wprintf(L"处理器个数:%d\r\n", sbi.NumberOfProcessors);
}
else
{
wprintf(L"ZwQuerySystemInfomation Error\n");
}

DWORD dwNeedSize = 0;
BYTE* pBuffer = NULL;

wprintf(L"\t----所有进程信息----\t\n");
PSYSTEM_PROCESS_INFORMATION psp = NULL;
       //查询进程数量
status = ZwQuerySystemInformation(SystemProcessInformation, NULL, 0, &dwNeedSize);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
pBuffer = new BYTE[dwNeedSize];
           //查询进程信息
status = ZwQuerySystemInformation(SystemProcessInformation, (PVOID)pBuffer, dwNeedSize, NULL);
if (status == STATUS_SUCCESS)
{
psp = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
wprintf(L"\tPID\t线程数\t工作集大小\t进程名\n");
do {
                   //获取进程号
wprintf(L"\t%d", psp->UniqueProcessId);
                   //获取线程数量
wprintf(L"\t%d", psp->NumberOfThreads);
                   //获取工作集大小
wprintf(L"\t%d", psp->WorkingSetSize / 1024);
                   //获取路径
wprintf(L"\t%s\n", psp->ImageName.Buffer);
                   //移动
psp = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)psp + psp->NextEntryOffset);
} while (psp->NextEntryOffset != 0);
delete[]pBuffer;
pBuffer = NULL;
}
else if (status == STATUS_UNSUCCESSFUL) {
wprintf(L"\n STATUS_UNSUCCESSFUL");
}
else if (status == STATUS_NOT_IMPLEMENTED) {
wprintf(L"\n STATUS_NOT_IMPLEMENTED");
}
else if (status == STATUS_INVALID_INFO_CLASS) {
wprintf(L"\n STATUS_INVALID_INFO_CLASS");
}
else if (status == STATUS_INFO_LENGTH_MISMATCH) {
wprintf(L"\n STATUS_INFO_LENGTH_MISMATCH");
}
}
}
}

进程隐藏

通过上述分析可以知道遍历进程的方式有三种,分别是利用CreateToolhelp32SnapshotEnumProcesses以及ZwQuerySystemInfomation函数

但是CreateToolhelp32SnapshotEnumProcesses函数底层都是调用了ZwQuerySystemInfomation函数,因此我们只需要钩取该函数即可。

由于测试环境是Win11,因此需要判断在Win11情况下底层是否还是调用了ZwQuerySystemInfomation函数。

可以看到在Win11下还是会调用ZwQuerySystemInfomation函数,在用户态下该函数的名称为NtQuerySystemInformation函数。

这里采用内联钩取的方式对ZwQuerySystemInfomation进行钩取处理,具体怎么钩取在浅谈内联钩取原理与实现已经介绍过了,这里就不详细说明了。这里对自定义的ZwQuerySystemInfomation函数进行说明。

首先第一步需要进行脱钩处理,因为后续需要用到初始的ZwQuerySystemInfomation函数,紧接着获取待钩取函数的地址即可。

...
   //脱钩
   UnHook("ntdll.dll", "ZwQuerySystemInformation", g_pOrgBytes);
   HMODULE hModule = GetModuleHandleA("ntdll.dll");
//获取待钩取函数的地址
   PROC    pfnOld = GetProcAddress(hModule, "ZwQuerySystemInformation");
//调用原始的ZwQuerySystemInfomation函数
   NTSTATUS status = ((NTQUERYSYSTEMINFORMATION)pfnOld)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
...

为了隐藏指定进程,我们需要遍历进程信息,找到目标进程并且删除该进程信息实现隐藏的效果。这里需要知道的是进程信息都存储在SYSTEM_PROCESS_INFORMATION结构体中,该结构体是通过单链表对进程信息进行链接。因此我们通过匹配进程名称找到对应的SYSTEM_PROCESS_INFORMATION结构体,然后进行删除即可,效果如下图。

通过单链表中删除节点的操作,取出目标进程的结构体。代码如下

...
pCur = (PSYSTEM_PROCESS_INFORMATION)(SystemInformation);
       while (true)
      {
           if (!lstrcmpi(pCur->ImageName.Buffer, L"test.exe"))
          {
               //需要隐藏的进程是最后一个节点
               if (pCur->NextEntryOffset == 0)
                   pPrev->NextEntryOffset = 0;
               //不是最后一个节点,则将该节点取出
               else
                   pPrev->NextEntryOffset += pCur->NextEntryOffset;

          }
           //不是需要隐藏的节点,则继续遍历
           else
               pPrev = pCur;
           //链表遍历完毕
           if (pCur->NextEntryOffset == 0)
               break;
           pCur = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)pCur + pCur->NextEntryOffset);
      }
...

完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/inlineHook.c

但是采用内联钩取的方法去钩取任务管理器就会出现一个问题,这里将断点取消,利用内联钩取的方式去隐藏进程。

首先利用bl命令查看断点

紧着利用 bc [ID]删除断点

在注入之后任务管理器会在拷贝的时候发生异常

在经过一番调试后发现,由于多线程共同执行导致原本需要可写权限的段被修改为只读权限

windbg可以用使用!vprot + address查看指定地址的权限,可以看到由于程序往只读权限的地址进行拷贝处理,所以导致了异常。

但是在执行拷贝阶段是先修改了该地址为可写权限,那么导致该原因的情况就是其他线程执行了权限恢复后切换到该线程中进行写,所以导致了这个问题。

因此内联钩取是存在多线程安全的问题,此时可以使用微软自己构建的钩取库Detours,可以在钩取过程中确保线程安全。

Detours

项目地址:https://github.com/microsoft/Detours

环境配置

参考:https://www.cnblogs.com/linxmouse/p/14168712.html

使用vcpkg下载

vcpkg.exe install detours:x86-windows
vcpkg.exe install detours:x64-windows
vcpkg.exe integrate install

实例

挂钩

利用Detours挂钩非常简单,只需要根据下列顺序,并且将自定义函数的地址与被挂钩的地址即可完成挂钩处理。

...
  //用于确保在 DLL 注入或加载时,恢复被 Detours 修改的进程镜像,保持稳定性
DetourRestoreAfterWith();
       //开始一个新的事务来附加或分离
       DetourTransactionBegin();
//进行线程上下文的更新
       DetourUpdateThread(GetCurrentThread());
//挂钩
       DetourAttach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
//提交事务
       error = DetourTransactionCommit();
...

脱钩

然后根据顺序完成脱钩即可。

...
  //开始一个新的事务来附加或分离
DetourTransactionBegin();
//进行线程上下文的更新
       DetourUpdateThread(GetCurrentThread());
//脱钩
       DetourDetach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
//提交事务
       error = DetourTransactionCommit();
...

挂钩的原理

从上述可以看到,Detours是通过事务确保了在DLL加载与卸载时后的原子性,但是如何确保多线程安全呢?后续通过调试去发现。

可以利用x ntdl!ZwQuerySystemInformation查看函数地址,可以看到函数的未被挂钩前的情况如下图。

挂钩之后原始的指令被修改为一个跳转指令把前八个字节覆盖掉,剩余的3字节用垃圾指令填充。

该地址里面又是一个jmp指令,并且完成间接寻址的跳转。

该地址是自定义函数ZwQuerySystemInformationEx,因此该间接跳转是跳转到的自定义函数内部。

跳转到TrueZwQuerySystemInformation内部发现ZwQuerySystemInformation函数内部的八字节指令被移动到该函数内部。紧接着又完成一个跳转。

该跳转到ZwQuerySystemInformation函数内部紧接着完成ZwQuerySystemInformation函数的调用。

综上所述,整体流程如下图。实际上Detours实际上使用的是热补丁的思路,但是Detours并不是直接在原始的函数空间中进行补丁,而是开辟了一段临时空间,将指令存储在里面。因此在挂钩后不需要进行脱钩处理就可以调用原始函数。因此就不存在多线程中挂钩与脱钩的冲突。

完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/detoursHook.c