浅谈进程隐藏技术
2025-1-15 08:10:38 Author: www.secpulse.com(查看原文) 阅读量:4 收藏

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

● 浅谈调试模式钩取

● 浅谈热补丁

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

● 导入地址表钩取技术

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

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

CreateToolhelp32Snapshot

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

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

int main()
{
   
   setlocale(LC_ALL, "zh_CN.UTF-8");
   
   HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
   if (hSnapshot == INVALID_HANDLE_VALUE)
   {
       std::cout << "Create Error" << std::endl;
       exit(-1);
   }

   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   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++)
   {
       
       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");
   
   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,可以在钩取过程中确保线程安全。

项目地址: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挂钩非常简单,只需要根据下列顺序,并且将自定义函数的地址与被挂钩的地址即可完成挂钩处理。

...
       
       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

本文作者:[email protected]

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/205188.html


文章来源: https://www.secpulse.com/archives/205188.html
如有侵权请联系:admin#unsafe.sh