BX2trace 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bx2trace/__init__.py +70 -0
- bx2trace/core/__init__.py +0 -0
- bx2trace/core/api_hooker.py +192 -0
- bx2trace/core/constants.py +124 -0
- bx2trace/core/debug_thread.py +543 -0
- bx2trace/core/memory_scanner.py +173 -0
- bx2trace/core/models.py +183 -0
- bx2trace/core/process_launcher.py +135 -0
- bx2trace/core/win_structs.py +250 -0
- bx2trace/extractors/__init__.py +0 -0
- bx2trace/extractors/entropy.py +51 -0
- bx2trace/extractors/strings.py +59 -0
- bx2trace/memory/__init__.py +0 -0
- bx2trace/memory/reader.py +180 -0
- bx2trace-1.0.0.dist-info/METADATA +156 -0
- bx2trace-1.0.0.dist-info/RECORD +19 -0
- bx2trace-1.0.0.dist-info/WHEEL +5 -0
- bx2trace-1.0.0.dist-info/licenses/LICENSE +21 -0
- bx2trace-1.0.0.dist-info/top_level.txt +1 -0
bx2trace/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# bx2trace - Windows Process Tracer & Live Memory Extractor
|
|
2
|
+
# =========================================================
|
|
3
|
+
|
|
4
|
+
# Core contracts & data types
|
|
5
|
+
from bx2trace.core.models import (
|
|
6
|
+
TraceEvent,
|
|
7
|
+
TraceMode,
|
|
8
|
+
Severity,
|
|
9
|
+
EngineConfig,
|
|
10
|
+
BaseDetector,
|
|
11
|
+
CURRENT_SCHEMA_VERSION,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Execution engines
|
|
15
|
+
from bx2trace.core.debug_thread import DebugThread, TrackedProcess
|
|
16
|
+
from bx2trace.core.memory_scanner import MemoryScannerThread
|
|
17
|
+
from bx2trace.core.process_launcher import (
|
|
18
|
+
create_process_debug,
|
|
19
|
+
ArchitectureMismatchError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Raw memory access
|
|
23
|
+
from bx2trace.memory.reader import (
|
|
24
|
+
open_process_for_dump,
|
|
25
|
+
enumerate_regions,
|
|
26
|
+
read_region,
|
|
27
|
+
read_committed_regions,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Extractors (platform-agnostic)
|
|
31
|
+
from bx2trace.extractors.strings import (
|
|
32
|
+
extract_strings,
|
|
33
|
+
extract_ascii_strings,
|
|
34
|
+
extract_utf16le_strings,
|
|
35
|
+
diff_new_strings,
|
|
36
|
+
URL_PATTERN,
|
|
37
|
+
IPV4_PATTERN,
|
|
38
|
+
)
|
|
39
|
+
from bx2trace.extractors.entropy import (
|
|
40
|
+
shannon_entropy,
|
|
41
|
+
entropy_by_page,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Useful constants
|
|
45
|
+
from bx2trace.core.constants import (
|
|
46
|
+
PAGE_EXECUTE_READWRITE,
|
|
47
|
+
PAGE_EXECUTE_READ,
|
|
48
|
+
PAGE_READWRITE,
|
|
49
|
+
PAGE_READONLY,
|
|
50
|
+
MEM_COMMIT,
|
|
51
|
+
PROTECTIONS_OF_INTEREST,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__version__ = "0.1.0"
|
|
55
|
+
__author__ = "bx2trace contributors"
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"TraceEvent", "TraceMode", "Severity", "EngineConfig", "BaseDetector",
|
|
59
|
+
"CURRENT_SCHEMA_VERSION",
|
|
60
|
+
"DebugThread", "TrackedProcess", "MemoryScannerThread",
|
|
61
|
+
"create_process_debug", "ArchitectureMismatchError",
|
|
62
|
+
"open_process_for_dump", "enumerate_regions",
|
|
63
|
+
"read_region", "read_committed_regions",
|
|
64
|
+
"extract_strings", "extract_ascii_strings", "extract_utf16le_strings",
|
|
65
|
+
"diff_new_strings", "URL_PATTERN", "IPV4_PATTERN",
|
|
66
|
+
"shannon_entropy", "entropy_by_page",
|
|
67
|
+
"PAGE_EXECUTE_READWRITE", "PAGE_EXECUTE_READ",
|
|
68
|
+
"PAGE_READWRITE", "PAGE_READONLY",
|
|
69
|
+
"MEM_COMMIT", "PROTECTIONS_OF_INTEREST",
|
|
70
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/api_hooker.py
|
|
3
|
+
==================
|
|
4
|
+
Management of API hooks using the INT3 (0xCC) software breakpoint method.
|
|
5
|
+
Provides resolution of API addresses and management of the original bytes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ctypes
|
|
9
|
+
from ctypes import wintypes
|
|
10
|
+
from typing import Dict, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HookManager:
|
|
14
|
+
"""
|
|
15
|
+
Manages API hooks for a debugged process.
|
|
16
|
+
Handles address resolution, INT3 placement, and original byte tracking.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.hooks = {} # (pid, addr) -> byte # Address -> Original Byte
|
|
21
|
+
self.api_names: Dict[int, str] = {} # Address -> API Name
|
|
22
|
+
self.kernel32 = ctypes.windll.kernel32
|
|
23
|
+
|
|
24
|
+
def resolve_api(self, dll_name: str, function_name: str) -> Optional[int]:
|
|
25
|
+
"""
|
|
26
|
+
Resolves the address of an API in the target process.
|
|
27
|
+
Since system DLLs are loaded at the same address across processes (ASLR),
|
|
28
|
+
we can resolve it locally.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
h_module = self.kernel32.GetModuleHandleW(dll_name)
|
|
32
|
+
if not h_module:
|
|
33
|
+
h_module = self.kernel32.LoadLibraryW(dll_name)
|
|
34
|
+
|
|
35
|
+
if not h_module:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
addr = self.kernel32.GetProcAddress(h_module, function_name.encode('ascii'))
|
|
39
|
+
if addr == 0:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# Check for JMP/Forwarder (simple check)
|
|
43
|
+
first_byte = ctypes.c_ubyte()
|
|
44
|
+
ctypes.memmove(ctypes.byref(first_byte), addr, 1)
|
|
45
|
+
if first_byte.value == 0xE9: # JMP
|
|
46
|
+
# This is a jump, we might be hitting a hot-patch point or a wrapper
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
return addr
|
|
50
|
+
except Exception:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def apply_hook(self, process_handle: int, address: int, api_name: str, pid: int) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Places an INT3 (0xCC) hook at the specified address.
|
|
56
|
+
Backups the original byte for restoration later.
|
|
57
|
+
"""
|
|
58
|
+
if (pid, address) in self.hooks:
|
|
59
|
+
return True # Already hooked
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Read original byte
|
|
63
|
+
old_byte = ctypes.c_ubyte()
|
|
64
|
+
bytes_read = ctypes.c_size_t()
|
|
65
|
+
if not self.kernel32.ReadProcessMemory(
|
|
66
|
+
process_handle,
|
|
67
|
+
ctypes.c_void_p(address),
|
|
68
|
+
ctypes.byref(old_byte),
|
|
69
|
+
1,
|
|
70
|
+
ctypes.byref(bytes_read)
|
|
71
|
+
) or bytes_read.value != 1:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
# Check if it's already 0xCC (maybe another debugger or already hooked)
|
|
75
|
+
if old_byte.value == 0xCC:
|
|
76
|
+
# Still store it so we can handle the breakpoint if it's hit
|
|
77
|
+
if (pid, address) not in self.hooks:
|
|
78
|
+
self.hooks[(pid, address)] = 0xCC # We don't know original, but we track it
|
|
79
|
+
self.api_names[address] = api_name
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
# Ensure memory is writable (VirtualProtectEx)
|
|
83
|
+
old_protect = wintypes.DWORD()
|
|
84
|
+
PAGE_EXECUTE_READWRITE = 0x40
|
|
85
|
+
if not self.kernel32.VirtualProtectEx(
|
|
86
|
+
process_handle,
|
|
87
|
+
ctypes.c_void_p(address),
|
|
88
|
+
1,
|
|
89
|
+
PAGE_EXECUTE_READWRITE,
|
|
90
|
+
ctypes.byref(old_protect)
|
|
91
|
+
):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# Write 0xCC (INT3)
|
|
95
|
+
int3 = ctypes.c_ubyte(0xCC)
|
|
96
|
+
bytes_written = ctypes.c_size_t()
|
|
97
|
+
|
|
98
|
+
success = self.kernel32.WriteProcessMemory(
|
|
99
|
+
process_handle,
|
|
100
|
+
ctypes.c_void_p(address),
|
|
101
|
+
ctypes.byref(int3),
|
|
102
|
+
1,
|
|
103
|
+
ctypes.byref(bytes_written)
|
|
104
|
+
) and bytes_written.value == 1
|
|
105
|
+
|
|
106
|
+
# Restore protection
|
|
107
|
+
temp_protect = wintypes.DWORD()
|
|
108
|
+
self.kernel32.VirtualProtectEx(
|
|
109
|
+
process_handle,
|
|
110
|
+
ctypes.c_void_p(address),
|
|
111
|
+
1,
|
|
112
|
+
old_protect,
|
|
113
|
+
ctypes.byref(temp_protect)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if not success:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
self.hooks[(pid, address)] = old_byte.value
|
|
120
|
+
self.api_names[address] = api_name
|
|
121
|
+
|
|
122
|
+
# Flush instruction cache
|
|
123
|
+
self.kernel32.FlushInstructionCache(process_handle, ctypes.c_void_p(address), 1)
|
|
124
|
+
|
|
125
|
+
# Verify write
|
|
126
|
+
verify_byte = ctypes.c_ubyte()
|
|
127
|
+
self.kernel32.ReadProcessMemory(process_handle, ctypes.c_void_p(address), ctypes.byref(verify_byte), 1,
|
|
128
|
+
None)
|
|
129
|
+
if verify_byte.value != 0xCC:
|
|
130
|
+
# print(f"Verification failed at {hex(address)}: expected 0xCC, got {hex(verify_byte.value)}")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
return True
|
|
134
|
+
except Exception:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
def remove_hook(self, process_handle: int, address: int, pid: int) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Restores the original byte at the specified address.
|
|
140
|
+
"""
|
|
141
|
+
if (pid, address) not in self.hooks:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Ensure memory is writable (VirtualProtectEx)
|
|
146
|
+
old_protect = wintypes.DWORD()
|
|
147
|
+
PAGE_EXECUTE_READWRITE = 0x40
|
|
148
|
+
if not self.kernel32.VirtualProtectEx(
|
|
149
|
+
process_handle,
|
|
150
|
+
ctypes.c_void_p(address),
|
|
151
|
+
1,
|
|
152
|
+
PAGE_EXECUTE_READWRITE,
|
|
153
|
+
ctypes.byref(old_protect)
|
|
154
|
+
):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
original_byte = ctypes.c_ubyte(self.hooks[(pid, address)])
|
|
158
|
+
bytes_written = ctypes.c_size_t()
|
|
159
|
+
|
|
160
|
+
success = self.kernel32.WriteProcessMemory(
|
|
161
|
+
process_handle,
|
|
162
|
+
ctypes.c_void_p(address),
|
|
163
|
+
ctypes.byref(original_byte),
|
|
164
|
+
1,
|
|
165
|
+
ctypes.byref(bytes_written)
|
|
166
|
+
) and bytes_written.value == 1
|
|
167
|
+
|
|
168
|
+
# Restore protection
|
|
169
|
+
temp_protect = wintypes.DWORD()
|
|
170
|
+
self.kernel32.VirtualProtectEx(
|
|
171
|
+
process_handle,
|
|
172
|
+
ctypes.c_void_p(address),
|
|
173
|
+
1,
|
|
174
|
+
old_protect,
|
|
175
|
+
ctypes.byref(temp_protect)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if not success:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
self.kernel32.FlushInstructionCache(process_handle, ctypes.c_void_p(address), 1)
|
|
182
|
+
return True
|
|
183
|
+
except Exception:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def is_our_hook(self, address: int, pid: int) -> bool:
|
|
187
|
+
"""Checks if the breakpoint hit is one of our hooks."""
|
|
188
|
+
return (pid, address) in self.hooks
|
|
189
|
+
|
|
190
|
+
def get_api_name(self, address: int) -> str:
|
|
191
|
+
"""Returns the name of the API at the hooked address."""
|
|
192
|
+
return self.api_names.get(address, "UnknownAPI")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/constants.py
|
|
3
|
+
==================
|
|
4
|
+
All raw Windows API constants in one place.
|
|
5
|
+
Rule: Any magic number in the code is written here only once, and the rest of the files import it by name.
|
|
6
|
+
|
|
7
|
+
Reference source for all these values: winbase.h / winnt.h (Official Microsoft Documentation).
|
|
8
|
+
Do not modify a value here unless verified against official documentation — any error here fails silently
|
|
9
|
+
in a completely different place from this file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# ============================================================
|
|
13
|
+
# Process Creation Flags (Passed to CreateProcessW)
|
|
14
|
+
# ============================================================
|
|
15
|
+
|
|
16
|
+
# ⚠️ CRITICAL FIX: The following two values are easily inverted by mistake because they are numerically sequential.
|
|
17
|
+
DEBUG_PROCESS = 0x00000001
|
|
18
|
+
# ↑ Monitors the launched process + every child process it spawns (via CreateProcess internally)
|
|
19
|
+
# automatically, without any extra effort. This is the correct choice for our project.
|
|
20
|
+
|
|
21
|
+
DEBUG_ONLY_THIS_PROCESS = 0x00000002
|
|
22
|
+
# ↑ Monitors only the launched process. Any child process it spawns runs completely free
|
|
23
|
+
# without any monitoring or reporting. Only use this if you intentionally want to ignore children.
|
|
24
|
+
|
|
25
|
+
CREATE_NEW_CONSOLE = 0x00000010
|
|
26
|
+
CREATE_SUSPENDED = 0x00000004
|
|
27
|
+
# ↑ Useful in the future: starts the process "frozen" before executing any instruction, giving you a chance
|
|
28
|
+
# to set Hardware Breakpoints before anything runs at all.
|
|
29
|
+
|
|
30
|
+
# ============================================================
|
|
31
|
+
# ContinueDebugEvent — How we respond to each event
|
|
32
|
+
# ============================================================
|
|
33
|
+
|
|
34
|
+
DBG_CONTINUE = 0x00010002
|
|
35
|
+
# ↑ "Handle this exception as if it didn't happen" — we use this for our own events
|
|
36
|
+
# (like Single-Step from a Hardware Breakpoint we set).
|
|
37
|
+
|
|
38
|
+
DBG_EXCEPTION_NOT_HANDLED = 0x80010001
|
|
39
|
+
# ↑ "Pass this exception to the target program's own exception handler"
|
|
40
|
+
# This is the correct default for any exception we didn't intentionally cause — many Packers
|
|
41
|
+
# use intentional exceptions as part of their unpacking mechanism, and if you accidentally
|
|
42
|
+
# reply DBG_CONTINUE, the target program crashes because its internal handler
|
|
43
|
+
# never received the exception it was expecting.
|
|
44
|
+
|
|
45
|
+
INFINITE = 0xFFFFFFFF
|
|
46
|
+
|
|
47
|
+
# ============================================================
|
|
48
|
+
# Debug Event Type Codes (DEBUG_EVENT.dwDebugEventCode)
|
|
49
|
+
# ============================================================
|
|
50
|
+
|
|
51
|
+
EXCEPTION_DEBUG_EVENT = 1
|
|
52
|
+
CREATE_THREAD_DEBUG_EVENT = 2
|
|
53
|
+
CREATE_PROCESS_DEBUG_EVENT = 3
|
|
54
|
+
EXIT_THREAD_DEBUG_EVENT = 4
|
|
55
|
+
EXIT_PROCESS_DEBUG_EVENT = 5
|
|
56
|
+
LOAD_DLL_DEBUG_EVENT = 6
|
|
57
|
+
UNLOAD_DLL_DEBUG_EVENT = 7
|
|
58
|
+
OUTPUT_DEBUG_STRING_EVENT = 8
|
|
59
|
+
RIP_EVENT = 9
|
|
60
|
+
|
|
61
|
+
EVENT_CODE_NAMES = {
|
|
62
|
+
EXCEPTION_DEBUG_EVENT: "EXCEPTION",
|
|
63
|
+
CREATE_THREAD_DEBUG_EVENT: "CREATE_THREAD",
|
|
64
|
+
CREATE_PROCESS_DEBUG_EVENT: "CREATE_PROCESS",
|
|
65
|
+
EXIT_THREAD_DEBUG_EVENT: "EXIT_THREAD",
|
|
66
|
+
EXIT_PROCESS_DEBUG_EVENT: "EXIT_PROCESS",
|
|
67
|
+
LOAD_DLL_DEBUG_EVENT: "LOAD_DLL",
|
|
68
|
+
UNLOAD_DLL_DEBUG_EVENT: "UNLOAD_DLL",
|
|
69
|
+
OUTPUT_DEBUG_STRING_EVENT: "OUTPUT_DEBUG_STRING",
|
|
70
|
+
RIP_EVENT: "RIP",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# ============================================================
|
|
74
|
+
# Common Exception Codes (EXCEPTION_RECORD.ExceptionCode)
|
|
75
|
+
# ============================================================
|
|
76
|
+
|
|
77
|
+
EXCEPTION_BREAKPOINT = 0x80000003
|
|
78
|
+
EXCEPTION_SINGLE_STEP = 0x80000004
|
|
79
|
+
# ↑ We receive this code after any Hardware Breakpoint we set (DR0-DR3) —
|
|
80
|
+
# important for the future "function tracing" feature.
|
|
81
|
+
EXCEPTION_ACCESS_VIOLATION = 0xC0000005
|
|
82
|
+
|
|
83
|
+
# ============================================================
|
|
84
|
+
# CONTEXT Flags (x64)
|
|
85
|
+
# ============================================================
|
|
86
|
+
CONTEXT_AMD64 = 0x00100000
|
|
87
|
+
CONTEXT_CONTROL = CONTEXT_AMD64 | 0x00000001
|
|
88
|
+
CONTEXT_INTEGER = CONTEXT_AMD64 | 0x00000002
|
|
89
|
+
CONTEXT_SEGMENTS = CONTEXT_AMD64 | 0x00000004
|
|
90
|
+
CONTEXT_FLOATING_POINT = CONTEXT_AMD64 | 0x00000008
|
|
91
|
+
CONTEXT_DEBUG_REGISTERS = CONTEXT_AMD64 | 0x00000010
|
|
92
|
+
CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT
|
|
93
|
+
CONTEXT_ALL = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS | CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS
|
|
94
|
+
|
|
95
|
+
# ============================================================
|
|
96
|
+
# Memory Protections (VirtualQueryEx / VirtualProtect)
|
|
97
|
+
# ============================================================
|
|
98
|
+
|
|
99
|
+
PAGE_NOACCESS = 0x01
|
|
100
|
+
PAGE_READONLY = 0x02
|
|
101
|
+
PAGE_READWRITE = 0x04
|
|
102
|
+
PAGE_EXECUTE = 0x10
|
|
103
|
+
PAGE_EXECUTE_READ = 0x20
|
|
104
|
+
PAGE_EXECUTE_READWRITE = 0x40
|
|
105
|
+
# ↑ This specific combination (Executable + Writable at the same time) is the most important
|
|
106
|
+
# "Red Flag" for catching self-unpacking moments — very few legitimate programs
|
|
107
|
+
# need a page with this combination during normal execution.
|
|
108
|
+
|
|
109
|
+
MEM_COMMIT = 0x1000
|
|
110
|
+
MEM_FREE = 0x10000
|
|
111
|
+
|
|
112
|
+
PROTECTIONS_OF_INTEREST = {
|
|
113
|
+
PAGE_EXECUTE_READWRITE,
|
|
114
|
+
PAGE_READWRITE,
|
|
115
|
+
PAGE_EXECUTE_READ,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# ============================================================
|
|
119
|
+
# OpenProcess Access Rights (Used in MEMORY_DUMP mode only — already running process)
|
|
120
|
+
# ============================================================
|
|
121
|
+
|
|
122
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
|
123
|
+
PROCESS_VM_READ = 0x0010
|
|
124
|
+
PROCESS_DUMP_ACCESS = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
|