1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//! The only privileged thing Lodestone does: open a process and read/write its
//! memory. Everything above this file is pure interpretation of those bytes.
//!
//! Deliberately *no* injection primitives live here — no VirtualAllocEx, no
//! CreateRemoteThread, no LoadLibrary. A true external never puts a single byte
//! of its own code inside the target.
#![cfg(windows)]
use std::ffi::{c_void, OsString};
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, MAX_PATH};
use windows_sys::Win32::System::Diagnostics::Debug::{ReadProcessMemory, WriteProcessMemory};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS,
};
use windows_sys::Win32::System::ProcessStatus::{
EnumProcessModulesEx, GetModuleFileNameExW, GetModuleInformation, LIST_MODULES_ALL, MODULEINFO,
};
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
PROCESS_VM_OPERATION,
};
pub type Result<T> = std::result::Result<T, String>;
/// Read + write + query. No PROCESS_CREATE_THREAD: we never run code in there.
const ACCESS: u32 =
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION;
#[derive(Debug, Clone)]
pub struct ProcInfo {
pub pid: u32,
pub name: String,
}
/// Snapshot every running process. Used to find `javaw.exe`.
pub fn list_processes() -> Result<Vec<ProcInfo>> {
let mut out = Vec::new();
// SAFETY: snapshot handle is checked and closed below.
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap.is_null() {
return Err(format!("CreateToolhelp32Snapshot: {}", last_err()));
}
let mut e: PROCESSENTRY32W = std::mem::zeroed();
e.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
let mut ok = Process32FirstW(snap, &mut e);
while ok != 0 {
out.push(ProcInfo {
pid: e.th32ProcessID,
name: wide_to_string(&e.szExeFile),
});
ok = Process32NextW(snap, &mut e);
}
CloseHandle(snap);
}
Ok(out)
}
#[derive(Debug, Clone)]
pub struct Module {
pub name: String,
pub path: String,
pub base: u64,
pub size: u32,
}
/// An attached process. Owns the OS handle; closes it on drop.
pub struct Process {
pid: u32,
handle: isize,
}
// SAFETY: ReadProcessMemory/WriteProcessMemory are safe to call concurrently on
// one handle, and we never mutate `handle` after construction.
unsafe impl Send for Process {}
unsafe impl Sync for Process {}
impl Drop for Process {
fn drop(&mut self) {
// SAFETY: handle came from OpenProcess and is closed exactly once.
unsafe { CloseHandle(self.handle as HANDLE) };
}
}
impl Process {
pub fn attach(pid: u32) -> Result<Self> {
// SAFETY: valid access mask; null return means failure.
let h = unsafe { OpenProcess(ACCESS, 0, pid) };
if h.is_null() {
return Err(format!("OpenProcess({pid}): {}", last_err()));
}
Ok(Self { pid, handle: h as isize })
}
pub fn pid(&self) -> u32 {
self.pid
}
/// Read `buf.len()` bytes at `addr`. Partial reads are an error: a short
/// read here almost always means a stale pointer, and silently zero-filling
/// would turn that into a confusing wrong answer higher up.
pub fn read_into(&self, addr: u64, buf: &mut [u8]) -> Result<()> {
let mut got: usize = 0;
// SAFETY: buf is a valid local slice; the kernel validates `addr`.
let ok = unsafe {
ReadProcessMemory(
self.handle as HANDLE,
addr as *const c_void,
buf.as_mut_ptr() as *mut c_void,
buf.len(),
&mut got,
)
};
if ok == 0 || got != buf.len() {
return Err(format!(
"read {:#x} ({} bytes): {}",
addr,
buf.len(),
last_err()
));
}
Ok(())
}
pub fn read_bytes(&self, addr: u64, len: usize) -> Result<Vec<u8>> {
let mut v = vec![0u8; len];
self.read_into(addr, &mut v)?;
Ok(v)
}
pub fn write_bytes(&self, addr: u64, data: &[u8]) -> Result<()> {
let mut put: usize = 0;
// SAFETY: data is a valid local slice; the kernel validates `addr`.
let ok = unsafe {
WriteProcessMemory(
self.handle as HANDLE,
addr as *const c_void,
data.as_ptr() as *const c_void,
data.len(),
&mut put,
)
};
if ok == 0 || put != data.len() {
return Err(format!("write {:#x} ({} bytes): {}", addr, data.len(), last_err()));
}
Ok(())
}
/// Best-effort read that yields `None` instead of an error. The heap walk
/// hits unmapped pages constantly; those are expected, not exceptional.
pub fn try_read<const N: usize>(&self, addr: u64) -> Option<[u8; N]> {
let mut b = [0u8; N];
self.read_into(addr, &mut b).ok().map(|_| b)
}
pub fn u8(&self, a: u64) -> Result<u8> {
Ok(self.read_bytes(a, 1)?[0])
}
pub fn u16(&self, a: u64) -> Result<u16> {
let mut b = [0u8; 2];
self.read_into(a, &mut b)?;
Ok(u16::from_le_bytes(b))
}
pub fn u32(&self, a: u64) -> Result<u32> {
let mut b = [0u8; 4];
self.read_into(a, &mut b)?;
Ok(u32::from_le_bytes(b))
}
pub fn i32(&self, a: u64) -> Result<i32> {
Ok(self.u32(a)? as i32)
}
pub fn u64(&self, a: u64) -> Result<u64> {
let mut b = [0u8; 8];
self.read_into(a, &mut b)?;
Ok(u64::from_le_bytes(b))
}
pub fn f32(&self, a: u64) -> Result<f32> {
Ok(f32::from_bits(self.u32(a)?))
}
pub fn f64(&self, a: u64) -> Result<f64> {
Ok(f64::from_bits(self.u64(a)?))
}
pub fn write_u8(&self, a: u64, v: u8) -> Result<()> {
self.write_bytes(a, &[v])
}
pub fn write_u32(&self, a: u64, v: u32) -> Result<()> {
self.write_bytes(a, &v.to_le_bytes())
}
pub fn write_i32(&self, a: u64, v: i32) -> Result<()> {
self.write_bytes(a, &v.to_le_bytes())
}
pub fn write_u64(&self, a: u64, v: u64) -> Result<()> {
self.write_bytes(a, &v.to_le_bytes())
}
pub fn write_f32(&self, a: u64, v: f32) -> Result<()> {
self.write_bytes(a, &v.to_le_bytes())
}
pub fn write_f64(&self, a: u64, v: f64) -> Result<()> {
self.write_bytes(a, &v.to_le_bytes())
}
/// NUL-terminated ASCII/UTF-8 C string, read in chunks so we don't fault on
/// a string that sits near the end of a page.
pub fn cstring(&self, addr: u64, max: usize) -> Result<String> {
let mut out = Vec::new();
let mut a = addr;
while out.len() < max {
let chunk = self.read_bytes(a, 32)?;
if let Some(p) = chunk.iter().position(|&c| c == 0) {
out.extend_from_slice(&chunk[..p]);
return Ok(String::from_utf8_lossy(&out).into_owned());
}
out.extend_from_slice(&chunk);
a += 32;
}
Ok(String::from_utf8_lossy(&out).into_owned())
}
pub fn modules(&self) -> Result<Vec<Module>> {
let mut handles = vec![0usize; 1024];
let mut needed: u32 = 0;
// SAFETY: handles is sized in bytes for the call; needed is written back.
let ok = unsafe {
EnumProcessModulesEx(
self.handle as HANDLE,
handles.as_mut_ptr() as *mut _,
(handles.len() * std::mem::size_of::<usize>()) as u32,
&mut needed,
LIST_MODULES_ALL,
)
};
if ok == 0 {
return Err(format!("EnumProcessModulesEx: {}", last_err()));
}
let count = (needed as usize / std::mem::size_of::<usize>()).min(handles.len());
let mut out = Vec::with_capacity(count);
for &h in &handles[..count] {
let mut namebuf = [0u16; MAX_PATH as usize];
// SAFETY: h is a module handle from the enumeration above.
let n = unsafe {
GetModuleFileNameExW(
self.handle as HANDLE,
h as *mut _,
namebuf.as_mut_ptr(),
namebuf.len() as u32,
)
};
let path = if n > 0 {
String::from_utf16_lossy(&namebuf[..n as usize])
} else {
String::new()
};
let mut mi: MODULEINFO = unsafe { std::mem::zeroed() };
// SAFETY: mi is a valid out-param of the documented size.
let ok = unsafe {
GetModuleInformation(
self.handle as HANDLE,
h as *mut _,
&mut mi,
std::mem::size_of::<MODULEINFO>() as u32,
)
};
if ok == 0 {
continue;
}
let name = path.rsplit('\\').next().unwrap_or("").to_string();
out.push(Module {
name,
path,
base: mi.lpBaseOfDll as u64,
size: mi.SizeOfImage,
});
}
Ok(out)
}
pub fn module(&self, name: &str) -> Result<Module> {
self.modules()?
.into_iter()
.find(|m| m.name.eq_ignore_ascii_case(name))
.ok_or_else(|| format!("module {name} not loaded"))
}
/// Resolve exported symbols out of a module's PE export directory, read
/// straight from the target's mapped image. `jvm.dll` publishes the whole
/// VMStructs table this way, which is how we avoid hardcoding any offset.
pub fn exports(&self, m: &Module) -> Result<Vec<(String, u64)>> {
let base = m.base;
if self.u16(base)? != 0x5A4D {
return Err("not an MZ image".into());
}
let e_lfanew = self.u32(base + 0x3C)? as u64;
let nt = base + e_lfanew;
if self.u32(nt)? != 0x0000_4550 {
return Err("not a PE image".into());
}
// COFF header is 20 bytes; the optional header follows.
let opt = nt + 4 + 20;
let magic = self.u16(opt)?;
// PE32+ puts the data directories at +112, PE32 at +96.
let dir_off = if magic == 0x20B { 112 } else { 96 };
let export_rva = self.u32(opt + dir_off)? as u64;
if export_rva == 0 {
return Ok(Vec::new());
}
let ed = base + export_rva;
let n_names = self.u32(ed + 24)? as usize;
let func_rva = self.u32(ed + 28)? as u64;
let name_rva = self.u32(ed + 32)? as u64;
let ord_rva = self.u32(ed + 36)? as u64;
// Bulk-read the three parallel arrays instead of 3 reads per symbol.
let names = self.read_bytes(base + name_rva, n_names * 4)?;
let ords = self.read_bytes(base + ord_rva, n_names * 2)?;
let mut out = Vec::with_capacity(n_names);
for i in 0..n_names {
let nrva = u32::from_le_bytes(names[i * 4..i * 4 + 4].try_into().unwrap()) as u64;
let ord = u16::from_le_bytes(ords[i * 2..i * 2 + 2].try_into().unwrap()) as u64;
let name = self.cstring(base + nrva, 256)?;
let addr_rva = self.u32(base + func_rva + ord * 4)? as u64;
out.push((name, base + addr_rva));
}
Ok(out)
}
}
fn wide_to_string(w: &[u16]) -> String {
let end = w.iter().position(|&c| c == 0).unwrap_or(w.len());
OsString::from_wide(&w[..end]).to_string_lossy().into_owned()
}
fn last_err() -> String {
std::io::Error::last_os_error().to_string()
}