Skip to content

Commit 4c7fc84

Browse files
feat: add reassembly functions with trampoline and padding support, and include assembly tests
1 parent 5718491 commit 4c7fc84

File tree

6 files changed

+520
-12
lines changed

6 files changed

+520
-12
lines changed

include/blook/disassembly.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,6 @@ template <typename Range> class DisassembleRange {
150150
current_value = InstructionCtx{r.value(), address};
151151
ptr += size;
152152
address += size;
153-
154-
if (ptr == range_end) {
155-
over = true;
156-
}
157153
}
158154
}
159155
};

include/blook/memo.h

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,20 @@ class Pointer {
257257
}
258258

259259
[[nodiscard]] MemoryPatch
260-
reassembly(std::function<void(zasm::x86::Assembler)>);
260+
reassembly(std::function<void(zasm::x86::Assembler&)>);
261261

262262
[[nodiscard]] MemoryPatch reassembly_thread_pause();
263263

264+
// Reassembly with trampoline support
265+
// Allocates new memory, writes user code + jump to trampoline
266+
// Original location gets a jump to the new memory
267+
[[nodiscard]] std::expected<MemoryPatch, std::string>
268+
try_reassembly_with_trampoline(
269+
std::function<void(zasm::x86::Assembler&)> func);
270+
271+
[[nodiscard]] MemoryPatch
272+
reassembly_with_trampoline(std::function<void(zasm::x86::Assembler&)> func);
273+
264274
std::optional<Function> guess_function(size_t max_scan_size = 50000);
265275

266276
std::optional<Pointer> find_upwards(std::initializer_list<uint8_t> pattern,
@@ -275,6 +285,12 @@ class Pointer {
275285
MemoryRange range_to(Pointer ptr);
276286

277287
MemoryRange range_size(std::size_t size);
288+
289+
// Select next N instructions and return a MemoryRange covering them
290+
std::expected<MemoryRange, std::string>
291+
try_range_next_instr(int num_of_instructions);
292+
293+
MemoryRange range_next_instr(int num_of_instructions);
278294
};
279295

280296
struct ScopedSetMemoryRWX {
@@ -507,6 +523,15 @@ class MemoryRange : public Pointer {
507523
int32_t crc32() const;
508524

509525
[[nodiscard]] disasm::DisassembleRange<MemoryRange> disassembly() const;
526+
527+
// Reassembly with padding support
528+
// If the new code is smaller, pad with NOPs
529+
// If the new code is larger, return unexpected
530+
[[nodiscard]] std::expected<MemoryPatch, std::string>
531+
try_reassembly_with_padding(std::function<void(zasm::x86::Assembler&)> func);
532+
533+
[[nodiscard]] MemoryPatch
534+
reassembly_with_padding(std::function<void(zasm::x86::Assembler&)> func);
510535
};
511536

512537
static_assert(std::sentinel_for<decltype(std::declval<MemoryRange>().begin()),

src/memory.cpp

Lines changed: 191 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
#include "blook/disassembly.h"
44

5+
#include "blook/hook.h"
56
#include "blook/memo.h"
67
#include "blook/process.h"
78
#include "zasm/formatter/formatter.hpp"
89
#include <algorithm>
910
#include <cstdint>
1011
#include <utility>
1112

13+
1214
#include <iostream>
1315

1416
namespace blook {
@@ -22,7 +24,7 @@ Pointer::Pointer(std::shared_ptr<Process> proc, size_t offset)
2224
: _offset(offset), proc(std::move(proc)) {}
2325

2426
MemoryPatch
25-
Pointer::reassembly(std::function<void(zasm::x86::Assembler)> func) {
27+
Pointer::reassembly(std::function<void(zasm::x86::Assembler&)> func) {
2628
using namespace zasm;
2729
Program program(utils::compileMachineMode());
2830
x86::Assembler b(program);
@@ -153,6 +155,45 @@ MemoryRange Pointer::range_size(std::size_t size) {
153155
return MemoryRange{*this, size};
154156
}
155157

158+
std::expected<MemoryRange, std::string>
159+
Pointer::try_range_next_instr(int num_of_instructions) {
160+
if (num_of_instructions <= 0) {
161+
return MemoryRange{*this, 0};
162+
}
163+
164+
try {
165+
auto disasm = this->range_size(num_of_instructions * 15).disassembly();
166+
size_t total_size = 0;
167+
int count = 0;
168+
169+
for (const auto &instr : disasm) {
170+
if (count >= num_of_instructions) {
171+
break;
172+
}
173+
total_size += instr->getLength();
174+
count++;
175+
}
176+
177+
if (count < num_of_instructions) {
178+
return std::unexpected(
179+
std::format("Could only find {} instructions, requested {}", count,
180+
num_of_instructions));
181+
}
182+
183+
return MemoryRange{*this, total_size};
184+
} catch (const std::exception &e) {
185+
return std::unexpected(
186+
std::format("Failed to get instruction range: {}", e.what()));
187+
}
188+
}
189+
190+
MemoryRange Pointer::range_next_instr(int num_of_instructions) {
191+
auto result = try_range_next_instr(num_of_instructions);
192+
if (!result) {
193+
throw std::runtime_error(result.error());
194+
}
195+
return *result;
196+
}
156197

157198
MemoryRange::MemoryRange(std::shared_ptr<Process> proc, void *offset,
158199
size_t size)
@@ -224,7 +265,7 @@ MemoryRange::find_one_remote(std::vector<uint8_t> pattern) const {
224265
return res.ptr;
225266
}
226267
MemoryPatch Pointer::reassembly_thread_pause() {
227-
return reassembly([](zasm::x86::Assembler a) {
268+
return reassembly([](zasm::x86::Assembler& a) {
228269
#if defined(BLOOK_ARCHITECTURE_X86_64) || defined(BLOOK_ARCHITECTURE_X86_32)
229270
// EB FE: jmp $0
230271
a.db(0xeb);
@@ -234,7 +275,155 @@ MemoryPatch Pointer::reassembly_thread_pause() {
234275
#endif
235276
});
236277
}
278+
279+
std::expected<MemoryPatch, std::string> Pointer::try_reassembly_with_trampoline(
280+
std::function<void(zasm::x86::Assembler&)> func) {
281+
using namespace zasm;
282+
283+
try {
284+
// Calculate min_bytes based on the jump instruction size
285+
// We need enough space for push/ret trick
286+
// For x64: push imm32 (6) + mov [rsp+4], imm32 (8) + ret (1) = 15 bytes
287+
// For x86: push imm32 (5) + ret (1) = 6 bytes
288+
size_t min_bytes;
289+
if constexpr (utils::compileArchitecture() == utils::Architecture::x86_64) {
290+
min_bytes = 15;
291+
} else {
292+
min_bytes = 6;
293+
}
294+
295+
// Step 1: Create trampoline for original code
296+
Trampoline trampoline = Trampoline::make(*this, min_bytes, true);
297+
298+
Pointer new_mem = this->malloc_rx_near_this(1024);
299+
if (new_mem.data() == nullptr) {
300+
return std::unexpected("Failed to allocate memory for user code");
301+
}
302+
303+
Program user_program(utils::compileMachineMode());
304+
x86::Assembler user_asm(user_program);
305+
306+
func(user_asm);
307+
308+
auto trampoline_addr = (size_t)trampoline.pTrampoline.data();
309+
auto new_mem_addr = (size_t)new_mem.data();
310+
311+
if constexpr (utils::compileArchitecture() == utils::Architecture::x86_64) {
312+
user_asm.jmp(Imm64(trampoline_addr));
313+
} else {
314+
user_asm.jmp(Imm32(trampoline_addr));
315+
}
316+
317+
Serializer user_serializer;
318+
if (auto err = user_serializer.serialize(user_program, new_mem_addr);
319+
err != ErrorCode::None) {
320+
return std::unexpected(std::format("Failed to serialize user code: {} {}",
321+
err.getErrorName(),
322+
err.getErrorMessage()));
323+
}
324+
325+
{
326+
ScopedSetMemoryRWX protector(new_mem, user_serializer.getCodeSize());
327+
new_mem.write_bytearray(
328+
std::span<const uint8_t>((const uint8_t *)user_serializer.getCode(),
329+
user_serializer.getCodeSize()));
330+
}
331+
332+
Program hook_program(utils::compileMachineMode());
333+
x86::Assembler hook_asm(hook_program);
334+
335+
auto orig_addr = (size_t)this->data();
336+
337+
if constexpr (utils::compileArchitecture() == utils::Architecture::x86_64) {
338+
hook_asm.jmp(Imm64(new_mem_addr));
339+
} else {
340+
hook_asm.jmp(Imm32(new_mem_addr));
341+
}
342+
343+
// Serialize hook code
344+
Serializer hook_serializer;
345+
if (auto err = hook_serializer.serialize(hook_program, orig_addr);
346+
err != ErrorCode::None) {
347+
return std::unexpected(std::format("Failed to serialize hook code: {} {}",
348+
err.getErrorName(),
349+
err.getErrorMessage()));
350+
}
351+
352+
const auto hook_size = hook_serializer.getCodeSize();
353+
if (hook_size > min_bytes) {
354+
return std::unexpected(
355+
std::format("Hook code size ({}) exceeds minimum bytes ({})",
356+
hook_size, min_bytes));
357+
}
358+
359+
// Create patch with padding
360+
std::vector<uint8_t> patch_data(min_bytes);
361+
std::memcpy(patch_data.data(), hook_serializer.getCode(), hook_size);
362+
363+
return MemoryPatch{*this, patch_data};
364+
365+
} catch (const std::exception &e) {
366+
return std::unexpected(
367+
std::format("Failed to create trampoline reassembly: {}", e.what()));
368+
}
369+
}
370+
371+
MemoryPatch Pointer::reassembly_with_trampoline(
372+
std::function<void(zasm::x86::Assembler&)> func) {
373+
auto result = try_reassembly_with_trampoline(func);
374+
if (!result) {
375+
throw std::runtime_error(result.error());
376+
}
377+
return std::move(*result);
378+
}
379+
237380
bool Pointer::is_valid() const {
238381
return proc != nullptr && proc->check_valid((void *)_offset);
239382
}
383+
384+
std::expected<MemoryPatch, std::string>
385+
MemoryRange::try_reassembly_with_padding(
386+
std::function<void(zasm::x86::Assembler&)> func) {
387+
using namespace zasm;
388+
Program program(utils::compileMachineMode());
389+
x86::Assembler b(program);
390+
func(b);
391+
392+
const auto code_size = utils::estimateCodeSize(program);
393+
394+
// Check if code is too large
395+
if (code_size > _size) {
396+
return std::unexpected(
397+
std::format("Generated code size ({}) exceeds available space ({})",
398+
code_size, _size));
399+
}
400+
401+
std::vector<uint8_t> data(_size);
402+
403+
Serializer serializer;
404+
if (auto err = serializer.serialize(program, this->_offset);
405+
err != zasm::ErrorCode::None)
406+
return std::unexpected(std::format("JIT Serialization failure: {} {}",
407+
err.getErrorName(),
408+
err.getErrorMessage()));
409+
410+
// Copy generated code
411+
std::memcpy(data.data(), serializer.getCode(), code_size);
412+
413+
// Fill remaining space with NOPs
414+
if (code_size < _size) {
415+
std::memset(data.data() + code_size, 0x90, _size - code_size);
416+
}
417+
418+
return MemoryPatch{*this, data};
419+
}
420+
421+
MemoryPatch MemoryRange::reassembly_with_padding(
422+
std::function<void(zasm::x86::Assembler&)> func) {
423+
auto result = try_reassembly_with_padding(func);
424+
if (!result) {
425+
throw std::runtime_error(result.error());
426+
}
427+
return std::move(*result);
428+
}
240429
} // namespace blook

src/tests/test_asm.asm

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
; Test assembly functions for reassembly tests
2+
3+
.code
4+
5+
; Simple add function: int asm_add_function(int a, int b)
6+
; Windows x64: rcx = a, rdx = b, return in eax
7+
; Windows x86: stack-based
8+
asm_add_function PROC
9+
IFDEF RAX
10+
; x64 version
11+
mov eax, ecx
12+
add eax, edx
13+
ret
14+
ELSE
15+
; x86 version
16+
mov eax, dword ptr [esp + 4]
17+
add eax, dword ptr [esp + 8]
18+
ret
19+
ENDIF
20+
asm_add_function ENDP
21+
22+
; extern volatile int g_trampoline_counter;
23+
EXTRN g_trampoline_counter:DWORD
24+
25+
; void asm_counter_function(int increment_by)
26+
; Increments g_trampoline_counter by the given amount
27+
asm_counter_function PROC
28+
IFDEF RAX
29+
; x64 version: rcx = increment_by
30+
push rax
31+
push rdx
32+
lea rax, g_trampoline_counter
33+
mov edx, dword ptr [rax]
34+
add edx, ecx
35+
mov dword ptr [rax], edx
36+
pop rdx
37+
pop rax
38+
ret
39+
ELSE
40+
; x86 version: stack-based
41+
push eax
42+
push ecx
43+
mov eax, offset g_trampoline_counter
44+
mov ecx, dword ptr [eax]
45+
add ecx, dword ptr [esp + 12] ; increment_by parameter
46+
mov dword ptr [eax], ecx
47+
pop ecx
48+
pop eax
49+
ret
50+
ENDIF
51+
asm_counter_function ENDP

0 commit comments

Comments
 (0)