all repos — gemini-redirect @ bb4c617475d7a9c420ce2ec06fbf54e2e4a5e01b

content/blog/woce-6.md (view raw)

  1+++
  2title = "Writing our own Cheat Engine: Pointers"
  3date = 2021-03-13
  4updated = 2021-03-13
  5[taxonomies]
  6category = ["sw"]
  7tags = ["windows", "rust", "hacking"]
  8+++
  9
 10This is part 6 on the *Writing our own Cheat Engine* series:
 11
 12* [Part 1: Introduction](/blog/woce-1) (start here if you're new to the series!)
 13* [Part 2: Exact Value scanning](/blog/woce-2)
 14* [Part 3: Unknown initial value](/blog/woce-3)
 15* [Part 4: Floating points](/blog/woce-4)
 16* [Part 5: Code finder](/blog/woce-5)
 17* Part 6: Pointers
 18
 19In part 5 we wrote our very own debugger. We learnt that Cheat Engine is using hardware breakpoints to watch memory change, and how to do the same ourselves. We also learnt that hardware points are not the only way to achieve the effect of watchpoints, although they certainly are the fastest and cleanest approach.
 20
 21In this post, we will be reusing some of that knowledge to find out a closely related value, the *pointer* that points to the real value[^1]. As a quick reminder, a pointer is nothing but an `usize`[^2] representing the address of another portion of memory, in this case, the actual value we will be scanning for. A pointer is a value that, well, points elsewhere. In Rust we normally use reference instead, which are safer (typed and their lifetime is tracked) than pointers, but in the end we can achieve the same with both.
 22
 23Why care about pointers? It turns out that things, such as your current health in-game, are very unlikely to end up in the same memory position when you restart the game (or even change to another level, or even during gameplay). So, if you perform a scan and find that the address where your health is stored is `0x73AABABE`, you might be tempted to save it and reuse it next time you launch the game. Now you don't need to scan for it again! Alas, as soon as you restart the game, the health is now stored at `0x5AADBEEF`.
 24
 25Not all hope is lost! The game must *somehow* have a way to reliably find this value, and the way it's done is with pointers. There will always be some base address that holds a pointer, and the game code knows where to find this pointer. If we are also able to find the pointer at said base address, and follow it ourselves ("dereferencing" it), we can perform the same steps the game is doing, and reliably find the health no matter how much we restart the game[^3].
 26
 27## Code finder
 28
 29<details open><summary>Cheat Engine Tutorial: Step 6</summary>
 30
 31> In the previous step I explained how to use the Code finder to handle changing locations. But that method alone makes it difficult to find the address to set the values you want. That's why there are pointers:
 32>
 33> At the bottom you'll find 2 buttons. One will change the value, and the other changes the value AND the location of the value. For this step you don't really need to know assembler, but it helps a lot if you do.
 34>
 35> First find the address of the value. When you've found it use the function to find out what accesses this address.
 36>
 37> Change the value again, and a item will show in the list. Double click that item. (or select and click on more info) and a new window will open with detailed information on what happened when the instruction ran.
 38>
 39> If the assembler instruction doesn't have anything between a '[' and ']' then use another item in the list. If it does it will say what it think will be the value of the pointer you need.
 40>
 41> Go back to the main cheat engine window (you can keep this extra info window open if you want, but if you close it, remember what is between the \[ and \]) and do a 4 byte scan in hexadecimal for the value the extra info told you. When done scanning it may return 1 or a few hundred addresses. Most of the time the address you need will be the smallest one. Now click on manually add and select the pointer checkbox.
 42>
 43> The window will change and allow you to type in the address of a pointer and a offset. Fill in as address the address you just found. If the assembler instruction has a calculation (e.g: [esi+12]) at the end then type the value in that's at the end. else leave it 0. If it was a more complicated instruction look at the calculation.
 44>
 45> Example of a more complicated instruction:
 46>
 47> [EAX*2+EDX+00000310] eax=4C and edx=00801234.
 48>
 49> In this case EDX would be the value the pointer has, and EAX\*2+00000310 the offset, so the offset you'd fill in would be 2\*4C+00000310=3A8.  (this is all in hex, use calc.exe from windows in scientific mode to calculate).
 50>
 51> Back to the tutorial, click OK and the address will be added, If all went right the address will show P->xxxxxxx, with xxxxxxx being the address of the value you found. If thats not right, you've done something wrong. Now, change the value using the pointer you added in 5000 and freeze it. Then click Change pointer, and if all went right the next button will become visible.
 52>
 53> *extra*: And you could also use the pointer scanner to find the pointer to this address.
 54
 55</details>
 56
 57## On-access watchpoints
 58
 59Last time we managed to learn how hardware breakpoints were being set by observing Cheat Engine's behaviour. I think it's now time to handle this properly instead. We'll check out the [CPU Registers x86 page on OSDev][dbg-reg] to learn about it:
 60
 61* DR0, DR1, DR2 and DR3 can hold a memory address each. This address will be used by the breakpoint.
 62* DR4 is actually an [obsolete synonym][dr4] for DR6.
 63* DR5 is another obsolete synonym, this time for DR7.
 64* DR6 is debug status. The four lowest bits indicate which breakpoint was hit, and the four highest bits contain additional information. We should make sure to clear this ourselves when a breakpoint is hit.
 65* DR7 is debug control, which we need to study more carefully.
 66
 67Each debug register DR0 through DR3 has two corresponding bits in DR7, starting from the lowest-order bit, to indicate whether the corresponding register is a **L**ocal or **G**lobal breakpoint. So it looks like this:
 68
 69```
 70  Meaning: [ .. .. | G3 | L3 | G2 | L2 | G1 | L1 | G0 | L0 ]
 71Bit-index:   31-08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00
 72```
 73
 74Cheat Engine was using local breakpoints, because the zeroth bit was set. Probably because we don't want these breakpoints to infect other programs! Because we were using only one breakpoint, only the lowermost bit was being set. The local 1st, 2nd and 3rd bits were unset.
 75
 76Now, each debug register DR0 through DR4 has four additional bits in DR7, two for the **C**ondition and another two for the **S**ize:
 77
 78```
 79  Meaning: [   S3  |   C3  |   S2  |   C2  |   S1  |   C1  |   S0  |   C0  | .. .. ]
 80Bit-index:   31 30 | 29 28 | 27 26 | 25 24 | 23 22 | 21 20 | 19 18 | 17 16 | 15-00
 81```
 82
 83The two bits of the condition mean the following:
 84
 85* `00` execution breakpoint.
 86* `01` write watchpoint.
 87* `11` read/write watchpoint.
 88* `10` unsupported I/O read/write.
 89
 90When we were using Cheat Engine to add write watchpoints, the bits 17 and 16 were indeed set to `01`, and the bits 19 and 18 were set to `11`. Hm, but *11<sub>2</sub>&nbsp;=&nbsp;3<sub>10</sub>*&nbsp;, and yet, we were watching writes to 4 bytes. So what's up with this? Is there a different mapping for the size which isn't documented at the time of writing? Seems we need to learn from Cheat Engine's behaviour one more time.
 91
 92For reference, this is what DR7 looked like when we added a single write watchpoint:
 93
 94```
 95hex: 000d_0001
 96bin: 00000000_00001101_00000000_00000001
 97```
 98
 99And this is the code I will be using to check the breakpoints of different sizes:
100
101```
102thread::enum_threads(pid)
103    .unwrap()
104    .into_iter()
105    .for_each(|tid| {
106        let thread = thread::Thread::open(tid).unwrap();
107        let ctx = thread.get_context().unwrap();
108        eprintln!("hex: {:08x}", ctx.Dr7);
109        eprintln!("bin: {:032b}", ctx.Dr7);
110    });
111```
112
113Let's compare this to watchpoints for sizes 1, 2, 4 and 8 bytes:
114
115```
1161 byte
117hex: 0001_0401
118bin: 00000000_00000001_00000100_00000001
119
1202 bytes
121hex: 0005_0401
122bin: 00000000_00000101_00000100_00000001
123
1244 bytes
125hex: 000d_0401
126bin: 00000000_00001101_00000100_00000001
127
1288 bytes
129hex: 0009_0401
130bin: 00000000_00001001_00000100_00000001
131                            ^ wut?
132```
133
134I have no idea what's up with that stray tenth bit. Its use does not seem documented, and things worked fine without it, so we'll ignore it. The lowest bit is set to indicate we're using DR0, bits 17 and 16 represent the write watchpoint, and the size seems to be as follows:
135
136* `00` for a single byte.
137* `01` for two bytes (a "word").
138* `11` for four bytes (a "double word").
139* `10` for eight bytes (a "quadruple word").
140
141Doesn't make much sense if you ask me, but we'll roll with it. Just to confirm, this is what the "on-access" breakpoint looks like according to Cheat Engine:
142
143```
144hex: 000f_0401
145bin: 00000000_00001111_00000100_00000001
146```
147
148So it all checks out! The bit pattern is `11` for read/write (technically, a write is also an access). Let's implement this!
149
150## Proper breakpoint handling
151
152The first thing we need to do is represent the possible breakpoint conditions:
153
154```rust
155#[repr(u8)]
156pub enum Condition {
157    Execute = 0b00,
158    Write = 0b01,
159    Access = 0b11,
160}
161```
162
163And also the legal breakpoint sizes:
164
165```rust
166#[repr(u8)]
167pub enum Size {
168    Byte = 0b00,
169    Word = 0b01,
170    DoubleWord = 0b11,
171    QuadWord = 0b10,
172}
173```
174
175We are using `#[repr(u8)]` so that we can convert a given variant into the corresponding bit pattern. With the right types defined in order to set a breakpoint, we can start implementing the method that will set them (inside `impl Thread`):
176
177```rust
178pub fn add_breakpoint(&self, addr: usize, cond: Condition, size: Size) -> io::Result<Breakpoint> {
179    let mut context = self.get_context()?;
180    todo!()
181}
182```
183
184First, let's try finding an "open spot" where we could set our breakpoint. We will "slide" a the `0b11` bitmask over the lowest eight bits, and if and only if both the local and global bits are unset, then we're free to set a breakpoint at this index[^4]:
185
186```rust
187let index = (0..4)
188    .find_map(|i| ((context.Dr7 & (0b11 << (i * 2))) == 0).then(|| i))
189    .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no debug register available"))?;
190```
191
192Once an `index` is found, we can set the address we want to watch in the corresponding register and update the debug control bits:
193
194```rust
195let addr = addr as u64;
196match index {
197    0 => context.Dr0 = addr,
198    1 => context.Dr1 = addr,
199    2 => context.Dr2 = addr,
200    3 => context.Dr3 = addr,
201    _ => unreachable!(),
202}
203
204let clear_mask = !((0b1111 << (16 + index * 4)) | (0b11 << (index * 2)));
205context.Dr7 &= clear_mask;
206
207context.Dr7 |= 1 << (index * 2);
208
209let sc = (((size as u8) << 2) | (cond as u8)) as u64;
210context.Dr7 |= sc << (16 + index * 4);
211
212self.set_context(&context)?;
213Ok(Breakpoint {
214    thread: self,
215    clear_mask,
216})
217```
218
219Note that we're first creating a "clear mask". We switch on all the bits that we may use for this breakpoint, and then negate. Effectively, `Dr7 & clear_mask` will make sure we don't leave any bit high on accident. We apply the mask before OR-ing the rest of bits to also clear any potential garbage on the size and condition bits. Next, we set the bit to enable the new local breakpoint, and also store the size and condition bits at the right location.
220
221With the context updated, we can set it back and return the `Breakpoint`. It stores the `thread` and the `clear_mask` so that it can clean up on `Drop`. We are technically relying on `Drop` to run behaviour here, but the cleanup is done on a best-effort basis. If the user intentionally forgets the `Breakpoint`, maybe they want the `Breakpoint` to forever be set.
222
223This logic is begging for a testcase though; I'll split it into a new `Breakpoint::update_dbg_control` method and test that out:
224
225```rust
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn brk_add_one() {
233        // DR7 starts with garbage which should be respected.
234        let (clear_mask, dr, dr7) =
235            Breakpoint::update_dbg_control(0x1700, Condition::Write, Size::DoubleWord).unwrap();
236
237        assert_eq!(clear_mask, 0xffff_ffff_fff0_fffc);
238        assert_eq!(dr, DebugRegister::Dr0);
239        assert_eq!(dr7, 0x0000_0000_000d_1701);
240    }
241
242    #[test]
243    fn brk_add_two() {
244        let (clear_mask, dr, dr7) = Breakpoint::update_dbg_control(
245            0x0000_0000_000d_0001,
246            Condition::Write,
247            Size::DoubleWord,
248        )
249        .unwrap();
250
251        assert_eq!(clear_mask, 0xffff_ffff_ff0f_fff3);
252        assert_eq!(dr, DebugRegister::Dr1);
253        assert_eq!(dr7, 0x0000_0000_00dd_0005);
254    }
255
256    #[test]
257    fn brk_try_add_when_max() {
258        assert!(Breakpoint::update_dbg_control(
259            0x0000_0000_dddd_0055,
260            Condition::Write,
261            Size::DoubleWord
262        )
263        .is_none());
264    }
265}
266```
267
268```
269running 3 tests
270test thread::tests::brk_add_one ... ok
271test thread::tests::brk_add_two ... ok
272test thread::tests::brk_try_add_when_max ... ok
273```
274
275Very good! With proper breakpoint handling usable, we can continue.
276
277## Inferring the pointer value
278
279After scanning memory for the location we're looking for (say, our current health), we then add an access watchpoint, and wait for an exception to occur. As a reminder, here's the page with the [Debugging Events][dbg-events]:
280
281```rust
282let addr = ...;
283let mut threads = ...;
284
285let _watchpoints = threads
286    .iter_mut()
287    .map(|thread| {
288        thread
289            .add_breakpoint(addr, thread::Condition::Access, thread::Size::DoubleWord)
290            .unwrap()
291    })
292    .collect::<Vec<_>>();
293
294loop {
295    let event = debugger.wait_event(None).unwrap();
296    if event.dwDebugEventCode == winapi::um::minwinbase::EXCEPTION_DEBUG_EVENT {
297        let exc = unsafe { event.u.Exception() };
298        if exc.ExceptionRecord.ExceptionCode == winapi::um::minwinbase::EXCEPTION_SINGLE_STEP {
299            todo!();
300        }
301    }
302    debugger.cont(event, true).unwrap();
303}
304```
305
306Now, inside the `todo!()` we will want to do a few things, namely printing out the instructions "around this location" and dumping the entire thread context on screen. To print the instructions, we need to import `iced_x86` again, iterate over all memory regions to find the region where the exception happened, read the corresponding bytes, decode the instructions, and when we find the one with a corresponding instruction pointer, print "around it":
307
308```rust
309use iced_x86::{Decoder, DecoderOptions, Formatter, Instruction, NasmFormatter};
310
311let addr = exc.ExceptionRecord.ExceptionAddress as usize;
312let region = process
313    .memory_regions()
314    .into_iter()
315    .find(|region| {
316        let base = region.BaseAddress as usize;
317        base <= addr && addr < base + region.RegionSize
318    })
319    .unwrap();
320
321let bytes = process
322    .read_memory(region.BaseAddress as usize, region.RegionSize)
323    .unwrap();
324
325let mut decoder = Decoder::new(64, &bytes, DecoderOptions::NONE);
326decoder.set_ip(region.BaseAddress as _);
327
328let mut formatter = NasmFormatter::new();
329let mut output = String::new();
330
331let instructions = decoder.into_iter().collect::<Vec<_>>();
332for (i, ins) in instructions.iter().enumerate() {
333    if ins.next_ip() as usize == addr {
334        let low = i.saturating_sub(5);
335        let high = (i + 5).min(instructions.len());
336        for j in low..high {
337            let ins = &instructions[j];
338            print!("{} {:016X} ", if j == i { ">>>" } else { "   " }, ins.ip());
339            let k = (ins.ip() - region.BaseAddress as usize as u64) as usize;
340            let instr_bytes = &bytes[k..k + ins.len()];
341            for b in instr_bytes.iter() {
342                print!("{:02X}", b);
343            }
344            if instr_bytes.len() < 10 {
345                for _ in 0..10usize.saturating_sub(instr_bytes.len()) {
346                    print!("  ");
347                }
348            }
349
350            output.clear();
351            formatter.format(ins, &mut output);
352            println!(" {}", output);
353        }
354        break;
355    }
356}
357debugger.cont(event, true).unwrap();
358break;
359```
360
361The result is pretty fancy:
362
363```
364    000000010002CAAC 48894DF0             mov [rbp-10h],rcx
365    000000010002CAB0 488955F8             mov [rbp-8],rdx
366    000000010002CAB4 48C745D800000000     mov qword [rbp-28h],0
367    000000010002CABC 90                   nop
368    000000010002CABD 488B050CA02D00       mov rax,[rel 100306AD0h]
369>>> 000000010002CAC4 8B00                 mov eax,[rax]
370    000000010002CAC6 8945EC               mov [rbp-14h],eax
371    000000010002CAC9 B9E8030000           mov ecx,3E8h
372    000000010002CACE E88D2FFEFF           call 000000010000FA60h
373    000000010002CAD3 8945E8               mov [rbp-18h],eax
374```
375
376Cool! So `rax` is holding an address, meaning it's a pointer, and the value it reads (dereferences) is stored back into `eax` (because it does not need `rax` anymore). Alas, the current thread context has the register state *after* the instruction was executed, and `rax` no longer contains the address at this point. However, notice how the previous instruction writes a fixed value to `rax`, and then that value is used to access memory, like so:
377
378```rust
379let eax = memory[memory[0x100306AD0]];
380```
381
382The value at `memory[0x100306AD0]` *is* the pointer! No offsets are used, because nothing is added to the pointer after it's read. This means that, if we simply scan for the address we were looking for, we should find out where the pointer is stored:
383
384```rust
385let addr = ...;
386let scan = process.scan_regions(&regions, Scan::Exact(addr as u64));
387
388scan.into_iter().for_each(|region| {
389    region.locations.iter().for_each(|ptr_addr| {
390        println!("[{:x}] = {:x}", ptr_addr, addr);
391    });
392});
393```
394
395And just like that:
396
397```
398[100306ad0] = 15de9f0
399```
400
401Notice how the pointer address found matches with the offset used by the instructions:
402
403```
404    000000010002CABD 488B050CA02D00       mov rax,[rel 100306AD0h]
405           this is the same as the value we just found ^^^^^^^^^^
406```
407
408Very interesting indeed. We were actually very lucky to have only found a single memory location containing the pointer value, `0x15de9f0`. Cheat Engine somehow knows that this value is always stored at `0x100306ad0` (or rather, at `Tutorial-x86_64.exe+306AD0`), because the address shows green. How does it do this?
409
410## Base addresses
411
412Remember back in [part 2](/blog/woce-2) when we introduced the memory regions? They're making a comeback! A memory region contains both the current memory protection option *and* the protection level when the region was created. If we try printing out the protection levels for both the memory region containing the value, and the memory region containing the pointer, this is what we get (the addresses differ from the ones previously because I restarted the tutorial):
413
414```
415Region holding the value:
416    BaseAddress: 0xb0000
417    AllocationBase: 0xb0000
418    AllocationProtect: 0x4
419    RegionSize: 1007616
420    State: 4096
421    Protect: 4
422    Type: 0x20000
423
424Region holding the pointer:
425    BaseAddress: 0x100304000
426    AllocationBase: 0x100000000
427    AllocationProtect: 0x80
428    RegionSize: 28672
429    State: 4096
430    Protect: 4
431    Type: 0x1000000
432```
433
434Interesting! According to the [`MEMORY_BASIC_INFORMATION` page][meminfo], the type for the first region is `MEM_PRIVATE`, and the type for the second region is `MEM_IMAGE` which:
435
436> Indicates that the memory pages within the region are mapped into the view of an image section.
437
438The protection also changes from `PAGE_EXECUTE_WRITECOPY` to simply `PAGE_READWRITE`, but I don't think it's relevant. Neither the type seems to be much more relevant. In [part 2](/blog/woce-2) we also mentioned the concept of "base address", but decided against using it, because starting to look for regions at address zero seemed to work fine. However, it would make sense that fixed "addresses" start at some known "base". Let's try getting the [base address for all loaded modules][baseaddr]. Currently, we only get the address for the base module, in order to retrieve its name, but now we need them all:
439
440```rust
441pub fn enum_modules(&self) -> io::Result<Vec<winapi::shared::minwindef::HMODULE>> {
442    let mut size = 0;
443    if unsafe {
444        winapi::um::psapi::EnumProcessModules(
445            self.handle.as_ptr(),
446            ptr::null_mut(),
447            0,
448            &mut size,
449        )
450    } == FALSE
451    {
452        return Err(io::Error::last_os_error());
453    }
454
455    let mut modules = Vec::with_capacity(size as usize / mem::size_of::<HMODULE>());
456    if unsafe {
457        winapi::um::psapi::EnumProcessModules(
458            self.handle.as_ptr(),
459            modules.as_mut_ptr(),
460            (modules.capacity() * mem::size_of::<HMODULE>()) as u32,
461            &mut size,
462        )
463    } == FALSE
464    {
465        return Err(io::Error::last_os_error());
466    }
467
468    unsafe {
469        modules.set_len(size as usize / mem::size_of::<HMODULE>());
470    }
471
472    Ok(modules)
473}
474```
475
476The first call is used to retrieve the correct `size`, then we allocate just enough, and make the second call. The returned type are pretty much memory addresses, so let's see if we can find regions that contain them:
477
478```rust
479let mut bases = 0;
480let modules = process.enum_modules().unwrap();
481let regions = process.memory_regions();
482regions.iter().for_each(|region| {
483    if modules.iter().any(|module| {
484        let base = region.AllocationBase as usize;
485        let addr = *module as usize;
486        base <= addr && addr < base + region.RegionSize
487    }) {
488        bases += 1;
489    }
490});
491
492println!(
493    "{}/{} regions have a module address within them",
494    bases,
495    regions.len()
496);
497```
498
499```
50041/353 regions have a module address within them
501```
502
503Exciting stuff! It appears `base == addr` also does the trick[^5], so now we could build a `bases: HashSet<usize>` and simply check if `bases.contains(&region.AllocationBase as usize)` to determine whether `region` is a base address or not[^6]. So there we have it! The address holding the pointer value does fall within one of these "base regions". You can also get the name from one of these module addresses, and print it in the same way as Cheat Engine does it (such as `Tutorial-x86_64.exe+306AD0`).
504
505## Finale
506
507So, there's no "automated" solution to all of this? That's the end? Well, yes, once you have a pointer you can dereference it once and then write to the given address to complete the tutorial step! I can understand how this would feel a bit underwhelming, but in all fairness, we were required to pretty-print assembly to guess what pointer address we could potentially need to look for. There is an [stupidly large amount of instructions][isa], and I'm sure a lot of them can access memory, so automating that would be rough. We were lucky that the instructions right before the one that hit the breakpoint were changing the memory address, but you could imagine this value coming from somewhere completely different. It could also be using a myriad of different techniques to apply the offset. I would argue manual intervention is a must here[^7].
508
509We have learnt how to pretty-print instructions, and had a very gentle introduction to figuring out what we may need to look for. The code to retrieve the loaded modules, and their corresponding regions, will come in handy later on. Having access to this information lets us know when to stop looking for additional pointers. As soon as a pointer is found within a memory region corresponding to a base module, we're done! Also, I know the title doesn't really much the contents of this entry (sorry about that), but I'm just following the convention of calling it whatever the Cheat Engine tutorial calls them.
510
511The [code for this post][code] is available over at my GitHub. You can run `git checkout step6` after cloning the repository to get the right version of the code, although you will have to `checkout` to individual commits if you want to review, for example, how the instructions were printed out. Only the code necessary to complete the step is included at the `step6` tag.
512
513In the next post, we'll tackle the sixth step of the tutorial: Code Injection. This will be pretty similar to part 5, but instead of writing out a simple NOP instruction, we will have to get a bit more creative.
514
515### Footnotes
516
517[^1]: This will only be a gentle introduction to pointers. Part 8 of this series will have to rely on more advanced techniques.
518
519[^2]: Kind of. The size of a pointer isn't necessarily the size as `usize`, although `usize` is guaranteed to be able of representing every possible address. For our purposes, we can assume a pointer is as big as `usize`.
520
521[^3]: Game updates are likely to pull more code and shuffle stuff around. This is unfortunately a difficult problem to solve. But storing a pointer which is usable across restarts for as long as the game doesn't update is still a pretty darn big improvement over having to constantly scan for the locations we care about. Although if you're smart enough to look for certain unique patterns, even if the code is changed, finding those patterns will give you the new updated address, so it's not *impossible*.
522
523[^4]: `bool::then` is a pretty recent addition at the time of writing (1.50.0), so make sure you `rustup update` if it's erroring out!
524
525[^5]: I wasn't sure if there would be some metadata before the module base address but within the region, so I went with the range check. What *is* important however is using `AllocationBase`, not `BaseAddress`. They're different, and this did bite me.
526
527[^6]: As usual, I have no idea if this is how Cheat Engine is doing it, but it seems reasonable.
528
529[^6]: But nothing's stopping you from implementing some heuristics to get the job done for you. If you run some algorithm in your head to find what the pointer value could be, you can program it in Rust as well, although I don't think it's worth the effort.
530
531[dbg-reg]: https://wiki.osdev.org/CPU_Registers_x86#Debug_Registers
532[dr4]: https://en.wikipedia.org/wiki/X86_debug_register
533[dbg-events]: https://docs.microsoft.com/en-us/windows/win32/debug/debugging-events
534[meminfo]: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information
535[baseaddr]: https://stackoverflow.com/a/26573045/4759433
536[isa]: https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-2a-manual.html
537[code]: https://github.com/lonami/memo