Skip to content

Commit 9f75cd8

Browse files
committed
add create_capture_hook test
1 parent 24c9de4 commit 9f75cd8

File tree

7 files changed

+87
-48
lines changed

7 files changed

+87
-48
lines changed

memobj/allocation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def __exit__(self, *_):
7878
def closed(self) -> bool:
7979
return self._is_closed
8080

81-
def allocate(self, size: int) -> Allocation:
81+
def allocate(self, size: int, *, preferred_start: int | None = None) -> Allocation:
8282
"""
8383
Allocates a block of memory for the process.
8484
@@ -89,11 +89,12 @@ def allocate(self, size: int) -> Allocation:
8989
9090
Args:
9191
size (int): The size of the memory block to allocate in bytes.
92+
preferred_start: The preferred start address of the allocation
9293
9394
Returns:
9495
Allocation: An object representing the allocated memory block.
9596
"""
96-
address = self.process.allocate_memory(size)
97+
address = self.process.allocate_memory(size, preferred_start=preferred_start)
9798
allocation = Allocation(address, self.process, size)
9899
self.allocations.append(allocation)
99100
return allocation

memobj/hook.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def get_code(
113113
"""Called when the hook is activated to get the code to put in the hook"""
114114
raise NotImplementedError()
115115

116-
def allocate_variable(self, name: str, size: int) -> Allocation:
116+
def allocate_variable(self, name: str, size: int, *, preferred_start: int | None = None) -> Allocation:
117117
"""
118118
Allocate a variable of the specified size for use in the hook, retrievable with get_variable.
119119
@@ -122,6 +122,7 @@ def allocate_variable(self, name: str, size: int) -> Allocation:
122122
The name of the variable to allocate.
123123
size: int
124124
The size of the memory block to allocate.
125+
preferred_start: The preferred start address of the allocation
125126
126127
Returns:
127128
Allocation
@@ -134,7 +135,7 @@ def allocate_variable(self, name: str, size: int) -> Allocation:
134135
if self._variables.get("name") is not None:
135136
raise ValueError(f"Variable {name} is already allocated")
136137

137-
allocation = self.allocator.allocate(size)
138+
allocation = self.allocator.allocate(size, preferred_start=preferred_start)
138139
self._variables[name] = allocation
139140
return allocation
140141

@@ -338,7 +339,7 @@ def get_hook_tail(self, jump_address: int) -> tuple[list[Instruction], int]:
338339
else:
339340
bitness = 32
340341

341-
decoder = Decoder(bitness, search_bytes, ip=jump_address)
342+
decoder = Decoder(bitness, search_bytes, ip=0)
342343

343344
for instruction in decoder:
344345
# NOTE: this is not a None check, Instruction has special bool() handling
@@ -503,7 +504,7 @@ def get_code(self) -> list[Instruction]:
503504
@dataclass
504505
class RegisterCaptureSettings:
505506
register: RegisterType
506-
derefference: bool = True
507+
derefference: bool = False
507508
offset: int = 0
508509

509510

memobj/process/base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,20 @@ def executable_path(self) -> Path:
4949
"""
5050
raise NotImplementedError()
5151

52-
@functools.cached_property
52+
@property
5353
def pointer_format_string(self) -> str:
5454
if self.process_64_bit:
5555
return "Q"
5656
else:
5757
return "I"
5858

59+
@property
60+
def pointer_type(self) -> Type:
61+
if self.process_64_bit:
62+
return Type.unsigned8
63+
else:
64+
return Type.unsigned4
65+
5966
@functools.cached_property
6067
def pointer_size(self) -> int:
6168
if self.process_64_bit:
@@ -92,12 +99,13 @@ def from_id(cls, pid: int) -> Self:
9299
# TODO
93100
# def itererate_modules(self): ...
94101

95-
def allocate_memory(self, size: int) -> int:
102+
def allocate_memory(self, size: int, *, preferred_start: int | None = None) -> int:
96103
"""
97104
Allocate <size> amount of memory in the process
98105
99106
Args:
100107
size: The amount of memory to allocate
108+
preferred_start: The preferred start address of the allocation
101109
102110
Returns:
103111
The start address of the allocated memory

tests/conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
from pathlib import Path
34

45
import pytest
56

@@ -16,3 +17,21 @@ def process() -> Process:
1617
return WindowsProcess.from_id(os.getpid())
1718
case _:
1819
raise RuntimeError("Unsupported platform")
20+
21+
22+
@pytest.fixture(scope="session")
23+
def test_binaries() -> tuple[Path, Path]:
24+
library_root = Path(__file__).parent.parent
25+
26+
assert library_root.name == "memobj"
27+
assert (library_root / "README.md").exists() is True
28+
29+
release_dir = library_root / "target" / "release"
30+
31+
exe_path = (release_dir / "inject_target.exe").resolve()
32+
dll_path = (release_dir / "test_inject.dll").resolve()
33+
34+
if not exe_path.exists() or not dll_path.exists():
35+
pytest.skip("Test binaries not found")
36+
37+
return exe_path, dll_path

tests/manual/test_binaries/inject_target/src/main.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
use std::str::FromStr;
22

33
fn main() {
4-
std::hint::black_box(add_five);
5-
std::hint::black_box(create_player);
6-
std::hint::black_box(uses_player);
7-
8-
let value = std::hint::black_box(add_five(60 * 1_000_000));
9-
10-
// This process just waits, so it can be injected into.
11-
//std::thread::sleep(std::time::Duration::from_secs(60 * 5));
12-
//std::thread::sleep(std::time::Duration::from_secs(60 * 1_000_000));
13-
std::thread::sleep(std::time::Duration::from_secs(value as u64));
4+
loop {
5+
std::thread::sleep(std::time::Duration::from_secs(1));
6+
std::hint::black_box(add_five(1));
7+
std::hint::black_box(create_player());
8+
}
149
}
1510

1611
// static gives us a global address rather than in-lining the value
@@ -52,18 +47,21 @@ impl Player {
5247

5348
// TODO: keep these from getting compiled away
5449
#[no_mangle]
50+
#[inline(never)]
5551
extern "C" fn add_five(value: u32) -> u32 {
5652
value + FIVE
5753
}
5854

5955
#[no_mangle]
56+
#[inline(never)]
6057
extern "C" fn create_player() -> () {
6158
let player = Player::new();
6259

6360
uses_player(&player);
6461
}
6562

6663
#[no_mangle]
64+
#[inline(never)]
6765
extern "C" fn uses_player(player: &Player) -> () {
6866
// cmp with object offset
6967
if player.health > 100.0 {

tests/manual/test_binaries/test_inject/src/lib.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH;
66
use windows::Win32::System::Console::AllocConsole;
77

88

9-
// #[no_mangle]
10-
// extern "system" fn DllMain(_: *const u8, _: u32, _: *const u8) -> u32 { 1 }
11-
129
#[no_mangle]
1310
extern "system" fn DllMain(
1411
_hinst_dll: *const u8,
@@ -33,11 +30,14 @@ pub extern "C" fn create_file_at_path(path: *const c_char) -> bool {
3330

3431
let c_str = unsafe { CStr::from_ptr(path) };
3532

36-
if let Ok(path_str) = c_str.to_str() {
37-
if let Ok(mut file) = File::create(path_str) {
38-
return file.write_all(b"Injected").is_ok()
39-
}
40-
}
33+
let Ok(path_str) = c_str.to_str() else {
34+
return false;
35+
};
36+
37+
let Ok(mut file) = File::create(path_str) else {
38+
return false;
39+
};
40+
41+
file.write_all(b"Injected").is_ok()
4142

42-
false
4343
}

tests/manual/test_dll_injection.py

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,49 @@
11
import subprocess
2-
import pytest
3-
from pathlib import Path
2+
3+
from iced_x86 import Register
4+
import regex
45

56
import memobj
7+
from memobj.utils import ValueWaiter
8+
from memobj.hook import create_capture_hook, RegisterCaptureSettings
69

7-
import pytest
810

11+
def test_dll_injection(test_binaries):
12+
exe_path, dll_path = test_binaries
913

10-
def test_dll_injection():
11-
# there is probably a better way to get this
12-
library_root = Path(__file__).parent.parent.parent
14+
proc = subprocess.Popen([str(exe_path)])
15+
try:
16+
process = memobj.WindowsProcess.from_id(proc.pid)
17+
assert process.inject_dll(dll_path), "DLL injection failed"
18+
19+
module = process.get_module_named(dll_path.name)
20+
assert module is not None, "Failed to find injected DLL in remote process"
21+
finally:
22+
proc.terminate()
1323

14-
assert library_root.name == "memobj"
15-
assert (library_root / "README.md").exists() is True
1624

17-
dll_path = (
18-
library_root / "target/release/test_inject.dll"
19-
).resolve()
20-
exe_path = (
21-
library_root / "target/release/inject_target.exe"
22-
).resolve()
25+
def test_create_capture_hook(test_binaries):
26+
exe_path, _ = test_binaries
2327

24-
if not dll_path.exists():
25-
pytest.skip(f"Test DLL not found at {dll_path}")
26-
if not exe_path.exists():
27-
pytest.skip(f"Test EXE not found at {exe_path}")
28+
# TODO: allow passing addresses so we can do a symbol lookup
29+
PlayerCaptureHook = create_capture_hook(
30+
pattern=regex.escape(bytes.fromhex("48 83 EC 28 F3 0F 10 41 04 0F 2E 05 40 52 01 00 76 0D 8B 09 E8 17 FF FF FF 01 05 21 D0 01 00 48 83 C4 28 C3")),
31+
module="inject_target.exe",
32+
bitness=64,
33+
register_captures=[RegisterCaptureSettings(Register.RCX, derefference=False)],
34+
)
2835

2936
proc = subprocess.Popen([str(exe_path)])
3037
try:
3138
process = memobj.WindowsProcess.from_id(proc.pid)
32-
assert process.inject_dll(dll_path), "DLL injection failed"
39+
hook = PlayerCaptureHook(process)
40+
hook.activate()
41+
rcx_capture = hook.get_variable("RCX_capture")
3342

34-
module = process.get_module_named(dll_path.name)
35-
assert module is not None, "Failed to find injected DLL in remote process"
43+
waiter = ValueWaiter(lambda: rcx_capture.read_typed(process.pointer_type))
44+
address = waiter.wait_for_value(0, inverse=True, timeout=60)
45+
46+
assert address != 0
3647
finally:
3748
proc.terminate()
49+

0 commit comments

Comments
 (0)