$npx -y skills add HLND2T/CS2_VibeSignatures --skill generate-signature-for-structoffsetGenerate and validate unique byte signatures for instructions containing a struct member offset using IDA Pro MCP. Use this skill when you need a signature for an instruction like mov [rcx+1A8h], eax or cmp dword ptr [rdi+0B0h], 0, where the struct offset must be explicitly fixed
| 1 | # Generate Signature for Struct Offset |
| 2 | |
| 3 | Generate a unique hex byte signature that locates an instruction containing a specific struct member offset (for example: `mov [rcx+1A8h], eax`, `cmp dword ptr [rdi+0B0h], 0`). |
| 4 | |
| 5 | ## Core Concept |
| 6 | |
| 7 | For struct-offset signatures, we signature the **instruction containing the struct offset**, not the function body itself. |
| 8 | |
| 9 | Hard requirements: |
| 10 | 1. The target instruction must be fully fixed (no wildcard bytes at all). |
| 11 | 2. The displacement bytes carrying `struct_offset` in the target instruction must be explicitly included (not wildcarded). |
| 12 | 3. Instructions other than the target instruction may use wildcarding. |
| 13 | 4. Signature length grows by complete instruction boundaries and stops at the shortest unique prefix. |
| 14 | |
| 15 | Strategy: |
| 16 | - **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. `offset_sig_disp` is always `0` — the signature always starts at the target instruction. |
| 17 | |
| 18 | ## Prerequisites |
| 19 | |
| 20 | - Target instruction address (the instruction that contains the struct offset) |
| 21 | - Expected `struct_offset` value (e.g. `0x1A8`) |
| 22 | - IDA Pro MCP connection |
| 23 | |
| 24 | ## Method |
| 25 | |
| 26 | ### 1. Generate and Validate Signature (Single Step) |
| 27 | |
| 28 | Use a single `py_eval` call that: |
| 29 | - Validates the input instruction contains the expected `struct_offset` displacement. |
| 30 | - Collects instruction bytes from the target instruction forward, tests uniqueness. |
| 31 | - Forward-only expansion (no backward expansion — `offset_sig_disp` is always `0`). |
| 32 | - Enforces **no wildcard on the target instruction**. |
| 33 | - Computes both VA and RVA for the target instruction. |
| 34 | - Outputs the shortest unique signature as `struct_sig` with metadata. |
| 35 | |
| 36 | ```python |
| 37 | mcp__ida-pro-mcp__py_eval code=""" |
| 38 | import idaapi, ida_bytes, idautils, ida_ua, ida_segment, json |
| 39 | |
| 40 | def main(): |
| 41 | target_inst = <inst_addr> |
| 42 | target_struct_offset = <struct_offset> # e.g. 0x1A8 from "mov [rcx+1A8h], eax" |
| 43 | min_sig_bytes = 6 |
| 44 | max_sig_bytes = 96 |
| 45 | max_instructions = 64 |
| 46 | |
| 47 | # --- Binary search wrapper (IDA 9.0+ find_bytes -> older bin_search fallback) --- |
| 48 | def raw_bin_search(ea, max_ea, data, mask, flags=0): |
| 49 | if hasattr(ida_bytes, 'find_bytes'): |
| 50 | return ida_bytes.find_bytes(data, ea, range_end=max_ea, mask=mask, flags=flags) |
| 51 | return ida_bytes.bin_search(ea, max_ea, data, mask, len(data), flags) |
| 52 | |
| 53 | f = idaapi.get_func(target_inst) |
| 54 | if not f: |
| 55 | print(json.dumps({ |
| 56 | "inst_va": hex(target_inst), |
| 57 | "error": "target instruction is not inside a known function", |
| 58 | "status": "failed" |
| 59 | })) |
| 60 | return |
| 61 | |
| 62 | insn0 = idautils.DecodeInstruction(target_inst) |
| 63 | if not insn0 or insn0.size <= 0: |
| 64 | print(json.dumps({ |
| 65 | "inst_va": hex(target_inst), |
| 66 | "error": "failed to decode target instruction", |
| 67 | "status": "failed" |
| 68 | })) |
| 69 | return |
| 70 | |
| 71 | raw0 = ida_bytes.get_bytes(target_inst, insn0.size) |
| 72 | if not raw0: |
| 73 | print(json.dumps({ |
| 74 | "inst_va": hex(target_inst), |
| 75 | "error": "failed to read target instruction bytes", |
| 76 | "status": "failed" |
| 77 | })) |
| 78 | return |
| 79 | |
| 80 | def find_struct_disp_matches(insn, raw, expected): |
| 81 | hits = [] |
| 82 | for op in insn.ops: |
| 83 | ot = int(op.type) |
| 84 | if ot == int(idaapi.o_void): |
| 85 | continue |
| 86 | if ot not in (int(idaapi.o_displ), int(idaapi.o_mem), int(idaapi.o_imm)): |
| 87 | continue |
| 88 | |
| 89 | for attr in ("offb", "offo"): |
| 90 | off = int(getattr(op, attr, 0)) |
| 91 | if off <= 0 or off >= insn.size: |
| 92 | continue |
| 93 | |
| 94 | sizes = [] |
| 95 | dsz = ida_ua.get_dtype_size(getattr(op, "dtype", getattr(op, "dtyp", 0))) |
| 96 | if dsz > 0: |
| 97 | sizes.append(dsz) |
| 98 | for s in (1, 2, 4, 8): |
| 99 | if s not in sizes: |
| 100 | sizes.append(s) |
| 101 | |
| 102 | for sz in sizes: |
| 103 | if off + sz > insn.size: |
| 104 | continue |
| 105 | unsigned_val = int.from_bytes(raw[off:off + sz], "little", signed=False) |
| 106 | signed_val = int.from_bytes(raw[off:off + sz], "little", signed=True) |
| 107 | expected_mod = expected & ((1 << (8 * sz)) - 1) |
| 108 | if unsigned_val == expected_mod or signed_val == expected: |
| 109 | hits.append((off, sz, unsigne |