paavohtl Claude Opus 4.6 (1M context) commited on
Commit
05dc1e9
·
1 Parent(s): 569ffc0

feat: add absolute addresses, offsets, and pointer chains to debug CLI

Browse files

- abs: prefix skips ASLR rebase (e.g. abs:0x7FFF0000)
- +offset and [offset] syntax on addresses and chain segments
- Pointer chain syntax: "0x7A0884->0xA0->0x488" walks and derefs
- Server-side ReadChain request with step trace output
- Shell redirect detection warns about unquoted -> arguments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-debug-cli/src/main.rs CHANGED
@@ -56,11 +56,22 @@ fn main() {
56
  eprintln!("Error: read requires an address");
57
  process::exit(1);
58
  });
59
- let addr = parse_addr(addr_str);
 
 
 
 
 
 
60
  let len = positional.get(2)
61
  .map(|s| parse_u32(s))
62
  .unwrap_or(256);
63
- Request::Read { addr, len }
 
 
 
 
 
64
  }
65
  Some(cmd) => {
66
  eprintln!("Unknown command: {cmd}");
@@ -116,6 +127,26 @@ fn print_response(response: &Response, format: Format) {
116
  }
117
  }
118
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  Response::Error { message } => {
120
  eprintln!("Server error: {message}");
121
  process::exit(1);
@@ -184,7 +215,73 @@ enum Format {
184
  Raw,
185
  }
186
 
187
- fn parse_addr(s: &str) -> u32 {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
189
  u32::from_str_radix(s, 16).unwrap_or_else(|_| {
190
  eprintln!("Error: invalid hex address '{s}'");
@@ -214,7 +311,14 @@ fn print_usage() {
214
  eprintln!("Commands:");
215
  eprintln!(" ping Check if server is running");
216
  eprintln!(" help List server commands");
217
- eprintln!(" read <addr> [len] Read memory (addr is Ghidra VA, len default 256)");
 
 
 
 
 
 
 
218
  eprintln!();
219
  eprintln!("Options:");
220
  eprintln!(" --port <N> Server port (default: 19840)");
 
56
  eprintln!("Error: read requires an address");
57
  process::exit(1);
58
  });
59
+ // Detect shell eating '->' as redirect: address ends with '-'
60
+ if addr_str.ends_with('-') {
61
+ eprintln!("Error: address '{}' looks truncated — did the shell eat '->'?", addr_str);
62
+ eprintln!(" Hint: quote the argument: read \"0x7A0884->0xA0->0x0\"");
63
+ process::exit(1);
64
+ }
65
+ let expr = parse_address_expr(addr_str);
66
  let len = positional.get(2)
67
  .map(|s| parse_u32(s))
68
  .unwrap_or(256);
69
+ match expr {
70
+ AddressExpr::Simple { addr, absolute } =>
71
+ Request::Read { addr, len, absolute },
72
+ AddressExpr::Chain { addr, chain, absolute } =>
73
+ Request::ReadChain { addr, chain, len, absolute },
74
+ }
75
  }
76
  Some(cmd) => {
77
  eprintln!("Unknown command: {cmd}");
 
127
  }
128
  }
129
  }
130
+ Response::ReadChainResult { steps, ghidra_addr, runtime_addr, data, pointers } => {
131
+ match format {
132
+ Format::Hex => {
133
+ println!("Pointer chain ({} steps):", steps.len());
134
+ for (i, step) in steps.iter().enumerate() {
135
+ println!(
136
+ " [{}] *(ghidra:0x{:08X}) = 0x{:08X} + 0x{:X} = 0x{:08X}",
137
+ i, step.deref_addr, step.value, step.offset, step.result_addr
138
+ );
139
+ }
140
+ println!();
141
+ print_hex_read(*ghidra_addr, *runtime_addr, data, pointers);
142
+ }
143
+ Format::Raw => {
144
+ let stdout = io::stdout();
145
+ let mut out = stdout.lock();
146
+ let _ = out.write_all(data);
147
+ }
148
+ }
149
+ }
150
  Response::Error { message } => {
151
  eprintln!("Server error: {message}");
152
  process::exit(1);
 
215
  Raw,
216
  }
217
 
218
+ /// Parsed address expression — either a simple address or a pointer chain.
219
+ enum AddressExpr {
220
+ /// Simple address (with optional offset already applied)
221
+ Simple { addr: u32, absolute: bool },
222
+ /// Pointer chain: start address + list of deref offsets
223
+ Chain { addr: u32, chain: Vec<u32>, absolute: bool },
224
+ }
225
+
226
+ /// Parse an address expression.
227
+ ///
228
+ /// Syntax:
229
+ /// 0x669F8C — Ghidra VA
230
+ /// abs:0x7FFF0000 — absolute runtime address
231
+ /// 0x669F8C+0x10 — Ghidra VA + hex offset
232
+ /// 0x669F8C[0x10] — same as +0x10 (bracket notation)
233
+ /// 0x7A0884->0xA0->0x2C — pointer chain (deref at each ->)
234
+ /// abs:0x7FFF0000->4->0 — absolute + chain
235
+ ///
236
+ /// Chain semantics: read DWORD at addr, add first offset, read DWORD, add
237
+ /// second offset, ... display memory at final address.
238
+ fn parse_address_expr(s: &str) -> AddressExpr {
239
+ let (s, absolute) = if let Some(rest) = s.strip_prefix("abs:") {
240
+ (rest, true)
241
+ } else {
242
+ (s.as_ref(), false)
243
+ };
244
+
245
+ // Check for chain syntax: contains "->"
246
+ if s.contains("->") {
247
+ let parts: Vec<&str> = s.split("->").collect();
248
+ let base = parse_addr_with_offset(parts[0]);
249
+ let chain: Vec<u32> = parts[1..].iter().map(|p| parse_compound_offset(p)).collect();
250
+ return AddressExpr::Chain { addr: base, chain, absolute };
251
+ }
252
+
253
+ // Simple address with optional offset
254
+ AddressExpr::Simple { addr: parse_addr_with_offset(s), absolute }
255
+ }
256
+
257
+ /// Parse a single address token, possibly with +offset or [offset].
258
+ fn parse_addr_with_offset(s: &str) -> u32 {
259
+ let (base_str, offset) = split_compound(s);
260
+ parse_hex(base_str).wrapping_add(offset)
261
+ }
262
+
263
+ /// Parse a chain offset segment, possibly with +offset or [offset].
264
+ /// E.g. "0x488+0x10" → 0x498, "0xA0[4]" → 0xA4, "0x510" → 0x510.
265
+ fn parse_compound_offset(s: &str) -> u32 {
266
+ let (base_str, offset) = split_compound(s);
267
+ parse_u32(base_str).wrapping_add(offset)
268
+ }
269
+
270
+ /// Split a string on '+' or '[' into (base, offset). Returns (s, 0) if no compound.
271
+ fn split_compound(s: &str) -> (&str, u32) {
272
+ if let Some(pos) = s.find('+') {
273
+ let (base, off) = s.split_at(pos);
274
+ (base, parse_u32(&off[1..]))
275
+ } else if let Some(pos) = s.find('[') {
276
+ let base = &s[..pos];
277
+ let off_str = s[pos + 1..].trim_end_matches(']');
278
+ (base, parse_u32(off_str))
279
+ } else {
280
+ (s, 0u32)
281
+ }
282
+ }
283
+
284
+ fn parse_hex(s: &str) -> u32 {
285
  let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
286
  u32::from_str_radix(s, 16).unwrap_or_else(|_| {
287
  eprintln!("Error: invalid hex address '{s}'");
 
311
  eprintln!("Commands:");
312
  eprintln!(" ping Check if server is running");
313
  eprintln!(" help List server commands");
314
+ eprintln!(" read <addr> [len] Read memory at address (default len=256)");
315
+ eprintln!();
316
+ eprintln!("Address syntax:");
317
+ eprintln!(" 0x669F8C Ghidra VA (rebased automatically)");
318
+ eprintln!(" abs:0x7FFF0000 Absolute runtime address (no rebase)");
319
+ eprintln!(" 0x669F8C+0x10 Address + hex offset");
320
+ eprintln!(" 0x669F8C[16] Address + decimal offset (bracket notation)");
321
+ eprintln!(" 0x7A0884->0xA0->0x2C Pointer chain (deref at each ->)");
322
  eprintln!();
323
  eprintln!("Options:");
324
  eprintln!(" --port <N> Server port (default: 19840)");
crates/openwa-debug-proto/src/lib.rs CHANGED
@@ -27,7 +27,23 @@ pub struct PointerInfo {
27
  pub enum Request {
28
  Ping,
29
  Help,
30
- Read { addr: u32, len: u32 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
  #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -47,6 +63,18 @@ pub enum Response {
47
  data: Vec<u8>,
48
  pointers: Vec<PointerInfo>,
49
  },
 
 
 
 
 
 
 
 
 
 
 
 
50
  Error { message: String },
51
  }
52
 
 
27
  pub enum Request {
28
  Ping,
29
  Help,
30
+ Read { addr: u32, len: u32, #[serde(default)] absolute: bool },
31
+ /// Walk a pointer chain: start at `addr`, then for each offset in `chain`,
32
+ /// deref the current DWORD and add the offset. Read `len` bytes at the end.
33
+ ReadChain { addr: u32, chain: Vec<u32>, len: u32, absolute: bool },
34
+ }
35
+
36
+ /// One step in a resolved pointer chain, for display.
37
+ #[derive(Debug, Clone, Serialize, Deserialize)]
38
+ pub struct ChainStep {
39
+ /// Address we read from
40
+ pub deref_addr: u32,
41
+ /// Value (DWORD) we read
42
+ pub value: u32,
43
+ /// Offset added after deref
44
+ pub offset: u32,
45
+ /// Resulting address (value + offset)
46
+ pub result_addr: u32,
47
  }
48
 
49
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
63
  data: Vec<u8>,
64
  pointers: Vec<PointerInfo>,
65
  },
66
+ ReadChainResult {
67
+ /// Each deref step in the chain (for trace output)
68
+ steps: Vec<ChainStep>,
69
+ /// Final address (Ghidra VA)
70
+ ghidra_addr: u32,
71
+ /// Final address (runtime)
72
+ runtime_addr: u32,
73
+ /// Memory at the final address
74
+ data: Vec<u8>,
75
+ /// Pointer annotations in the final data
76
+ pointers: Vec<PointerInfo>,
77
+ },
78
  Error { message: String },
79
  }
80
 
crates/openwa-wormkit/src/debug_server.rs CHANGED
@@ -110,11 +110,24 @@ fn handle_request(request: Request) -> Response {
110
  },
111
  ],
112
  },
113
- Request::Read { addr, len } => handle_read(addr, len),
 
114
  }
115
  }
116
 
117
- fn handle_read(ghidra_addr: u32, len: u32) -> Response {
 
 
 
 
 
 
 
 
 
 
 
 
118
  if len == 0 {
119
  return Response::Error {
120
  message: "len must be > 0".into(),
@@ -126,9 +139,7 @@ fn handle_read(ghidra_addr: u32, len: u32) -> Response {
126
  };
127
  }
128
 
129
- let wa_base = rb(va::IMAGE_BASE);
130
- let delta = wa_base.wrapping_sub(va::IMAGE_BASE);
131
- let runtime_addr = ghidra_addr.wrapping_add(delta);
132
 
133
  unsafe {
134
  if !mem::can_read(runtime_addr, len) {
@@ -159,3 +170,81 @@ fn handle_read(ghidra_addr: u32, len: u32) -> Response {
159
  }
160
  }
161
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  },
111
  ],
112
  },
113
+ Request::Read { addr, len, absolute } => handle_read(addr, len, absolute),
114
+ Request::ReadChain { addr, chain, len, absolute } => handle_read_chain(addr, &chain, len, absolute),
115
  }
116
  }
117
 
118
+ /// Compute ASLR delta and resolve an address to (ghidra, runtime).
119
+ fn resolve_addr(addr: u32, absolute: bool) -> (u32, u32, u32) {
120
+ let wa_base = rb(va::IMAGE_BASE);
121
+ let delta = wa_base.wrapping_sub(va::IMAGE_BASE);
122
+ let (ghidra_addr, runtime_addr) = if absolute {
123
+ (addr.wrapping_sub(delta), addr)
124
+ } else {
125
+ (addr, addr.wrapping_add(delta))
126
+ };
127
+ (ghidra_addr, runtime_addr, delta)
128
+ }
129
+
130
+ fn handle_read(addr: u32, len: u32, absolute: bool) -> Response {
131
  if len == 0 {
132
  return Response::Error {
133
  message: "len must be > 0".into(),
 
139
  };
140
  }
141
 
142
+ let (ghidra_addr, runtime_addr, delta) = resolve_addr(addr, absolute);
 
 
143
 
144
  unsafe {
145
  if !mem::can_read(runtime_addr, len) {
 
170
  }
171
  }
172
  }
173
+
174
+ fn handle_read_chain(addr: u32, chain: &[u32], len: u32, absolute: bool) -> Response {
175
+ if len == 0 || len > MAX_READ_SIZE {
176
+ return Response::Error {
177
+ message: format!("len must be 1..{}", MAX_READ_SIZE),
178
+ };
179
+ }
180
+ if chain.is_empty() {
181
+ return handle_read(addr, len, absolute);
182
+ }
183
+
184
+ let (_, mut current_runtime, delta) = resolve_addr(addr, absolute);
185
+ let mut steps = Vec::new();
186
+
187
+ // Walk the chain: for each offset, deref current address then add offset
188
+ for &offset in chain.iter() {
189
+ unsafe {
190
+ if !mem::can_read(current_runtime, 4) {
191
+ return Response::Error {
192
+ message: format!(
193
+ "Chain broke: cannot read DWORD at runtime:0x{:08X} (ghidra:0x{:08X})",
194
+ current_runtime,
195
+ current_runtime.wrapping_sub(delta)
196
+ ),
197
+ };
198
+ }
199
+ let value = *(current_runtime as *const u32);
200
+ if value == 0 {
201
+ return Response::Error {
202
+ message: format!(
203
+ "Chain broke: NULL pointer at runtime:0x{:08X} (ghidra:0x{:08X})",
204
+ current_runtime,
205
+ current_runtime.wrapping_sub(delta)
206
+ ),
207
+ };
208
+ }
209
+ let result_addr = value.wrapping_add(offset);
210
+ steps.push(ChainStep {
211
+ deref_addr: current_runtime.wrapping_sub(delta), // ghidra VA
212
+ value,
213
+ offset,
214
+ result_addr,
215
+ });
216
+ current_runtime = result_addr;
217
+ }
218
+ }
219
+
220
+ // Now read memory at the final address
221
+ let final_ghidra = current_runtime.wrapping_sub(delta);
222
+
223
+ unsafe {
224
+ if !mem::can_read(current_runtime, len) {
225
+ return Response::Error {
226
+ message: format!(
227
+ "Cannot read {} bytes at final address runtime:0x{:08X} (ghidra:0x{:08X})",
228
+ len, current_runtime, final_ghidra
229
+ ),
230
+ };
231
+ }
232
+
233
+ let mut data = vec![0u8; len as usize];
234
+ std::ptr::copy_nonoverlapping(
235
+ current_runtime as *const u8,
236
+ data.as_mut_ptr(),
237
+ len as usize,
238
+ );
239
+
240
+ let pointers = mem::classify_region(&data, 0, delta);
241
+
242
+ Response::ReadChainResult {
243
+ steps,
244
+ ghidra_addr: final_ghidra,
245
+ runtime_addr: current_runtime,
246
+ data,
247
+ pointers,
248
+ }
249
+ }
250
+ }