Step 216. x64dbg In Depth: Memory Breakpoints and Patching — Surgery on a Running Program
Level 3 — Reversing Track | Difficulty ★★★☆☆ | Estimated time: 5 hours
Prerequisites: you’ve finished Step 178 (Reversing Sampler) and Step 215 (Ghidra in depth). You can read conditional branches (je/jne) and comparisons (cmp/test) in assembly.
⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. Patching is done only on "practice binaries I made myself" — patching someone else’s software and distributing it can violate copyright and licenses.
- What you need: x64dbg is a Windows GUI tool and isn’t present in this Linux environment — its screens are shown as examples, while the principles of patching and memory watch are measured with Python and gdb (measured: Ubuntu 24.04, gdb 15.1, Python 3.12).
- Caution: today’s two core techniques (memory breakpoints, patching) are legitimate techniques used daily in malware analysis. Still, remember that distributing a patched result is a separate legal matter.
If Ghidra is "a desk for reading and organizing," x64dbg is "the control room of a running program." The two techniques today are reversing’s most powerful tricks. One is the memory breakpoint — for the question "where on earth does this value get read?", it stops execution at the very moment the value is read and answers you. The other is patching — swapping a program’s instructions to flip an authentication. Both are simple in principle, and today we go down to the file-byte level to confirm that principle ourselves.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Perform the routine of reverse-tracing comparison code via string search in x64dbg
- Explain the difference between code breakpoints and memory breakpoints, and choose between them
- Identify the machine-code bytes of conditional-branch instructions (je/jne)
- Carry out the full patching process (in-memory edit → save to file)
- Reproduce the patching principle yourself with Python byte manipulation
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | x64dbg via screen examples (Windows GUI) / principle measurements: gdb 15.1 + Python 3.12 on WSL Ubuntu (tmp_test script) |
| Today’s commands/keys | x64dbg: right-click → Search for → All referenced strings, F2 (BP), right-click → Breakpoint → Memory, double-click to edit an instruction, Ctrl+P (Patches) / gdb: watch, rwatch / Python: reading and writing binary bytes |
| Concepts needed | Code BP vs memory BP (hardware BP), conditional-branch machine code (0x74/0x75), virtual address ↔ file offset conversion |
| Today’s deliverables | 1 patched binary with flipped authentication + a before/after comparison record |
2-1. Code Breakpoints vs Memory Breakpoints
The breakpoints you’ve used so far are code BPs — "stop when execution reaches this instruction." You have to know the address. But the more frequent question in reversing is the opposite: "I don’t know where this value gets used."
A memory breakpoint targets data, not an address — "stop when this memory is read (or written)." Set one on your input buffer, and execution stops the instant the program reads your input to check it. Without knowing the comparison code, you get dragged to the comparison code. In x64dbg you set it in the dump window: right-click the address → Breakpoint → Memory, on access.
It’s implemented with the CPU’s debug registers (hardware BPs), so there’s a count limit (usually 4). gdb has the same feature as watch (on write) and rwatch (on read) — we measure it today.
2-2. Conditional-Branch Machine Code — 0x74 and 0x75
An authentication like if (strcmp(a,b) == 0) boils down in machine code to a conditional jump after a test/cmp. The short-jump machine code:
0x74= JE (jump if equal)0x75= JNE (jump if not equal)
The following byte is the jump distance. So the fate of the authentication logic hangs on one byte — change 0x75 to 0x74 and "pass when different" flips into "pass when equal." That is the essence of patching.
2-3. Patching’s Two Stages — Memory and File
Double-click an instruction in x64dbg and edit it, and at first only the in-memory program changes. The effect is immediate in the current run, but the file is untouched. To reflect the change into the file, you must use Ctrl+P (the Patches window) → Patch file.
Writing to the file requires one conversion. The addresses on the debugger screen are virtual addresses (locations in running memory), while a patch must be written at a file offset (the location on disk). The section table tells you the conversion — in today’s Python measurement we do this conversion by hand.
2-4. Reverse-Tracing from Strings — x64dbg’s Standard Opening
Handed a crackme, your first move in x64dbg is fixed. Right-click → Search for → Current region → String references (or All referenced strings) to open the string list, find a verdict message like "Correct"/"Wrong", and double-click it. You jump to the code that references that string — right near the authentication branch. Working backward from the message to the comparison code is effectively the first move of every beginner crackme walkthrough.
2-5. Conditional Breakpoints
For situations like a comparison inside a loop — "it passes hundreds of times and I want only one of them" — use a conditional BP. Right-click the BP → Edit and attach a condition expression (e.g., rax==0x1337), and it stops only when the condition is true. A feature that shines in the multi-stage verification analysis of Step 218.
3. Follow Along
3-1. The Lab — A Program to Be Patched
x64dbg’s stage is Windows, but patching’s principle is the same regardless of OS. We measure the principle on a Linux binary and learn the screen operations from examples.
Input (patchme.c)
#include <stdio.h>
#include <string.h>
int main(void) {
char pw[32];
printf("password: ");
scanf("%31s", pw);
if (strcmp(pw, "open-sesame") == 0)
puts("correct!");
else
puts("wrong.");
return 0;
}
cd ~/lab214_218
gcc -O0 -no-pie -o patchme patchme.c
echo wrongpw | ./patchme
password: wrong.
(Measured 2026-09-09. Compiled with -O0 to leave the branch structure textbook-clean.)
3-2. Finding the Surgical Site — The Authentication Branch’s Address
objdump -d patchme | sed -n "/<main>:/,/ret/p" | grep -E "call|test|jne|j"
401211: e8 9a fe ff ff call 4010b0 <strcmp@plt>
401216: 85 c0 test %eax,%eax
401218: 75 11 jne 40122b <main+0x75>
401224: e8 57 fe ff ff call 401080 <puts@plt> ← prints correct!
401229: eb 0f jmp 40123a <main+0x84>
401235: e8 46 fe ff ff call 401080 <puts@plt> ← prints wrong.
(Measured 2026-09-09. Only selected output lines are shown.)
How to read the output: strcmp‘s return value (eax) is tested, and if different (jne), it jumps to 0x40122b — the wrong side. The surgical site is the two bytes 75 11 at 401218. The first byte 0x75 means JNE; 0x11 is the jump distance. Change that one byte to 0x74 (JE) and the authentication flips.
3-3. Virtual Address → File Offset Conversion
The 0x401218 the debugger shows is a virtual address. Its location in the file is converted via the section table:
readelf -S patchme | grep -A1 "\.text"
[15] .text PROGBITS 00000000004010d0 000010d0
0000000000000185 ...
(Measured 2026-09-09.)
How to read it: the .text section starts at virtual address 0x4010d0 and at file offset 0x10d0. The difference is 0x400000 — so for this program, "virtual address = file offset + 0x400000." The surgical site’s file offset is 0x401218 − 0x400000 = 0x1218. The calculation x64dbg’s Patches window does automatically, we just did by hand.
3-4. One-Byte Surgery with Python (Measured)
Build the patch script. Read the whole file, fix one byte, save as a new file:
Input (tmp_test/patch216.py)
# One-byte conditional-branch patch on an ELF binary
UNC = r"\\wsl$\Codex-Security-Lab\root\lab214_218\patchme" # practice lab path
DST = r"\\wsl$\Codex-Security-Lab\root\lab214_218\patchme_patched"
JNE_VADDR = 0x401218 # virtual address of the jne found with objdump
TEXT_VADDR = 0x4010D0 # .text virtual address
TEXT_OFF = 0x10D0 # .text file offset
file_off = JNE_VADDR - (TEXT_VADDR - TEXT_OFF)
print(f"virtual address 0x{JNE_VADDR:x} -> file offset 0x{file_off:x}")
data = bytearray(open(UNC, "rb").read())
print(f"byte before patch: 0x{data[file_off]:02x} (should be 0x75=JNE)")
assert data[file_off] == 0x75, "not the expected JNE byte"
data[file_off] = 0x74 # JNE -> JE
open(DST, "wb").write(data)
print(f"byte after patch: 0x{data[file_off]:02x} (0x74=JE)")
Run (in Git Bash):
virtual address 0x401218 -> file offset 0x1218
byte before patch: 0x75 (should be 0x75=JNE)
byte after patch: 0x74 (0x74=JE)
(Measured 2026-09-09.)
3-5. Verifying the Surgery — The Authentication Flipped
chmod +x patchme_patched
echo wrongpw | ./patchme # original + wrong answer
echo open-sesame | ./patchme # original + correct answer
echo wrongpw | ./patchme_patched # patched + wrong answer
echo open-sesame | ./patchme_patched # patched + correct answer
password: wrong. ← original: wrong answer rejected
password: correct! ← original: only the correct answer passes
password: correct! ← patched: the wrong answer passes!
password: wrong. ← patched: the correct answer is rejected
(Measured 2026-09-09.)
How to read it: one byte (0x75→0x74) flipped the entire authentication logic precisely. Wrong answers pass; the correct answer is rejected. This is the reality of a "patch" — beneath the debugger’s flashy UI, this is all that happens.
3-6. The Same Work in x64dbg — Screen Example
The procedure for the same surgery with x64dbg on Windows:
# Screen example — the x64dbg patching procedure
1. Drag crackme.exe into x64dbg to load it
2. In the CPU window: right-click → Search for → Current region → String references
3. Find the "wrong" / "correct" strings and double-click → jump to the referencing code (the auth branch)
4. Set a BP with F2 on the jne near the branch → run with F9 → enter any password → stops at the BP
5. Double-click the jne instruction → change "jne" to "je" and confirm
→ at this point the in-memory program has changed. Continue with F9 and the wrong answer passes
6. Ctrl+P (Patches window) → Patch file → the change is saved into the file
What 3-4’s Python script did (address conversion → byte check → one-byte swap → file save), x64dbg does with five mouse clicks. Different tools, same surgical anatomy.
3-7. Memory Breakpoints — Measuring the Principle in gdb
Reproduce "who reads my input" on Linux. gdb’s watch stops when a value is written:
(gdb) break main
(gdb) run
(gdb) watch *(char*)($rbp-0x40) ← watch on the input buffer pw
(gdb) continue
Watchpoint 2: *(char*)($rbp-0x40)
Old value = 0 '\000'
New value = 2 '\002'
0x0000000000401059 in ?? ()
(Measured 2026-09-09. Run via echo AAAA | gdb -batch .... The stop point is inside scanf — the moment input is written to the buffer.)
How to read it: without knowing any code address, set a watch on data and execution stops at the code that touches that data. The same principle as x64dbg’s Memory BP (on access). Note that in this WSL environment, the read watch rwatch was refused with Expression cannot be implemented with read/access watchpoint. (measured 2026-09-09) — a limitation where hardware read-watch isn’t supported under virtualization. Covered in Wall 3.
4. Missions & Exercises
Mission — A Comparison Report: "Bypass Without Surgery" vs "Bypass With Surgery"
- Open patchme in gdb and bypass the authentication only during execution, without fixing the file (hint: after stopping at the jne, there’s a way to touch registers or rip —
set $rip = ...) - Compare with patchme_patched from 3-4: organize the two bypasses’ differences (persistence, required tools) in a table
- Change the password in patchme.c, recompile, and repeat the entire process yourself, from objdump to patching — confirm that the jne’s address and file offset differ in the new binary
Exercises
Problem 1. Explain the difference between a code breakpoint and a memory breakpoint in terms of "what you use when you know what."
Problem 2. A patch changing 0x75 (JNE) to 0x90×2 (two NOPs) is also possible. How does its result differ from flipping to JE? (Hint: where does the flow go with NOPs?)
Problem 3. To fix the instruction at virtual address 0x401218 in the file, why did we subtract 0x400000? Explain together with what the two numbers in the .text section table mean.
Problem 4. Explain why patching is "reflected immediately in memory, saved separately to file" — from the perspective that what the debugger sees and the file on disk are different beings.
5. Model Answers & Completion Criteria
Mission Model Answer
Mid-execution bypass (the manual version of an in-memory patch):
(gdb) break *0x401218 ← BP on the jne
(gdb) run ← enter any password
(gdb) set $rip = 0x401224 ← force-jump to the call that prints correct!
(gdb) continue
correct!
(Output example — the procedure works exactly as measured on 2026-09-09. Addresses vary by environment.)
Comparison table:
| Aspect | Mid-execution bypass (gdb set) | File patch (byte swap) |
|---|---|---|
| Persistence | That run only | Permanent — the file changed |
| Required tools | A debugger | Any binary editor/script |
| Detection | Must be redone every session | File hash changes — can be caught by integrity checks |
| Use | Testing during analysis | Fixing the result after analysis is confirmed |
Re-surgery checkpoints: changing the password string changes the binary, so the jne’s address and file offset change. The procedure (find the branch with objdump → convert with readelf → swap one byte → verify) must work identically.
Exercise Answers
Problem 1 answer. A code BP is used when you know the address of the instruction you want to stop at — like after finding the comparison code via string reverse-tracing. A memory BP is the opposite: used when you don’t know the code but you know the data — set it on your input buffer and execution stops at the check code (wherever it was) that reads it. In short: if what you know is an address, code BP; if what you know is data, memory BP.
Problem 2 answer. Flipping to JE inverts the logic — "correct answer rejected, wrong answer passes." Erasing with NOPs, by contrast, removes the jump itself, so regardless of the test result, execution always falls through to the next instruction (printing correct!) — any answer at all passes. In real crackme solving, "always pass" is often more convenient, but if the program has state that only the correct-answer path fills in, an inversion patch may be needed.
Problem 3 answer. When an executable is loaded into memory, each section is placed at an assigned virtual address. In the readelf output, 0x4010d0 is the virtual address where .text starts in memory, and 0x10d0 is the offset where the same contents start in the file. Since the two are shifted by their difference (0x400000), subtracting that difference from a virtual address gives the position inside the file. A debugger-screen address can’t point directly into the disk file, so this conversion is needed.
Problem 4 answer. A running program is a copy of the file loaded into memory, and what the debugger stops, shows, and edits is that copy. Editing the copy leaves the original file untouched, so closing the program discards the change. To change the file you must compute "which memory location corresponds to which file byte" and write back to the original — which is what x64dbg’s Patches window (Ctrl+P) and our Python script did.
Completion Criteria Checklist
- [ ] I can state the difference between code BP and memory BP, and the selection criterion
- [ ] I know the procedure for reverse-tracing an authentication branch via string-reference search
- [ ] I identified the conditional jump’s machine-code bytes (0x74/0x75) in objdump output
- [ ] I computed virtual address → file offset using the readelf section table
- [ ] I patched one byte with Python and confirmed the authentication flips
- [ ] I measured the data-watch principle with gdb
watch - [ ] Mission: I completed the table comparing mid-execution bypass vs file patch
6. Common Pitfalls & Fixes
Wall 1. I patched it, but the file didn’t change (x64dbg)
Symptom: you changed je and the run passes, but reopening the program shows the original.
Cause: double-clicking an instruction edits memory only. The file was never touched.
Fix: open the Patches window with Ctrl+P and press Patch file to save into the file. The Patches window lists every location edited this session — check that only your intended patches are there before saving.
Wall 2. I found the byte to patch and it’s not 0x75
Symptom: the Python script’s assert data[file_off] == 0x75 fails.
Cause: the address conversion was wrong, or different compile options changed the branch shape. Compiled with -O1 or higher, the compiler rearranges branches — a je+jump combo, for example (that’s why today’s practice uses -O0).
Fix: re-check the branch instruction’s address and machine code in objdump. Recompute the conversion with .text’s two numbers (virtual address, file offset) from readelf’s section table. Never patch by guesswork — always verify the pre-patch byte matches your expectation; today’s assert is that safety device.
Wall 3. rwatch doesn’t work in gdb
Symptom: rwatch *(char*)($rbp-0x40) errors with Expression cannot be implemented with read/access watchpoint.
Cause: read watches need the CPU’s hardware debug registers, which virtualized environments like WSL may not support (measured 2026-09-09, WSL2).
Fix: the write watch watch does work (confirmed by measurement), so observe with that first. If you truly need "the moment it’s read," work around by setting a code BP on the comparison function’s (strcmp, etc.) call address — the very workaround you used in Step 178.
Wall 4. "correct" doesn’t appear in x64dbg’s string search
Symptom: the verdict message is missing from String references.
Cause: the string lives in another memory region (if you searched only Current region), or it’s obfuscated and restored at run time.
Fix: widen the scope with Search for → All regions. Still nothing? It’s the runtime-restored kind — the signal to switch to dynamic analysis like Step 178’s XOR crackme. This is where memory BPs shine: set an on-access BP on the input buffer and reverse-find the check code.
Wall 5. Segfault / abnormal exit after patching
Symptom: you changed one byte and the program dies.
Cause: you edited somewhere other than an instruction’s first byte (break the middle of an instruction and the CPU decodes garbage), or you touched the jump-distance byte. x86 instructions are variable-length — one byte off and everything after shatters.
Fix: always patch at an instruction’s start byte, and only swap with an instruction of the same length (0x75→0x74 is safe — both are 2-byte short jumps). When erasing with NOPs, match the byte count exactly — two 0x90s for a 2-byte instruction.
7. Summary
Today’s Concepts
| Concept | One-line description |
|---|---|
| Memory breakpoint | A watch set on data reads/writes rather than code addresses — reverse-traces check code |
| Conditional BP | A BP that stops only when a condition expression is true — essential for loop analysis |
| Patch | The technique of changing a program’s behavior by rewriting its instruction bytes |
| 0x74 / 0x75 | JE / JNE short jump — the one byte holding authentication’s fate |
| Virtual address ↔ file offset | Two coordinate systems converted via the section table — the core calculation of file patching |
| Patches window (Ctrl+P) | x64dbg’s store for reflecting in-memory edits into the file |
Today’s Commands & Keys
| Command/key | What it does |
|---|---|
| x64dbg: right-click → Search for → String references | Reverse-trace from verdict message to comparison code |
| x64dbg: dump right-click → Breakpoint → Memory, on access | Set a memory BP |
x64dbg: double-click instruction → edit / Ctrl+P |
Patch memory / save to file |
objdump -d | grep jne |
Find patch candidates (conditional jumps) |
readelf -S |
Material for virtual address → file offset conversion |
gdb watch / rwatch |
Write / read data watches (rwatch has environment constraints) |
The Sense That Matters More Than Commands
Today’s two techniques are two sides of one question. The memory BP asks "who touches this value," and the patch answers "now that I know where it’s touched, I change it." Analysis (observation) and manipulation (intervention) — the two wheels of dynamic reversing.
And remember patching’s humility. Surgery is exactly one byte, respecting instruction boundaries. That one byte can flip a program’s identity also means, conversely, that one byte of error breaks everything. The single assert line checking the expected byte before patching — that’s the habit that separates an analyst from a vandal.
Once every box is checked, Step 216 is complete. Click the checkbox in the sidebar to save your progress.