吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1005|回复: 13
收起左侧

[C&C++ 原创] SigilHook:用 AngelScript 做 x86/x64 函数 Hook 的注入式运行时

  [复制链接]
StackPointer 发表于 2026-9-30 18:42
本帖最后由 StackPointer 于 2026-10-5 18:07 编辑

分享一下我自己做的项目 SigilHook

GitHub:
https://github.com/StackAndPointer/SigilHook

发布页:
https://github.com/StackAndPointer/SigilHook/releases

一句话概括:这是一个基于 PolyHook 2、用 AngelScript 驱动脚本 Hook 的 x86/x64 注入式运行时。目标是让常见的 Hook 逻辑尽量写在脚本里,改逻辑不用反复编译 DLL、重启工具链。

在 PolyHook 2 基础上的拓展:

  • 原始 PolyHook 2 的核心是 C++ 类接口(Detour、BreakPointHook、IatHook、EatHook、VFuncSwapHook、VTableSwapHook 等)。SigilHook 在这些之上加了一层稳定的 C ABI,全部导出函数用 extern "C" 和 sigilhook_* 前缀,用 opaque handle 传递,不把 C++ 类 ABI、STL 容器和异常暴露给调用方。
  • 加了真正的可注入输出:编译出 SigilHook.dll,带 DllMain 和独立初始化线程,初始化放在工作线程而不是 loader lock 里。
  • 用 AsmJit 重做了运行时回调桥。原来的 ILCallback 只适合简单回调,存在回调内存生命周期、错误处理和线程安全问题,不能直接给脚本用。SigilHook 重写了 JIT 回调,支持参数、返回值、GPR、flags 的可读写。
  • 在 PolyHook 的基础上补了调用约定层:cdecl、stdcall、fastcall、thiscall、vectorcall,以及自定义 usercall 映射(argN=<reg|stack+offset>、ret=<reg>、x86 cleanup=<bytes>)。这块是靠 AsmJit 生成汇编桩实现的,x86/x64 共用一套描述语法。
  • 补了 mid-hook 语义:shHookMid + shResumeMid,把指令指针重定向到 trampoline,在原函数入口前插入代码再继续执行原函数体。
  • 补了通用寄存器、XMM 寄存器、flags 的脚本级读写,包括低 8/16/32 位的部分读写(shReg16 / shSetReg16 等),SP 只读以避免破坏回调返回路径。
  • 补了内存读写、内存保护修改、特征码扫描、Zydis 反汇编、CMP/TEST 标志计算、FXSAVE/FXRSTOR、可执行返回和栈指针跳转片段等辅助能力。
  • 补了热重载:命名管道 \.\pipe\SigilHook.<pid>,配套 SigilHookReload.bat,可按进程或全量触发热重载,不需要重新注入。
  • 补了原生 DLL 动态绑定:运行时 LoadLibraryW/GetProcAddress、PE 位数校验、导出缓存、引用计数、逆序 FreeLibrary,还有 header_to_ash.py 把受支持的 C ABI 头文件转成 AngelScript 包装。

在 AngelScript 基础上的拓展:

  • 把整个脚本目录递归编译成一个 SigilHook.Application 模块,所有 .as 文件都是这个模块的 section。这样拆文件的 .as 之间可以直接互调全局函数、直接读写共享全局变量,不需要 import/export,写起来接近普通 C/C++ 项目。
  • 加了 .ash 头文件语义:支持 #include "..." 和 #include <...>,相对当前文件和脚本根目录解析,规范化绝对路径做循环检测,同一个头文件在应用内只展开一次,不要求护宏。可选识别并移除 #pragma once。这不是完整的 C 预处理器,#ifndef/#define 之类不支持。
  • 严格单入口:只允许根目录 main.as 提供 void main() 和可选 void unload()。缺 main.as、缺 main()、入口写在别的文件、重复入口都会返回 SIGILHOOK_ERROR_SCRIPT 并写明确日志,避免一个目录里的测试脚本被误当成入口。
  • 加了 AngelScript 的完整绑定层:核心 Hook、Detour、Breakpoint、IAT/EAT、VFunc/VTable、内存、寄存器、XMM、flags、调用约定、usercall、JIT、脚本入口调用、共享值、日志和错误码,都注册成脚本对象或全局函数。标准头 scripts/SigilHook.ash 是唯一入口,脚本侧看到一个统一 API 面。
  • 加了自己的 .ash 标准库 scripts/SigilHook.ash:常量、枚举、便捷包装、保留 C ABI 状态的 Sh...Status 版本、寄存器/XMM/flags 读写、内存与扫描、汇编与反汇编、DLL 调用包装等。
  • 加了跨模块能力:SigilHook 的应用层是单模块共享全局;AngelScript 原生多模块仍然受保护,跨模块调用需要 import ... from "Module" 声明和 BindAllImportedFunctions(),原始全局变量在模块间不共享。两条路径都有测试覆盖。
  • 加了运行时生命周期管理:独立工作线程初始化 AngelScript,DllMain 只做最少工作;停止时先拒绝新回调,用 AngelScript 行回调请求活动脚本中止,带超时的 sigilhook_runtime_stop_with_timeout(),无法结束则返回 BUSY 并保持 engine/hooks 存活,避免半初始化状态和悬空跳转。
  • 加了脚本异常回传和状态接口:脚本编译错误、Hook 错误、运行日志统一写到 <dll-dir>\SigilHook\logs\SigilHook.log,调用方可以通过 shLastError / shStatusString 等拿到错误。
  • 原生 AngelScript 代码本身没有大改,主要是构建集成和外围运行时接入;SigilHook 自己的绑定和运行时代码是新增的。

它能做什么:

  • 编译出可注入的 SigilHook.dll,x86/x64 分开构建,只注入同位数进程
  • DLL 同目录下的 SigilHook\ 目录递归加载 .as / .ash,脚本编译成一个 Application 模块
  • 只认 main.as 作为入口,其他 .as 可以像正常 C/C++ 项目一样拆文件、共享全局函数和全局变量
  • .ash 作为脚本头文件使用,支持 #include、嵌套 include、自动 include-once
  • 支持 detour、软件断点、硬件断点、IAT、EAT、VFunc、VTable 等 Hook 类型
  • 支持 cdecl / stdcall / fastcall / thiscall / vectorcall,以及自定义 usercall 映射
  • 脚本里能读改通用寄存器、flags、XMM0-XMM15,能改参数和返回值
  • 支持 callOriginal、skipOriginal、early return、mid-hook
  • 支持内存读写、内存保护修改、特征码扫描、Zydis 反汇编
  • 自带命名管道热重载,改脚本后可以用 SigilHookReload.bat 触发,不用重新注入
  • 提供了 C ABI 和标准头 scripts/SigilHook.ash,C/C++ 侧和脚本侧都能接
  • 还带了一个 header_to_ash.py,可以把受支持的 C ABI 头文件转成 AngelScript 包装

一些设计取向:

  • 运行时不暴露 C++ 类 ABI,导出统一是 C ABI + opaque handle
  • 地址统一用 uint64_t 传递,兼容 x86/x64
  • x86 和 x64 DLL 分开构建,避免位数不匹配
  • 脚本停跑/热重载有超时和 busy 保护,不会直接撕掉正在执行的 Hook 回调

使用它的典型流程大概是:

  1. 把 SigilHook.dll 注入目标进程
  2. 在 DLL 同目录放 SigilHook\main.as
  3. main.as 里 include SigilHook.ash
  4. 写 Hook、写回调逻辑
  5. 改脚本后跑 SigilHookReload.bat 热重载

比较适合的用途:

  • 快速试 Hook 逻辑,不想每改一行都重编译
  • 需要覆盖多种调用约定,或者目标函数是非标准 usercall
  • 想用脚本管理一批地址、函数 Hook 和内存 patch

欢迎使用、报 bug、提 issue。

免费评分

参与人数 8威望 +2 吾爱币 +109 热心值 +7 收起 理由
sarex + 1 + 1 谢谢@Thanks!
yixi + 1 + 1 我很赞同!
fengbolee + 1 + 1 用心讨论,共获提升!
RustYou + 1 + 1 用心讨论,共获提升!
zzage + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
5omggx + 1 用心讨论,共获提升!
helian147 + 1 + 1 热心回复!
Sound + 3 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| StackPointer 发表于 2026-10-1 13:15
本帖最后由 StackPointer 于 2026-10-4 17:28 编辑

这是修改PVZ的示例,热重载现在是手动触发形式,或许可以改成检查代码是否被修改触发。

// Copyright (c) 2026 StackAndPointer
// SPDX-License-Identifier: MIT
//
// Plants vs. Zombies x86 target:
//   Board::Update at 0x415D40 receives Board* this in ECX.

include "SigilHook.ash"

uint64 g_boardUpdateHook = SH_INVALID_HANDLE;

void onBoardUpdate() {
const uint64 board = shReg(SH_REG_CX);

array<uint64> args(2);
args[0] = 1;
args[1] = board;

uint64 result = 0;
const uint8 status = shCallUsercall(
    0x41B960,
    "void",
    "int,void*",
    "usercall:ret=none;arg0=ecx;arg1=eax",
    args,
    result);
if (status != SH_OK) {
    shLog("Board::AddSunMoney call failed: " + shStatusString(status));
}

// Call the relocated original Update after inserting the AddSunMoney call.
const uint64 original = shTrampoline(g_boardUpdateHook);
if (original == 0) {
    shLog("Board::Update trampoline is unavailable");
    shSkipOriginal();
    return;
}

array<uint64> originalArgs(1);
originalArgs[0] = board;
const uint8 originalStatus = shCallUsercall(
    original,
    "void",
    "void*",
    "usercall:ret=none;arg0=ecx",
    originalArgs,
    result);
if (originalStatus != SH_OK) {
    shLog("Board::Update original call failed: " + shStatusString(originalStatus));
}

shSkipOriginal();

}

void main() {
installAddSunMoneyHook();

g_boardUpdateHook = shHookUsercall(
    0x415D40,
    "void onBoardUpdate()",
    "void:void*",
    "usercall:ret=none;arg0=ecx");
if (!shIsValidHook(g_boardUpdateHook)) {
    shLog("Board::Update hook failed: " + shLastError());
}

}

void unload() {
if (shIsValidHook(g_boardUpdateHook)) {
shDestroyHook(g_boardUpdateHook);
g_boardUpdateHook = SH_INVALID_HANDLE;
}
uninstallAddSunMoneyHook();
}
// Copyright (c) 2026 StackAndPointer
// SPDX-License-Identifier: MIT
//
// Plants vs. Zombies x86 target:
//   Board::AddSunMoney at 0x41B960 receives theAmount in ECX and this in EAX.

include "SigilHook.ash"

uint64 g_addSunMoneyHook = SH_INVALID_HANDLE;

void onAddSunMoney() {
const uint64 amount = shReg(SH_REG_CX);
shSetReg(SH_REG_CX, amount * 0);
shKeepOriginal();
}

void installAddSunMoneyHook() {
g_addSunMoneyHook = shHookUsercall(
0x41B960,
"void onAddSunMoney()",
"void:int,void*",
"usercall:ret=none;arg0=ecx;arg1=eax");
if (!shIsValidHook(g_addSunMoneyHook)) {
shLog("Board::AddSunMoney hook failed: " + shLastError());
}
}

void uninstallAddSunMoneyHook() {
if (shIsValidHook(g_addSunMoneyHook)) {
shDestroyHook(g_addSunMoneyHook);
g_addSunMoneyHook = SH_INVALID_HANDLE;
}
}

zhiqingchun 发表于 2026-10-2 09:42
楼主这个设计思路很赞。之前自己写过基于 PolyHook 2 的 detour 项目,最大的痛点就是每次调整逻辑都要重新编译再注入,联调效率特别低。把逻辑层抽到 AngelScript 里,改脚本就能热重载,这一步对逆向时的快速试错太友好了。

有个细节想请教一下:用 AsmJit 重做的运行时回调桥,脚本回调是跑在目标线程的 hot path 上的,AngelScript 这边有没有做 GC 或者异常时的保护?比如回调里脚本抛异常、或者正好撞上 GC 停顿,会不会把被 hook 的线程卡住?另外 trampoline 的生命周期是怎么管理的,热重载时旧的 trampoline 是延迟释放还是直接 free 掉?

还有个小想法,mid-hook 配合 usercall 自定义调用约定,在分析加了 VM 的函数时应该很好用,可以直接在入口处把寄存器快照打出来。期待后续能加个 watch-and-log 的示例脚本,刚上手的人可以直接照着改。
Turbo86 发表于 2026-10-1 00:47
Sound 发表于 2026-10-1 02:36
不错不错,跟PolyHook 2 比 小而精~
RustYou 发表于 2026-10-1 11:47
感谢开源~学习一下思路
我的爱是你 发表于 2026-10-1 14:06
眼睛有点近视,看这种排版贴有点费劲;所以让ai阅读了一遍。
看下来,不愧是能加精的帖子; 快速试 Hook 逻辑,确实避免了麻烦。
 楼主| StackPointer 发表于 2026-10-2 10:50
zhiqingchun 发表于 2026-10-2 09:42
楼主这个设计思路很赞。之前自己写过基于 PolyHook 2 的 detour 项目,最大的痛点就是每次调整逻辑都要重新 ...

AngelScript 的自动 GC 是在 asCContext::Execute() 返回路径里、同一个脚本线程上增量执行,且内部有 TRYENTERCRITICALSECTION(gcCollecting)。Trampoline现在是直接free的。有问题可以提提issues
Cleverwwh 发表于 2026-10-4 08:25
感谢分享,SigilHook函数Hook工具很实用,逆向开发必备,已收藏!
Iamx688110 发表于 2026-10-4 13:46
把帖子代码甩markdown里再发出来呗
直接复制粘贴有点像AI
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-10-5 20:03

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表