Patch and thread rules
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.
Your first rule
A rule has a place (where) and a replacement (what).
"mnemonic":"RDTSC"Every instruction of this kind. RDTSC reads the processor's time counter: how a program measures time.
"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"}- 1Find the instruction. The debugger shows each one as module and offset, like app.exe+0x4f10: that offset is the RVA. Nothing to compute.
- 2Write the rule: the place (
"module"and"rva") and the replacement ("replace_hex"or"steps"). - 3Run 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"}# 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.Every key
| Key | Meaning |
|---|---|
"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. |
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.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"}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 rax, r1 rbx, r2 rcx, r3 rdx, r4 rsi, r5 rdi, r6 to r13 for r8 to r15, r14 rbp, r15 rsp, r16 rip.
The rule's own notepad: the program never sees it.
Decimal or 0x hexadecimal: an immediate.
| For | Operation | What it does | Example |
|---|---|---|---|
| Run | passthrough | Run 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 |
| Set | load_imm rD,n | rD = n (a 64-bit number). | load_imm r0,0x7FFFFFFF |
| Set | mov_rr rD,rS | Copy a register: rD = rS. | mov_rr r0,r1 |
| Set | add_ri rD,rS,n | rD = rS + n. | add_ri r0,r1,16 |
| Bits | or_rr rD,rS | rD |= rS (bitwise or). | or_rr r0,r1 |
| Bits | xor_rr rD,rS | rD ^= rS (xor with itself sets it to 0). | xor_rr r0,r0 |
| Bits | and_ri rD,mask | rD &= a 32-bit mask (keeps only the bits of the mask). | and_ri r0,0xFF |
| Bits | test_ri rD,mask | Test rD with a 32-bit mask. | test_ri r0,1 |
| Bits | shl_ri rD,n | Shift rD left by n bits. | shl_ri r3,32 |
| Bits | shr_ri rD,n | Shift rD right by n bits. | shr_ri r0,8 |
| Scratch | get_constant tD,n | Put a number in a scratch register: tD = n. | get_constant t0,4096 |
| Scratch | copy_reg_to_temp tD,rS | Keep a copy of a register: tD = rS. | copy_reg_to_temp t0,r2 |
| Scratch | copy_temp_to_reg rD,tS | Put a scratch value back in a register: rD = tS. | copy_temp_to_reg r0,t0 |
| Scratch | copy_temp_to_temp tD,tS | tD = tS. | copy_temp_to_temp t1,t0 |
| Memory | get_read_address tD[,i] | tD = the address the instruction reads (the i-th operand, 0 if omitted). | get_read_address t0 |
| Memory | get_write_address tD | tD = the address the instruction writes. | get_write_address t1 |
| Memory | get_read_value tD,tAddr | tD = the value found at that address, for a read operand. | get_read_value t2,t0 |
| Memory | get_write_value tD,tAddr | tD = the value found at that address, for a written operand. | get_write_value t3,t1 |
| Memory | read_temp_offset tD,rBase | Scratch register and the memory at [rBase] (read direction). | read_temp_offset t0,r5 |
| Memory | write_temp_offset tD,rBase | Scratch register and the memory at [rBase] (write direction). | write_temp_offset t0,r5 |
| Memory | read_temp_shadow tD,n | Scratch 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 |
| Memory | write_temp_shadow tD,n | Scratch register and the rule's own slot n (write direction). | write_temp_shadow t0,0 |
| Flags | mask_trap_flag | Mask 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"}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*"}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).
| Rule | The threads it names | The 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.
| Key | Matches |
|---|---|
"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"}Words used here
| Word | Means |
|---|---|
| RVA | An instruction's place in its module: its offset from the module's start. The same from one run to the next. |
| Mnemonic | An instruction's name: RDTSC, CPUID, MOV. |
| Signature | An instruction's first bytes in hex, with ?? where any byte will do. |
| Register | One of the processor's small fast variables (rax, rbx, rcx…). |
| Scratch register | A variable of the rule only: t0, t1… |
| Immediate | A number written in a step: 42, 0x2A. |
| Passthrough | Run the original instruction as the program wrote it. |
Protovision