0. 前言
上篇文章中我使用PPLDump在services.exe这个PPL级别为WinTcb-Light
的进程中进行shellcode注入,并成功在诸如wininit.exe或MsMpEng.exe等进程中上线CS
但上线后却无法创建进程,CS中一切需要启动新进程的操作都无法完成 (诸如shell / execute / execute-assembly等等) ,但不需要创建进程的操作如文件系统访问 / 进程注入都正常
经过昨天的研究我找到了原因并写了一个BOF来解决这个问题,本文将阐述产生这个问题的原因
1. 从MSDN入手
创建进程时返回Error Code 87,为Invalid Parameters。所以我仔细查看了MSDN文档中kernel32!CreateProcess
每个参数的说明,发现了bInheritHandles
参数有下述说明
也就是说,一个PPL进程中启动非PPL进程时是不能要求子进程继承句柄的。因为services.exe是PPL进程,而cmd.exe不是,所以在调用kernel32!CreateProcess
时该参数必须设置为FALSE,否则就会返回Error Code 87
2. beacon中的CreateProcess
我测试了beacon中所有的创建进程的操作,都是将该参数设置为TRUE的。至于为什么这样做,是因为像shell命令需要这样来获取命令执行的输出
beacon获取命令执行输出是通过kernel32!CreatePipe
创建匿名管道,然后将子进程的stdout设置为该匿名管道,父进程从管道中读取子进程,但这样做的前提是,bInheritHandles
参数需要为TRUE,这样子进程才能继承到匿名管道的句柄。这种利用匿名管道在父子进程间通信的例子借用之前写的一个项目https://github.com/EddieIvan01/win32api-practice/blob/master/PrintSpoofer-webshell/PrintSpoofer/PrintSpoofer.cpp#L443
BOOL CreateProcessWithOutput(HANDLE hToken) {
BOOL result;
DWORD SessionId;
PROCESS_INFORMATION pi;
STARTUPINFO si;
SECURITY_ATTRIBUTES sa = { 0 };
sa.bInheritHandle = TRUE;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
ZeroMemory(&si, sizeof(STARTUPINFO));
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
DWORD sessionId = WTSGetActiveConsoleSessionId();
HANDLE hReadPipe = NULL;
HANDLE hWritePipe = NULL;
result = CreatePipe(&hReadPipe, &hWritePipe, &sa, 0);
if (!result) {
printf("n[-] CreatePipe error: %dn", GetLastError());
return result;
}
si.cb = sizeof(si);
si.hStdError = hWritePipe;
si.hStdOutput = hWritePipe;
si.wShowWindow = SW_HIDE;
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.lpDesktop = (LPWSTR)L"winsta0\default";
fflush(stdout);
wchar_t command[256];
wcscpy_s(command, L"cmd.exe /c ");
wcsncat_s(command, g_pwszCommandLine, wcslen(g_pwszCommandLine));
LPWSTR pwszCurrentDirectory = (LPWSTR)malloc(sizeof(WCHAR) * 512);
GetCurrentDirectoryW(512, pwszCurrentDirectory);
if (!CreateProcessWithTokenW(hToken, LOGON_WITH_PROFILE, NULL, command, 0, NULL, pwszCurrentDirectory, &si, &pi)) {
wprintf(L"CreateProcessWithTokenW() failed. Error: %dn", GetLastError());
goto CLEANUP;
} else {
wprintf(L"[+] CreateProcessWithTokenW() OKn");
goto RECV_OUTPUT;
}
CLEANUP:
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(hReadPipe);
free(pwszCurrentDirectory);
return result;
RECV_OUTPUT:
const int dwResultBufferSize = 1024;
// const DWORD timeout = 1000 * 10;
char pszResultBuffer[dwResultBufferSize];
// WaitForSingleObject(pi.hProcess, timeout);
CloseHandle(hWritePipe);
printf("n====================CMD===================n%wsn", command);
printf("n==================OUTPUT==================n");
DWORD n = 0;
do {
RtlZeroMemory(pszResultBuffer, dwResultBufferSize);
if (!ReadFile(hReadPipe, pszResultBuffer, dwResultBufferSize, &n, NULL)) break;
printf("%s", pszResultBuffer);
// if (pszResultBuffer[n] == EOF) break;
} while (n);
printf("n==========================================");
goto CLEANUP;
};
3. 解决方法
解决方法无非有两种:
第一是魔改CS的代码,把继承句柄的参数给改为FALSE,但这样会影响像shell这种命令正常获取输出
第二是通过BOF来实现创建进程并获取输出,因为BOF是在当前进程空间加载执行,所以可以完美解决
这里有一个问题,如何获取进程输出?因为前面提到子进程无法继承父进程句柄,所以最优解匿名管道无法使用。那么还有写文件 / 命名管道 / 共享内存 / 套接字等方法,我暂时使用了写文件这个最简单的办法,后续再根据实际情况进行优化
4. 最终效果
我注册了一个tshell命令来调用BOF,用法和shell一样
注入到高PPL级别的进程例如WinTcb-Light
级别的wininit.exe可以防止被内存扫描,因为像Windows Defender / 腾讯管家等的服务进程都是Antimalware-Light
级别,是无法打开WinTcb-Light
级别进程句柄进行扫描的
另外注入到系统关键进程和AV自身的服务进程的效果也是不言自明
注入到wininit.exe
注入到Windows Defender服务进程MsMpEng.exe
原文始发于微信公众号(0x4d5a):解决PPL注入技术的bug
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论