$npx -y skills add HLND2T/CS2_VibeSignatures --skill generate-signature-for-globalvarGenerate and validate unique byte signatures for global variable using IDA Pro MCP. Use this skill when you need to create a pattern-scanning signature for a global variable that can reliably locate it across binary updates. Triggers: global variable signature, signature for glob
| 1 | # Generate Signature for Global Variable |
| 2 | |
| 3 | Generate a unique hex byte signature that locates an **instruction accessing a global variable** using fully programmatic wildcard detection and validation — no manual byte analysis required. |
| 4 | |
| 5 | ## Core Concept |
| 6 | |
| 7 | Since global variable addresses change between binary updates, we don't signature the GV itself. Instead, we: |
| 8 | 1. Find an instruction that **references** the global variable (mov/lea/cmp/etc.) |
| 9 | 2. Generate a signature to locate that **instruction** |
| 10 | 3. At runtime, parse the instruction to **resolve** the actual GV address |
| 11 | |
| 12 | ### RIP-Relative Addressing (x86-64) |
| 13 | |
| 14 | In x86-64, most global variable accesses use RIP-relative addressing: |
| 15 | ``` |
| 16 | GV_Address = Instruction_Address + Instruction_Length + RIP_Offset |
| 17 | ``` |
| 18 | |
| 19 | Where: |
| 20 | - `Instruction_Address` = address found by pattern scan |
| 21 | - `Instruction_Length` = total bytes of the instruction (opcode + ModR/M + offset) |
| 22 | - `RIP_Offset` = signed 32-bit displacement (last 4 bytes of instruction) |
| 23 | |
| 24 | ## Prerequisites |
| 25 | |
| 26 | - Global variable address. `qword_XXXXXX` for example. |
| 27 | - IDA Pro MCP connection |
| 28 | |
| 29 | ## Method |
| 30 | |
| 31 | ### 1. Generate and Validate Signature (Single Step) |
| 32 | |
| 33 | Use a single `py_eval` call that: |
| 34 | - Discovers candidate instructions accessing the GV via `DataRefsTo` |
| 35 | - Verifies each candidate resolves to the target GV via RIP-relative displacement |
| 36 | - Collects instruction stream with auto-wildcarding for each candidate |
| 37 | - Tracks instruction boundaries so prefixes always cover complete instructions |
| 38 | - Progressively tests at each instruction boundary via binary search |
| 39 | - Outputs the shortest unique signature with full metadata |
| 40 | |
| 41 | **Note**: If you already know the GV-accessing instruction address, set `target_inst = <inst_addr>`. If you know the containing function, set `target_func = <func_addr>`. |
| 42 | |
| 43 | ```python |
| 44 | mcp__ida-pro-mcp__py_eval code=""" |
| 45 | import idaapi, ida_bytes, idautils, ida_ua, ida_segment, json |
| 46 | |
| 47 | def main(): |
| 48 | target_gv = <gv_addr> |
| 49 | target_inst = None # Set to instruction address if known, e.g. 0x1804F3DF3 |
| 50 | target_func = None # Set to function address to restrict search, e.g. 0x1804F3DA0 |
| 51 | min_sig_bytes = 8 |
| 52 | max_sig_bytes = 96 |
| 53 | max_instructions = 64 |
| 54 | max_candidates = 32 |
| 55 | |
| 56 | # --- Binary search wrapper (IDA 9.0+ find_bytes -> older bin_search fallback) --- |
| 57 | def raw_bin_search(ea, max_ea, data, mask, flags=0): |
| 58 | if hasattr(ida_bytes, 'find_bytes'): |
| 59 | return ida_bytes.find_bytes(data, ea, range_end=max_ea, mask=mask, flags=flags) |
| 60 | return ida_bytes.bin_search(ea, max_ea, data, mask, len(data), flags) |
| 61 | |
| 62 | # --- Search bounds --- |
| 63 | seg = ida_segment.get_segm_by_name(".text") |
| 64 | if seg: |
| 65 | search_start, search_end = seg.start_ea, seg.end_ea |
| 66 | else: |
| 67 | search_start, search_end = idaapi.cvar.inf.min_ea, idaapi.cvar.inf.max_ea |
| 68 | |
| 69 | def resolve_disp_off(insn_ea, insn, raw): |
| 70 | cand_offsets = set() |
| 71 | for op in insn.ops: |
| 72 | if int(op.type) == int(idaapi.o_void): |
| 73 | continue |
| 74 | offb = int(getattr(op, 'offb', 0)) |
| 75 | offo = int(getattr(op, 'offo', 0)) |
| 76 | if offb > 0 and offb + 4 <= insn.size: |
| 77 | cand_offsets.add(offb) |
| 78 | if offo > 0 and offo + 4 <= insn.size: |
| 79 | cand_offsets.add(offo) |
| 80 | for off in sorted(cand_offsets): |
| 81 | disp_i32 = int.from_bytes(raw[off:off + 4], 'little', signed=True) |
| 82 | resolved = (insn_ea + insn.size + disp_i32) & 0xFFFFFFFFFFFFFFFF |
| 83 | if resolved == target_gv: |
| 84 | return off |
| 85 | return None |
| 86 | |
| 87 | def collect_and_validate(inst_ea, disp_off): |
| 88 | f = idaapi.get_func(inst_ea) |
| 89 | if not f: |
| 90 | return None |
| 91 | limit_end = min(f.end_ea, inst_ea + max_sig_bytes) |
| 92 | sig_tokens = [] |
| 93 | inst_boundaries = [] |
| 94 | cursor = inst_ea |
| 95 | first_len = None |
| 96 | while cursor < f.end_ea and cursor < limit_end and len(sig_tokens) < max_sig_bytes: |
| 97 | insn = idautils.DecodeInstruction(cursor) |
| 98 | if not insn or insn.size <= 0: |
| 99 | break |
| 100 | raw = ida_bytes.get_bytes(cursor, insn.size) |
| 101 | if not raw: |
| 102 | break |
| 103 | wild = set() |
| 104 | for op in insn.ops: |
| 105 | ot = int(op.type) |
| 106 | if ot == int(idaapi.o_void): |
| 107 | continue |
| 108 | if ot in (int(idaapi.o_imm), int(idaapi.o_near), int(idaapi.o_far), int(idaapi.o_mem), int(idaapi.o_displ)): |
| 109 | offb = int(getattr(op, 'offb', 0)) |
| 110 | if offb > 0 and offb < insn.size: |
| 111 | dsz = ida_ua.get_dtype_size(getattr(op, 'dtype', getattr(op, 'dtyp', 0))) |