Reading
Let's start with reading.
Windows provide a function to do this called ReadProcessMemory.
All memory operations require a process handle which we can obtain using the OpenProcess function.
First let's write the most primitive read function that reads x amount of bytes and write it to a buffer and return the actual size read.
This allows for partial reads in case the full read failed.
We will wrap the process handle in a MemoryAccessor type so it's easier to work with.
Instead of using ReadProcessMemory, I will be using NtReadVirtualMemory instead as it has less overhead.
const std = @import("std");
const win32 = std.os.windows; // I recommend using https://github.com/marlersoft/zigwin32 instead
const NTSTATUS = i32;
pub extern "ntdll" fn NtReadVirtualMemory(
hProcess: ?win32.HANDLE,
lpBaseAddress: usize,
lpBuffer: ?*anyopaque,
nSize: usize,
lpNumberOfBytesRead: ?*usize,
) callconv(.winapi) NTSTATUS;
pub const MemoryAccessor = struct {
process_handle: win32.HANDLE,
pub fn init(process_handle: win32.HANDLE) @This() {
return .{
.process_handle = process_handle,
};
}
pub fn readBytes(this: @This(), address: usize, destination: []u8) !void {
var bytes_read: usize = undefined;
if(NtReadVirtualMemory(this.process_handle, address, destination.ptr, destination.len, &bytes_read) < 0)
return error.ReadVirtualMemoryFailed;
}
};
// Example usage
pub fn main() !void {
const process_handle = win32.GetCurrentProcess(); // Replace with call to OpenProcess
// Create memory accessor
const memory_accessor: MemoryAccessor = .init(process_handle);
const test_str = "I like cheese and cookies";
const test_address = @intFromPtr(test_str);
// print address of test_str
std.debug.print("Address at: {X}\n", .{ test_address });
// Read 25 bytes at the address of the string and write to buf
var buf: [25]u8 = undefined;
try memory_accessor.readBytes(test_address, &buf);
// Print
std.debug.print("Read: {s}", .{ buf });
}
#include <Windows.h>
#include <stdio.h>
#include <stdint.h>
typedef enum MemoryResult {
MEMORY_OK,
MEMORY_ERROR_READ_VIRTUAL_MEMORY
} MemoryResult;
typedef struct MemoryAccessor {
HANDLE process_handle;
} MemoryAccessor;
void MemoryAccessor_init(MemoryAccessor* const memory_accessor, const HANDLE process_handle) {
memory_accessor->process_handle = process_handle;
}
NTSYSCALLAPI NTSTATUS NTAPI NtReadVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_Out_writes_bytes_to_(NumberOfBytesToRead, *NumberOfBytesRead) PVOID Buffer,
_In_ SIZE_T NumberOfBytesToRead,
_Out_opt_ PSIZE_T NumberOfBytesRead
);
MemoryResult MemoryAccessor_readBytes(const MemoryAccessor* const memory_accessor, const uintptr_t address, uint8_t* const destination, const size_t num_of_bytes) {
size_t num_of_bytes_read;
if(NtReadVirtualMemory(memory_accessor->process_handle, (void*)address, destination, num_of_bytes, &num_of_bytes_read) < 0)
return MEMORY_ERROR_READ_VIRTUAL_MEMORY;
return MEMORY_OK;
}
// Example usage
int main() {
const HANDLE process_handle = GetCurrentProcess(); // Replace with OpenProcess
// Create memory accessor
MemoryAccessor memory_accessor;
MemoryAccessor_init(&memory_accessor, process_handle);
const char* const test_str = "I like cheese and cookies";
const uintptr_t test_address = (uintptr_t)test_str;
// Print address of test_str
printf("Address at: %llx\n", test_address);
// Read 25 bytes at the address of the string and write to buf
char buf[26];
if(MemoryAccessor_readBytes(&memory_accessor, test_address, (uint8_t*)&buf, 25) != MEMORY_OK) {
puts("Failed to read memory");
return -1;
}
// Set null terminator and print
buf[25] = 0;
printf("Read: %s\n", buf);
return 0;
}
#include <Windows.h>
#include <array>
#include <iostream>
#include <cstdint>
#include <expected>
#include <variant>
#include <span>
extern "C" NTSYSCALLAPI NTSTATUS NTAPI NtReadVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_Out_writes_bytes_to_(NumberOfBytesToRead, *NumberOfBytesRead) PVOID Buffer,
_In_ SIZE_T NumberOfBytesToRead,
_Out_opt_ PSIZE_T NumberOfBytesRead
);
class MemoryAccessor {
private:
HANDLE process_handle;
public:
MemoryAccessor(HANDLE _process_handle) : process_handle(_process_handle) {}
std::expected<std::monostate, std::string> readBytes(const size_t address, std::span<uint8_t> bytes) const {
size_t bytes_read;
if(NtReadVirtualMemory(this->process_handle, reinterpret_cast<void*>(address), bytes.data(), bytes.size_bytes(), &bytes_read) < 0)
std::unexpected("Error: Failed to read virtual memory");
return std::monostamte();
}
};
// Example usage
int main() {
const HANDLE process_handle = GetCurrentProcess(); // Replace with call to OpenProcess
// Create memory accessor
const MemoryAccessor memory_accessor = MemoryAccessor(process_handle);
const char* const test_str = "I like cheese and cookies";
const uintptr_t test_address = reinterpret_cast<std::uintptr_t>(test_str);
// Print address of test_str
std::cout << "Address at: " << std::hex << test_address << '\n';
// Read 25 bytes at the address of the string and write to buf
std::array<char, 26> buf;
if(auto result = memory_accessor.readBytes(test_address, std::span(reinterpret_cast<uint8_t*>(buf.data()), buf.size())); !result) {
std::cout << result.error() << '\n';
return -1;
}
// Set null terminator and print
buf.at(25) = 0;
std::cout << "Read: " << buf.data() << '\n';
return 0;
}
As you can see, the Zig version is much easier to read and write and way shorter and less complicated than the c and c++ version.
That's why I recommend using Zig for this.
Now that we got our basic functions for reading bytes, let's extend it so we can read values and array of values directly.
We'll implement `read` which will allow us to read a value directly and `readSlice` to allow us to read an array of values.
// In MemoryAccessor
pub fn readSlice(this: @This(), address: usize, T: type, destination: []T) !void {
try this.readBytes(address, @as([*]u8, @ptrCast(destination.ptr))[0..destination.len * @sizeOf(T)]);
}
pub fn read(this: @This(), address: usize, T: type) !T {
var result: T = undefined;
try this.readBytes(address, @as([*]u8, @ptrCast(&result))[0..@sizeOf(T)]);
return result;
}
// End
// Example usage
pub fn main() !void {
const process_handle = win32.GetCurrentProcess(); // Replace with call to OpenProcess
// Create memory accessor
const memory_accessor: MemoryAccessor = .init(process_handle);
const test_value: u32 = 67;
const test_value_address = @intFromPtr(&test_value);
// Print address of test_value
std.debug.print("Value Address at: {X}\n", .{ test_value_address });
// Read u32 from test_value_address
const value_read_result = try memory_accessor.read(test_value_address, u32);
// Print read value
std.debug.print("Read value: {d}\n", .{ value_read_result });
const test_slice: [3]u32 = .{ 69, 420, 67 };
const test_slice_address = @intFromPtr(&test_slice);
// Print address of test_slice
std.debug.print("Value Slice at: {X}\n", .{ test_slice_address });
// Read 3 u32 from test_slice_address
var read_slice: [3]u32 = undefined;
try memory_accessor.readSlice(test_slice_address, u32, &read_slice);
// Print slice read
std.debug.print("Read slice:", .{});
for(read_slice) |val|
std.debug.print(" {d}", .{ val });
std.debug.print("\n", .{});
}
#define MemoryAccessor_readSlice(memory_accessor, address, destination, len) MemoryAccessor_readBytes((memory_accessor), (address), (uint8_t*)(destination), (len * sizeof(typeof(*(destination)))))
#define MemoryAccessor_read(memory_accessor, address, destination) MemoryAccessor_readBytes((memory_accessor), (address), (uint8_t*)(destination), sizeof(typeof(*(destination))))
// Example usage
int main() {
const HANDLE process_handle = GetCurrentProcess(); // Replace with OpenProcess
// Create memory accessor
MemoryAccessor memory_accessor;
MemoryAccessor_init(&memory_accessor, process_handle);
const uint32_t test_value = 67;
const uintptr_t test_value_address = (uintptr_t)&test_value;
// Print address of test_value
printf("Value Address at: %llx\n", test_value_address);
// Read u32 from test_value_address
uint32_t value_read;
if(MemoryAccessor_read(&memory_accessor, test_value_address, &value_read) != MEMORY_OK) {
puts("Failed to read value");
return -1;
}
// Print read value
printf("Read value: %u\n", value_read);
const uint32_t test_slice[] = { 69, 420, 67 };
const uintptr_t test_slice_address = (uintptr_t)test_slice;
// Print address of test_slice
printf("Value Slice at: %llx\n", test_slice_address);
// Read 3 u32 from test_slice_address
uint32_t read_slice[3];
if(MemoryAccessor_readSlice(&memory_accessor, test_slice_address, read_slice, sizeof(read_slice)/sizeof(*read_slice)) != MEMORY_OK) {
puts("Failed to read slice");
return -1;
}
// Print slice read
fputs("Read slice:", stdout);
for(uint32_t* value = read_slice;value < read_slice + sizeof(read_slice)/sizeof(*read_slice);++value)
printf(" %u", *value);
puts("");
return 0;
}
// In MemoryAccessor
template<typename T>
std::expected<std::monostate, std::string> readSlice(const size_t address, std::span<T> slice) const {
return this->readBytes(address, std::span(reinterpret_cast<uint8_t*>(slice.data()), slice.size_bytes()));
}
template<typename T>
std::expected<T, std::string> read(const size_t address) const {
T result;
auto read_result = this->readBytes(address, std::span(reinterpret_cast<uint8_t*>(&result), sizeof(T)));
if(!read_result)
return std::unexpected(read_result.error());
return result;
}
// End
// Example usage
int main() {
const HANDLE process_handle = GetCurrentProcess(); // Replace with call to OpenProcess
// Create memory accessor
const MemoryAccessor memory_accessor = MemoryAccessor(process_handle);
const uint32_t test_value = 67;
const uintptr_t test_value_address = reinterpret_cast<std::uintptr_t>(&test_value);
// Print address of test_value
std::cout << "Value Address at: " << std::hex << test_value_address << '\n';
// Read u32 from test_value_address
auto value_read_result = memory_accessor.read<uint32_t>(test_value_address);
if(!value_read_result) {
std::cout << value_read_result.error() << '\n';
return -1;
}
// Print read value
std::cout << "Read value: " << std::dec << value_read_result.value() << '\n';
const std::array<uint32_t, 3> test_slice { 69, 420, 67 };
const uintptr_t test_slice_address = reinterpret_cast<std::uintptr_t>(test_slice.data());
// Print address of test_slice
std::cout << "Value Slice at: " << std::hex << test_slice_address << '\n';
// Read 3 u32 from test_slice_address
std::array<uint32_t, 3> read_slice;
auto slice_read_result = memory_accessor.readSlice(test_slice_address, std::span(read_slice.begin(), read_slice.end()));
if(!slice_read_result) {
std::cout << value_read_result.error() << '\n';
return -1;
}
// Print slice read
std::cout << "Read slice:" << std::dec;
for(const auto value : read_slice)
std::cout << ' ' << value;
std::cout << std::endl;
return 0;
}
Writing
For writing, it is pretty simple, we can just copy our code from before and replace it with `NtWriteVirtualMemory`.
pub extern "ntdll" fn NtWriteVirtualMemory(
hProcess: ?win32.HANDLE,
lpBaseAddress: usize,
lpBuffer: ?*const anyopaque,
nSize: usize,
lpNumberOfBytesWritten: ?*usize,
) callconv(.winapi) NTSTATUS;
// In MemoryAccessor
pub fn writeBytes(this: @This(), address: usize, bytes: []const u8) !void {
var bytes_written: usize = undefined;
if(NtWriteVirtualMemory(this.process_handle, address, bytes.ptr, bytes.len, &bytes_written) < 0)
return error.WriteVirtualMemoryFailed;
}
pub fn writeSlice(this: @This(), address: usize, T: type, bytes: []const T) !void {
try this.writeBytes(address, @as([*]const u8, @ptrCast(bytes.ptr))[0..bytes.len * @sizeOf(T)]);
}
pub fn write(this: @This(), address: usize, T: anytype) !void {
try this.writeBytes(address, @as([*]const u8, @ptrCast(&T))[0..@sizeOf(@TypeOf(T))]);
}
// End
// Example usage
pub fn main() !void {
const process_handle = win32.GetCurrentProcess(); // Replace with call to OpenProcess
// Create memory accessor
const memory_accessor: MemoryAccessor = .init(process_handle);
var test_value: u32 = 67;
const test_value_address = @intFromPtr(&test_value);
// Print address of test_value
std.debug.print("Value Address at: {X}\n", .{ test_value_address });
// write u32 to test_value_address
try memory_accessor.write(test_value_address, @as(u32, 420));
// Print test_value
std.debug.print("Test value: {d}\n", .{ test_value });
var test_slice: [3]u32 = .{ 69, 420, 67 };
const test_slice_address = @intFromPtr(&test_slice);
// Print address of test_slice
std.debug.print("Value Slice at: {X}\n", .{ test_slice_address });
// Write 3 u32 to test_slice_address
try memory_accessor.writeSlice(test_slice_address, u32, &[_]u32 { 67, 67, 69 });
// Print test_slice
std.debug.print("Test slice:", .{});
for(test_slice) |val|
std.debug.print(" {d}", .{ val });
std.debug.print("\n", .{});
}
NTSYSCALLAPI NTSTATUS NTAPI NtWriteVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_reads_bytes_(NumberOfBytesToWrite) PVOID Buffer,
_In_ SIZE_T NumberOfBytesToWrite,
_Out_opt_ PSIZE_T NumberOfBytesWritten
);
MemoryResult MemoryAccessor_writeBytes(const MemoryAccessor* const memory_accessor, const uintptr_t address, const uint8_t* const source, const size_t num_of_bytes) {
size_t num_of_bytes_written;
if(NtWriteVirtualMemory(memory_accessor->process_handle, (void*)address, (void*)source, num_of_bytes, &num_of_bytes_written) < 0)
return MEMORY_ERROR_WRITE_VIRTUAL_MEMORY;
return MEMORY_OK;
}
#define MemoryAccessor_writeSlice(memory_accessor, address, source, len) MemoryAccessor_writeBytes((memory_accessor), (address), (const uint8_t*)(source), (len * sizeof(typeof(*(source)))))
#define MemoryAccessor_write(memory_accessor, address, source) MemoryAccessor_writeBytes((memory_accessor), (address), (const uint8_t*)(source), sizeof(typeof(*source)))
// Example usage
int main() {
const HANDLE process_handle = GetCurrentProcess(); // Replace with OpenProcess
// Create memory accessor
MemoryAccessor memory_accessor;
MemoryAccessor_init(&memory_accessor, process_handle);
uint32_t test_value = 67;
const uintptr_t test_value_address = (uintptr_t)&test_value;
// Print address of test_value
printf("Value Address at: %llx\n", test_value_address);
// Write u32 from test_value_address
const uint32_t to_write = 420;
if(MemoryAccessor_write(&memory_accessor, test_value_address, &to_write) != MEMORY_OK) {
puts("Failed to write value");
return -1;
}
// Print test_value
printf("Test value: %u\n", test_value);
uint32_t test_slice[] = { 69, 420, 67 };
const uintptr_t test_slice_address = (uintptr_t)test_slice;
// Print address of test_slice
printf("Value Slice at: %llx\n", test_slice_address);
// Write 3 u32 from test_slice_address
uint32_t write_slice[] = { 67, 67, 69 };
if(MemoryAccessor_writeSlice(&memory_accessor, test_slice_address, write_slice, sizeof(write_slice)/sizeof(*write_slice)) != MEMORY_OK) {
puts("Failed to write slice");
return -1;
}
// Print test_slice
fputs("Test slice:", stdout);
for(uint32_t* value = (uint32_t*)test_slice;value < test_slice + sizeof(test_slice)/sizeof(*test_slice);++value)
printf(" %u", *value);
puts("");
return 0;
}
extern "C" NTSYSCALLAPI NTSTATUS NTAPI NtWriteVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_reads_bytes_(NumberOfBytesToWrite) const void * Buffer,
_In_ SIZE_T NumberOfBytesToWrite,
_Out_opt_ PSIZE_T NumberOfBytesWritten
);
// In MemoryAccessor
std::expected<std::monostate, std::string> writeBytes(const size_t address, std::span<const uint8_t> bytes) const {
size_t bytes_written;
if(NtWriteVirtualMemory(this->process_handle, reinterpret_cast<void*>(address), reinterpret_cast<const void*>(bytes.data()), bytes.size_bytes(), &bytes_written) < 0)
std::unexpected("Error: Failed to write virtual memory");
return std::monostate();
}
template<typename T>
std::expected<std::monostate, std::string> writeSlice(const size_t address, std::span<T> slice) const {
return this->writeBytes(address, std::span(reinterpret_cast<const uint8_t*>(slice.data()), slice.size_bytes()));
}
template<typename T>
std::expected<std::monostate, std::string> write(const size_t address, const T value) const {
return this->writeBytes(address, std::span(reinterpret_cast<const uint8_t*>(&value), sizeof(T)));
}
// End
// Example usage
int main() {
const HANDLE process_handle = GetCurrentProcess(); // Replace with call to OpenProcess
// Create memory accessor
const MemoryAccessor memory_accessor = MemoryAccessor(process_handle);
uint32_t test_value = 67;
const uintptr_t test_value_address = reinterpret_cast<std::uintptr_t>(&test_value);
// Print address of test_value
std::cout << "Value Address at: " << std::hex << test_value_address << '\n';
// Write u32 from test_value_address
auto value_read_result = memory_accessor.write<uint32_t>(test_value_address, 420);
if(!value_read_result) {
std::cout << value_read_result.error() << '\n';
return -1;
}
// Print test_value
std::cout << "Test value: " << std::dec << test_value << '\n';
std::array<uint32_t, 3> test_slice { 69, 420, 67 };
const uintptr_t test_slice_address = reinterpret_cast<std::uintptr_t>(test_slice.data());
// Print address of test_slice
std::cout << "Value Slice at: " << std::hex << test_slice_address << '\n';
// Read 3 u32 from test_slice_address
const std::array<uint32_t, 3> write_slice { 67, 67, 69 };
auto slice_read_result = memory_accessor.writeSlice(test_slice_address, std::span(write_slice.begin(), write_slice.end()));
if(!slice_read_result) {
std::cout << value_read_result.error() << '\n';
return -1;
}
// Print test_slice
std::cout << "Test slice:" << std::dec;
for(const auto value : test_slice)
std::cout << ' ' << value;
std::cout << std::endl;
return 0;
}
In the next chapter, we will implement page iterator.