Activating Chrome DevTools Protocol in Memory: Bypassing –remote-debugging-port Restrictions

September 2026

Introduction

The Chrome Debugging Protocol (CDP) has long been a known offensive vector during red team engagements. Over the years it has been heavily abused, particularly by infostealers, to extract cookies and other session-relevant data from running browser instances.

CDP limitations

What CDP can do:
  • Dump cookies, browsing history, and autofill entries
  • Act as an in-browser RAT on the victim’s machine
  • Perform browser pivoting during post-exploitation
  • Tunnel C2 egress through WebSocket channels
What CDP cannot do:
  • Extract saved logins
  • Read credit card numbers
  • Other credential material
(these reside in a dedicated on-disk store protected by App-Bound Encryption)

How it is traditionally used

The operator launches chrome.exe from the command line with the debugging flag:

Start-Process "Chrome" "--remote-debugging-port=9222"

This binds the CDP listener to port 9222, reachable at localhost:9222.
From here, several endpoints become available. For example:

GET /json/version

 


Port 9222 is bound exclusively to localhost, which by itself is useless to a remote operator. A port-forward is needed to expose the interface externally. The following command, executed on the target, does the job:

netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=48333 connectaddress=127.0.0.1 connectport=9222

This proxies all traffic from 0.0.0.0:48333 into 127.0.0.1:9222.

The host firewall will likely block inbound connections on the new port, so a rule must be added:

At this point CDP is accessible remotely through:

http://<target_ip>:48333

Alternatively, if the implant supports a SOCKS Proxy or (Reverse Port Forwarding), all of this can be avoided in favor of a more OPSEC-safe approach: the implant communicates directly with 127.0.0.1:9222 over the loopback interface, and the operator reaches it through the SOCKS tunnel.

Why this is a problem

Google has grown increasingly aware of how CDP is being abused and has started shipping mitigations: https://developer.chrome.com/blog/remote-debugging-port

The core restriction: when chrome.exe is invoked with --remote-debugging-port, a non-default --user-data-dir must also be specified. A non-default user data directory is an empty profile. It contains none of the cookies, sessions, autofill data, or stored credentials that make CDP worthwhile in the first place.

A second issue is detectability. Spawning a new chrome.exe process with --remote-debugging-port on its command line is trivially flagged by any decent EDR.

How we solved it

We need a way to activate CDP without ever passing --remote-debugging-port as a command-line flag.

The technique described below activates CDP directly in memory, inside the already-running Chrome process.

Chrome’s core implementation lives inside chrome.dll. Specific functions and structures within this module are invoked when --remote-debugging-port is present on the command line.


  1. DevToolsAgentHost::StartRemoteDebuggingServer  https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc;l=215

  2. std::unique_ptr<DevToolsSocketFactory> is an abstract C++ class exposing a virtual method called CreateForHttpServer. The shellcode needs to fabricate a convincing fake of this object.

  3. Internally, Chrome relies on its custom allocator, PartitionAlloc (https://chromium.googlesource.com/chromium/src/+/master/base/allocator/partition_allocator/PartitionAlloc.md), whenever it allocates a DevToolsSocketFactory instance.

chrome.dll does not make our life easy: none of the functions we need are exported. One reliable way to locate them at runtime is by scanning the module’s memory for their byte-level signatures.

Byte-signature scanning

Three things are required:

  • The byte signature for DevToolsAgentHost::StartRemoteDebuggingServer
  • The vtable address for content::DevToolsSocketFactory
  • The entry point of the PartitionAlloc allocator function

A WinDbg query to enumerate every StartRemoteDebugging-related symbol inside chrome.dll:

x chrome!*StartRemoteDebugging*

Disassembly then reveals the signature we need:

For content::DevToolsSocketFactory, we look for the signature of CreateForHttpServer, a virtual function entry in the vftable. Once we resolve this function’s address at runtime, we can work backwards to locate the vtable itself.

TCPServerSocketFactory can substitute for DevToolsSocketFactory as a vtable donor.

We do not actually need CreateForHttpServer itself; we need the vtable that contains it. How do we get from the function address to its parent vtable? Vtables are allocated in the .rdata section, so we scan the entire .rdata region looking for the resolved function address. When we find a match, we know we are inside the correct vtable.

To determine where .rdata begins: chrome.dll base address + .rdata RVA.

00007ffdd0ec1d68 00007ffdc48c30d0 00007ffdc75e4060

These should be the function addresses we are after. To verify, we dump 8 bytes before the first pointer:

This is the TCPServerSocketFactory vtable. The different symbol names are a side effect of Identical COMDAT Folding (ICF) in the PDB: multiple symbols map to the same address.

As a final confirmation, ln (list nearest symbols) resolves the address:

ln 00007ffdc2dacc60

To locate PartitionAlloc, we scan for the signature of an internal Chrome function called chrome!malloc. Allocating through that wrapper works just as well for our purposes.

Architecture

The post-exploitation module is split into two components:

  • A loader
  • A shellcode payload

Loader

The loader’s job is to inject the shellcode into the PPID (Parent Process ID) of chrome.exe.

It is critical that the shellcode runs within the PPID’s address space and executes from its main thread: chrome.exe!WinMainCRTStartup.

Hijacking the main thread, however, is a fragile and crash-prone operation. We need a mechanism that lets the thread execute our code without being suspended or interrupted in its normal routine.

Special APCs are exactly what this calls for. Unlike regular APCs, which require the target thread to enter an alertable wait state, Special APCs execute asynchronously on a thread even while it is actively running.

The loader therefore follows these steps:

  1. Identify the chrome.exe PPID (or accept it as an argument)
  2. Standard injection primitive: OpenProcess(), VirtualAllocEx(), WriteProcessMemory()
  3. Locate the main thread inside the PPID (typically the first thread spawned by the process)
  4. Resolve NtQueueApcThreadEx and enqueue the Special APC targeting that main thread

https://repnz.github.io/posts/apc/user-apc/

Shellcode

The shellcode begins by resolving at runtime all the modules and functions it depends on (including chrome.dll), then launches the signature-scanning phase.

Example of a signature pattern:

Given a signature and a module base address, a scanning routine resolves the .text section boundaries of chrome.dll and walks forward from the section start until it matches the supplied pattern:

The signature can reside anywhere within the target function; it is not anchored to the prologue or epilogue. Once a match is found, RtlLookupFunctionEntry() is used to recover the function’s actual start address from the address of the matched signature.

After all required addresses have been resolved, the next step is to craft the fake server_socket_factory that StartRemoteDebuggingServer expects as its argument. Including padding, the object is 16 bytes wide, so chrome!malloc must allocate exactly that amount.

Allocating through chrome!malloc is non-negotiable. Using HeapAlloc, the CRT malloc, or any external allocator will crash Chrome, because PartitionAlloc enforces strict metadata checks on every pointer it later frees.

The layout of the allocated object:

+0x00: [vtable_addr]  → points to TCPServerSocketFactory::vftable in .rdata
+0x08: [0x2406]       → 9222 in little-endian
+0x0A: [0x0000...]    → padding (zeroed)

At this point, StartRemoteDebuggingServer can finally be called, and CDP comes alive inside the existing Chrome process, without a single command-line flag having been touched.

Conclusion

The technique works reliably across multiple Chrome major releases and extends to other Chromium-based browsers such as Brave.

Source code of the proof of concept: https://github.com/Nuf1p/RepentanceStealer/

The entire procedure, from injection to a live CDP session, has been recorded and demonstrated end-to-end. Watch the full walkthrough here: Activating Chrome DevTools Protocol in Memory: Bypassing –remote-debugging-port Restrictions

The feature is available now in ZAIUX® Framework. Are you curious to see it in action?