Skip to content

Commit bfef26f

Browse files
committed
Add write tracer, search history, DMA health monitoring, and UI improvements
- Write Tracer panel: reverse control flow analysis to find memory write handlers with direct writes table and call graph tree view - Search history: persistent query history with dropdown in pattern/memory/disasm panels - DMA health monitoring: periodic test reads with connection status banner - Data type inspector sidebar in memory viewer - Toast notification system for async operation feedback - Cached process list with dirty-flag pattern to avoid per-frame sort - Disassembler refactored to use RAII (unique_ptr) for Zydis decoder/formatter - Thread safety fixes for async progress state (mutex + atomics) - Safe hex parsing via ParseHexAddress() replacing raw strtoull calls - OpenGL texture leak fix in shutdown path - CS2 schema type safety improvements
1 parent 4165f3e commit bfef26f

27 files changed

Lines changed: 1617 additions & 145 deletions

mcp_bridge.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,28 @@ const MCP_TOOLS = [
210210
required: ['pid', 'address', 'size']
211211
}
212212
},
213+
{
214+
name: 'write_memory',
215+
description: 'Write data to process memory via DMA. Takes a hex string of bytes to write at the specified address. Returns the address and number of bytes written. Requires allow_write to be enabled in config.',
216+
inputSchema: {
217+
type: 'object',
218+
properties: {
219+
pid: {
220+
type: 'integer',
221+
description: 'Process ID to write to'
222+
},
223+
address: {
224+
type: 'string',
225+
description: 'Memory address in hexadecimal (e.g., "0x7FF600000000" or "7FF600000000")'
226+
},
227+
data: {
228+
type: 'string',
229+
description: 'Hex string of bytes to write (e.g., "90 90 90" or "909090"). Spaces optional.'
230+
}
231+
},
232+
required: ['pid', 'address', 'data']
233+
}
234+
},
213235
{
214236
name: 'scan_pattern',
215237
description: 'Scan for byte pattern in process memory using IDA-style pattern syntax. Returns array of matching addresses.',
@@ -1654,6 +1676,7 @@ const TOOL_ENDPOINT_MAP = {
16541676
'get_health': { method: 'GET', path: '/health' },
16551677
'get_version': { method: 'GET', path: '/version' },
16561678
'read_memory': { method: 'POST', path: '/tools/read_memory' },
1679+
'write_memory': { method: 'POST', path: '/tools/write_memory' },
16571680
'scan_pattern': { method: 'POST', path: '/tools/scan_pattern' },
16581681
'scan_pattern_async': { method: 'POST', path: '/tools/scan_pattern_async' },
16591682
'scan_strings': { method: 'POST', path: '/tools/scan_strings' },

src/analysis/disassembler.cpp

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,56 +19,45 @@ Disassembler::~Disassembler() {
1919

2020
Disassembler::Disassembler(Disassembler&& other) noexcept
2121
: is_64bit_(other.is_64bit_)
22-
, decoder_(other.decoder_)
23-
, formatter_(other.formatter_) {
24-
other.decoder_ = nullptr;
25-
other.formatter_ = nullptr;
22+
, decoder_(std::move(other.decoder_))
23+
, formatter_(std::move(other.formatter_)) {
2624
}
2725

2826
Disassembler& Disassembler::operator=(Disassembler&& other) noexcept {
2927
if (this != &other) {
30-
CleanupDecoder();
3128
is_64bit_ = other.is_64bit_;
32-
decoder_ = other.decoder_;
33-
formatter_ = other.formatter_;
34-
other.decoder_ = nullptr;
35-
other.formatter_ = nullptr;
29+
decoder_ = std::move(other.decoder_);
30+
formatter_ = std::move(other.formatter_);
3631
}
3732
return *this;
3833
}
3934

4035
void Disassembler::InitDecoder() {
41-
decoder_ = new ZydisDecoder;
42-
formatter_ = new ZydisFormatter;
36+
decoder_ = std::make_unique<ZydisDecoder>();
37+
formatter_ = std::make_unique<ZydisFormatter>();
4338

4439
ZydisMachineMode mode = is_64bit_ ? ZYDIS_MACHINE_MODE_LONG_64 : ZYDIS_MACHINE_MODE_LEGACY_32;
4540
ZydisStackWidth stack_width = is_64bit_ ? ZYDIS_STACK_WIDTH_64 : ZYDIS_STACK_WIDTH_32;
4641

47-
if (!ZYAN_SUCCESS(ZydisDecoderInit(decoder_, mode, stack_width))) {
42+
if (!ZYAN_SUCCESS(ZydisDecoderInit(decoder_.get(), mode, stack_width))) {
4843
CleanupDecoder();
4944
return;
5045
}
5146

52-
if (!ZYAN_SUCCESS(ZydisFormatterInit(formatter_, ZYDIS_FORMATTER_STYLE_INTEL))) {
47+
if (!ZYAN_SUCCESS(ZydisFormatterInit(formatter_.get(), ZYDIS_FORMATTER_STYLE_INTEL))) {
5348
CleanupDecoder();
5449
return;
5550
}
5651

5752
// Configure formatter
58-
ZydisFormatterSetProperty(formatter_, ZYDIS_FORMATTER_PROP_FORCE_SIZE, ZYAN_FALSE);
59-
ZydisFormatterSetProperty(formatter_, ZYDIS_FORMATTER_PROP_UPPERCASE_MNEMONIC, ZYAN_TRUE);
60-
ZydisFormatterSetProperty(formatter_, ZYDIS_FORMATTER_PROP_UPPERCASE_REGISTERS, ZYAN_TRUE);
53+
ZydisFormatterSetProperty(formatter_.get(), ZYDIS_FORMATTER_PROP_FORCE_SIZE, ZYAN_FALSE);
54+
ZydisFormatterSetProperty(formatter_.get(), ZYDIS_FORMATTER_PROP_UPPERCASE_MNEMONIC, ZYAN_TRUE);
55+
ZydisFormatterSetProperty(formatter_.get(), ZYDIS_FORMATTER_PROP_UPPERCASE_REGISTERS, ZYAN_TRUE);
6156
}
6257

6358
void Disassembler::CleanupDecoder() {
64-
if (decoder_) {
65-
delete decoder_;
66-
decoder_ = nullptr;
67-
}
68-
if (formatter_) {
69-
delete formatter_;
70-
formatter_ = nullptr;
71-
}
59+
decoder_.reset();
60+
formatter_.reset();
7261
}
7362

7463
std::optional<InstructionInfo> Disassembler::DisassembleOne(const uint8_t* data,
@@ -81,7 +70,7 @@ std::optional<InstructionInfo> Disassembler::DisassembleOne(const uint8_t* data,
8170
ZydisDecodedInstruction instruction;
8271
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
8372

84-
if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(decoder_, data, size,
73+
if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(decoder_.get(), data, size,
8574
&instruction, operands))) {
8675
return std::nullopt;
8776
}
@@ -95,7 +84,7 @@ std::optional<InstructionInfo> Disassembler::DisassembleOne(const uint8_t* data,
9584

9685
// Format instruction
9786
char buffer[256];
98-
if (ZYAN_SUCCESS(ZydisFormatterFormatInstruction(formatter_, &instruction,
87+
if (ZYAN_SUCCESS(ZydisFormatterFormatInstruction(formatter_.get(), &instruction,
9988
operands, instruction.operand_count_visible,
10089
buffer, sizeof(buffer), address, nullptr))) {
10190
info.full_text = buffer;
@@ -149,10 +138,20 @@ std::optional<InstructionInfo> Disassembler::DisassembleOne(const uint8_t* data,
149138

150139
// Check for memory access
151140
info.is_memory_access = false;
141+
info.is_memory_write = false;
142+
info.is_memory_read = false;
152143
for (size_t i = 0; i < instruction.operand_count; i++) {
153144
if (operands[i].type == ZYDIS_OPERAND_TYPE_MEMORY) {
154145
info.is_memory_access = true;
155146

147+
// Determine read/write via Zydis operand actions
148+
if (operands[i].actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) {
149+
info.is_memory_write = true;
150+
}
151+
if (operands[i].actions & ZYDIS_OPERAND_ACTION_MASK_READ) {
152+
info.is_memory_read = true;
153+
}
154+
156155
// Calculate memory address if possible
157156
ZyanU64 result;
158157
if (ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&instruction, &operands[i], address, &result))) {

src/analysis/disassembler.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <cstdint>
66
#include <optional>
77
#include <map>
8+
#include <memory>
89

910
// Forward declare Zydis types to avoid header inclusion in header
1011
struct ZydisDecoder_;
@@ -48,6 +49,8 @@ struct InstructionInfo {
4849
bool is_ret;
4950
bool is_conditional;
5051
bool is_memory_access;
52+
bool is_memory_write; // Memory operand is written (MOV [addr], ..., ADD [addr], ...)
53+
bool is_memory_read; // Memory operand is read
5154

5255
// Branch target (if applicable)
5356
std::optional<uint64_t> branch_target;
@@ -155,8 +158,8 @@ class Disassembler {
155158
void CleanupDecoder();
156159

157160
bool is_64bit_;
158-
ZydisDecoder_* decoder_ = nullptr;
159-
ZydisFormatter_* formatter_ = nullptr;
161+
std::unique_ptr<ZydisDecoder_> decoder_;
162+
std::unique_ptr<ZydisFormatter_> formatter_;
160163
};
161164

162165
/**

0 commit comments

Comments
 (0)