Skip to content

Commit 88bd8fe

Browse files
committed
Check hooks skill
1 parent eef48d9 commit 88bd8fe

4 files changed

Lines changed: 888 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
name: 检查钩子 Check Hooks
3+
description: "Checks newly added Syringe hooks (DEFINE_HOOK / DEFINE_HOOK_AGAIN) on the current branch for common errors: insufficient size (< 5 bytes), conflicts with hooks from other engine extensions, instruction boundary misalignment, and register/stack variable extraction issues. Uses HookAnalysis.log for conflict detection and IDA MCP for deep instruction analysis."
4+
---
5+
6+
#### Helper Scripts
7+
8+
All scripts in this directory. The AI MUST use them — do not reimplement parsing logic.
9+
10+
| Script | Purpose |
11+
|--------|---------|
12+
| `discover_hooks.py` | Discovers new/modified DEFINE_HOOK / DEFINE_HOOK_AGAIN from git. Two modes: auto-detect (no args) or `--commit <sha/name>`. Supports fuzzy commit name resolution (searches last 30 commits). Outputs JSON with `hooks` array, each having `address`, `size`, `name`, `file`, `returns`. Use `--json-only` for piping. |
13+
| `check_hook_conflicts.py` | Reads a JSON array of new hooks from stdin (or a file argument) and checks them against `HookAnalysis.log` for Problem 0 (size < 5) and Problem 1 (conflicts). Outputs JSON with `errors` and `notes` arrays. |
14+
| `parse_hook_log.py` | Parses `HookAnalysis.log` (GBK encoding) and outputs all existing hooks as JSON. Typically not called directly — used by `check_hook_conflicts.py`. |
15+
| `HookAnalysis.log` | Pre-generated hook analysis report from SyringeIH. Read-only reference. |
16+
17+
#### Workflow
18+
19+
This skill checks all newly added `DEFINE_HOOK` and `DEFINE_HOOK_AGAIN` macro invocations on the current branch for four classes of problems.
20+
21+
**Step 0: Discover new hooks**
22+
23+
Use `discover_hooks.py` to find which hooks need checking. Two modes:
24+
25+
**Mode A — Specify a commit** (user provides a SHA or commit name):
26+
```
27+
python discover_hooks.py --commit <sha>
28+
```
29+
30+
**IMPORTANT — Terminal reliability:** The script captures all git output internally via `capture_output=True` and sets `GIT_PAGER=cat` as a safety measure. It CANNOT be affected by the terminal's pager/less configuration. If the script runs and produces output that is not what you expected, READ the output carefully — do NOT assume the terminal is broken. Valid outputs include:
31+
- `"action": "resolve"` + `candidates` array: the script IS working correctly, it just needs you to pick a SHA
32+
- `"action": "error"`: there is a specific error message explaining what went wrong
33+
- A `hooks` array with the discovered hooks — the script ran successfully
34+
35+
If the script appears to "fail" (you ran it but didn't get hooks), first check what it actually output, then check this document for what to do next.
36+
37+
The script first tries `git show <sha>` directly (git natively resolves partial SHAs). If that fails (e.g., the user provided a non-SHA name like "Country"), the script outputs the last 30 commits as a `candidates` list. The AI MUST:
38+
39+
1. Examine the `candidates` array in the JSON output. Find the commit(s) whose `message` field matches the user's description.
40+
2. Pick the most relevant SHA and re-run with it directly: `python discover_hooks.py --commit <sha>`
41+
3. If multiple candidates match closely, present them to the user and ask which one.
42+
4. **Do NOT fall back to manually reading source files to find hooks.** The script is the only correct way to determine which hooks were added by a commit.
43+
44+
**CRITICAL — Never guess which files were modified based on the commit title:** If you cannot get the script to run successfully (e.g., the script says the commit is not found), do NOT try to infer the relevant source file from the commit title or commit message keywords. Instead:
45+
- First try the commit SHA directly: `python discover_hooks.py --commit <sha>`
46+
- If the commit is not in the local repo, run `git fetch upstream` first, then try again
47+
- Only if all git/npm approaches fail should you consult the user for the correct SHA
48+
49+
**Mode B — Auto-detect** (user does not specify a commit):
50+
```
51+
python discover_hooks.py
52+
```
53+
54+
The script automatically determines the diff range with this priority:
55+
1. Uncommitted changes (including new/untracked source files) + unpushed commits on current branch → checks both
56+
2. Only unpushed commits → checks those
57+
3. Neither unpushed nor uncommitted → falls back to `develop...HEAD` (tries `origin/develop` if `develop` doesn't exist locally)
58+
4. If none of the above work → tries to find the branch fork point via `git merge-base`
59+
5. If all attempts fail → reports no changes found
60+
61+
The output is a JSON object with a `hooks` array. Each hook has: `address`, `name`, `size`, `file`, `returns`. The `returns` field is auto-detected from the diff context: `"0"`, `"0x<hex>"`, `"R->Origin() + N"`, or `"?"` if undetermined.
62+
63+
If no hooks are found, the script reports a warning. Present this to the user and stop.
64+
65+
**Important:** If `returns` is `"?"`, the AI MUST read the hook function body from the source file to determine the actual return behavior before proceeding.
66+
67+
**Step 1: Problem 0 & 1 — Size and conflict checks (scripted)**
68+
69+
Pipe the discovered hooks directly to the conflict checker:
70+
```
71+
python discover_hooks.py --json-only | python check_hook_conflicts.py
72+
```
73+
74+
Or with a commit:
75+
```
76+
python discover_hooks.py --commit <sha> --json-only | python check_hook_conflicts.py
77+
```
78+
79+
If `returns` was `"?"`, first fix it in the JSON, or save the corrected JSON to a temp file and pass it as an argument instead of piping.
80+
81+
The script:
82+
- Reports **Problem 0** errors for any hook with `size < 5`
83+
- Reports **Problem 1** conflicts: partial address range overlaps and return address overlaps
84+
- Notes exact overlaps (stacked hooks) as informational — not errors
85+
86+
Interpret the JSON output. The `errors` array contains issues that need fixing. The `notes` array contains informational items (stacked hooks, OK confirmations, etc.).
87+
88+
For each error, present it to the user clearly:
89+
90+
**Problem 0** (from script output):
91+
> **Problem 0: Insufficient hook size**
92+
> Hook `HookName` at `0x<addr>` has size `0x<size>` (< 5). The JMP instruction requires at least 5 bytes. Increase the size to cover the full instruction(s) at this address.
93+
94+
**Problem 1 — Stacked hook** (from `notes` with `type: "stacked"`):
95+
> ℹ️ **Problem 1: Stacked hook (not an error, verify intent)**
96+
> Hook `NewHookName` at `0x<addr>` (size `0x<size>`) exactly matches existing hook `ExistingHookName` from `<DLL>`. The second hook will execute after the first returns 0. Verify this is intended.
97+
98+
**Problem 1 — Partial overlap** (from `errors` with `type: "conflict"`):
99+
> **Problem 1: Hook address range conflict**
100+
> Hook `NewHookName` at `0x<addr>` (size `0x<size>`, range `[0x<start>, 0x<end>)`) conflicts with existing hook `ExistingHookName` from `<DLL>` at `0x<existing_addr>` (size `0x<existing_size>`, range `[0x<existing_start>, 0x<existing_end>)`).
101+
102+
**Problem 1 — Return address conflict** (from `errors` with `type: "return_conflict"`):
103+
> **Problem 1: Return address conflict**
104+
> Hook `NewHookName` at `0x<addr>` returns to `0x<ret_addr>`, which falls within existing hook `ExistingHookName` from `<DLL>` covering `[0x<start>, 0x<end>)`.
105+
106+
If no conflicts were found for a hook, the script outputs a note with `type: "ok"`.
107+
108+
**Step 2: Problem 2 & 3 — Instruction boundary and variable validation via IDA MCP**
109+
110+
Attempt to connect to the IDA MCP server. Check if `gamemd.exe` is the loaded IDB.
111+
112+
**If IDA MCP is not available or gamemd.exe is not loaded:**
113+
114+
> ⚠️ **IDA MCP server is not available.** Skipping Problem 2 (instruction boundary) and Problem 3 (variable extraction) checks. Connect the IDA MCP server with gamemd.exe loaded for full validation.
115+
116+
Skip Step 2 entirely.
117+
118+
**If IDA MCP is available:**
119+
120+
**Problem 2 — Instruction boundary check:**
121+
122+
For each new hook:
123+
1. Use the IDA MCP to verify the hook address is at the start of an x86 instruction.
124+
2. Use the IDA MCP to verify that `addr + size` is also at an instruction boundary (the hook covers complete instructions).
125+
3. For fixed return addresses, verify they are at instruction boundaries.
126+
127+
If any check fails:
128+
> **Problem 2: Instruction boundary issue**
129+
> Hook `HookName` at `0x<addr>` (size `0x<size>`) — <specific issue, e.g. "address is in the middle of an instruction" or "size does not end at an instruction boundary" or "return address 0x<ret> is not at an instruction start">. Disassemble the area at this address to find the correct boundaries.
130+
131+
**Problem 3 — Variable extraction validation:**
132+
133+
For each new hook, inspect the function body for `GET`, `GET_STACK`, `REF_STACK`, `LEA_STACK` macros and register writes (`R->EAX(value)`, `R->ECX(value)`, `R->STACK(offset, value)`, etc.). Use IDA MCP to decompile or disassemble the code around the hook address and verify the register/stack state matches.
134+
135+
For `GET(type, var, reg)`:
136+
- Check what `reg` holds at the hook point according to IDA
137+
- If the type declared in GET differs from what IDA suggests, warn the user
138+
139+
For `GET_STACK(type, var, offset)` / `REF_STACK(type, var, offset)`:
140+
- Check the stack layout at the hook point per IDA
141+
- If the offset suggests a different type or value, warn the user
142+
143+
If a mismatch is found:
144+
> ⚠️ **Problem 3: Variable extraction may be incorrect**
145+
> At `0x<addr>`: `GET(<type>, <var>, <reg>)``<reg>` appears to hold `<actual_type>` based on IDA analysis. Verify the register assignment at this address.
146+
147+
If all Problem 3 checks pass: "✓ Variable extraction checks passed."
148+
149+
**Step 3: Summary**
150+
151+
After all checks, print a summary listing all checked hooks and any problems found, grouped by severity (❌ errors first, then ⚠️ warnings, then ℹ️ notes). If no problems were found at all: "✅ All checks passed. No issues found with the new hooks."
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env python3
2+
"""Check new hooks against existing hooks in HookAnalysis.log for conflicts.
3+
4+
Usage:
5+
python check_hook_conflicts.py <new_hooks_json>
6+
7+
new_hooks_json is a JSON file (or '-' for stdin) containing an array of new hook objects:
8+
[{"address": "0x46BDD9", "size": 5, "name": "MyHook", "returns": "0x46BDE0"}]
9+
10+
"returns" can be:
11+
- "0" or missing/empty → returns 0 (resolves to hook address, safe, no return-check needed)
12+
- "0x..." → fixed return address to check
13+
- "R->Origin() + N" → relative return (hook address + N) to check
14+
15+
Output is JSON with conflict results.
16+
"""
17+
18+
import json
19+
import os
20+
import sys
21+
import re
22+
23+
# Ensure the script's own directory is in sys.path so that
24+
# 'import parse_hook_log' works regardless of the working directory.
25+
_script_dir = os.path.dirname(os.path.abspath(__file__))
26+
if _script_dir not in sys.path:
27+
sys.path.insert(0, _script_dir)
28+
29+
def parse_existing_hooks(log_path):
30+
"""Parse HookAnalysis.log and return list of existing hook dicts."""
31+
import parse_hook_log
32+
return parse_hook_log.parse_hook_log(log_path)
33+
34+
35+
def check_hooks(new_hooks, existing_hooks):
36+
results = []
37+
errors = []
38+
notes = []
39+
40+
for nh in new_hooks:
41+
addr = nh['address']
42+
addr_int = int(addr, 16)
43+
size = nh['size']
44+
name = nh.get('name', '<unknown>')
45+
ret = nh.get('returns', '0') # returns "0", "0x...", or "R->Origin() + N"
46+
range_start = addr_int
47+
range_end = addr_int + size
48+
49+
# Resolve return address
50+
ret_addr = None
51+
if ret == '0' or not ret:
52+
ret_addr = None # means safe, no return-check needed
53+
elif re.match(r'^0x[0-9A-Fa-f]+$', ret):
54+
ret_addr = int(ret, 16)
55+
else:
56+
m = re.match(r'R->Origin\(\)\s*\+\s*(\d+)', ret)
57+
if m:
58+
ret_addr = addr_int + int(m.group(1))
59+
60+
# Check Problem 0: size >= 5
61+
if size < 5:
62+
errors.append({
63+
'problem': 'Problem 0',
64+
'hook': name,
65+
'address': addr,
66+
'message': f"Hook '{name}' at {addr} has size {size} (< 5). The JMP instruction requires at least 5 bytes."
67+
})
68+
69+
# Check Problem 1: conflicts
70+
found_conflict = False
71+
for eh in existing_hooks:
72+
e_range_start = eh['range_start']
73+
e_range_end = eh['range_end']
74+
e_addr = eh['address']
75+
76+
# Check address range overlap
77+
if range_start < e_range_end and range_end > e_range_start:
78+
if range_start == e_range_start and range_end == e_range_end:
79+
# Exact overlap - stacked hooks, not an error
80+
notes.append({
81+
'problem': 'Problem 1',
82+
'hook': name,
83+
'address': addr,
84+
'size': size,
85+
'existing_hook': eh['name'],
86+
'existing_dll': eh['dll'],
87+
'existing_address': e_addr,
88+
'existing_size': eh['size'],
89+
'type': 'stacked',
90+
'message': (
91+
f"Hook '{name}' at {addr} (size {size}) exactly matches "
92+
f"existing hook '{eh['name']}' from {eh['dll']}. "
93+
f"This is a stacked hook — the second will execute after the first returns 0. "
94+
f"Verify this is intended."
95+
)
96+
})
97+
else:
98+
# Partial overlap - conflict
99+
errors.append({
100+
'problem': 'Problem 1',
101+
'hook': name,
102+
'address': addr,
103+
'size': size,
104+
'range': f"[0x{range_start:08X}, 0x{range_end:08X})",
105+
'existing_hook': eh['name'],
106+
'existing_dll': eh['dll'],
107+
'existing_address': e_addr,
108+
'existing_size': eh['size'],
109+
'existing_range': f"[0x{e_range_start:08X}, 0x{e_range_end:08X})",
110+
'type': 'conflict',
111+
'message': (
112+
f"Hook '{name}' at {addr} (size {size}, range "
113+
f"[0x{range_start:08X}, 0x{range_end:08X})) conflicts with "
114+
f"existing hook '{eh['name']}' from {eh['dll']} at "
115+
f"{e_addr} (size {eh['size']}, range "
116+
f"[0x{e_range_start:08X}, 0x{e_range_end:08X})). "
117+
f"The address ranges overlap."
118+
)
119+
})
120+
found_conflict = True
121+
122+
# Check return address
123+
if ret_addr is not None:
124+
if e_range_start <= ret_addr < e_range_end:
125+
errors.append({
126+
'problem': 'Problem 1',
127+
'hook': name,
128+
'address': addr,
129+
'returns': ret,
130+
'return_addr': f"0x{ret_addr:08X}",
131+
'existing_hook': eh['name'],
132+
'existing_dll': eh['dll'],
133+
'existing_range': f"[0x{e_range_start:08X}, 0x{e_range_end:08X})",
134+
'type': 'return_conflict',
135+
'message': (
136+
f"Hook '{name}' at {addr} returns to 0x{ret_addr:08X}, "
137+
f"which falls within existing hook '{eh['name']}' from {eh['dll']} "
138+
f"covering [0x{e_range_start:08X}, 0x{e_range_end:08X})."
139+
)
140+
})
141+
142+
if not found_conflict:
143+
notes.append({
144+
'problem': 'Problem 1',
145+
'hook': name,
146+
'address': addr,
147+
'type': 'ok',
148+
'message': f"No conflicts detected for hook '{name}' at {addr}."
149+
})
150+
151+
return {'errors': errors, 'notes': notes}
152+
153+
154+
def main():
155+
if len(sys.argv) < 2:
156+
new_hooks = json.load(sys.stdin)
157+
else:
158+
new_hooks_input = sys.argv[1]
159+
if new_hooks_input == '-':
160+
new_hooks = json.load(sys.stdin)
161+
else:
162+
with open(new_hooks_input, 'r', encoding='utf-8') as f:
163+
new_hooks = json.load(f)
164+
165+
script_dir = os.path.dirname(os.path.abspath(__file__))
166+
log_path = os.path.join(script_dir, 'HookAnalysis.log')
167+
existing_hooks = parse_existing_hooks(log_path)
168+
169+
results = check_hooks(new_hooks, existing_hooks)
170+
json.dump(results, sys.stdout, indent=2, ensure_ascii=False)
171+
172+
173+
if __name__ == '__main__':
174+
main()

0 commit comments

Comments
 (0)