$npx -y skills add HLND2T/CS2_VibeSignatures --skill generate-signature-for-functionGenerate and validate unique byte signatures for functions using IDA Pro MCP. Use this skill when you need to create a pattern-scanning signature for a function that can reliably locate it across binary updates. Triggers: generate signature, byte signature, pattern signature, fun
| 1 | # Generate Signature for Function |
| 2 | |
| 3 | Generate a unique hex byte signature for a function using fully programmatic wildcard detection and validation — no manual byte analysis required. |
| 4 | |
| 5 | ## Prerequisites |
| 6 | |
| 7 | - Function address (from decompilation, xrefs, or rename) |
| 8 | - IDA Pro MCP connection |
| 9 | |
| 10 | ## Method |
| 11 | |
| 12 | ### 1. Generate and Validate Signature (Single Step) |
| 13 | |
| 14 | Use a single `py_eval` call that: |
| 15 | - Resolves the input address to the actual function start |
| 16 | - Decodes instructions and programmatically determines wildcard positions |
| 17 | - Tracks instruction boundaries so prefixes always cover complete instructions |
| 18 | - Progressively tests at each instruction boundary via binary search |
| 19 | - Outputs the shortest unique signature directly |
| 20 | |
| 21 | **Note**: The input address may be in the middle of a function. The script automatically resolves it to the actual function start. |
| 22 | |
| 23 | ```python |
| 24 | mcp__ida-pro-mcp__py_eval code=""" |
| 25 | import idaapi, ida_bytes, idautils, ida_ua, ida_segment, json |
| 26 | |
| 27 | input_addr = <func_addr> |
| 28 | min_sig_bytes = 6 |
| 29 | max_sig_bytes = 96 |
| 30 | max_instructions = 64 |
| 31 | |
| 32 | # --- Binary search wrapper (IDA 9.0+ find_bytes -> older bin_search fallback) --- |
| 33 | def raw_bin_search(ea, max_ea, data, mask, flags=0): |
| 34 | if hasattr(ida_bytes, 'find_bytes'): |
| 35 | return ida_bytes.find_bytes(data, ea, range_end=max_ea, mask=mask, flags=flags) |
| 36 | return ida_bytes.bin_search(ea, max_ea, data, mask, len(data), flags) |
| 37 | |
| 38 | # --- Resolve to actual function start --- |
| 39 | func = idaapi.get_func(input_addr) |
| 40 | if not func: |
| 41 | print(json.dumps({"error": f"{hex(input_addr)} is not inside a known function", "status": "failed"})) |
| 42 | raise SystemExit |
| 43 | |
| 44 | func_addr = func.start_ea |
| 45 | if func_addr != input_addr: |
| 46 | print(f"NOTE: Resolved {hex(input_addr)} -> function start at {hex(func_addr)}") |
| 47 | |
| 48 | # --- Collect instruction bytes with auto-wildcarding --- |
| 49 | limit_end = min(func.end_ea, func_addr + max_sig_bytes) |
| 50 | sig_tokens = [] |
| 51 | inst_boundaries = [] # cumulative byte count at end of each instruction |
| 52 | cursor = func_addr |
| 53 | |
| 54 | while cursor < func.end_ea and cursor < limit_end and len(sig_tokens) < max_sig_bytes: |
| 55 | insn = idautils.DecodeInstruction(cursor) |
| 56 | if not insn or insn.size <= 0: |
| 57 | break |
| 58 | raw = ida_bytes.get_bytes(cursor, insn.size) |
| 59 | if not raw: |
| 60 | break |
| 61 | |
| 62 | wild = set() |
| 63 | |
| 64 | # Auto-wildcard volatile operand bytes (imm/near/far/mem/displ) |
| 65 | for op in insn.ops: |
| 66 | op_type = int(op.type) |
| 67 | if op_type == int(idaapi.o_void): |
| 68 | continue |
| 69 | if op_type in (int(idaapi.o_imm), int(idaapi.o_near), int(idaapi.o_far), int(idaapi.o_mem), int(idaapi.o_displ)): |
| 70 | offb = int(op.offb) |
| 71 | if offb > 0 and offb < insn.size: |
| 72 | dsz = ida_ua.get_dtype_size(getattr(op, 'dtype', getattr(op, 'dtyp', 0))) |
| 73 | if dsz <= 0: |
| 74 | dsz = insn.size - offb |
| 75 | end = min(insn.size, offb + dsz) |
| 76 | for i in range(offb, end): |
| 77 | wild.add(i) |
| 78 | offo = int(op.offo) |
| 79 | if offo > 0 and offo < insn.size: |
| 80 | dsz2 = ida_ua.get_dtype_size(getattr(op, 'dtype', getattr(op, 'dtyp', 0))) |
| 81 | if dsz2 <= 0: |
| 82 | dsz2 = insn.size - offo |
| 83 | end2 = min(insn.size, offo + dsz2) |
| 84 | for i in range(offo, end2): |
| 85 | wild.add(i) |
| 86 | |
| 87 | # Special handling for call/jump instructions |
| 88 | b0 = raw[0] |
| 89 | if b0 in (0xE8, 0xE9, 0xEB): |
| 90 | for i in range(1, insn.size): |
| 91 | wild.add(i) |
| 92 | elif b0 == 0x0F and insn.size >= 2 and (raw[1] & 0xF0) == 0x80: |
| 93 | for i in range(2, insn.size): |
| 94 | wild.add(i) |
| 95 | elif 0x70 <= b0 <= 0x7F: |
| 96 | for i in range(1, insn.size): |
| 97 | wild.add(i) |
| 98 | |
| 99 | for idx in range(insn.size): |
| 100 | sig_tokens.append("??" if idx in wild else f"{raw[idx]:02X}") |
| 101 | |
| 102 | inst_boundaries.append(len(sig_tokens)) |
| 103 | cursor += insn.size |
| 104 | |
| 105 | if not sig_tokens: |
| 106 | print(json.dumps({"error": f"no instruction bytes at {hex(func_addr)}", "status": "failed"})) |
| 107 | raise SystemExit |
| 108 | |
| 109 | # --- Search bounds --- |
| 110 | seg = ida_segment.get_segm_by_name(".text") |
| 111 | if seg: |
| 112 | search_start, search_end = seg.start_ea, seg.end_ea |
| 113 | else: |
| 114 | search_start, search_end = idaapi.cvar.inf.min_ea, idaapi.cvar.inf.max_ea |
| 115 | |
| 116 | # --- Progressive search at instruction boundaries only --- |
| 117 | best_sig = None |
| 118 | |
| 119 | for boundary in inst_boundaries: |
| 120 | if boundary < min_sig_bytes: |
| 121 | continue |
| 122 | |
| 123 | prefix_tokens = sig_tokens[:boundary] |
| 124 | if all(t == "??" for t in prefix_tokens): |
| 125 | continue |
| 126 | |
| 127 | data = bytes(0 if t == "??" else int(t, 16) for t in prefix_tokens) |
| 128 | mask = bytes(0x00 if t == "??" else 0xFF for t in prefix_tokens |