$npx -y skills add HLND2T/CS2_VibeSignatures --skill generate-signature-for-patchGenerate and validate unique byte signatures for instructions that need to be runtime-patched using IDA Pro MCP. Use this skill when you need a signature to locate a specific instruction for patching (e.g., force/skip a branch, NOP a call, change an immediate operand). Triggers:
| 1 | # Generate Signature for Patch |
| 2 | |
| 3 | Generate a unique hex byte signature that locates an instruction to be patched at runtime, along with the replacement `patch_bytes`. |
| 4 | |
| 5 | ## Core Concept |
| 6 | |
| 7 | For patch signatures, we signature the **instruction to be patched**. The signature uniquely identifies the location so a runtime patcher can find and overwrite the original bytes with `patch_bytes`. |
| 8 | |
| 9 | Hard requirements: |
| 10 | 1. The target instruction must be fully fixed (no wildcard bytes at all). |
| 11 | 2. Instructions other than the target instruction may use wildcarding. |
| 12 | 3. Signature length grows by complete instruction boundaries and stops at the shortest unique prefix. |
| 13 | 4. `patch_bytes` are determined by the LLM based on the desired patch effect, **not** by the script. |
| 14 | 5. `len(patch_bytes)` must equal `len(original_instruction_bytes)` (pad with `0x90` NOP if the replacement is shorter). |
| 15 | |
| 16 | Strategy: |
| 17 | - **Forward-only expansion:** Expand only forward (after target instruction). The signature may extend beyond the current function boundary into CC padding or the next function. `patch_sig_disp` is always `0` — the signature always starts at the target instruction. |
| 18 | |
| 19 | ## Prerequisites |
| 20 | |
| 21 | - Target instruction address (the instruction to be patched) |
| 22 | - Desired patch effect description (e.g., "skip if-branch", "NOP out function call", "change immediate value") |
| 23 | - IDA Pro MCP connection |
| 24 | |
| 25 | ## Method |
| 26 | |
| 27 | ### 1. Determine `patch_bytes` (LLM Step — before running the script) |
| 28 | |
| 29 | Examine the target instruction and its context using IDA Pro MCP, then determine the appropriate `patch_bytes`. |
| 30 | |
| 31 | Common patch patterns: |
| 32 | - **Skip conditional branch (near jcc `0F 8x rel32` → `jmp rel32`):** Replace first 6 bytes with `E9 <new_rel32> 90`. Compute `new_rel32` = original branch target − (patch_addr + 5). |
| 33 | - **Skip conditional branch (short jcc `7x rel8` → `jmp short`):** Replace `7x rel8` with `EB rel8`. |
| 34 | - **Force conditional branch to fall through:** NOP the entire jcc instruction (`90 90 ...`). |
| 35 | - **NOP a `call rel32`:** Replace `E8 xx xx xx xx` with `90 90 90 90 90`. |
| 36 | - **NOP a `call [reg+disp]`:** Replace all bytes with `90`. |
| 37 | - **Change immediate operand:** Modify the immediate bytes in-place. |
| 38 | |
| 39 | ### 2. Generate and Validate Signature (Single Step) |
| 40 | |
| 41 | Use a single `py_eval` call that: |
| 42 | - Collects instruction bytes from the target instruction forward, tests uniqueness. |
| 43 | - Forward-only expansion (no backward expansion — `patch_sig_disp` is always `0`). |
| 44 | - Enforces **no wildcard on the target instruction**. |
| 45 | - Computes both VA and RVA for the target instruction. |
| 46 | - Outputs the shortest unique signature as `patch_sig` with metadata. |
| 47 | |
| 48 | ```python |
| 49 | mcp__ida-pro-mcp__py_eval code=""" |
| 50 | import idaapi, ida_bytes, idautils, ida_ua, ida_segment, json |
| 51 | |
| 52 | def main(): |
| 53 | target_inst = <inst_addr> |
| 54 | min_sig_bytes = 6 |
| 55 | max_sig_bytes = 96 |
| 56 | max_instructions = 64 |
| 57 | |
| 58 | # --- Binary search wrapper (IDA 9.0+ find_bytes -> older bin_search fallback) --- |
| 59 | def raw_bin_search(ea, max_ea, data, mask, flags=0): |
| 60 | if hasattr(ida_bytes, 'find_bytes'): |
| 61 | return ida_bytes.find_bytes(data, ea, range_end=max_ea, mask=mask, flags=flags) |
| 62 | return ida_bytes.bin_search(ea, max_ea, data, mask, len(data), flags) |
| 63 | |
| 64 | image_base = idaapi.get_imagebase() |
| 65 | |
| 66 | f = idaapi.get_func(target_inst) |
| 67 | if not f: |
| 68 | print(json.dumps({ |
| 69 | "inst_va": hex(target_inst), |
| 70 | "error": "target instruction is not inside a known function", |
| 71 | "status": "failed" |
| 72 | })) |
| 73 | return |
| 74 | |
| 75 | insn0 = idautils.DecodeInstruction(target_inst) |
| 76 | if not insn0 or insn0.size <= 0: |
| 77 | print(json.dumps({ |
| 78 | "inst_va": hex(target_inst), |
| 79 | "error": "failed to decode target instruction", |
| 80 | "status": "failed" |
| 81 | })) |
| 82 | return |
| 83 | |
| 84 | raw0 = ida_bytes.get_bytes(target_inst, insn0.size) |
| 85 | if not raw0: |
| 86 | print(json.dumps({ |
| 87 | "inst_va": hex(target_inst), |
| 88 | "error": "failed to read target instruction bytes", |
| 89 | "status": "failed" |
| 90 | })) |
| 91 | return |
| 92 | |
| 93 | seg = ida_segment.get_segm_by_name(".text") |
| 94 | if seg: |
| 95 | search_start, search_end = seg.start_ea, seg.end_ea |
| 96 | else: |
| 97 | search_start, search_end = idaapi.cvar.inf.min_ea, idaapi.cvar.inf.max_ea |
| 98 | |
| 99 | # --- Helper: wildcard non-target instructions --- |
| 100 | def wildcard_instruction(addr, insn_obj, raw_bytes): |
| 101 | wild = set() |
| 102 | for op in insn_obj.ops: |
| 103 | ot = int(op.type) |
| 104 | if ot == int(idaapi.o_void): |
| 105 | continue |
| 106 | if o |