Docs · chapter 4

Patch and thread rules

Updated 14 November 2026 · version 0.17

In short: a patch rule says when the program reaches this instruction, do that instead: the program really behaves differently, which is how you remove a check, fix a clock or feed it another value. A thread rule says which threads are recorded. Both go in "patch_rules" of the config file.

1

Your first rule

A rule has a place (where) and a replacement (what).

Where
"mnemonic":"RDTSC"

Every instruction of this kind. RDTSC reads the processor's time counter: how a program measures time.

What
"steps":"load_imm r0,1; load_imm r3,0"

Put 1 in rax and 0 in rdx instead: the program now always sees the same instant.

{"id":"steady_clock","mnemonic":"RDTSC","steps":"load_imm r0,1; load_imm r3,0"}
  1. 1
    Find the instruction. The debugger shows each one as module and offset, like app.exe+0x4f10: that offset is the RVA. Nothing to compute.
  2. 2
    Write the rule: the place ("module" and "rva") and the replacement ("replace_hex" or "steps").
  3. 3
    Run again with it, from the config file or the window's Advanced settings, then check in the debugger that the instruction now does what you meant.
{"id":"nop_2byte","module":"app.exe","rva":"0x4f10","replace_hex":"90 90"}
The format
Each rule is one flat JSON object (no nesting) on one line of at most 2040 characters. In the config file the rules are an array; in a rules file they are one per line, blank lines and lines starting with # skipped. The file's shape is checked before the run starts. Each rule's content is read when the program starts: a rule that cannot work (a misspelled step, an argument out of range) is skipped, the others still apply. Up to 4096 code rules and 256 thread rules.
2

Every key

KeyMeaning
"id"A name for the rule, shown in the log (up to 63 characters). Optional, and worth writing.
"module"The module's file name, "app.exe" or "x.dll". Used with rva.
"rva"The instruction's offset in that module, hex ("0x1a2b0") or decimal. Resolved against the real address at run time.
"signature"The instruction's bytes start so, up to 256 bytes: two hex digits per byte, spaces between, ?? for any byte. Sees unpacked code too.
"mnemonic"Any instruction with that name: "RDTSC", "CPUID"…
"replace_hex"Raw machine code (up to 256 bytes) put in the instruction's place.
"steps"A chain of up to 64 steps, in one string, that replaces the instruction. With both, steps win.
"text_app" / "text_cmd"A text placed in the program's memory as UTF-16; a step gets its address with @text_app or @text_cmd. Up to 1023 characters each. No escapes: write a quote as '.
"thread" / "name"Makes it a thread rule: see Thread rules.
What a rule needs
At least one place (rva with module, signature or mnemonic) and one replacement (replace_hex or steps). Several places must all fit the instruction. A rule that would match every instruction is refused.
3

Where, and what

Choosing the instruction

By address, the most exact; by its bytes, wherever they are; by its name; or several at once, which must all fit:

{"id":"nop_2byte","module":"app.exe","rva":"0x4f10","replace_hex":"90 90"}
{"id":"steady_clock","signature":"0F 31","steps":"load_imm r0,1; load_imm r3,0"}
{"id":"quiet_cpuid","mnemonic":"CPUID","steps":"load_imm r0,0; load_imm r1,0; load_imm r2,0; load_imm r3,0"}
{"id":"rdtsc_only","mnemonic":"RDTSC","signature":"0F 31","steps":"load_imm r0,1; load_imm r3,0"}

Replacing it

Raw bytes: five NOPs over a five-byte call, so the call never happens; two bytes that set eax to zero.

{"id":"skip_call","module":"app.exe","rva":"0x2210","replace_hex":"90 90 90 90 90"}
{"id":"eax_zero","module":"app.exe","rva":"0x2240","replace_hex":"31 C0"}

Steps can run the original instruction (passthrough) and then change its result; without it, the steps are all the instruction now does.

{"id":"force_zero","module":"app.exe","rva":"0x2210","steps":"passthrough; load_imm r0,0"}
{"id":"shifted","module":"app.exe","rva":"0x2300","steps":"passthrough; mov_rr r0,r1; add_ri r0,r0,16"}
{"id":"seed","module":"app.exe","rva":"0x1200","steps":"load_imm r2,42"}
4

Steps: the small language

Operations separated by ;, arguments by commas, run in the order written. Names are not case sensitive; an operation takes up to four arguments. A misspelled step or an argument out of range makes the whole rule skipped: after a first run with a new rule, look at its instruction in the debugger.

r0 … r16
A register of the program

r0 rax, r1 rbx, r2 rcx, r3 rdx, r4 rsi, r5 rdi, r6 to r13 for r8 to r15, r14 rbp, r15 rsp, r16 rip.

t0, t1 …
A scratch register

The rule's own notepad: the program never sees it.

42 · 0x2A
A number

Decimal or 0x hexadecimal: an immediate.

ForOperationWhat it doesExample
RunpassthroughRun the original instruction, as the program wrote it. At most one per rule; where you put it in the chain decides whether it runs before or after the other steps.passthrough; load_imm r0,0
Setload_imm rD,nrD = n (a 64-bit number).load_imm r0,0x7FFFFFFF
Setmov_rr rD,rSCopy a register: rD = rS.mov_rr r0,r1
Setadd_ri rD,rS,nrD = rS + n.add_ri r0,r1,16
Bitsor_rr rD,rSrD |= rS (bitwise or).or_rr r0,r1
Bitsxor_rr rD,rSrD ^= rS (xor with itself sets it to 0).xor_rr r0,r0
Bitsand_ri rD,maskrD &= a 32-bit mask (keeps only the bits of the mask).and_ri r0,0xFF
Bitstest_ri rD,maskTest rD with a 32-bit mask.test_ri r0,1
Bitsshl_ri rD,nShift rD left by n bits.shl_ri r3,32
Bitsshr_ri rD,nShift rD right by n bits.shr_ri r0,8
Scratchget_constant tD,nPut a number in a scratch register: tD = n.get_constant t0,4096
Scratchcopy_reg_to_temp tD,rSKeep a copy of a register: tD = rS.copy_reg_to_temp t0,r2
Scratchcopy_temp_to_reg rD,tSPut a scratch value back in a register: rD = tS.copy_temp_to_reg r0,t0
Scratchcopy_temp_to_temp tD,tStD = tS.copy_temp_to_temp t1,t0
Memoryget_read_address tD[,i]tD = the address the instruction reads (the i-th operand, 0 if omitted).get_read_address t0
Memoryget_write_address tDtD = the address the instruction writes.get_write_address t1
Memoryget_read_value tD,tAddrtD = the value found at that address, for a read operand.get_read_value t2,t0
Memoryget_write_value tD,tAddrtD = the value found at that address, for a written operand.get_write_value t3,t1
Memoryread_temp_offset tD,rBaseScratch register and the memory at [rBase] (read direction).read_temp_offset t0,r5
Memorywrite_temp_offset tD,rBaseScratch register and the memory at [rBase] (write direction).write_temp_offset t0,r5
Memoryread_temp_shadow tD,nScratch register and the rule's own slot n (read direction): a value that lasts from one run of the rule to the next.read_temp_shadow t0,0
Memorywrite_temp_shadow tD,nScratch register and the rule's own slot n (write direction).write_temp_shadow t0,0
Flagsmask_trap_flagMask the trap flag (single-step).passthrough; mask_trap_flag

Text in the program's memory

A step only holds numbers, so a text is first placed in memory and the step is given its address. Here the first argument of a call (rcx, r2) is pointed at another path just before the call:

{"id":"other_target","module":"app.exe","rva":"0x3a40","steps":"load_imm r2,@text_app; passthrough","text_app":"C:/work/other.exe"}
Watching a call without changing it
No rule needed: everything is recorded. In the debugger, calls kernel32!CreateFileW lists every call with its arguments and result.

A rules file

With "patch_rules": "rules.jsonl" in the config file, next to it:

# time: a steady clock
{"id":"steady_clock","mnemonic":"RDTSC","signature":"0F 31","steps":"load_imm r0,1; load_imm r3,0"}

# threads that only add noise
{"id":"no-jobs","thread":"ignore","name":"Job*"}
5

Thread rules

Leave some threads out, or keep only the ones you name.

A thread rule has "thread":"ignore" or "thread":"record"; every field given must match. Threads are checked when they start, and again when they get a name (some programs name them late).

RuleThe threads it namesThe others
"thread":"ignore"Run natively, at full speed: nothing of them is recorded.Recorded, as usual.
"thread":"record"Recorded.Still run under the tracer, so the program behaves as usual, but are not recorded: what they write is unknown to the debugger.

As soon as one record rule exists, every thread no record rule names is off, the main thread included unless a rule names it. The capture then holds only the threads you chose.

KeyMatches
"module"The module that holds the thread's first function.
"rva"That function's offset in the module. Needs module.
"name"The thread's name, a pattern with * for any run of characters.
{"id":"no-jobs","thread":"ignore","name":"Job*"}
{"id":"no-worker","thread":"ignore","module":"worker.dll","rva":"0x4010"}
{"id":"only-checker","thread":"record","module":"app.exe","rva":"0x4010"}
6

Words used here

WordMeans
RVAAn instruction's place in its module: its offset from the module's start. The same from one run to the next.
MnemonicAn instruction's name: RDTSC, CPUID, MOV.
SignatureAn instruction's first bytes in hex, with ?? where any byte will do.
RegisterOne of the processor's small fast variables (rax, rbx, rcx…).
Scratch registerA variable of the rule only: t0, t1…
ImmediateA number written in a step: 42, 0x2A.
PassthroughRun the original instruction as the program wrote it.