Page Iterator
In this guide, we will write a simple memory page allocator and use it to scan for pointers
In this guide, we will write a simple memory page allocator and use it to scan for pointers
Memory is usually divided into different pages. Each page is usually aligned to 4096.
This is so that the OS can assign different protection and states to each page, and allow pages to be paged out or mapped from a file.
We will be writing a page iterator, allowing us to iterate through all the pages in a process.
Why do we want this?
We need this so we can scan memory effectively.
We don't want to scan unmapped regions as those would make our read function fail.
We also want to minimize the number of calls to our read function as syscalls can take a long time to complete.
With a page iterator, we can iterate through each page, read the entire page, then do the scanning on our own process, making it faster.
To do this, we can use the `VirtualQueryEx` function provided by Windows.
However, from the same reason as in the previous chapter, we will use `NtQueryVirtualMemory` instead.
pub extern "ntdll" fn NtQueryVirtualMemory(
hProcess: ?win32.HANDLE,
lpAddress: usize,
memoryInformationClass: u32,
lpBuffer: ?*win32.MEMORY_BASIC_INFORMATION,
dwLength: usize,
outLength: ?*usize,
) callconv(.winapi) NTSTATUS;
// In MemoryAccessor
pub fn getBasicInformation(this: @This(), address: usize) !win32.MEMORY_BASIC_INFORMATION {
var mbi: win32.MEMORY_BASIC_INFORMATION = undefined;
if(NtQueryVirtualMemory(this.process_handle, address, 0, &mbi, @sizeOf(win32.MEMORY_BASIC_INFORMATION), null) < 0)
return error.QueryVirtualMemoryFailed;
return mbi;
}
// End
pub const PageIterator = struct {
memory_accessor: MemoryAccessor,
cur: usize,
pub fn init(memory_accessor: MemoryAccessor) @This() {
return .{
.memory_accessor = memory_accessor,
.cur = 0,
};
}
pub fn next(this: *@This()) ?win32.MEMORY_BASIC_INFORMATION {
if (this.cur >= std.math.maxInt(usize))
return null;
const mbi = this.memory_accessor.getBasicInformation(this.cur) catch return null;
this.cur = @intFromPtr(mbi.BaseAddress) + mbi.RegionSize;
return mbi;
}
};
// 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 my_value: u32 = 0xDEADBEEF;
const my_pointer = &my_value;
std.debug.print("My pointer is at: {X}\n", .{ @intFromPtr(&my_pointer) });
// Example scan for pointers to my_value
const allocator = std.heap.smp_allocator;
var it: PageIterator = .init(memory_accessor);
var buffer: std.ArrayList(*const u32) = .empty;
defer buffer.deinit(allocator);
while (it.next()) |mbi| {
// Take only readable pages
if((mbi.Protect & (win32.PAGE_READONLY | win32.PAGE_READWRITE)) == 0 or (mbi.Protect & win32.PAGE_GUARD) != 0 or (mbi.State & (win32.MEM_COMMIT)) == 0)
continue;
try buffer.resize(allocator, mbi.RegionSize/@sizeOf(*const u32));
memory_accessor.readSlice(@intFromPtr(mbi.BaseAddress), *const u32, buffer.items) catch continue;
for(buffer.items, 0..) |value, i|
if(value == my_pointer)
std.debug.print("Found pointer at: {X}\n", .{ @intFromPtr(mbi.BaseAddress) + i * @sizeOf(*const u32) });
}
std.mem.doNotOptimizeAway(my_value);
std.mem.doNotOptimizeAway(my_pointer);
}
In the example, we create a variable to store a value, and take the address of that variable and store it in another variable (this is called a pointer).
We then print the address of the pointer then scan for the address of the pointer using the address of the first variable.
I got:
My pointer is at: 7FF6CE2471F8
Found pointer at: 8554DFF908
Found pointer at: 7FF6CE2471F8