目标与方法
目标是把 AutoHotkey64.exe 当作活体解释器分析,并提取足够多的结构来实现表达式求值、内存脚本加载、远程 Hook 和在线函数 Trace。方法是可重复的循环:先读源码找出候选字段,再检查进程内存确认真实偏移,把稳定逻辑编码成位置无关机器码,最后用结构不变量验证每个结果。
本页记录的就是这个循环。
PE 解析与节区映射
每个 x64 AutoHotkey v2 可执行文件都以 MZ DOS 头开始。PE 签名位于 e_lfanew 指向的偏移,可选头使用 PE32+ 魔数 0x20B。节表紧随可选头。
peOff := NumGet(base, 0x3C, "UInt")
numSec := NumGet(base + peOff + 6, "UShort")
optSize := NumGet(base + peOff + 20, "UShort")
secTab := base + peOff + 24 + optSize
loop numSec {
off := (A_Index - 1) * 40
name := StrGet(hdr.Ptr + off, 8, "UTF-8")
vsize := NumGet(hdr + off + 8, "UInt")
va := NumGet(hdr + off + 12, "UInt")
rawSize := NumGet(hdr + off + 16, "UInt")
}
扫描器把每个节区保存为 {rva, size, ptr},并按名称分类。三个节区最重要:
| 节区 | 作用 | 不变量 |
|---|---|---|
.text | 可执行代码 | 函数指针必须落在这里 |
.rdata | 只读字符串 | UTF-16 名字通常在这里 |
.data | 可写全局量 | 解释器表在这里 |
对于加壳构建,节名不可靠。扫描器会退回到基于内容的分类:非 .rsrc 的代码样节区当作可执行节区,可读的非执行节区当作数据/字符串存储。
解释器表发现
解释器在镜像中维护三张表。它们的 x64 记录步长是稳定的:
| 表 | 内容 | 记录步长 |
|---|---|---|
g_BIF | 脚本可见内置函数,如 Abs、Sin | 0x20 |
sMdFunc | 带类型签名的原生函数,如 MsgBox | 0x28 |
g_BIV_A | 以 A_ 开头的内置变量 | 0x18 |
扫描器不硬编码地址。它按指针对齐槽位扫描,只有同时满足以下条件才接受记录:
- 第一个 qword 解析为可打印 UTF-16 字符串;
- 第二个 qword 落在
.text; - 相邻记录按名字排序;
- 存在
Abs、BlockInput或AhkPath等锚点。
最长的合法连续区成为目标表。同一算法以 x64 机器码运行,因此扫描速度与逻辑不依赖 AHK 版本。
源码到内存的验证
源码提供名字与意图,不提供最终偏移。编译器、构建选项和调试器字段都会移动成员。因此流程先用源码形成假设,再用内存确认。
对于 script.h 中的 UserFunc,源码声明了:
class UserFunc : public Func
{
public:
int mInstances = 0;
Line *mJumpToLine = nullptr;
FuncParam *mParam = nullptr;
Object *mClass = nullptr;
...
VarList mVars {};
VarList mStaticVars {};
};
内存探针在两个代表性构建上确认了实际 x64 布局:
| 字段 | v2.1-alpha.30 | v2.0.26 |
|---|---|---|
mName | 0x28 | 0x20 |
mJumpToLine | 0x48 | 0x40 |
mParam | 0x50 | 0x48 |
mVars | 0x80 | 0x70 |
偏移不同,所以库在运行时发现它们,不维护版本表。
机器码提取流水线
稳定扫描器写在 lib/mcode/ 下的 C 源码中。构建使用 clang 生成无 CRT、位置无关的 COFF 对象:
clang -c -O2 -target x86_64-pc-windows-msvc -ffreestanding \
-fno-builtin -fno-stack-protector -fno-unwind-tables \
-fno-asynchronous-unwind-tables -fno-jump-tables scanner.c
tools/build_mcode.py 执行四步检查:
- 从 COFF 对象提取
.text; - 解码每条重定位记录;
- 只有目标仍在
.text内的IMAGE_REL_AMD64_REL32才被接受; - 任何引用逃出代码块的内容都会被拒绝。
最终十六进制字符串嵌入 ahk_hack_single.ahk。MCode() 把字符串解码为 Buffer,修改为可执行保护,并返回给 DllCall。
EvalNative:表达式求值分析
AutoHotkey 不会直接求值表达式字符串。脚本加载时,Line::ExpressionToPostfix 把表达式编译成 ExprTokenType 后缀数组,Line::ExpandExpression 之后再执行该数组。
EvalNative() 构造解析器本来会创建的临时 Line 和 ArgStruct,然后调用这两个函数。因此它复用了解释器自己的语法与变量解析。
内部定位器机器码发现以下符号:
| 符号 | 来源 | 发现方式 |
|---|---|---|
ExpressionToPostfix | 错误串 "Missing operand." | RIP 相对引用 + prologue 回退 |
ExpandExpression | 错误串 "Error evaluating expression." | RIP 相对引用 + prologue 回退 |
gScript | 最频繁的 lea rcx,[rip+disp] 目标 | 频率过滤 |
FindOrAddVar | 六参数 prologue | 签名匹配 |
SYM_INVALID | 后缀终止符 | 求值器内立即数 |
求值器使用的 token 布局:
| 字段 | 偏移 | 类型 |
|---|---|---|
| value | 0x00 | 指针/int64/double 联合 |
| marker/usage | 0x08 | 指针或 VarRefUsageType |
| symbol | 0x10 | 32 位 SymbolType |
| stride | 0x18 | 结构总大小 |
#Include ahk_hack_single.ahk
AhkMagic.Init()
MsgBox AhkMagic.EvalNative("1 + 2 * 3") ; 7
MsgBox AhkMagic.EvalNative("2 * 3.5") ; 7.0
MsgBox AhkMagic.EvalNative("Abs(-5)") ; 5
MsgBox AhkMagic.EvalNative("SubStr(`"abc`", 2)") ; "bc"
myVar := 42
MsgBox AhkMagic.EvalNative("myVar + 1") ; 43
EvalScript:解析器与内存加载
表达式求值无法定义函数或类。EvalScript() 通过内存 TextStream 把多行文本喂给解释器的 include 管线。
相关管线:
ResultType Script::LoadIncludedFile(TextStream *fp)
{
int source_file_index = mCurrFileIndex;
bool blocks_previously_open = mLineParent || mClassObjectCount;
auto module_previously_open = mCurrentModule;
bool caller_backcompatmode = mBackCompatMode;
...
}
加载前,库保存并在之后恢复:
mOpenBlock/mLineParent;mPendingRelatedLine;mLastParamInitializer;mPendingHotkey;mExprFuncIndex;mNextLineIsFunctionBody;mIgnoreNextBlockBegin;mBackCompatMode;mClassObjectCount;mCurrentModule与mLastModule。
解析器状态锚点从 LoadIncludedFile prologue 中读取:
q:指向mOpenBlock/mLineParent的偏移;d:指向mClassObjectCount的偏移。
TextStream 同时填充两种可能的 mData 区域,因此同一对象可用于 2.0 与 2.1 布局。
远程内省
AttachRemote(pid) 使用 OpenProcess 打开目标,读取 PE 镜像,扫描解释器表,并返回 hook map。
远程求值注入线程 stub、定位器和求值器 blob:
| 步骤 | 操作 |
|---|---|
| 1 | 用 VirtualAllocEx 分配 scratch 块 |
| 2 | 写入 stub、定位器、求值器、表达式、输出缓冲 |
| 3 | 用 CreateRemoteThread 创建远程线程 |
| 4 | 等待并读取结果缓冲 |
| 5 | 释放 scratch 块 |
RemoteEvalScript() 使用目标自己的 LoadIncludedFile 与 PreparseExpressions。注入线程会在 preparse 前设置 g->CurrentFunc,因此 class 方法与 Super 能正确解析。
hook := AhkMagic.AttachRemote(pid)
MsgBox AhkMagic.RemoteEval(hook, "1 + 2 * 3") ; 7
script := "
(
rhkAdd(a, b) {
return a + b
}
rhkAdd(1, 2)
)"
MsgBox AhkMagic.RemoteEvalScript(hook, script) ; 3
函数体替换交换 mJumpToLine:
AhkMagic.RemoteEvalScript(hook, "
(
NewA() {
return 42
}
NewA()
)")
AhkMagic.RemoteReplaceFuncBody(hook, "A", "NewA")
Send("{F9}")
目标开始输出 A()=42,原始输出为 A()=1。
动态偏移发现
远程布局扫描器从存活内存推导 mFuncs、mFuncsCount、函数名字段与 mJumpToLine:
- 候选 array/count 对通过读取每个
Func指针验证; mName是指向合法标识符字符串的指针;mJumpToLine指向包含回指所属Func的Line;- 回引用槽位在普通函数之间保持一致。
结构验证器随后发现:
| 结构 | 验证字段 |
|---|---|
Line | action、argc、arg 指针、attribute、next |
ArgStruct | is_expression、postfix 指针、deref 指针 |
ExprTokenType | stride、symbol 偏移、usage 偏移、value 偏移 |
DerefType | marker、var、type、length |
Token 数组通过走到 SYM_INVALID 并在终止符前要求一个 SYM_VAR token 来验证。DerefType 从运行进程中已有的 ArgStruct.deref 数组读取。
TraceFunction:基于克隆的 Trace
函数 Trace 比函数替换更难。最初实现重命名目标函数并注入 wrapper,结果失败,因为:
- 函数名同时存在于
Func.mName与全局Var.mName; mFuncs与VarList是排序数组;- 只改一边会破坏二分查找;
- 只替换函数体不会重新绑定参数
Var对象。
稳定修复来自阅读 UserFunc::Call:
bool UserFunc::Call(ResultToken &aResultToken, ExprTokenType *aParam[], int aParamCount)
{
...
for (j = 0; j < mParamCount; ++j)
{
FuncParam &this_formal_param = mParam[j];
...
}
}
调用路径通过 mParam[i].var 写入参数。因此两个函数对象可以共享同一组参数 Var,两个函数体都能看到相同值。
最终设计:
- 克隆
UserFunc对象; - 为克隆体创建
VAR_CONSTANT全局别名; - 注入调用克隆体的 wrapper;
- 把 wrapper 的参数
Var别名回原函数参数; - 替换原函数体指针;
- Untrace 时恢复原
mJumpToLine。
Wrapper 写入目标端全局字符串,因此 RemoteEval 可以读取 Trace 日志,而不依赖求值线程内的文件 I/O。
验证矩阵
tools/verify_all_runtimes.ps1 对本机每个运行时执行十三项测试:
| 测试 | 覆盖 |
|---|---|
ahk_live_cross_smoke | attach、快照、清单、替换、Watch |
ahk_live_test | 完整 Trace/替换路径 |
ahk_live_product_test | 会话与补丁回滚 |
ahk_live_six_test | 观测、故障、桌面、取证、代理、CSV |
ahk_live_benchmark | eval/快照/清单耗时 |
remote_hook_test | 远程求值、脚本、class、函数替换 |
evalscript_inproc | 进程内脚本加载 |
evalscript_repeat | 重复函数/class 加载 |
evalscript_class | class 定义 |
eval_object_syntax_test | 进程内对象返回值与语法错误存活 |
remote_empty_object_test | 远程对象返回值与语法错误存活 |
remote_empty_target_test | 无用户函数目标的探针注入重试 |
remote_deep_redirect_test | 直接与动态调用链上的深重定向 |
当前结果:
| 运行时家族 | 构建 | 结果 |
|---|---|---|
| 2.0 beta | beta.9、beta.10、beta.12、beta.13、beta.15 | 各 13/13 |
| 2.0 rc | rc.1、rc.3 | 各 13/13 |
| 2.0 stable | 0.0、0.2、0.3、0.4、0.26 | 各 13/13 |
| 2.1 alpha | alpha.1、alpha.4、alpha.13、alpha.16、alpha.30 | 各 13/13 |
| 本机开发副本 | 两个 2.0-beta 二进制 | 各 13/13 |
总计:19 个运行时,全部 13/13。
11.1 回归覆盖:错误捕获
GUI 会把每个 AhkLiveResult 转成可见的日志行。OnError() 同时把异常字段写入 GUI 日志和 %TEMP%\ahk_live_gui_error.log;SafeStr() 序列化 Map/Array 值,避免第二个错误再次弄崩日志器。Watch 与观测错误会作为 Error 对象转发给回调。--shot 会选中指定页签并写就绪标记,供完整窗口截图自动化使用。
AhkLive_OnError(e, exitCode := 0) {
msg := "ERR " e.What " | " e.Message
. " | " e.File ":" e.Line
FileAppend(msg "`n", A_Temp "\ahk_live_gui_error.log")
return true
}
OnError(AhkLive_OnError)
回归测试 tests\ahk_live_regression.ahk 覆盖构建 GUI 时发现的三类失败:
EvalScript之后执行TraceFunction,要求缓存script_layout从script_loc回填find_var;- 快照表达式在括号内含逗号,不能被当成配置分隔符拆分;
- 重复的顶层函数名,必须在
LoadIncludedFile执行前拒绝:
for name in AhkMagic._DeclaredFunctionNames(text) {
if AhkMagic._RemoteFindUserFuncs(h, layout, name).Length
throw Error("function name already exists in target: " name, -1)
}
新回归测试已在 2.1-alpha.30 与 2.0.26 通过。19 运行时十三项矩阵(现已包含上述四个鲁棒性回归)仍然是完整兼容门槛,19 个运行时全部 13/13。
安全与边界
这个库会分配可执行内存、执行机器码,并写入当前或目标进程的解释器内存。它不具备沙箱隔离。
规则:
- 只附加自己拥有的进程;
- 反复实验使用一次性目标;
- 补丁用
finally或AhkLivePatchSession.Rollback()恢复; - 永远不要 eval 不可信输入;
- 定位失败应视为错误,不猜测偏移。
加壳或恶意构建可能改节名或删字符串。内容分类扫描可以处理常见加壳器,但恶意构建仍可能绕过发现。
失败行为
每一条读取解释器内存或打补丁的路径都设计为显式失败:
| 领域 | 行为 |
|---|---|
| 表与导出扫描 | 记录数按扫描器契约封顶,每个名字指针使用前做内存映射检查;不支持的布局抛错而不是越界读取 |
| 语法错误 | EvalScript/RemoteEvalScript 用解释器自己的 /validate 预检文本(远程用目标自己的可执行文件);验证器静默运行并自行捕获输出(不弹窗),超时即杀进程树 |
| 远程超时 | 挂起的求值 10 秒后抛错并泄漏注入块,因为释放目标仍在执行的内存会损坏目标 |
| 对象结果 | EvalNative 返回活的 Func/Class 对象;RemoteEval 返回目标侧对象地址 |
| 空目标 | 目标没有用户函数时,RemoteEvalScript 注入随机命名探针函数并重试布局发现 |
| 补丁 | PatchBifObject 与 RemoteDeepRedirect 原子提交并支持回滚;发现探针在 finally 中恢复解释器快照 |
继续阅读
- 仓库:MonoEven/ahk-hack-library
- 单文件核心:ahk_hack_single.ahk
- 机器码源码:lib/mcode
- AhkLive 层:lib/ahk_live
- 完整矩阵报告:reports
附录:单文件核心源码
以下副本由构建脚本根据仓库内文件生成。
ahk_hack_single.ahk
; ahk-hack standalone core.
;
; One-file AutoHotkey v2 scanner for AutoHotkey executables. Copy this file
; anywhere and #Include it, or run ahk_hack_demo.ahk for a self-test.
; The x64 blob below is generated from lib/mcode/scanner.c by
; tools/build_mcode.py; it locates g_BIF, sMdFunc and g_BIV_A inside the
; running interpreter and copies entry metadata into a caller buffer.
MC_REMOTE_CALL_STUB_X64 := "564883ec304889ce0f1041284c8b49204c8b4118488b4908488b56100f11442420ff1689463831c04883c4305ec3"
MC_REMOTE_EVAL_STUB_X64 := "564881ec800000004889ce0f1041304c8b49284c8b4120488b4910488b56180f11442420ff1689c1894678b80100000085c9755b488b4638488b4e400f10000f104810488b40204c8b96800000000f1056604c8b4e584c8b4650488b56484c8954247048894424680f114c24580f114424480f57c00f114424380f11542420c744243001000000ff560889467c31c04881c4800000005ec3"
MC_PE_EXPORT_SCANNER_X64 := "415741564155415456575553504885c90f94c04885d2410f94c04108c0b8010000000f85f00000006681394d5a0f85e5000000448b413c41813c08504500000f85d30000006641817c08180b020f85c5000000418bb40888000000458b8c088c0000004885f6410f94c24585c9410f94c34508d34180fb0175190f57c00f1102be100000004531c94c890c3231c0e985000000458b4408504d85c0747b4439c673764589c94d89ca4901f24d39c2410f97c24183f928410f92c14508d17559448b740e18448b540e144d85d2410f94c14585f6410f95c34584d9753c4181fe0010000041b900100000450f42ce4c8d3c0e458b5f204b8d3c8b4c39c7771a8b7c0e244a8d1c4f4c39c3770d8b5c0e1c4e8d24934d39c476114883c4085b5d5f5e415c415d415e415fc38b740e10897424044c893a48c7421000000000be080000004585f60f843bffffff4801cb4901cb4801cf4e8d34cd000000004f8d34764531ff458b234539c473ae440fb72f4539ea76a5428b2cab4439c5739c4901cc4e89643a184a896c3a2044036c240446896c3a2842c7443a2c000000004983c3044883c7024983c7184d39fe75b5e9d6feffff"
MC_BIF_SCANNER_X64 := "4157415641554154565755534881ec780200004885c90f94c04885d2410f94c04108c0be010000000f85060700006681394d5a0f85fb0600008b413c813c08504500000f85eb06000066817c08180b020f85de060000440fb7540806664183fa1041b910000000450f42ca448b5c08504d8d040b4d85db4d0f44c3664585d20f8400030000440fb7540814410fb7f94801c84c01d0448d0cfd000000004f8d0c894c8d5424704531dbbb010000004531f631edeb4c660f7f442450440fb67c2452440fb66424534180f7614180f4744508fc440fb67c24544180f761400fb6ed4508e70f44eb662e0f1f8400000000004983c3284983c2204d39d90f84ed000000468b7c18204585ff460f447c1828468b6418244901cc4d01e74d39c74d89c54d0f42ef4d85c04d0f44ef4d89224d896a084e8b7c181866490f6ec7660f7f442460440fb66424604d897a134180fc2e759e66410f7ec741c1ef084180ff640f8450ffffff450fb6ff4183ff7274414183ff740f8577ffffff660f7f442430440fb67c2432440fb66424334180f7654180f4784508fc440fb67c24344180f774450fb6f64508e7440f44f3e940ffffff660f7f442440807c2442640f852fffffff807c2443610f8524ffffff807c2444740f8519ffffff66440fc5f80241c1ef084180ff61400fb6ede9f5feffff4584f60f95c14084ed0f95c020c889f9c1e1054531c0eb2d662e0f1f8400000000006642c784048000000000004531c946888c04820000004983c0204c39c10f8454010000460fb69404830000004180fa2e753e4280bc04840000007275434280bc04850000007375384280bc048600000072752d4280bc048700000063410f94c184c0746ceb21662e0f1f84000000000084c0758c4531c9eb570f1f80000000004531c984c074494180fa2e0f856fffffff4280bc048400000074754d4280bc04850000006575424280bc04860000007875374280bc048700000074410f94c1eb2b6666666666662e0f1f8400000000004180f10146888c048000000046888c0481000000e925ffffff4531c946888c04800000004280bc04840000007275304280bc04850000006475254280bc048600000061751a4280bc048700000074750f4280bc048800000061410f94c1eb034531c946888c04810000004280bc04840000006475284280bc048500000061751d4280bc04860000007475124280bc048700000061410f94c1e9a1feffff4531c9e999feffff31ff488d420848894424284989d74889542420488d4c247041b92000000089fa4531c0e8a303000085c00f847f0300004d8d7720498d47184c897424284889442420488d4c2470be0100000041b92800000089fa41b801000000e86c03000085c00f8448030000498d5f38498d473048895c24284889442420488d4c247041b91800000089fa41b802000000e83a03000085c00f84160300004c89f849c747102000000049c747282800000049c747401800000049c7474800000000418b570885d27e6dffca4881faff010000b9ff010000480f42ca488b10c1e1054883c1204531c00f1f8400000000004e8b0c024e894c00504e8b4c02084e894c0058460fb64c021046894c0060460fb64c021146894c0064460fb64c021246894c006842c744006c000000004983c0204c39c175ba418b0e85c90f8ea5010000ffc94881f9ff010000baff010000480f42d1488b4818488d1492488d14d5280000004531c0662e0f1f8400000000004e8b0c014e898c00504000004e8b4c01084e898c0058400000460fb64c011046888c0060400000460fb64c011146888c0061400000460fb64c011246888c0062400000460fb64c011346888c0063400000460fb64c011446888c0064400000460fb64c011546888c0065400000460fb64c011646888c0066400000460fb64c011746888c0067400000460fb64c011846888c0068400000460fb64c011946888c0069400000460fb64c011a46888c006a400000460fb64c011b46888c006b400000460fb64c011c46888c006c400000460fb64c011d46888c006d400000460fb64c011e46888c006e400000460fb64c011f46888c006f400000460fb64c012046888c0070400000460fb64c012146888c0071400000460fb64c012246888c0072400000460fb64c012346888c0073400000460fb64c012446888c0074400000460fb64c012546888c0075400000460fb64c012646888c0076400000460fb64c012746888c00774000004983c0284c39c20f858afeffff448b034585c00f8e8c00000041ffc84181f8ff000000baff000000410f42d0488b4830ffc231f64585c074704189d14181e1fe0100004531d24531c04e8b1c114e899c10509000004e8b5c11084e899c10589000004e8b5c11104e899c10609000004e8b5c11184e899c10689000004e8b5c11204e899c10709000004e8b5c11284e899c10789000004983c0024983c2304d39c175a6eb0731f6eb324531c0f6c201742a48055090000049c1e0034b8d14404c8b04114c8904104c8b4411084c89441008488b4c111048894c101089f04881c4780200005b5d5f5e415c415d415e415fc3660f1f4400004157415641554154565755534883ec3885d20f8e270500004c894c241889d64889f748c1e70531db41be4000000048c74424200000000048c744242800000000eb1a66666666662e0f1f84000000000048ffc34839f30f84c80400004889da48c1e205488d0411807c11110075068078120074dc4c8b204c8b7808498d4424104c39f877cb4d8b2c24eb2e4c8b0c24488b4424284939c1490f47c14889442428488b442420480f47c5488944242066904883c5184c39fd77974c89e54c89e84983c4084c8b6d084531d2eb0d0f1f40004983c2204c39d774d742807c111100750842807c11120074e74e8b4c11084929c176dd4a39041177d74983f90272b149d1e94983f9404d0f43ce49ffc94989c2450fb71a664585db741f4183c3814983e9010f92c2664183fba172844983c20284d274dce977ffffff4531c9eb0f66904983c1204c39cf0f8463ffffff42807c09100074eb4e396c090876e44e392c0977de0fb7104183f8010f84de0200004585c00f854d0300006683fa410f852effffff66837802620f8523ffffff41b90600000066837804730f8512ffffff6641833c01000f8506ffffff488b4424184801e84883c01041b9010000004c39f80f87cafeffff488b4424184801e84889ea48895424104889c24c8b10488b400848894424084531dbeb140f1f80000000004983c3204c39df0f8492feffff42807c191100750842807c19120074e34a8b4419084c29d076d94e39141977d34883f8020f8268feffff48895424304c890c2448d1e84883f840490f43c648ffc84d89d36666666666662e0f1f840000000000410fb7136685d2742383c2814883e801410f92c16683faa10f821dfeffff4983c3024584c974d9e90ffeffff31c04c8b0c24488b542408eb140f1f80000000004883c0204839c70f84f2fdffff807c01100074ec483954010876e54839140177df4183f80275406641833a590f85620100006641837a02590f85560100006641837a04590f854a0100006641837a06590f853e0100006641837a08000f8532010000e925020000488b442410488b104531c9eb094983c1204c39cf742542807c091100750842807c09120074e74a8b4409084829d076dd4a39140977d748d1e8eb0231c04531c9eb094983c1204c39cf745542807c091100750842807c09120074e74e8b5c09084d29d376dd4e39140977d74d89d949d1e94889542408488d50ff4883fa3f488b542408490f43c64939c14c0f43c84983fb024c0f42c84c894c24104d85c9eb704989d1488d50ff4883fa3f4c89ca490f43c648894424104c8b0c240fb702450fb71a6685c07451664585db744b4889542408418d53bf458d4b2080fa1a410fb6d1410f43d3448d48bf448d58204180f91a450fb6cb440f43c84138d175254983c202488b5424084883c20248ff4c24104c8b0c2475a5eb15664439d8760fe975fcffff4c8b0c240f836bfcffff49ffc1488b5424184c8b542430498d04124c01d24883c2104c39fa4c89d20f8688fdffffe942fcffff6683fa420f8559fcffff668378026c0f854efcffff668378046f0f8543fcffff66837806630f8538fcffff668378086b0f852dfcffff6683780a490f8522fcffff6683780c6e0f8517fcffff6683780e700f850cfcffff66837810750f8501fcffff41b91400000066837812740f85f0fbffffe9d9fcffff6683fa410f85e1fbffff66837802680f85d6fbffff668378046b0f85cbfbffff66837806500f85c0fbffff66837808610f85b5fbffff6683780a740f85aafbffff41b90e0000006683780c680f8599fbffffe982fcffff49ffc1e96bfbffff4c8b4424204d85c00f94c0488b5424284883fa0a0f92c108c1740431c0eb1b488b8424a00000004c8900488b8424a8000000488910b8010000004883c4385b5d5f5e415c415d415e415fc3"
; Decode a raw hex machine-code blob and return an executable Buffer.
MCode(hex) {
size := StrLen(hex) // 2
buf := Buffer(size)
loop size {
byte := Integer("0x" SubStr(hex, 2 * A_Index - 1, 2))
NumPut("UChar", byte, buf, A_Index - 1)
}
oldProtect := 0
if !DllCall("VirtualProtect", "Ptr", buf.Ptr, "UPtr", size, "UInt", 0x40, "UInt*", oldProtect)
throw Error("VirtualProtect failed", -1)
return buf
}
class AhkMagic {
static scanner := 0
static exportScanner := 0
static inprocEval := 0
static memScriptLoader := 0
static internalLocator := 0
static internalLocated := false
static evalScriptLocated := false
static evalLayoutLocated := false
static evalMFuncsOff := 0
static evalMFuncsCountOff := 0
static evalMLastLineOff := 0
static evalMJumpLineOff := 0
static evalCurrOff := 0
static evalStructOffsets := 0
static evalLayoutArr := 0
static evalParserLocated := false
static evalParser := Map()
static evalPreparse := 0
static evalPreprocess := 0
static evalOpenInclude := 0
static evalLoadTs := 0
static evalSrcCount := 0
static evalGptr := 0
static exprToPostfix := 0
static expandSingleArg := 0
static currLineSlot := 0
static crtFree := 0
static gScript := 0
static finalizeExpr := 0
static findOrAddVar := 0
static symInvalid := 73
static moduleBase := 0
static bifTablePtr := 0
static bifCount := 0
static bifStride := 0
static mdfuncTablePtr := 0
static mdfuncCount := 0
static mdfuncStride := 0
static bivTablePtr := 0
static bivCount := 0
static bivStride := 0
static bif := Map()
static mdfunc := Map()
static biv := Map()
static Init() {
if AhkMagic.scanner
return
if A_PtrSize != 8
throw Error("this mcode blob is x64 only", -1)
try {
AhkMagic._InitTables()
} catch as e {
; A half-initialized state would make later calls fail with
; misleading errors; reset so Init can be retried cleanly.
AhkMagic.scanner := 0
AhkMagic.bif.Clear()
AhkMagic.mdfunc.Clear()
AhkMagic.biv.Clear()
throw e
}
}
; Parse the scanner output into bif/mdfunc/biv maps. The scanner copies
; at most 512/512/256 entries into the 64 KiB output buffer; larger
; tables are an explicit unsupported-layout error, never a silent read.
static _InitTables() {
AhkMagic.scanner := MCode(MC_BIF_SCANNER_X64)
AhkMagic.moduleBase := DllCall("GetModuleHandle", "Str", A_AhkPath, "Ptr")
if !AhkMagic.moduleBase
throw Error("GetModuleHandle failed", -1)
out := Buffer(64 * 1024)
rc := DllCall(AhkMagic.scanner.Ptr, "Ptr", AhkMagic.moduleBase, "Ptr", out.Ptr, "Int")
if rc != 0
throw Error("AhkScanTables failed with rc=" rc, -1)
AhkMagic.bifTablePtr := NumGet(out, 0, "Ptr")
AhkMagic.bifCount := NumGet(out, 8, "Int64")
AhkMagic.bifStride := NumGet(out, 16, "Int64")
AhkMagic.mdfuncTablePtr := NumGet(out, 24, "Ptr")
AhkMagic.mdfuncCount := NumGet(out, 32, "Int64")
AhkMagic.mdfuncStride := NumGet(out, 40, "Int64")
AhkMagic.bivTablePtr := NumGet(out, 48, "Ptr")
AhkMagic.bivCount := NumGet(out, 56, "Int64")
AhkMagic.bivStride := NumGet(out, 64, "Int64")
; The copy loops below run on raw pointer arithmetic without any
; buffer object, so validate every count and stride BEFORE looping.
if AhkMagic.bifCount < 1 or AhkMagic.bifCount > 512
throw Error("g_BIF table count out of scanner contract: "
. AhkMagic.bifCount, -1)
if AhkMagic.mdfuncCount < 1 or AhkMagic.mdfuncCount > 512
throw Error("sMdFunc table count out of scanner contract: "
. AhkMagic.mdfuncCount, -1)
if AhkMagic.bivCount < 1 or AhkMagic.bivCount > 256
throw Error("g_BIV_A table count out of scanner contract: "
. AhkMagic.bivCount, -1)
if AhkMagic.bifStride != 0x20 or AhkMagic.mdfuncStride != 0x28
or AhkMagic.bivStride != 0x18
throw Error("unexpected interpreter table strides", -1)
bifBase := 80
bifStride := 32
loop AhkMagic.bifCount {
p := out.Ptr + bifBase + (A_Index - 1) * bifStride
namePtr := NumGet(p, 0, "Ptr")
fnPtr := NumGet(p, 8, "Ptr")
min := NumGet(p, 16, "UInt")
max := NumGet(p, 20, "UInt")
fid := NumGet(p, 24, "UInt")
if !AhkMagic._Readable(namePtr, 8)
throw Error("g_BIF entry " A_Index " has an invalid name pointer", -1)
name := StrGet(namePtr, 256, "UTF-16")
AhkMagic.bif[name] := Map(
"index", A_Index - 1,
"rva", fnPtr - AhkMagic.moduleBase,
"min", min,
"max", max,
"fid", fid
)
}
mdfBase := 80 + 512 * 32
mdfStride := 40
loop AhkMagic.mdfuncCount {
p := out.Ptr + mdfBase + (A_Index - 1) * mdfStride
namePtr := NumGet(p, 0, "Ptr")
fnPtr := NumGet(p, 8, "Ptr")
retType := NumGet(p, 16, "UChar")
if !AhkMagic._Readable(namePtr, 8)
throw Error("sMdFunc entry " A_Index " has an invalid name pointer", -1)
name := StrGet(namePtr, 256, "UTF-16")
AhkMagic.mdfunc[name] := Map(
"index", A_Index - 1,
"rva", fnPtr - AhkMagic.moduleBase,
"ret", retType
)
}
bivBase := 80 + 512 * 32 + 512 * 40
bivStride := 24
loop AhkMagic.bivCount {
p := out.Ptr + bivBase + (A_Index - 1) * bivStride
namePtr := NumGet(p, 0, "Ptr")
getter := NumGet(p, 8, "Ptr")
setter := NumGet(p, 16, "Ptr")
if !AhkMagic._Readable(namePtr, 8)
throw Error("g_BIV_A entry " A_Index " has an invalid name pointer", -1)
name := StrGet(namePtr, 256, "UTF-16")
AhkMagic.biv[name] := Map(
"index", A_Index - 1,
"getter_rva", getter ? getter - AhkMagic.moduleBase : 0,
"setter_rva", setter ? setter - AhkMagic.moduleBase : 0
)
}
}
; True when [ptr, ptr+size) falls inside committed readable memory.
; Used before dereferencing pointers that come from scanned tables.
static _Readable(ptr, size) {
if !ptr or ptr >= 0x7FFFFFFFFFFF
return false
mbi := Buffer(48)
if !DllCall("VirtualQuery", "Ptr", ptr, "Ptr", mbi.Ptr, "UPtr", 48)
return false
state := NumGet(mbi, 32, "UInt")
protect := NumGet(mbi, 36, "UInt")
if state != 0x1000 ; MEM_COMMIT
return false
if protect & 0x100 ; PAGE_GUARD
return false
readable := (protect & 0x02) or (protect & 0x04) or (protect & 0x08)
or (protect & 0x20) or (protect & 0x40) or (protect & 0x80)
if !readable
return false
regionBase := NumGet(mbi, 0, "Ptr")
regionSize := NumGet(mbi, 24, "Ptr")
return ptr >= regionBase and ptr + size <= regionBase + regionSize
}
static BifRva(name) {
AhkMagic.Init()
if !AhkMagic.bif.Has(name)
throw Error("builtin not found: " name)
return AhkMagic.bif[name]["rva"]
}
static BifAddr(name) {
AhkMagic.Init()
return AhkMagic.moduleBase + AhkMagic.BifRva(name)
}
static PatchBif(name, newName) {
AhkMagic.Init()
if !AhkMagic.bif.Has(name)
throw Error("builtin not found: " name)
if !AhkMagic.bif.Has(newName)
throw Error("builtin not found: " newName)
entryAddr := AhkMagic.bifTablePtr
+ AhkMagic.bif[name]["index"] * AhkMagic.bifStride
+ A_PtrSize
oldPtr := NumGet(entryAddr, 0, "Ptr")
expected := AhkMagic.moduleBase + AhkMagic.bif[name]["rva"]
if oldPtr != expected
throw Error("table pointer mismatch for " name
": got 0x" Format("{:X}", oldPtr)
", expected 0x" Format("{:X}", expected))
NumPut("Ptr", AhkMagic.moduleBase + AhkMagic.bif[newName]["rva"], entryAddr)
return oldPtr
}
static RestoreBif(name, oldPtr) {
AhkMagic.Init()
if !AhkMagic.bif.Has(name)
throw Error("builtin not found: " name)
entryAddr := AhkMagic.bifTablePtr
+ AhkMagic.bif[name]["index"] * AhkMagic.bifStride
+ A_PtrSize
NumPut("Ptr", oldPtr, entryAddr)
}
; Deep patch: change the mBIF field of an already-resolved built-in Func
; object, so even direct calls compiled at load time (e.g. Abs(1)) are
; redirected. Works together with PatchBif on the table itself.
static PatchBifObject(fnObj, newName) {
AhkMagic.Init()
if !(fnObj is Func)
throw TypeError("expected a Func object")
if !AhkMagic.bif.Has(newName)
throw Error("builtin not found: " newName)
fnName := fnObj.Name
if !AhkMagic.bif.Has(fnName)
throw Error("function object is not a built-in: " fnName)
oldAddr := AhkMagic.moduleBase + AhkMagic.bif[fnName]["rva"]
newAddr := AhkMagic.moduleBase + AhkMagic.bif[newName]["rva"]
rawPtr := ObjPtr(fnObj)
off := AhkMagic._FindBifPtrOffset(rawPtr, oldAddr)
if off < 0
throw Error("could not locate mBIF in function object for " fnName)
; Patch the table first, then the object. If the object write ever
; fails the table patch is rolled back, so callers never observe a
; half-patched state.
old := AhkMagic.PatchBif(fnName, newName)
try
NumPut("Ptr", newAddr, rawPtr, off)
catch as e {
AhkMagic.RestoreBif(fnName, old)
throw e
}
return Map(
"fnName", fnName,
"newName", newName,
"oldPtr", old,
"offset", off
)
}
static RestoreBifObject(fnObj, state) {
AhkMagic.Init()
if !(fnObj is Func)
throw TypeError("expected a Func object")
fnName := fnObj.Name
if state["fnName"] != fnName
throw Error("state does not match function object " fnName)
if !AhkMagic.bif.Has(state["newName"])
throw Error("builtin not found: " state["newName"])
rawPtr := ObjPtr(fnObj)
current := AhkMagic.moduleBase + AhkMagic.bif[state["newName"]]["rva"]
off := AhkMagic._FindBifPtrOffset(rawPtr, current)
if off < 0
throw Error("could not locate mBIF in function object for " fnName)
NumPut("Ptr", state["oldPtr"], rawPtr, off)
AhkMagic.RestoreBif(fnName, state["oldPtr"])
}
static _FindBifPtrOffset(rawPtr, targetAddr) {
loop 64 {
off := (A_Index - 1) * 8
if NumGet(rawPtr, off, "Ptr") = targetAddr
return off
}
return -1
}
; General PE export-table scanner (works for any loaded DLL/EXE module).
static ScanExports(moduleBase) {
if !(moduleBase is Integer) or !moduleBase
throw TypeError("moduleBase must be a nonzero module address", -1)
if !AhkMagic.exportScanner
AhkMagic.exportScanner := MCode(MC_PE_EXPORT_SCANNER_X64)
out := Buffer(100 * 1024)
rc := DllCall(
AhkMagic.exportScanner.Ptr,
"Ptr", moduleBase,
"Ptr", out.Ptr,
"Int"
)
if rc != 0
throw Error("AhkScanExports failed with rc=" rc)
count := NumGet(out, 8, "Int64")
; The scanner writes at most 4096 entries into the 100 KiB buffer.
if count < 0 or count > 4096
throw Error("export count out of scanner contract: " count, -1)
result := Map()
loop count {
p := out.Ptr + 24 + (A_Index - 1) * 24
namePtr := NumGet(p, 0, "Ptr")
fnRva := NumGet(p, 8, "Int64")
ordinal := NumGet(p, 16, "UInt")
if !AhkMagic._Readable(namePtr, 8)
throw Error("export entry " A_Index " has an invalid name pointer", -1)
result[StrGet(namePtr, 1024, "UTF-8")] := Map(
"rva", fnRva,
"ordinal", ordinal
)
}
return result
}
; ------------------------------------------------------------------
; True in-process Eval. Locates the interpreter's own expression
; compiler/evaluator, builds a temporary Line/ArgStruct, and calls
; them directly in this process.
; ------------------------------------------------------------------
static _ModuleSections() {
base := AhkMagic.moduleBase
peOff := NumGet(base, 0x3C, "UInt")
numSections := NumGet(base + peOff + 6, "UShort")
optSize := NumGet(base + peOff + 20, "UShort")
secTable := base + peOff + 24 + optSize
result := Map()
loop numSections {
hdr := secTable + (A_Index - 1) * 40
name := StrGet(hdr, 8, "UTF-8")
vsize := NumGet(hdr + 8, "UInt")
va := NumGet(hdr + 12, "UInt")
result[name] := Map(
"rva", va,
"size", vsize,
"ptr", base + va
)
}
pdata := result.Has(".pdata") ? result[".pdata"] : 0
for _, sec in result
sec["pdata"] := pdata
return result
}
static _TextSection(secs) {
if secs.Has(".text")
return secs[".text"]
for name, sec in secs
if name != ".rsrc"
return sec
throw Error("text section not found")
}
static _FindUtf16(secs, text) {
len := StrLen(text)
needle := Buffer(2 * (len + 1), 0)
StrPut(text, needle, "UTF-16")
hits := []
for name, sec in secs {
if name = ".rsrc"
continue
if !sec["ptr"] or sec["size"] < 2 * len
continue
p := sec["ptr"]
count := sec["size"] >= 2 * len
? Min((sec["size"] - 2 * len) // 2 + 1, 0x100000)
: 0
if count <= 0
continue
loop count {
off := (A_Index - 1) * 2
ok := true
loop len {
if NumGet(p + off + 2 * (A_Index - 1), "UShort")
!= NumGet(needle, 2 * (A_Index - 1), "UShort") {
ok := false
break
}
}
if ok and NumGet(p + off + 2 * len, "UShort") = 0
hits.Push(sec["rva"] + off)
}
}
return hits
}
static _RipRefs(sec, targetRva) {
p := sec["ptr"]
size := sec["size"]
refs := []
if size < 8
return refs
loop size - 7 {
i := A_Index - 1
b := NumGet(p + i, "UChar")
if b != 0x48 and b != 0x4C
continue
if NumGet(p + i + 1, "UChar") != 0x8D
continue
reg := NumGet(p + i + 2, "UChar")
if reg != 0x05 and reg != 0x0D and reg != 0x15 and reg != 0x1D
and reg != 0x25 and reg != 0x2D and reg != 0x35 and reg != 0x3D
continue
disp := NumGet(p + i + 3, "Int")
next := sec["rva"] + i + 7
if next + disp = targetRva
refs.Push(sec["rva"] + i)
}
return refs
}
static _PdataFunctionStart(pdata, rva, sec) {
if !pdata or pdata["size"] < 12
throw Error("PE .pdata section is missing", -1)
p := pdata["ptr"]
count := pdata["size"] // 12
lo := 1
hi := count
idx := 0
while lo <= hi {
mid := (lo + hi) // 2
off := (mid - 1) * 12
begin := NumGet(p + off, "UInt")
end := NumGet(p + off + 4, "UInt")
if rva < begin {
hi := mid - 1
continue
}
if rva >= end {
lo := mid + 1
continue
}
idx := mid
break
}
if !idx
throw Error("function start not found for RVA "
. Format("0x{:X}", rva), -1)
loop idx {
current := idx - A_Index + 1
off := (current - 1) * 12
begin := NumGet(p + off, "UInt")
if AhkMagic._IsFunctionEntryCode(sec, begin)
return begin
}
throw Error("logical function entry not found for RVA "
. Format("0x{:X}", rva), -1)
}
static _PdataEntryStart(pdata, rva) {
if !pdata or pdata["size"] < 12
return 0
p := pdata["ptr"]
count := pdata["size"] // 12
lo := 1
hi := count
while lo <= hi {
mid := (lo + hi) // 2
off := (mid - 1) * 12
begin := NumGet(p + off, "UInt")
end := NumGet(p + off + 4, "UInt")
if rva < begin {
hi := mid - 1
continue
}
if rva >= end {
lo := mid + 1
continue
}
return begin
}
return 0
}
static _IsFunctionEntryCode(sec, rva) {
off := rva - sec["rva"]
if off < 0 or off + 4 > sec["size"]
return false
p := sec["ptr"]
b0 := NumGet(p + off, "UChar")
b1 := NumGet(p + off + 1, "UChar")
b2 := NumGet(p + off + 2, "UChar")
b3 := NumGet(p + off + 3, "UChar")
if b0 = 0x48 and b1 = 0x89
and ((b2 = 0x5C and b3 = 0x24)
or (b2 = 0x54 and b3 = 0x24)
or (b2 = 0x4C and b3 = 0x24))
return true
if b0 = 0x4C and b1 = 0x89
and ((b2 = 0x44 and b3 = 0x24)
or (b2 = 0x4C and b3 = 0x24))
return true
if b0 = 0x40 and b1 >= 0x50 and b1 <= 0x57
return true
if b0 = 0x55 or b0 = 0x53 or b0 = 0x56 or b0 = 0x57
or b0 = 0x41
return true
return false
}
static _FnStart(sec, refRva) {
if !sec.Has("pdata")
throw Error("section has no PE .pdata context", -1)
return AhkMagic._PdataFunctionStart(sec["pdata"], refRva, sec)
}
static _FindCallers(sec, targetRva) {
p := sec["ptr"]
size := sec["size"]
calls := []
if size < 6
return calls
loop size - 5 {
i := A_Index - 1
if NumGet(p + i, "UChar") != 0xE8
continue
disp := NumGet(p + i + 1, "Int")
if sec["rva"] + i + 5 + disp = targetRva
calls.Push(sec["rva"] + i)
}
return calls
}
static _BestStart(sec, refs) {
starts := Map()
for ref in refs {
start := AhkMagic._FnStart(sec, ref)
starts[start] := starts.Has(start) ? starts[start] + 1 : 1
}
best := 0
bestCount := 0
for start, count in starts {
if count > bestCount {
best := start
bestCount := count
}
}
return best
}
static _CurrLineSlotAddr() {
AhkMagic.Init()
if !AhkMagic.biv.Has("LineNumber")
throw Error("A_LineNumber getter not found")
p := AhkMagic.moduleBase + AhkMagic.biv["LineNumber"]["getter_rva"]
if NumGet(p, "UChar") != 0x48 or NumGet(p + 1, "UChar") != 0x8B
or NumGet(p + 2, "UChar") != 0x05
throw Error("unexpected A_LineNumber getter code")
disp := NumGet(p + 3, "Int")
return p + 7 + disp
}
static _LocateInternalFunctions() {
if AhkMagic.internalLocated
return
secs := AhkMagic._ModuleSections()
text := AhkMagic._TextSection(secs)
postfixRefs := AhkMagic._RipRefs(text, AhkMagic._FindUtf16(secs, "Missing operand.")[1])
postfix2 := AhkMagic._BestStart(text, postfixRefs)
if !postfix2
throw Error("ExpressionToPostfix not found")
expandRefs := AhkMagic._RipRefs(text, AhkMagic._FindUtf16(secs, "Error evaluating expression.")[1])
expand := AhkMagic._BestStart(text, expandRefs)
if !expand
throw Error("ExpandExpression not found")
AhkMagic.exprToPostfix := AhkMagic.moduleBase + postfix2
AhkMagic.expandSingleArg := AhkMagic.moduleBase + expand
AhkMagic.currLineSlot := AhkMagic._CurrLineSlotAddr()
if !AhkMagic.internalLocator
AhkMagic.internalLocator := MCode(MC_INTERNAL_LOCATOR_X64)
locOut := Buffer(64, 0)
rc := DllCall(
AhkMagic.internalLocator.Ptr,
"Ptr", AhkMagic.moduleBase,
"UInt64", text["rva"],
"UInt64", text["size"],
"UInt64", postfix2,
"UInt64", expand,
"Ptr", locOut.Ptr,
"Int"
)
if rc != 0
throw Error("internal locator failed with rc=" rc)
AhkMagic.gScript := NumGet(locOut, 0, "Ptr")
AhkMagic.finalizeExpr := NumGet(locOut, 8, "Ptr")
AhkMagic.findOrAddVar := NumGet(locOut, 16, "Ptr")
AhkMagic.crtFree := NumGet(locOut, 24, "Ptr")
AhkMagic.symInvalid := NumGet(locOut, 32, "UInt")
AhkMagic.internalLocated := true
}
static _LocateCrtFree() {
secs := AhkMagic._ModuleSections()
text := AhkMagic._TextSection(secs)
postfixRva := AhkMagic.exprToPostfix - AhkMagic.moduleBase
callers := AhkMagic._FindCallers(text, postfixRva)
for callerRva in callers {
off := callerRva - text["rva"] + 5
p := text["ptr"] + off
loop 96 {
i := A_Index - 1
if NumGet(p + i, "UChar") != 0xE8
continue
disp := NumGet(p + i + 1, "Int")
target := text["rva"] + off + i + 5 + disp
if target != postfixRva
return AhkMagic.moduleBase + target
}
}
throw Error("CRT free not found")
}
static _LocatePreprocessFunc(sec) {
p := sec["ptr"]
size := sec["size"]
count := size - 8
loop count {
i := A_Index - 1
if NumGet(p + i, "UChar") != 0x41
or NumGet(p + i + 1, "UChar") != 0x0F
or NumGet(p + i + 2, "UChar") != 0xB6
continue
m := NumGet(p + i + 3, "UChar")
if m != 0x76 and m != 0x7E
continue
if NumGet(p + i + 4, "UChar") != 0x23
continue
limit := Min(size - 4, i + 0x300)
j := i + 5
while j < limit {
if NumGet(p + j, "UChar") = 0x80
and NumGet(p + j + 1, "UChar") = 0x78
and NumGet(p + j + 2, "UChar") = 0x23
and NumGet(p + j + 3, "UChar") = 0x02
return AhkMagic._FnStart(sec, sec["rva"] + i)
j += 1
}
}
return 0
}
static _LocateGptr(sec, preparseRva) {
callers := AhkMagic._FindCallers(sec, preparseRva)
for caller in callers {
start := AhkMagic._FnStart(sec, caller)
p := sec["ptr"]
off := start - sec["rva"]
limit := Min(sec["size"] - 16, off + 0x4000)
i := off
while i < limit {
if NumGet(p + i, "UChar") = 0xCC
and NumGet(p + i + 1, "UChar") = 0xCC
break
if NumGet(p + i, "UChar") = 0x48
and NumGet(p + i + 1, "UChar") = 0x8B
and NumGet(p + i + 2, "UChar") = 0x05 {
j := i + 7
while j < Min(i + 24, limit) {
b0 := NumGet(p + j, "UChar")
b1 := NumGet(p + j + 1, "UChar")
b2 := NumGet(p + j + 2, "UChar")
b3 := NumGet(p + j + 3, "UChar")
if b0 = 0x48
and b1 = 0x89
and (b2 = 0x58 or b2 = 0x50)
and (b3 = 0x28 or b3 = 0x50) {
disp := NumGet(p + i + 3, "Int")
return sec["rva"] + i + 7 + disp
}
j += 1
}
}
i += 1
}
}
return 0
}
static _LocateLoadTs(sec, openRva) {
callers := AhkMagic._FindCallers(sec, openRva)
for caller in callers {
p := sec["ptr"]
base := sec["rva"]
off := caller - base + 5
limit := Min(sec["size"] - 5, off + 0x300)
i := off
while i < limit {
isCmp := NumGet(p + i, "UChar") = 0x83
and NumGet(p + i + 1, "UChar") = 0xF8
and NumGet(p + i + 2, "UChar") = 0x03
isCmp2 := NumGet(p + i, "UChar") = 0x3D
and NumGet(p + i + 1, "UChar") = 0x03
if isCmp or isCmp2 {
j := i + 3
callLimit := Min(sec["size"] - 5, j + 0x200)
while j < callLimit {
if NumGet(p + j, "UChar") = 0xE8 {
disp := NumGet(p + j + 1, "Int")
target := base + j + 5 + disp
if AhkMagic._PdataEntryStart(sec["pdata"], target) = target
return target
}
j += 1
}
break
}
i += 1
}
}
return 0
}
static _LocateOpenInclude(secs) {
text := AhkMagic._TextSection(secs)
anchors := [
'%s file "%s" cannot be opened',
'#Include'
]
for anchor in anchors {
hit := AhkMagic._FindUtf16(secs, anchor)
if !hit.Length
continue
refs := AhkMagic._RipRefs(text, hit[1])
start := AhkMagic._BestStart(text, refs)
if start
return start
}
return 0
}
static _LocateSrcCount(sec, openRva) {
callers := AhkMagic._FindCallers(sec, openRva)
for caller in callers {
p := sec["ptr"]
base := sec["rva"]
off := caller - base
i := off - 5
min := Max(0, off - 0x100)
while i >= min {
b := NumGet(p + i, "UChar")
if b = 0x8B
and (NumGet(p + i + 1, "UChar") = 0x2D
or NumGet(p + i + 1, "UChar") = 0x1D
or NumGet(p + i + 1, "UChar") = 0x35
or NumGet(p + i + 1, "UChar") = 0x3D) {
disp := NumGet(p + i + 2, "Int")
target := base + i + 6 + disp
if target > 0x10000 {
val := NumGet(AhkMagic.moduleBase + target, "Int")
if val >= 0 and val < 100000
return target
}
}
if b = 0x44
and NumGet(p + i + 1, "UChar") = 0x8B
and (NumGet(p + i + 2, "UChar") = 0x2D
or NumGet(p + i + 2, "UChar") = 0x3D) {
disp := NumGet(p + i + 3, "Int")
target := base + i + 7 + disp
if target > 0x10000 {
val := NumGet(AhkMagic.moduleBase + target, "Int")
if val >= 0 and val < 100000
return target
}
}
i -= 1
}
}
return 0
}
static _LocateEvalScriptFunctions() {
if AhkMagic.evalScriptLocated
return
AhkMagic._LocateInternalFunctions()
secs := AhkMagic._ModuleSections()
text := AhkMagic._TextSection(secs)
postfixRva := AhkMagic.exprToPostfix - AhkMagic.moduleBase
callers := AhkMagic._FindCallers(text, postfixRva)
if !callers.Length
throw Error("PreparseExpressions not found")
preparse := AhkMagic._FnStart(text, callers[1])
preprocess := AhkMagic._LocatePreprocessFunc(text)
if !preprocess
throw Error("PreprocessLocalVars not found")
open := AhkMagic._LocateOpenInclude(secs)
if !open
throw Error("OpenIncludedFile not found")
loadTs := AhkMagic._LocateLoadTs(text, open)
if !loadTs
throw Error("LoadIncludedFile(TextStream) not found")
srcCount := 0
if !RegExMatch(A_AhkVersion, "^2\.1")
srcCount := AhkMagic._LocateSrcCount(text, open)
gptr := AhkMagic._LocateGptr(text, preparse)
if !gptr
throw Error("g pointer not found")
AhkMagic.evalPreparse := AhkMagic.moduleBase + preparse
AhkMagic.evalPreprocess := AhkMagic.moduleBase + preprocess
AhkMagic.evalOpenInclude := AhkMagic.moduleBase + open
AhkMagic.evalLoadTs := AhkMagic.moduleBase + loadTs
AhkMagic.evalSrcCount := srcCount ? AhkMagic.moduleBase + srcCount : 0
AhkMagic.evalGptr := AhkMagic.moduleBase + gptr
AhkMagic.evalScriptLocated := true
}
static _EvalStructOffsets() {
if !AhkMagic.evalStructOffsets
AhkMagic._DiscoverEvalLayout()
return AhkMagic.evalStructOffsets
}
; Assemble the 21-slot layout array consumed by the eval machine-code
; blob. Every slot comes from runtime discovery; slots with no
; discovered equivalent are derived from a discovered anchor and
; validated by _ArgShapeInProc/_RemoteArgShape.
static _BuildEvalLayout(structOffsets, shape) {
layout := Buffer(21 * 8, 0)
put := (slot, val) => NumPut("UInt64", val, layout, slot * 8)
put(0, structOffsets["line_action"])
put(1, structOffsets["line_argc"])
put(2, 0) ; line number: never written, purely cosmetic in errors
put(3, structOffsets["line_arg"])
put(4, shape["type"])
put(5, structOffsets["arg_expression"])
put(6, shape["len"])
put(7, shape["text"])
put(8, shape["deref"])
put(9, structOffsets["arg_postfix"])
put(10, shape["max_stack"])
put(11, shape["max_alloc"])
put(12, structOffsets["token_value"])
put(13, structOffsets["token_symbol"])
put(14, structOffsets["token_stride"])
put(15, structOffsets["token_usage"])
put(16, structOffsets["deref_marker"])
put(17, structOffsets["deref_var"])
put(18, structOffsets["deref_type"])
put(19, structOffsets["deref_len"])
put(20, Max(24, structOffsets["deref_len"] + 4
, structOffsets["deref_marker"] + 8
, structOffsets["deref_var"] + 8))
return layout
}
; Fallback layout for remote evaluation when the target layout has not
; been discovered yet. The blob validates the assumed token layout
; before its first write and reports status 9 on a mismatch, so these
; defaults can never corrupt a target with a different layout.
static _DefaultEvalLayout() {
layout := Buffer(21 * 8, 0)
vals := [0, 1, 0, 8, 0, 1, 4, 8, 16, 24, 32, 36
, 0, 16, 24, 8, 0, 8, 16, 20, 24]
loop 21
NumPut("UInt64", vals[A_Index], layout, (A_Index - 1) * 8)
return layout
}
; Derive the ArgStruct fields from the discovered arg_postfix anchor and
; validate them against the live arg. text=postfix-16, len=postfix-12,
; deref=postfix-8, type=isExpr-1, max_stack/max_alloc=postfix+8/+12.
; Every derived field must be self-consistent on the live arg or
; discovery fails loudly.
static _ArgShapeInProc(argPtr, postfixOff, isexprOff) {
typeOff := isexprOff - 1
if typeOff < 0 or NumGet(argPtr, typeOff, "UChar") != 0
throw Error("arg type field validation failed", -1)
textOff := postfixOff - 16
lenOff := postfixOff - 20
derefOff := postfixOff - 8
if textOff < 0 or lenOff < 0 or derefOff < 0
throw Error("arg field derivation out of range", -1)
textPtr := NumGet(argPtr, textOff, "Ptr")
if !AhkMagic._Readable(textPtr, 8)
throw Error("arg text pointer unreadable", -1)
alen := NumGet(argPtr, lenOff, "UInt")
if alen = 0 or alen > 4096
throw Error("arg length field implausible: " alen, -1)
if StrLen(StrGet(textPtr, alen, "UTF-16")) != alen
throw Error("arg text/length mismatch", -1)
dref := NumGet(argPtr, derefOff, "Ptr")
if dref and !AhkMagic._Readable(dref, 24)
throw Error("arg deref pointer unreadable", -1)
return Map(
"type", typeOff,
"len", lenOff,
"text", textOff,
"deref", derefOff,
"max_stack", postfixOff + 8,
"max_alloc", postfixOff + 12)
}
; Remote mirror of _ArgShapeInProc: find a live expression arg among the
; discovered functions and validate the derived field offsets by content.
static _RemoteArgShape(h, layout, s) {
for q in layout["funcs"] {
try
jump := AhkMagic._RPtr(h, q + layout["mjump_line_off"])
catch
continue
if !jump
continue
try
argc := NumGet(AhkMagic._RemoteRead(h, jump + s["line_argc"], 1)
, 0, "UChar")
catch
continue
if !argc
continue
try
argPtr := AhkMagic._RPtr(h, jump + s["line_arg"])
catch
continue
try
postfix := AhkMagic._RPtr(h, argPtr + s["arg_postfix"])
catch
continue
if !postfix
continue
try {
typeOff := s["arg_expression"] - 1
if typeOff < 0
continue
if NumGet(AhkMagic._RemoteRead(h, argPtr + typeOff, 1)
, 0, "UChar") != 0
continue
textOff := s["arg_postfix"] - 16
lenOff := s["arg_postfix"] - 20
derefOff := s["arg_postfix"] - 8
if textOff < 0 or lenOff < 0 or derefOff < 0
continue
textPtr := AhkMagic._RPtr(h, argPtr + textOff)
alen := NumGet(AhkMagic._RemoteRead(h, argPtr + lenOff, 4)
, 0, "UInt")
if alen = 0 or alen > 4096
continue
if StrLen(AhkMagic._RemoteReadString(h, textPtr, alen))
!= alen
continue
dref := AhkMagic._RPtr(h, argPtr + derefOff)
if dref
AhkMagic._RemoteRead(h, dref, 24)
return Map(
"type", typeOff,
"len", lenOff,
"text", textOff,
"deref", derefOff,
"max_stack", s["arg_postfix"] + 8,
"max_alloc", s["arg_postfix"] + 12)
} catch
continue
}
throw Error("remote arg shape not found", -1)
}
static _DiscoverCurrOff() {
if AhkMagic.evalCurrOff
return AhkMagic.evalCurrOff
secs := AhkMagic._ModuleSections()
text := AhkMagic._TextSection(secs)
preparseRva := AhkMagic.evalPreparse - AhkMagic.moduleBase
callers := AhkMagic._FindCallers(text, preparseRva)
for caller in callers {
p := text["ptr"]
base := text["rva"]
off := AhkMagic._FnStart(text, caller) - base
limit := Min(text["size"] - 16, off + 0x4000)
i := off
while i < limit {
if NumGet(p + i, "UChar") = 0xCC
and NumGet(p + i + 1, "UChar") = 0xCC
break
if NumGet(p + i, "UChar") = 0x48
and NumGet(p + i + 1, "UChar") = 0x8B
and NumGet(p + i + 2, "UChar") = 0x05 {
j := i + 7
while j < Min(i + 24, limit) {
if NumGet(p + j, "UChar") = 0x48
and (NumGet(p + j + 1, "UChar") = 0x89)
and (NumGet(p + j + 2, "UChar") = 0x58
or NumGet(p + j + 2, "UChar") = 0x50) {
AhkMagic.evalCurrOff := NumGet(p + j + 3, "UChar")
return AhkMagic.evalCurrOff
}
j += 1
}
}
i += 1
}
}
throw Error("g->curr offset not found")
}
static _DiscoverEvalLayout() {
if AhkMagic.evalLayoutLocated
return
AhkMagic._LocateEvalScriptFunctions()
AhkMagic._DiscoverCurrOff()
gScript := AhkMagic.gScript
g := NumGet(AhkMagic.evalGptr, "Ptr")
savedProbeCur := NumGet(g, AhkMagic.evalCurrOff, "Ptr")
gSnap := Buffer(0x100)
DllCall("RtlMoveMemory", "Ptr", gSnap.Ptr, "Ptr", g, "UPtr", 0x100)
NumPut("Ptr", 0, g, AhkMagic.evalCurrOff)
snap := Buffer(0x200)
DllCall("RtlMoveMemory", "Ptr", snap.Ptr, "Ptr", gScript, "UPtr", 0x200)
oldSrcCount := AhkMagic.evalSrcCount ? NumGet(AhkMagic.evalSrcCount, "Int") : 0
; The probe function name is randomized so it can never collide with
; a function the host script already defines.
probeName := "ahkHackLayoutProbe" Format("{:x}", A_TickCount)
probe := probeName "() {`n return A_Args.Length + 1`n}`n"
; The probe really loads a function and mutates interpreter state.
; Every failure below must restore the snapshot, otherwise the host
; interpreter is left with a stray function, a cleared g->curr and a
; modified source count.
try {
AhkMagic._LoadScriptMemory(probe)
countOff := 0
loop 0x200 // 4 {
off := (A_Index - 1) * 4
before := NumGet(snap, off, "Int")
after := NumGet(gScript, off, "Int")
if after = before + 1 and before >= 0 and after > 0 {
countOff := off
break
}
}
if !countOff
throw Error("probe load did not register a function: "
. "TextStream layout validation failed (mFuncsCount "
. "offset not found after rc=1 load)", -1)
oldCount := NumGet(snap, countOff, "Int")
funcsOff := 0
loop 0x200 // 8 {
off := (A_Index - 1) * 8
; gScript[0] is the variable array on every known build, and
; funcsOff=0 is also the "not found" sentinel below, so it
; must never participate in the match.
if off = 0
continue
p := NumGet(gScript, off, "Ptr")
if !(p > 0x10000 and p < 0x7fffffffffff)
continue
if DllCall("IsBadReadPtr", "Ptr", p, "UPtr", 0x100)
continue
newFunc := NumGet(p, oldCount * 8, "Ptr")
; The new entry plus its two predecessors must all look like
; 8-aligned objects: unrelated heap garbage adjacent to other
; arrays must not impersonate mFuncs.
ok := newFunc > 0x10000 and newFunc < 0x7fffffffffff
and Mod(newFunc, 8) = 0
if ok and oldCount >= 2 {
prev := NumGet(p, (oldCount - 1) * 8, "Ptr")
prev2 := NumGet(p, (oldCount - 2) * 8, "Ptr")
ok := prev > 0x10000 and prev < 0x7fffffffffff
and Mod(prev, 8) = 0
and prev2 > 0x10000 and prev2 < 0x7fffffffffff
and Mod(prev2, 8) = 0
}
if ok {
funcsOff := off
break
}
}
if !funcsOff
throw Error("mFuncs offset not found")
lastOff := -1
loop 0x200 // 8 {
off := (A_Index - 1) * 8
if off = funcsOff
continue
before := NumGet(snap, off, "Ptr")
after := NumGet(gScript, off, "Ptr")
if before != after and after > 0x10000 {
lastOff := off
break
}
}
if lastOff < 0 {
detail := "count=" countOff " funcs=" funcsOff
loop 0x200 // 8 {
off := (A_Index - 1) * 8
b := NumGet(snap, off, "Ptr")
a := NumGet(gScript, off, "Ptr")
if b != a
detail .= " @" Format("{:X}", off) " " b "->" a
. " bad=" DllCall("IsBadReadPtr", "Ptr", a, "UPtr", 0x40)
}
throw Error("mLastLine offset not found: " detail)
}
arrPtr := NumGet(gScript, funcsOff, "Ptr")
newFunc := NumGet(arrPtr, oldCount * 8, "Ptr")
oldLast := NumGet(snap, lastOff, "Ptr")
newLast := NumGet(gScript, lastOff, "Ptr")
jumpOff := 0
jumpLine := 0
loop 0x200 // 8 {
off := (A_Index - 1) * 8
p := NumGet(newFunc, off, "Ptr")
if p <= 0x10000 or p >= 0x7fffffffffff
continue
try {
lineData := Buffer(0x100)
DllCall("RtlMoveMemory", "Ptr", lineData.Ptr, "Ptr", p
, "UPtr", 0x100)
} catch
continue
found := false
loop lineData.Size // 8 {
if NumGet(lineData, (A_Index - 1) * 8, "Ptr") = newFunc {
found := true
break
}
}
if found {
jumpOff := off
jumpLine := p
break
}
}
if !jumpOff
throw Error("mJumpLine offset not found")
structOffsets := AhkMagic._DiscoverInProcStructs(
gScript, arrPtr, oldCount, newFunc, jumpOff)
; Derive and validate the remaining ArgStruct fields from the
; probe's own expression arg, then assemble the layout array the
; eval blob consumes.
probeArg := NumGet(jumpLine, structOffsets["line_arg"], "Ptr")
if !probeArg
throw Error("probe arg not found", -1)
argShape := AhkMagic._ArgShapeInProc(probeArg
, structOffsets["arg_postfix"]
, structOffsets["arg_expression"])
AhkMagic.evalStructOffsets := structOffsets
AhkMagic.evalLayoutArr := AhkMagic._BuildEvalLayout(
structOffsets, argShape)
AhkMagic.evalMFuncsOff := funcsOff
AhkMagic.evalMFuncsCountOff := countOff
AhkMagic.evalMLastLineOff := lastOff
AhkMagic.evalMJumpLineOff := jumpOff
} finally {
; Always roll the interpreter back to the pre-probe snapshot,
; whether discovery succeeded or not.
DllCall("RtlMoveMemory", "Ptr", gScript, "Ptr", snap.Ptr, "UPtr", 0x200)
if AhkMagic.evalSrcCount
NumPut("Int", oldSrcCount, AhkMagic.evalSrcCount)
DllCall("RtlMoveMemory", "Ptr", g, "Ptr", gSnap.Ptr, "UPtr", 0x100)
NumPut("Ptr", savedProbeCur, g, AhkMagic.evalCurrOff)
}
AhkMagic._DiscoverParserOffsets()
AhkMagic.evalLayoutLocated := true
}
; Discover the parser-state field offsets in THIS process. Both known
; region candidates (sentinel-anchored and qCmp-anchored) are built and
; validated against the live Script object; the strictly-validating
; candidate wins. No version family is assumed.
static _DiscoverParserOffsets() {
if AhkMagic.evalParserLocated
return
secs := AhkMagic._ModuleSections()
text := AhkMagic._TextSection(secs)
p := text["ptr"]
base := text["rva"]
off := AhkMagic.evalLoadTs - AhkMagic.moduleBase - base
limit := Min(text["size"] - 8, off + 0x100)
qCmp := -1
dCmp := -1
i := off
while i < limit {
b0 := NumGet(p + i, "UChar")
if b0 = 0x48 and NumGet(p + i + 1, "UChar") = 0x83 {
modrm := NumGet(p + i + 2, "UChar")
if modrm = 0x79 and NumGet(p + i + 4, "UChar") = 0 {
if qCmp < 0
qCmp := NumGet(p + i + 3, "UChar")
} else if modrm = 0xB9 and NumGet(p + i + 7, "UChar") = 0 {
if qCmp < 0
qCmp := NumGet(p + i + 3, "Int")
}
}
if b0 = 0x83 and NumGet(p + i + 1, "UChar") = 0xB9
and (i = off or NumGet(p + i - 1, "UChar") != 0x48)
and NumGet(p + i + 6, "UChar") = 0 {
if dCmp < 0
dCmp := NumGet(p + i + 2, "Int")
}
if qCmp >= 0 and dCmp >= 0
break
i += 1
}
if qCmp < 0 or dCmp < 0
throw Error("parser state anchors not found")
; Validate both candidates against the live Script object and pick
; the strictly-validating one.
gScript := AhkMagic.gScript
buf := Buffer(0x800)
DllCall("RtlMoveMemory", "Ptr", buf.Ptr, "Ptr", gScript, "UPtr", 0x800)
candidates := []
if dCmp > 0 and dCmp + 4 <= buf.Size
and NumGet(buf, dCmp, "Int") >= 0
and NumGet(buf, dCmp, "Int") <= 100000 {
exprIndexOff := 0
loop Min(0x40, dCmp) // 4 {
coff := dCmp - (A_Index - 1) * 4
if coff + 8 > buf.Size
continue
if NumGet(buf, coff, "Int") = 0x7fffffff
and NumGet(buf, coff + 4, "Int") = 0 {
exprIndexOff := coff + 4
break
}
}
if exprIndexOff and exprIndexOff + 8 <= buf.Size
candidates.Push(Map(
"rich", true,
"fields", Map(
"mclass_count", dCmp,
"mline_parent", exprIndexOff - 0x28,
"mpending_related", exprIndexOff - 0x20,
"mlast_param_init", exprIndexOff - 0x18,
"mpending_hotkey", exprIndexOff - 0x10,
"mexpr_func", exprIndexOff - 8,
"mexpr_func_index", exprIndexOff,
"mnext_func_body", exprIndexOff + 4,
"mignore_block", exprIndexOff + 5,
"mbackcompat", exprIndexOff + 6,
"mcurrent_module", exprIndexOff - 0x48,
"mlast_module", exprIndexOff - 0x40)))
}
if qCmp > 0 and qCmp + 0x28 <= buf.Size
candidates.Push(Map(
"fields", Map(
"mopen", qCmp,
"mpending_parent", qCmp + 8,
"mpending_related", qCmp + 16,
"mlast_param_init", qCmp + 24,
"mnext_func_body", qCmp + 32,
"mclass_count", dCmp)))
best := 0
bestScore := -1
for cand in candidates {
fields := cand["fields"]
valid := true
for key, foff in fields {
; Value-level checks are deliberately absent: the parser
; fields are saved and restored verbatim, so any plausible
; offset round-trips harmlessly. The only hard requirement
; is that every offset stays inside the Script object.
if !(foff is Integer) or foff <= 0 or foff >= 0x800
or foff + 8 > buf.Size {
valid := false
break
}
}
if valid {
; The sentinel-anchored candidate has the richer field set
; and only exists when its anchor was actually found.
score := cand.Has("rich") ? 10 : 0
if score > bestScore {
bestScore := score
best := cand["fields"]
}
}
}
if !best
throw Error("parser state layout candidates all failed validation"
, -1)
AhkMagic.evalParser := best
AhkMagic.evalParserLocated := true
}
static EvalNative(expr) {
AhkMagic.Init()
if !(expr is String)
throw TypeError("expr must be a string", -1)
if StrLen(expr) > 4096
throw ValueError("expr too long", -1)
if !AhkMagic.inprocEval
AhkMagic.inprocEval := MCode(MC_INPROC_EVAL_X64)
AhkMagic._LocateInternalFunctions()
; The eval blob takes every Line/ArgStruct/token/deref offset from a
; runtime-discovered layout array; discover it once and reuse.
AhkMagic._DiscoverEvalLayout()
layoutArr := AhkMagic.evalLayoutArr
if !layoutArr
throw Error("eval layout not discovered", -1)
scratch := Buffer(8 * 1024 * 1024, 0)
out := Buffer(512, 0)
exprBuf := Buffer((StrLen(expr) + 1) * 2, 0)
StrPut(expr, exprBuf, "UTF-16")
rc := DllCall(
AhkMagic.inprocEval.Ptr,
"Ptr", AhkMagic.exprToPostfix,
"Ptr", AhkMagic.expandSingleArg,
"Ptr", AhkMagic.currLineSlot,
"Ptr", scratch.Ptr,
"Ptr", exprBuf.Ptr,
"Ptr", out.Ptr,
"Int", 1,
"Ptr", 0,
"Ptr", 0,
"Ptr", AhkMagic.gScript,
"Ptr", AhkMagic.finalizeExpr,
"Ptr", AhkMagic.findOrAddVar,
"Ptr", AhkMagic.crtFree,
"Ptr", AhkMagic.symInvalid,
"Ptr", layoutArr.Ptr,
"Int"
)
if rc != 0
throw Error("EvalNative harness failed with rc=" rc)
status := NumGet(out, 0, "UInt")
if status = 9
throw Error("EvalNative token layout mismatch; "
. "unsupported runtime layout", -1)
if status = 10
throw Error("EvalNative layout validation failed", -1)
if status != 0
throw Error("EvalNative failed with status=" status)
type := NumGet(out, 4, "UInt")
if type = 1
return NumGet(out, 8, "Int64")
if type = 2
return NumGet(out, 8, "Double")
if type = 0
return StrGet(NumGet(out, 16, "Ptr"), "UTF-16")
if type = 5 {
; SYM_OBJECT: wrap the interpreter's live object. The eval
; harness never finalizes its result token, so the object stays
; alive; the reference returned here belongs to the caller.
ptr := NumGet(out, 8, "Ptr")
if !ptr
throw Error("EvalNative returned a null object", -1)
return ObjFromPtrAddRef(ptr)
}
throw Error("EvalNative returned unknown symbol " type)
}
static Summary() {
AhkMagic.Init()
return Format(
"module=0x{:X}`nbif={} @0x{:X} (stride 0x{:X})`n"
. "mdfunc={} @0x{:X} (stride 0x{:X})`n"
. "biv={} @0x{:X} (stride 0x{:X})",
AhkMagic.moduleBase,
AhkMagic.bifCount, AhkMagic.bifTablePtr, AhkMagic.bifStride,
AhkMagic.mdfuncCount, AhkMagic.mdfuncTablePtr, AhkMagic.mdfuncStride,
AhkMagic.bivCount, AhkMagic.bivTablePtr, AhkMagic.bivStride
)
}
static Eval(expr) {
AhkMagic.Init()
if !(expr is String)
throw TypeError("expr must be a string", -1)
if expr = ""
throw ValueError("expr must not be empty", -1)
if RegExMatch(expr, "[\r\n]")
return AhkMagic.EvalScript(expr)
try {
return AhkMagic.EvalNative(expr)
} catch as e {
; Expressions the in-process pipeline cannot evaluate yet fall
; back to the explicit subprocess implementation.
}
return AhkMagic.EvalSubprocess(expr)
}
static EvalScript(text) {
AhkMagic.Init()
if !(text is String)
throw TypeError("text must be a string", -1)
if Trim(text) = ""
throw ValueError("text must not be empty", -1)
; Pre-check with the interpreter's own /validate switch so a syntax
; error becomes a clean library error carrying the real parse
; message. The validator captures its output and kills its process
; tree on timeout, so it never leaves a dialog on screen; the loader
; rc check below remains as the fallback guard. A compiled host has
; no separate interpreter binary, so it keeps the fallback only.
if !A_IsCompiled
AhkMagic._ValidateScript(text, A_AhkPath)
AhkMagic._DiscoverEvalLayout()
off := AhkMagic._EvalStructOffsets()
currOff := AhkMagic.evalCurrOff
mfuncsOff := AhkMagic.evalMFuncsOff
mfuncsCountOff := AhkMagic.evalMFuncsCountOff
mlastLineOff := AhkMagic.evalMLastLineOff
mjumpLineOff := AhkMagic.evalMJumpLineOff
last := ""
for line in StrSplit(text, "`n", "`r") {
t := Trim(line)
if t = ""
continue
if RegExMatch(t, "^(if|else|for|while|loop|try|catch|finally|return|break|continue|class|static|global|local|throw)\b")
continue
if SubStr(t, -1) = "{"
continue
last := t
}
if last = ""
throw ValueError("no expression result found in script text", -1)
gScript := AhkMagic.gScript
oldLast := NumGet(gScript, mlastLineOff, "Ptr")
oldFuncCount := NumGet(gScript, mfuncsCountOff, "Int")
g := NumGet(AhkMagic.evalGptr, "Ptr")
savedCur := NumGet(g, currOff, "Ptr")
parser := AhkMagic.evalParser
savedState := []
for key in ["mopen", "mpending_parent", "mline_parent", "mpending_related"
, "mlast_param_init", "mpending_hotkey", "mexpr_func", "mcurrent_module"] {
if parser.Has(key)
savedState.Push([key, "Ptr", NumGet(gScript, parser[key], "Ptr")])
}
for key in ["mexpr_func_index", "mclass_count"] {
if parser.Has(key)
savedState.Push([key, "Int", NumGet(gScript, parser[key], "Int")])
}
for key in ["mnext_func_body", "mignore_block", "mbackcompat"] {
if parser.Has(key)
savedState.Push([key, "UChar", NumGet(gScript, parser[key], "UChar")])
}
try {
NumPut("Ptr", 0, g, currOff)
if !AhkMagic.memScriptLoader
AhkMagic.memScriptLoader := MCode(MC_MEM_SCRIPT_X64)
textBuf := Buffer((StrLen(text) + 1) * 2, 0)
StrPut(text, textBuf, "UTF-16")
scratch := Buffer(0x400, 0)
rc := DllCall(AhkMagic.memScriptLoader.Ptr
, "Ptr", AhkMagic.evalLoadTs
, "Ptr", gScript
, "Ptr", AhkMagic.evalSrcCount
, "Ptr", textBuf.Ptr
, "UInt", StrLen(text) * 2
, "Ptr", scratch.Ptr
, "Int")
if rc != 0
throw Error("LoadIncludedFile(memory) failed with rc=" rc)
funcsItem := NumGet(gScript, mfuncsOff, "Ptr")
funcCount := NumGet(gScript, mfuncsCountOff, "Int")
; The count drives raw pointer walks below; refuse values that
; cannot come from one script injection.
if funcCount < oldFuncCount
or funcCount > oldFuncCount + 4096
throw Error("mFuncs count changed unexpectedly: "
. oldFuncCount " -> " funcCount, -1)
if funcCount > oldFuncCount {
firstNew := oldLast ? NumGet(oldLast, off["line_next"], "Ptr") : 0
if firstNew {
rcTail := DllCall(AhkMagic.evalPreparse
, "Ptr", gScript
, "Ptr", firstNew
, "Int")
if rcTail != 1
throw Error("PreparseExpressions(tail) failed with rc=" rcTail)
}
loop funcCount - oldFuncCount {
idx := oldFuncCount + A_Index - 1
newFunc := NumGet(funcsItem + idx * 8, "Ptr")
jump := NumGet(newFunc, mjumpLineOff, "Ptr")
if !jump
continue
rc2 := DllCall(AhkMagic.evalPreparse
, "Ptr", gScript
, "Ptr", jump
, "Int")
if rc2 != 1
throw Error("PreparseExpressions failed with rc=" rc2)
NumPut("Ptr", newFunc, g, currOff)
line := jump
lineSteps := 0
while line {
if lineSteps > 100000
throw Error("line chain too long; layout likely wrong", -1)
lineSteps += 1
if NumGet(line, off["line_action"], "UChar") = 3
and NumGet(line, off["line_attribute"], "Ptr")
NumPut("Ptr", NumGet(line, off["line_attribute"], "Ptr")
, g, currOff)
argc := NumGet(line, off["line_argc"], "UChar")
if argc {
arg := NumGet(line, off["line_arg"], "Ptr")
if arg and NumGet(arg, off["arg_expression"]
, "UChar") {
postfix := NumGet(arg, off["arg_postfix"], "Ptr")
if postfix {
tokenSteps := 0
while NumGet(postfix, off["token_symbol"], "UInt") != AhkMagic.symInvalid {
if tokenSteps > 100000
throw Error("postfix token chain too long; layout likely wrong", -1)
tokenSteps += 1
if NumGet(postfix, off["token_symbol"], "UInt") = 4
and NumGet(postfix, off["token_usage"], "UInt") < 3 {
deref := NumGet(postfix, off["token_value"], "Ptr")
if deref {
derefType := NumGet(deref
, off["deref_type"], "UChar")
marker := NumGet(deref, off["deref_marker"], "Ptr")
len := NumGet(deref, off["deref_len"], "UInt")
if derefType = 7 {
NumPut("Ptr", NumGet(deref
, off["deref_var"], "Ptr")
, postfix, off["token_value"])
} else if derefType = 0 and marker and len > 0 and len <= 64 {
var := DllCall(AhkMagic.findOrAddVar
, "Ptr", gScript
, "Ptr", marker
, "UPtr", len
, "UInt", 0x103
, "Ptr")
if var
NumPut("Ptr", var, postfix, off["token_value"])
}
}
}
postfix += off["token_stride"]
}
}
}
}
line := NumGet(line, off["line_next"], "Ptr")
}
NumPut("Ptr", savedCur, g, currOff)
rc4 := DllCall(AhkMagic.evalPreprocess
, "Ptr", gScript
, "Ptr", newFunc
, "Int")
if rc4 != 1
throw Error("PreprocessLocalVars failed with rc=" rc4)
}
}
; Detach the newly added lines from the running line list so the
; active execution cannot continue into them. The new function
; remains reachable through mFuncs. The tail was already
; preparsed through firstNew above, so detaching now is safe.
if oldLast and off.Has("line_next") and mlastLineOff >= 0 {
NumPut("Ptr", 0, oldLast, off["line_next"])
NumPut("Ptr", oldLast, gScript, mlastLineOff)
}
NumPut("Ptr", 0, g, currOff)
r := AhkMagic.EvalNative(last)
NumPut("Ptr", savedCur, g, currOff)
return r
} finally {
for item in savedState
NumPut(item[2], item[3], gScript, parser[item[1]])
NumPut("Ptr", savedCur, g, currOff)
}
}
static _LoadScriptMemory(text) {
if !AhkMagic.memScriptLoader
AhkMagic.memScriptLoader := MCode(MC_MEM_SCRIPT_X64)
gScript := AhkMagic.gScript
textBuf := Buffer((StrLen(text) + 1) * 2, 0)
StrPut(text, textBuf, "UTF-16")
scratch := Buffer(0x400, 0)
rc := DllCall(AhkMagic.memScriptLoader.Ptr
, "Ptr", AhkMagic.evalLoadTs
, "Ptr", gScript
, "Ptr", AhkMagic.evalSrcCount
, "Ptr", textBuf.Ptr
, "UInt", StrLen(text) * 2
, "Ptr", scratch.Ptr
, "Int")
if rc != 0
throw Error("LoadIncludedFile(memory) failed with rc=" rc)
}
; ------------------------------------------------------------------
; Remote attach: read the three interpreter tables from another
; AutoHotkey process without injecting anything.
; ------------------------------------------------------------------
static _RemoteOpen(pid, write := false) {
access := 0x410
if write
access := 0x410 | 0x28
h := DllCall("OpenProcess", "UInt", access, "Int", 0, "UInt", pid, "Ptr")
if !h
throw Error("OpenProcess failed", -1)
return h
}
static _RemoteModuleBase(h, pid) {
hSnap := DllCall("CreateToolhelp32Snapshot", "UInt", 0x18, "UInt", pid, "Ptr")
if hSnap = -1 or !hSnap
throw Error("CreateToolhelp32Snapshot failed", -1)
buf := Buffer(1080)
NumPut("UInt", 1080, buf, 0)
best := Map("base", 0, "path", "")
try {
if !DllCall("Module32FirstW", "Ptr", hSnap, "Ptr", buf.Ptr)
throw Error("Module32FirstW failed", -1)
loop {
modBase := NumGet(buf, 24, "Ptr")
modName := StrGet(buf.Ptr + 48, 256, "UTF-16")
modPath := StrGet(buf.Ptr + 560, 260, "UTF-16")
if !best["base"] or InStr(modName, "AutoHotkey", false) {
best := Map("base", modBase, "path", modPath)
if InStr(modName, "AutoHotkey", false)
break
}
if !DllCall("Module32NextW", "Ptr", hSnap, "Ptr", buf.Ptr)
break
}
} finally {
DllCall("CloseHandle", "Ptr", hSnap)
}
if !best["base"]
throw Error("module base not found", -1)
return best
}
static _RemotePidByName(name) {
hSnap := DllCall("CreateToolhelp32Snapshot", "UInt", 0x2, "UInt", 0, "Ptr")
if hSnap = -1 or !hSnap
throw Error("CreateToolhelp32Snapshot failed", -1)
buf := Buffer(568)
NumPut("UInt", 568, buf, 0)
pid := 0
try {
if !DllCall("Process32FirstW", "Ptr", hSnap, "Ptr", buf.Ptr)
throw Error("Process32FirstW failed", -1)
loop {
exe := StrGet(buf.Ptr + 44, 260, "UTF-16")
if StrCompare(exe, name, false) = 0 {
pid := NumGet(buf, 8, "UInt")
break
}
if !DllCall("Process32NextW", "Ptr", hSnap, "Ptr", buf.Ptr)
break
}
} finally {
DllCall("CloseHandle", "Ptr", hSnap)
}
if !pid
throw Error("process not found: " name, -1)
return pid
}
static _RemoteRead(h, addr, size) {
buf := Buffer(size)
read := 0
if !DllCall("ReadProcessMemory", "Ptr", h, "Ptr", addr, "Ptr", buf.Ptr
, "UPtr", size, "UPtr*", &read)
throw Error("ReadProcessMemory failed at 0x" Format("{:X}", addr), -1)
; A partial success silently returns a half-garbage buffer and
; corrupts every downstream parse; fail loudly instead.
if read != size
throw Error("ReadProcessMemory short read at 0x"
. Format("{:X}", addr) ": " read "/" size, -1)
return buf
}
static _RemoteSections(h, base) {
dos := AhkMagic._RemoteRead(h, base, 0x40)
e_lfanew := NumGet(dos, 0x3C, "UInt")
pe := AhkMagic._RemoteRead(h, base + e_lfanew, 0x18)
if NumGet(pe, 0, "UInt") != 0x4550
throw Error("PE signature not found", -1)
num := NumGet(pe, 6, "UShort")
optSize := NumGet(pe, 20, "UShort")
secTable := base + e_lfanew + 24 + optSize
hdrs := AhkMagic._RemoteRead(h, secTable, num * 40)
secs := Map()
hasText := false
hasData := false
hasPdata := false
loop num {
off := (A_Index - 1) * 40
name := StrGet(hdrs.Ptr + off, 8, "UTF-8")
vsize := NumGet(hdrs, off + 8, "UInt")
va := NumGet(hdrs, off + 12, "UInt")
rawSize := NumGet(hdrs, off + 16, "UInt")
size := vsize ? vsize : rawSize
if name = ".text"
hasText := true
if name = ".rdata" or name = ".data"
hasData := true
if name = ".pdata"
hasPdata := true
secs[name] := Map("name", name, "rva", va, "size", size
, "start", base + va, "end", base + va + size
, "data", Buffer(0), "ptr", 0)
}
strict := hasText and hasData
for _, sec in secs {
if strict and sec["name"] != ".text"
and sec["name"] != ".rdata" and sec["name"] != ".data"
continue
if sec["name"] = ".rsrc"
continue
if sec["size"] > 0 and sec["size"] < 0x800000 {
try
sec["data"] := AhkMagic._RemoteRead(h, sec["start"], sec["size"])
catch
sec["data"] := Buffer(0)
sec["ptr"] := sec["data"].Ptr
}
}
pdata := 0
if hasPdata {
sec := secs[".pdata"]
if sec["size"] > 0 and sec["size"] < 0x800000 {
try {
sec["data"] := AhkMagic._RemoteRead(h, sec["start"]
, sec["size"])
sec["ptr"] := sec["data"].Ptr
pdata := sec
} catch
pdata := 0
}
}
for _, sec in secs
sec["pdata"] := pdata
return secs
}
static _RemoteReadUtf16(secs, addr, maxLen := 64) {
for _, sec in secs {
if addr >= sec["start"] and addr < sec["end"]
and sec["data"].Size > 0 {
off := addr - sec["start"]
if off < sec["data"].Size
return StrGet(sec["data"].Ptr + off, maxLen, "UTF-16")
}
}
return ""
}
static _RemoteStrValid(secs, addr) {
text := AhkMagic._RemoteReadUtf16(secs, addr, 64)
if text = "" or StrLen(text) > 64
return false
return RegExMatch(text, "^[\x20-\x7E]+$") ? true : false
}
static _RemotePtrInText(secs, ptr) {
hasText := false
for _, sec in secs
if sec["name"] = ".text"
hasText := true
for _, sec in secs {
if ptr < sec["start"] or ptr >= sec["end"]
continue
if sec["name"] = ".text"
return true
if !hasText and sec["name"] != ".rsrc"
return true
}
return false
}
static _RemoteTable(h, secs, base, kind) {
if kind = "bif" {
stride := 0x20
anchor := "Abs"
} else if kind = "mdfunc" {
stride := 0x28
anchor := "BlockInput"
} else {
stride := 0x18
anchor := "AhkPath"
}
bestStart := 0
bestCount := 0
for _, sec in secs {
if sec["data"].Size < 32
continue
p := sec["data"].Ptr
maxOff := sec["data"].Size - 16
off := 0
while off <= maxOff {
namePtr := NumGet(p + off, "Ptr")
fnPtr := NumGet(p + off + 8, "Ptr")
if AhkMagic._RemoteStrValid(secs, namePtr)
and AhkMagic._RemotePtrInText(secs, fnPtr) {
name := AhkMagic._RemoteReadUtf16(secs, namePtr, 64)
if name = anchor {
count := 1
prevName := name
q := off + stride
while q + 16 <= sec["data"].Size {
qNamePtr := NumGet(p + q, "Ptr")
qFnPtr := NumGet(p + q + 8, "Ptr")
if !AhkMagic._RemoteStrValid(secs, qNamePtr)
or !AhkMagic._RemotePtrInText(secs, qFnPtr)
break
qName := AhkMagic._RemoteReadUtf16(secs, qNamePtr, 64)
if StrCompare(qName, prevName, false) < 0
break
count += 1
prevName := qName
q += stride
}
if count > bestCount {
bestCount := count
bestStart := sec["rva"] + off
}
}
}
off += 8
}
}
empty := Map("found", false, "stride", stride, "count", 0
, "entries", Map(), "names", [])
if bestCount < 10
return empty
sec := 0
for _, s in secs
if bestStart >= s["rva"] and bestStart < s["rva"] + s["size"] {
sec := s
break
}
if !sec or sec["data"].Size = 0
return empty
entries := Map()
names := []
off := bestStart - sec["rva"]
loop bestCount {
p := sec["data"].Ptr + off
namePtr := NumGet(p, "Ptr")
name := AhkMagic._RemoteReadUtf16(secs, namePtr, 64)
if kind = "bif" {
entries[name] := Map(
"rva", NumGet(p + 8, "Ptr") - base,
"min", NumGet(p + 16, "UChar"),
"max", NumGet(p + 17, "UChar"),
"fid", NumGet(p + 18, "UChar"))
} else if kind = "mdfunc" {
entries[name] := Map(
"rva", NumGet(p + 8, "Ptr") - base,
"ret", NumGet(p + 16, "UChar"))
} else {
getter := NumGet(p + 8, "Ptr")
setter := NumGet(p + 16, "Ptr")
entries[name] := Map(
"getter_rva", getter - base,
"setter_rva", setter ? setter - base : 0)
}
names.Push(name)
off += stride
}
return Map("found", true, "table_rva", bestStart, "stride", stride
, "count", bestCount, "entries", entries, "names", names)
}
static _RemoteLocateInternal(secs, base) {
text := AhkMagic._TextSection(secs)
if !text["ptr"]
throw Error("remote text section not loaded", -1)
postfixRefs := AhkMagic._RipRefs(text
, AhkMagic._FindUtf16(secs, "Missing operand.")[1])
postfix2 := AhkMagic._BestStart(text, postfixRefs)
expandRefs := AhkMagic._RipRefs(text
, AhkMagic._FindUtf16(secs, "Error evaluating expression.")[1])
expand := AhkMagic._BestStart(text, expandRefs)
if !postfix2 or !expand
throw Error("remote expression functions not found", -1)
return Map("postfix_rva", postfix2, "expand_rva", expand)
}
static _RemoteCurrLineSlot(secs, base, getterRva) {
text := AhkMagic._TextSection(secs)
p := text["ptr"]
off := getterRva - text["rva"]
if NumGet(p + off, "UChar") != 0x48
or NumGet(p + off + 1, "UChar") != 0x8B
or NumGet(p + off + 2, "UChar") != 0x05
throw Error("unexpected remote A_LineNumber getter code", -1)
disp := NumGet(p + off + 3, "Int")
return base + getterRva + 7 + disp
}
static _HexBuffer(hex) {
size := StrLen(hex) // 2
buf := Buffer(size)
loop size {
byte := Integer("0x" SubStr(hex, 2 * A_Index - 1, 2))
NumPut("UChar", byte, buf, A_Index - 1)
}
return buf
}
static _RemoteWrite(h, addr, buf) {
written := 0
if !DllCall("WriteProcessMemory", "Ptr", h, "Ptr", addr
, "Ptr", buf.Ptr, "UPtr", buf.Size, "UPtr*", &written)
throw Error("WriteProcessMemory failed", -1)
if written != buf.Size
throw Error("WriteProcessMemory short write at 0x"
. Format("{:X}", addr) ": " written "/" buf.Size, -1)
}
static _RemoteReadString(h, addr, maxLen := 4096) {
out := Buffer(0)
chunk := 256
loop {
if out.Size >= maxLen
break
try
part := AhkMagic._RemoteRead(h, addr + out.Size, chunk)
catch
break
if part.Size = 0
break
combined := Buffer(out.Size + part.Size)
if out.Size
DllCall("RtlMoveMemory", "Ptr", combined.Ptr, "Ptr", out.Ptr
, "UPtr", out.Size)
DllCall("RtlMoveMemory", "Ptr", combined.Ptr + out.Size
, "Ptr", part.Ptr, "UPtr", part.Size)
out := combined
found := -1
loop out.Size // 2 {
if NumGet(out, (A_Index - 1) * 2, "UShort") = 0 {
found := A_Index - 1
break
}
}
if found >= 0
return StrGet(out.Ptr, found, "UTF-16")
}
return StrGet(out.Ptr, "UTF-16")
}
static AttachRemote(pid) {
if !(pid is Integer)
throw TypeError("pid must be an integer", -1)
h := AhkMagic._RemoteOpen(pid, false)
try {
mod := AhkMagic._RemoteModuleBase(h, pid)
secs := AhkMagic._RemoteSections(h, mod["base"])
bif := AhkMagic._RemoteTable(h, secs, mod["base"], "bif")
mdfunc := AhkMagic._RemoteTable(h, secs, mod["base"], "mdfunc")
biv := AhkMagic._RemoteTable(h, secs, mod["base"], "biv")
internal := Map()
try {
loc := AhkMagic._RemoteLocateInternal(secs, mod["base"])
textSec := AhkMagic._TextSection(secs)
loc["text_rva"] := textSec["rva"]
loc["text_size"] := textSec["size"]
if biv["found"] and biv["entries"].Has("LineNumber") {
loc["curr_line_slot"] := AhkMagic._RemoteCurrLineSlot(
secs, mod["base"]
, biv["entries"]["LineNumber"]["getter_rva"])
internal := loc
}
} catch as e {
internal := Map("error", e.What " | " e.Message)
}
return Map(
"pid", pid,
"module", mod["path"],
"image_base", mod["base"],
"builtins", bif,
"native_functions", mdfunc,
"builtin_vars", biv,
"internal", internal
)
} finally {
DllCall("CloseHandle", "Ptr", h)
}
}
static AttachRemoteByName(name) {
if !(name is String)
throw TypeError("name must be a string", -1)
pid := AhkMagic._RemotePidByName(name)
return AhkMagic.AttachRemote(pid)
}
static RemoteRedirect(hook, name, newName) {
if !(hook is Map) or !hook.Has("pid")
throw TypeError("hook must be an AttachRemote result", -1)
bif := hook["builtins"]
if !bif["found"] or !bif["entries"].Has(name)
or !bif["entries"].Has(newName)
throw Error("builtin redirect names not found", -1)
idx := 0
for n in bif["names"] {
if n = name
break
idx += 1
}
if idx >= bif["names"].Length
throw Error("builtin index not found", -1)
h := AhkMagic._RemoteOpen(hook["pid"], true)
try {
fnSlot := hook["image_base"] + bif["table_rva"]
+ idx * bif["stride"] + 8
newPtr := hook["image_base"] + bif["entries"][newName]["rva"]
data := Buffer(8)
NumPut("Ptr", newPtr, data, 0)
written := 0
if !DllCall("WriteProcessMemory", "Ptr", h, "Ptr", fnSlot
, "Ptr", data.Ptr, "UPtr", 8, "UPtr*", &written)
throw Error("WriteProcessMemory failed", -1)
} finally {
DllCall("CloseHandle", "Ptr", h)
}
return Map("src", name, "dst", newName, "fn_slot", fnSlot)
}
static RemoteDeepRedirect(hook, name, newName) {
if !(hook is Map) or !hook.Has("pid")
throw TypeError("hook must be an AttachRemote result", -1)
bif := hook["builtins"]
if !bif["found"] or !bif["entries"].Has(name)
or !bif["entries"].Has(newName)
throw Error("builtin redirect names not found", -1)
idx := 0
for n in bif["names"] {
if n = name
break
idx += 1
}
if idx >= bif["names"].Length
throw Error("builtin index not found", -1)
h := AhkMagic._RemoteOpen(hook["pid"], true)
try {
slot := hook["image_base"] + bif["table_rva"] + idx * bif["stride"]
slotBuf := AhkMagic._RemoteRead(h, slot, 16)
namePtr := NumGet(slotBuf, 0, "Ptr")
oldFn := NumGet(slotBuf, 8, "Ptr")
newFn := hook["image_base"] + bif["entries"][newName]["rva"]
; The vtable check below needs the module image bounds; read
; SizeOfImage from the target's PE header once. It sits at
; optional-header offset 56 (i.e. 24 + 56 into the PE header).
dos := AhkMagic._RemoteRead(h, hook["image_base"], 0x40)
peOff := NumGet(dos, 0x3C, "UInt")
pe := AhkMagic._RemoteRead(h, hook["image_base"] + peOff, 0x80)
imageSize := NumGet(pe, 80, "UInt")
imageEnd := hook["image_base"] + imageSize
patches := []
addr := 0
mbi := Buffer(48)
loop {
if !DllCall("VirtualQueryEx", "Ptr", h, "Ptr", addr
, "Ptr", mbi.Ptr, "UPtr", 48)
break
state := NumGet(mbi, 32, "UInt")
protect := NumGet(mbi, 36, "UInt")
memType := NumGet(mbi, 40, "UInt")
regionSize := NumGet(mbi, 24, "Int64")
baseAddr := NumGet(mbi, 0, "Ptr")
if state = 0x1000 and memType = 0x20000
and (protect & 0x4 or protect & 0x40)
and regionSize > 0 and regionSize < 0x8000000 {
chunkSize := 0x10000
off := 0
while off < regionSize {
want := Min(chunkSize, regionSize - off)
try
data := AhkMagic._RemoteRead(h, baseAddr + off, want)
catch {
off += want
continue
}
maxOff := data.Size - 8
pos := 0
while pos <= maxOff {
if NumGet(data, pos, "Ptr") = oldFn {
ok := false
start := Max(0, pos - 0x200)
end := Min(data.Size - 8, pos + 0x200)
j := start
while j <= end {
if NumGet(data, j, "Ptr") = namePtr {
ok := true
break
}
j += 8
}
; The name window alone can false-positive on
; unrelated structs that happen to hold both
; pointers. A real Func object matches one
; of two version layouts: 2.0 puts the
; vtable pointer right before the function
; pointer; 2.1 packs mMinParams/mMaxParams
; into the qword before it.
if ok {
vtOk := false
for bc in [pos - 0x10, pos - 0x08] {
if bc < 0 or bc + 8 > data.Size
continue
vt := NumGet(data, bc, "Ptr")
if vt >= hook["image_base"]
and vt < imageEnd {
vtOk := true
break
}
}
if !vtOk and pos >= 0x10 {
minP := bif["entries"][name]["min"]
maxP := bif["entries"][name]["max"]
packed := (minP << 32) | maxP
if NumGet(data, pos - 0x10, "UInt64")
= packed
vtOk := true
}
if vtOk
patches.Push(baseAddr + off + pos)
}
}
pos += 8
}
off += want
}
}
if regionSize <= 0
break
nextAddr := baseAddr + regionSize
if nextAddr <= addr
break
addr := nextAddr
}
if !patches.Length
throw Error("no Func object found for " name, -1)
data := Buffer(8)
NumPut("Ptr", newFn, data, 0)
; Snapshot every slot before writing so a mid-loop failure can
; roll the already-patched entries back instead of leaving the
; target half-redirected.
originals := []
for p in patches
originals.Push(AhkMagic._RemoteRead(h, p, 8))
written := []
try {
loop patches.Length {
AhkMagic._RemoteWrite(h, patches[A_Index], data)
written.Push(A_Index)
}
} catch as e {
for idx in written {
try
AhkMagic._RemoteWrite(h, patches[idx], originals[idx])
catch
continue
}
throw e
}
return Map("src", name, "dst", newName, "count", patches.Length)
} finally {
DllCall("CloseHandle", "Ptr", h)
}
}
static RemoteEval(hook, expr) {
if !(hook is Map) or !hook.Has("pid") or !hook.Has("internal")
or !hook["internal"].Has("postfix_rva")
throw Error("hook has no remote eval context", -1)
if !hook["internal"].Has("curr_line_slot")
or !hook["internal"]["curr_line_slot"]
throw Error("hook has no current-line slot; attach again", -1)
if !(expr is String)
throw TypeError("expr must be a string", -1)
if StrLen(expr) > 4096
throw ValueError("expr too long", -1)
internal := hook["internal"]
stub := AhkMagic._HexBuffer(MC_REMOTE_EVAL_STUB_X64)
locator := AhkMagic._HexBuffer(MC_INTERNAL_LOCATOR_X64)
evalBlob := AhkMagic._HexBuffer(MC_INPROC_EVAL_X64)
codeSize := stub.Size + locator.Size + evalBlob.Size
paramOff := (codeSize + 15) // 16 * 16
layoutOff := paramOff + 136
locOff := ((layoutOff + 21 * 8 + 15) // 16) * 16
outOff := locOff + 64
exprLen := (StrLen(expr) + 1) * 2
exprOff := outOff + 512
scratchOff := exprOff + exprLen
total := scratchOff + 8 * 1024 * 1024
; Prefer the discovered target layout; fall back to the validated
; defaults when the hook has no layout yet (the blob aborts with a
; clear status before its first write if the assumption is wrong).
layoutBuf := 0
if hook.Has("script_layout") and hook["script_layout"].Has("struct")
and hook["script_layout"].Has("shape") {
layoutBuf := AhkMagic._BuildEvalLayout(
hook["script_layout"]["struct"]
, hook["script_layout"]["shape"])
} else {
layoutBuf := AhkMagic._DefaultEvalLayout()
}
h := AhkMagic._RemoteOpen(hook["pid"], true)
try {
block := DllCall("VirtualAllocEx", "Ptr", h, "Ptr", 0
, "UPtr", total, "UInt", 0x3000, "UInt", 0x40, "Ptr")
if !block
throw Error("VirtualAllocEx failed", -1)
threadExited := false
try {
AhkMagic._RemoteWrite(h, block, stub)
AhkMagic._RemoteWrite(h, block + stub.Size, locator)
AhkMagic._RemoteWrite(h, block + stub.Size + locator.Size, evalBlob)
AhkMagic._RemoteWrite(h, block + layoutOff, layoutBuf)
param := Buffer(136)
NumPut("Ptr", block + stub.Size, param, 0)
NumPut("Ptr", block + stub.Size + locator.Size, param, 8)
NumPut("Ptr", hook["image_base"], param, 16)
NumPut("UInt64", internal["text_rva"], param, 24)
NumPut("UInt64", internal["text_size"], param, 32)
NumPut("UInt64", internal["postfix_rva"], param, 40)
NumPut("UInt64", internal["expand_rva"], param, 48)
NumPut("Ptr", block + locOff, param, 56)
NumPut("Ptr", hook["image_base"] + internal["postfix_rva"], param, 64)
NumPut("Ptr", hook["image_base"] + internal["expand_rva"], param, 72)
NumPut("Ptr", internal["curr_line_slot"], param, 80)
NumPut("Ptr", block + scratchOff, param, 88)
NumPut("Ptr", block + exprOff, param, 96)
NumPut("Ptr", block + outOff, param, 104)
NumPut("UInt64", 0, param, 112)
NumPut("Int", 0, param, 120)
NumPut("Int", 0, param, 124)
NumPut("Ptr", block + layoutOff, param, 128)
AhkMagic._RemoteWrite(h, block + paramOff, param)
exprBuf := Buffer(exprLen)
StrPut(expr, exprBuf, "UTF-16")
AhkMagic._RemoteWrite(h, block + exprOff, exprBuf)
tid := 0
thread := DllCall("CreateRemoteThread", "Ptr", h, "Ptr", 0
, "UPtr", 0, "Ptr", block, "Ptr", block + paramOff
, "UInt", 0, "UInt*", &tid, "Ptr")
if !thread
throw Error("CreateRemoteThread failed", -1)
try {
wait := DllCall("WaitForSingleObject", "Ptr", thread
, "UInt", 10000)
if wait = 0x102 {
; The remote thread is still executing the injected
; block. Freeing it would leave the target running
; freed memory; leak the block and fail loudly.
throw Error("remote eval timed out after 10s; "
. "injected block leaked to protect the target", -1)
}
if wait = 0xFFFFFFFF
throw Error("WaitForSingleObject failed for remote eval", -1)
if wait != 0
throw Error("WaitForSingleObject returned 0x"
. Format("{:X}", wait) " for remote eval", -1)
threadExited := true
paramBack := AhkMagic._RemoteRead(h, block + paramOff, 136)
locRc := NumGet(paramBack, 120, "Int")
evalRc := NumGet(paramBack, 124, "Int")
if locRc != 0
throw Error("remote internal locator rc=" locRc, -1)
if evalRc != 0
throw Error("remote eval rc=" evalRc, -1)
out := AhkMagic._RemoteRead(h, block + outOff, 512)
status := NumGet(out, 0, "UInt")
type := NumGet(out, 4, "UInt")
if status != 0
throw Error("remote eval status=" status, -1)
if type = 1
return NumGet(out, 8, "Int64")
if type = 2
return NumGet(out, 8, "Double")
if type = 0 {
ptr := NumGet(out, 16, "Ptr")
return ptr ? AhkMagic._RemoteReadString(h, ptr) : ""
}
if type = 5 {
; Objects cannot cross process boundaries; return the
; object's address in the target so callers can use
; it with the remote primitives.
ptr := NumGet(out, 8, "Ptr")
if !ptr
throw Error("remote eval returned a null object", -1)
return ptr
}
throw Error("remote eval returned unknown type " type, -1)
} finally {
DllCall("CloseHandle", "Ptr", thread)
}
} finally {
; Only free the block once the remote thread has exited.
; On timeout the block is deliberately leaked (see above).
if threadExited
DllCall("VirtualFreeEx", "Ptr", h, "Ptr", block, "UPtr", 0
, "UInt", 0x8000)
}
} finally {
DllCall("CloseHandle", "Ptr", h)
}
}
static _RemoteCall(h, fnAddr, args) {
stub := AhkMagic._HexBuffer(MC_REMOTE_CALL_STUB_X64)
param := Buffer(72)
NumPut("Ptr", fnAddr, param, 0)
loop 6 {
val := args.Has(A_Index) ? args[A_Index] : 0
NumPut("Ptr", val, param, 8 + 8 * (A_Index - 1))
}
NumPut("Int", 0, param, 56)
block := DllCall("VirtualAllocEx", "Ptr", h, "Ptr", 0
, "UPtr", stub.Size + 72, "UInt", 0x3000, "UInt", 0x40, "Ptr")
if !block
throw Error("VirtualAllocEx failed", -1)
threadExited := false
try {
AhkMagic._RemoteWrite(h, block, stub)
AhkMagic._RemoteWrite(h, block + stub.Size, param)
tid := 0
thread := DllCall("CreateRemoteThread", "Ptr", h, "Ptr", 0
, "UPtr", 0, "Ptr", block, "Ptr", block + stub.Size
, "UInt", 0, "UInt*", &tid, "Ptr")
if !thread
throw Error("CreateRemoteThread failed", -1)
try {
wait := DllCall("WaitForSingleObject", "Ptr", thread
, "UInt", 10000)
if wait = 0x102 {
; Thread still executing the injected block; freeing it
; would corrupt the target, so leak it and fail loudly.
throw Error("remote call timed out after 10s; "
. "injected block leaked to protect the target", -1)
}
if wait = 0xFFFFFFFF
throw Error("WaitForSingleObject failed for remote call", -1)
if wait != 0
throw Error("WaitForSingleObject returned 0x"
. Format("{:X}", wait) " for remote call", -1)
threadExited := true
back := AhkMagic._RemoteRead(h, block + stub.Size, 72)
return NumGet(back, 56, "Int")
} finally {
DllCall("CloseHandle", "Ptr", thread)
}
} finally {
if threadExited
DllCall("VirtualFreeEx", "Ptr", h, "Ptr", block, "UPtr", 0
, "UInt", 0x8000)
}
}
static _RemoteInternalLocator(h, secs, base, postfixRva, expandRva) {
text := AhkMagic._TextSection(secs)
locator := AhkMagic._HexBuffer(MC_INTERNAL_LOCATOR_X64)
block := DllCall("VirtualAllocEx", "Ptr", h, "Ptr", 0
, "UPtr", locator.Size + 64, "UInt", 0x3000, "UInt", 0x40, "Ptr")
if !block
throw Error("VirtualAllocEx failed", -1)
try {
AhkMagic._RemoteWrite(h, block, locator)
locOut := block + locator.Size
rc := AhkMagic._RemoteCall(h, block
, [base, text["rva"], text["size"], postfixRva, expandRva, locOut])
if rc != 0
throw Error("remote internal locator rc=" rc, -1)
return AhkMagic._RemoteRead(h, locOut, 64)
} finally {
DllCall("VirtualFreeEx", "Ptr", h, "Ptr", block, "UPtr", 0
, "UInt", 0x8000)
}
}
static _RemoteLoadScript(h, base, loc, text) {
memScript := AhkMagic._HexBuffer(MC_MEM_SCRIPT_X64)
textLen := (StrLen(text) + 1) * 2
scratchSize := 0x400
block := DllCall("VirtualAllocEx", "Ptr", h, "Ptr", 0
, "UPtr", memScript.Size + textLen + scratchSize
, "UInt", 0x3000, "UInt", 0x40, "Ptr")
if !block
throw Error("VirtualAllocEx failed", -1)
try {
AhkMagic._RemoteWrite(h, block, memScript)
textAddr := block + memScript.Size
scratchAddr := textAddr + textLen
textBuf := Buffer(textLen)
StrPut(text, textBuf, "UTF-16")
AhkMagic._RemoteWrite(h, textAddr, textBuf)
gscript := NumGet(loc["loc_out"], 0, "Ptr")
loadTs := base + loc["load_ts_rva"]
srcCount := loc.Has("src_count_rva") and loc["src_count_rva"]
? base + loc["src_count_rva"] : 0
return AhkMagic._RemoteCall(h, block
, [loadTs, gscript, srcCount, textAddr, StrLen(text) * 2
, scratchAddr])
} finally {
DllCall("VirtualFreeEx", "Ptr", h, "Ptr", block, "UPtr", 0
, "UInt", 0x8000)
}
}
static _RemoteCurrOff(secs, preparseRva) {
text := AhkMagic._TextSection(secs)
callers := AhkMagic._FindCallers(text, preparseRva)
for caller in callers {
p := text["ptr"]
base := text["rva"]
off := AhkMagic._FnStart(text, caller) - base
limit := Min(text["size"] - 16, off + 0x4000)
i := off
while i < limit {
if NumGet(p + i, "UChar") = 0xCC
and NumGet(p + i + 1, "UChar") = 0xCC
break
if NumGet(p + i, "UChar") = 0x48
and NumGet(p + i + 1, "UChar") = 0x8B
and NumGet(p + i + 2, "UChar") = 0x05 {
j := i + 7
while j < Min(i + 24, limit) {
if NumGet(p + j, "UChar") = 0x48
and NumGet(p + j + 1, "UChar") = 0x89
and (NumGet(p + j + 2, "UChar") = 0x58
or NumGet(p + j + 2, "UChar") = 0x50)
return NumGet(p + j + 3, "UChar")
j += 1
}
}
i += 1
}
}
return 0
}
static _RemoteParserAnchors(text, loadTsRva) {
p := text["ptr"]
base := text["rva"]
off := loadTsRva - base
limit := Min(text["size"] - 8, off + 0x100)
qCmp := -1
dCmp := -1
i := off
while i < limit {
b0 := NumGet(p + i, "UChar")
if b0 = 0x48 and NumGet(p + i + 1, "UChar") = 0x83 {
modrm := NumGet(p + i + 2, "UChar")
if modrm = 0x79 and NumGet(p + i + 4, "UChar") = 0
and qCmp < 0
qCmp := NumGet(p + i + 3, "UChar")
else if modrm = 0xB9 and NumGet(p + i + 7, "UChar") = 0
and qCmp < 0
qCmp := NumGet(p + i + 3, "Int")
}
if b0 = 0x83 and NumGet(p + i + 1, "UChar") = 0xB9
and (i = off or NumGet(p + i - 1, "UChar") != 0x48)
and NumGet(p + i + 6, "UChar") = 0
and dCmp < 0
dCmp := NumGet(p + i + 2, "Int")
if qCmp >= 0 and dCmp >= 0
break
i += 1
}
if qCmp < 0 or dCmp < 0
throw Error("parser state anchors not found", -1)
return Map("q", qCmp, "d", dCmp)
}
; Build BOTH known parser-region candidate layouts and pick the one
; that validates against the live target state. No version family is
; assumed: each candidate must satisfy its own structural checks
; (sentinel value, byte-field plausibility, pointer ranges) and the
; winner is the candidate that strictly validates. Ambiguous or
; failing candidates raise an explicit error.
static _RemoteDiscoverParser(h, gscript, qCmp, dCmp) {
buf := AhkMagic._RemoteRead(h, gscript, 0x800)
candidates := []
scores := []
; Candidate A: sentinel-anchored region ending at mClassObjectCount.
if dCmp > 0 and dCmp + 4 <= buf.Size
and NumGet(buf, dCmp, "Int") >= 0
and NumGet(buf, dCmp, "Int") <= 100000 {
exprIndexOff := 0
loop Min(0x40, dCmp) // 4 {
off := dCmp - (A_Index - 1) * 4
if off + 8 > buf.Size
continue
if NumGet(buf, off, "Int") = 0x7fffffff
and NumGet(buf, off + 4, "Int") = 0 {
exprIndexOff := off + 4
break
}
}
if exprIndexOff and exprIndexOff + 8 <= buf.Size
candidates.Push(Map(
"rich", true,
"fields", Map(
"mclass_count", dCmp,
"mline_parent", exprIndexOff - 0x28,
"mpending_related", exprIndexOff - 0x20,
"mlast_param_init", exprIndexOff - 0x18,
"mpending_hotkey", exprIndexOff - 0x10,
"mexpr_func", exprIndexOff - 8,
"mexpr_func_index", exprIndexOff,
"mnext_func_body", exprIndexOff + 4,
"mignore_block", exprIndexOff + 5,
"mbackcompat", exprIndexOff + 6,
"mcurrent_module", exprIndexOff - 0x48,
"mlast_module", exprIndexOff - 0x40)))
}
; Candidate B: qCmp-anchored region.
if qCmp > 0 and qCmp + 0x28 <= buf.Size
candidates.Push(Map(
"fields", Map(
"mopen", qCmp,
"mpending_parent", qCmp + 8,
"mpending_related", qCmp + 16,
"mlast_param_init", qCmp + 24,
"mnext_func_body", qCmp + 32,
"mclass_count", dCmp)))
best := 0
bestScore := -1
for cand in candidates {
fields := cand["fields"]
valid := true
for key, off in fields {
; Value-level checks are deliberately absent: the parser
; fields are saved and restored verbatim, so any plausible
; offset round-trips harmlessly. The only hard requirement
; is that every offset stays inside the Script object.
if !(off is Integer) or off <= 0 or off >= 0x800
or off + 8 > buf.Size {
valid := false
break
}
}
if valid {
; The sentinel-anchored candidate has the richer field set
; and only exists when its anchor was actually found.
score := cand.Has("rich") ? 10 : 0
if score > bestScore {
bestScore := score
best := cand
}
}
}
if !best
throw Error("parser state layout candidates all failed validation"
, -1)
return best["fields"]
}
static _RemoteLocateEvalScript(secs, base) {
text := AhkMagic._TextSection(secs)
postfixRefs := AhkMagic._RipRefs(text
, AhkMagic._FindUtf16(secs, "Missing operand.")[1])
postfixRva := AhkMagic._BestStart(text, postfixRefs)
expandRefs := AhkMagic._RipRefs(text
, AhkMagic._FindUtf16(secs, "Error evaluating expression.")[1])
expandRva := AhkMagic._BestStart(text, expandRefs)
if !postfixRva or !expandRva
throw Error("remote expression functions not found", -1)
callers := AhkMagic._FindCallers(text, postfixRva)
if !callers.Length
throw Error("PreparseExpressions not found", -1)
preparse := AhkMagic._FnStart(text, callers[1])
preprocess := AhkMagic._LocatePreprocessFunc(text)
if !preprocess
throw Error("PreprocessLocalVars not found", -1)
open := AhkMagic._LocateOpenInclude(secs)
if !open
throw Error("OpenIncludedFile not found", -1)
loadTs := AhkMagic._LocateLoadTs(text, open)
if !loadTs
throw Error("LoadIncludedFile(TextStream) not found", -1)
gptr := AhkMagic._LocateGptr(text, preparse)
if !gptr
throw Error("g pointer not found", -1)
currOff := AhkMagic._RemoteCurrOff(secs, preparse)
if !currOff
throw Error("g->curr offset not found", -1)
anchors := AhkMagic._RemoteParserAnchors(text, loadTs)
return Map(
"postfix_rva", postfixRva,
"expand_rva", expandRva,
"preparse_rva", preparse,
"preprocess_rva", preprocess,
"open_rva", open,
"load_ts_rva", loadTs,
"src_count_rva", 0,
"gptr_rva", gptr,
"curr_off", currOff,
"q_cmp", anchors["q"],
"d_cmp", anchors["d"],
"parser", Map())
}
static _RemoteLineRows(h, funcs, funcData, jumpOff, backOff) {
rows := []
for index, q in funcs {
linePtr := NumGet(funcData[index], jumpOff, "Ptr")
if linePtr <= 0x10000 or linePtr >= 0x7fffffffffff
continue
try
lineData := AhkMagic._RemoteRead(h, linePtr, 0x100)
catch
continue
if backOff + 8 <= lineData.Size
and NumGet(lineData, backOff, "Ptr") = q
rows.Push(Map("func", q, "ptr", linePtr, "data", lineData))
}
return rows
}
static _RemoteLineHasFuncRef(lineData, q) {
loop lineData.Size // 8 {
if NumGet(lineData, (A_Index - 1) * 8, "Ptr") = q
return true
}
return false
}
static _RemoteIsArgLike(h, p) {
try
argData := AhkMagic._RemoteRead(h, p, 40)
catch
return false
if NumGet(argData, 0, "UChar") > 2
or NumGet(argData, 1, "UChar") > 1
return false
if NumGet(argData, 4, "UInt") > 0x10000
return false
for off in [8, 16, 24] {
v := NumGet(argData, off, "Ptr")
if v and (v <= 0x10000 or v >= 0x7fffffffffff)
return false
}
return true
}
static _RemoteTokenValid(data, stride, symOff, sentinel) {
if symOff + 4 > stride
return 0
count := Min(data.Size // stride, 64)
seenVar := false
loop count {
sym := NumGet(data, (A_Index - 1) * stride + symOff, "UInt")
if sym = sentinel
return seenVar ? A_Index : 0
if sym > 0x1000
return 0
if sym = 4
seenVar := true
}
return 0
}
static _RemoteCollectPostfixes(h, rows) {
postfixes := []
seen := Map()
for row in rows {
lineData := row["data"]
loop lineData.Size // 8 {
p := NumGet(lineData, (A_Index - 1) * 8, "Ptr")
if p <= 0x10000 or p >= 0x7fffffffffff
continue
if !AhkMagic._RemoteIsArgLike(h, p)
continue
try
argData := AhkMagic._RemoteRead(h, p, 40)
catch
continue
loop argData.Size // 8 {
q := NumGet(argData, (A_Index - 1) * 8, "Ptr")
if q <= 0x10000 or q >= 0x7fffffffffff
continue
if seen.Has(q)
continue
seen[q] := true
postfixes.Push(q)
}
}
}
return postfixes
}
static _RemoteDiscoverTokens(h, postfixes, sentinel) {
bestStride := 0
bestSym := 0
bestScore := -1
for stride in [24, 32, 16, 40] {
for symOff in [16, 20, 12, 8, 4, 0] {
if symOff + 4 > stride
continue
ok := 0
bestLen := 0
for p in postfixes {
try
data := AhkMagic._RemoteRead(h, p, stride * 64)
catch
continue
got := AhkMagic._RemoteTokenValid(data, stride, symOff
, sentinel)
if got {
ok += 1
bestLen := Max(bestLen, got)
}
}
score := ok * 10000 - bestLen - stride
if score > bestScore {
bestScore := score
bestStride := stride
bestSym := symOff
}
}
}
if !bestStride
throw Error("dynamic token layout not found", -1)
varSym := 4
usageOff := 8
usageScore := -1
for uOff in [8, 12, 4, 20] {
if uOff = bestSym
continue
if uOff + 8 > bestStride
continue
score := 0
for p in postfixes {
try
data := AhkMagic._RemoteRead(h, p, bestStride * 64)
catch
continue
loop data.Size // bestStride {
off := (A_Index - 1) * bestStride
if NumGet(data, off + bestSym, "UInt") = varSym
and NumGet(data, off + uOff, "UInt64") < 0x1000
score += 1
}
}
if score > usageScore {
usageScore := score
usageOff := uOff
}
}
valueOff := 0
valueScore := -1
for vOff in [0, 8, 16] {
if vOff = bestSym
continue
score := 0
for p in postfixes {
try
data := AhkMagic._RemoteRead(h, p, bestStride * 64)
catch
continue
loop data.Size // bestStride {
off := (A_Index - 1) * bestStride
if NumGet(data, off + bestSym, "UInt") = varSym {
v := NumGet(data, off + vOff, "Ptr")
if v > 0x10000 and v < 0x7fffffffffff
score += 1
}
}
}
if score > valueScore {
valueScore := score
valueOff := vOff
}
}
return Map(
"stride", bestStride,
"symbol", bestSym,
"usage", usageOff,
"value", valueOff,
"var_symbol", varSym)
}
static _RemoteLineHeaderOff(rows) {
bestAction := 0
bestArgc := 1
bestScore := -1
loop 4 {
ao := A_Index - 1
loop 4 {
ac := A_Index - 1
if ac = ao
continue
score := 0
for row in rows {
data := row["data"]
if ao + 1 >= data.Size or ac + 1 >= data.Size
continue
if NumGet(data, ao, "UChar") <= 0x40
and NumGet(data, ac, "UChar") <= 0x10
score += 1
}
if score > bestScore {
bestScore := score
bestAction := ao
bestArgc := ac
}
}
}
if !bestScore
throw Error("dynamic Line header layout not found", -1)
return Map("action", bestAction, "argc", bestArgc)
}
static _RemoteLineArgOff(h, rows, tokens, sentinel) {
counts := Map()
argPostfixCounts := Map()
for row in rows {
lineData := row["data"]
loop lineData.Size // 8 {
lineOff := (A_Index - 1) * 8
p := NumGet(lineData, lineOff, "Ptr")
if p <= 0x10000 or p >= 0x7fffffffffff
continue
if !AhkMagic._RemoteIsArgLike(h, p)
continue
try
argData := AhkMagic._RemoteRead(h, p, 40)
catch
continue
loop argData.Size // 8 {
argOff := (A_Index - 1) * 8
q := NumGet(argData, argOff, "Ptr")
if q <= 0x10000 or q >= 0x7fffffffffff
continue
try
tokenData := AhkMagic._RemoteRead(h, q
, tokens["stride"] * 64)
catch
continue
if AhkMagic._RemoteTokenValid(tokenData
, tokens["stride"], tokens["symbol"], sentinel) {
counts[lineOff] := (counts.Has(lineOff)
? counts[lineOff] : 0) + 1
argPostfixCounts[argOff] := (argPostfixCounts.Has(argOff)
? argPostfixCounts[argOff] : 0) + 1
}
}
}
}
bestOff := 0
bestScore := 0
for off, score in counts {
if score > bestScore or (score = bestScore and bestOff
and off < bestOff) {
bestScore := score
bestOff := off
}
}
bestArgPostfix := 24
bestArgScore := 0
for off, score in argPostfixCounts {
if score > bestArgScore or (score = bestArgScore
and bestArgPostfix and off < bestArgPostfix) {
bestArgScore := score
bestArgPostfix := off
}
}
if !bestOff or !bestArgScore
throw Error("dynamic Line.mArg / ArgStruct.postfix not found", -1)
return Map("line_arg", bestOff, "arg_postfix", bestArgPostfix)
}
static _RemoteArgExpressionOff(h, rows, lineArg, argPostfix, tokens
, sentinel) {
counts := Map()
for row in rows {
arg := NumGet(row["data"], lineArg, "Ptr")
if arg <= 0x10000 or arg >= 0x7fffffffffff
continue
try
argData := AhkMagic._RemoteRead(h, arg, 40)
catch
continue
postfix := NumGet(argData, argPostfix, "Ptr")
if postfix <= 0x10000 or postfix >= 0x7fffffffffff
continue
try
tokenData := AhkMagic._RemoteRead(h, postfix
, tokens["stride"] * 64)
catch
continue
if !AhkMagic._RemoteTokenValid(tokenData, tokens["stride"]
, tokens["symbol"], sentinel)
continue
loop 8 {
off := A_Index - 1
if NumGet(argData, off, "UChar") = 1
counts[off] := (counts.Has(off) ? counts[off] : 0) + 1
}
}
bestOff := 1
bestScore := 0
for off, score in counts {
if score > bestScore or (score = bestScore and off < bestOff) {
bestScore := score
bestOff := off
}
}
if !bestScore
throw Error("dynamic ArgStruct.is_expression not found", -1)
return bestOff
}
static _RemoteLineNextOff(h, rows, lineArg, backOff) {
forward := Map()
total := Map()
loop 0x100 // 8 {
off := (A_Index - 1) * 8
if off = lineArg
continue
for row in rows {
p := NumGet(row["data"], off, "Ptr")
if p <= 0x10000 or p >= 0x7fffffffffff
or p = row["ptr"]
continue
try
lineData := AhkMagic._RemoteRead(h, p, 0x100)
catch
continue
if !AhkMagic._RemoteLineHasFuncRef(lineData, row["func"])
continue
total[off] := (total.Has(off) ? total[off] : 0) + 1
if p > row["ptr"]
forward[off] := (forward.Has(off) ? forward[off] : 0) + 1
}
}
bestOff := 0
bestForward := -1
bestTotal := -1
for off, score in forward {
t := total.Has(off) ? total[off] : 0
if score > bestForward
or (score = bestForward and t > bestTotal)
or (score = bestForward and t = bestTotal and bestOff
and off < bestOff) {
bestForward := score
bestTotal := t
bestOff := off
}
}
if !bestOff
throw Error("dynamic Line.mNextLine not found", -1)
return bestOff
}
static _RemoteLineAttributeOff(h, rows, lineArg, lineNext, backOff
, funcs) {
known := Map()
for q in funcs
known[q] := true
counts := Map()
for row in rows {
visited := Map()
stack := [row["ptr"]]
while stack.Length {
line := stack.Pop()
if visited.Has(line)
continue
visited[line] := true
try
lineData := AhkMagic._RemoteRead(h, line, 0x100)
catch
continue
loop lineData.Size // 8 {
off := (A_Index - 1) * 8
if off = lineArg or off = lineNext
continue
p := NumGet(lineData, off, "Ptr")
if known.Has(p) {
counts[off] := (counts.Has(off)
? counts[off] : 0) + 1
continue
}
if p <= 0x10000 or p >= 0x7fffffffffff or p = line
continue
if AhkMagic._RemoteLineHasFuncRef(lineData, row["func"])
and !visited.Has(p)
stack.Push(p)
}
}
}
bestOff := 0
bestScore := 0
for off, score in counts {
if score > bestScore or (score = bestScore and bestOff
and off < bestOff) {
bestScore := score
bestOff := off
}
}
if !bestOff
throw Error("dynamic Line.mAttribute not found", -1)
return bestOff
}
static _RemoteDerefArrayLike(data) {
if NumGet(data, 0, "Ptr") <= 0x10000
or NumGet(data, 0, "Ptr") >= 0x7fffffffffff
return false
loop 24 {
to := A_Index - 1
if to < 8 or to + 8 > data.Size
continue
if NumGet(data, to, "UChar") <= 7
and NumGet(data, to + 4, "UInt") <= 0x10000
return true
}
return false
}
static _RemoteDerefLayout(h, rows, lineArg) {
arrays := []
argDerefCounts := Map()
for row in rows {
arg := NumGet(row["data"], lineArg, "Ptr")
if arg <= 0x10000 or arg >= 0x7fffffffffff
continue
try
argData := AhkMagic._RemoteRead(h, arg, 40)
catch
continue
loop argData.Size // 8 {
off := (A_Index - 1) * 8
p := NumGet(argData, off, "Ptr")
if p <= 0x10000 or p >= 0x7fffffffffff
continue
try
data := AhkMagic._RemoteRead(h, p, 0x100)
catch
continue
if AhkMagic._RemoteDerefArrayLike(data) {
arrays.Push(data)
argDerefCounts[off] := (argDerefCounts.Has(off)
? argDerefCounts[off] : 0) + 1
}
}
}
if arrays.Length < 1
throw Error("dynamic ArgStruct.deref not found", -1)
argDerefOff := 0
argDerefScore := -1
for off, score in argDerefCounts {
if score > argDerefScore
or (score = argDerefScore and off < argDerefOff) {
argDerefScore := score
argDerefOff := off
}
}
if !argDerefScore
throw Error("dynamic ArgStruct.deref offset not found", -1)
bestStride := 24
bestType := 16
bestScore := -1
for stride in [24, 32, 16, 40] {
for typeOff in [16, 20, 24] {
if typeOff + 8 > stride
continue
score := 0
for data in arrays {
loop Min(data.Size // stride, 8) {
off := (A_Index - 1) * stride
marker := NumGet(data, off, "Ptr")
if marker <= 0x10000
or marker >= 0x7fffffffffff
break
if NumGet(data, off + typeOff, "UChar") <= 7
and NumGet(data, off + typeOff + 1, "UChar") <= 3
and NumGet(data, off + typeOff + 4, "UInt")
<= 0x10000
score += 1
}
}
if score > bestScore
or (score = bestScore and typeOff < bestType)
or (score = bestScore and typeOff = bestType
and stride < bestStride) {
bestScore := score
bestType := typeOff
bestStride := stride
}
}
}
if !bestScore
throw Error("dynamic DerefType layout not found", -1)
markerOff := bestType - 16
varOff := bestType - 8
markerScore := 0
for data in arrays {
if NumGet(data, markerOff, "Ptr") > 0x10000
and NumGet(data, markerOff, "Ptr") < 0x7fffffffffff
markerScore += 1
}
if !markerScore
throw Error("dynamic DerefType markers not found", -1)
return Map(
"arg_deref", argDerefOff,
"stride", bestStride,
"marker", markerOff,
"var", varOff,
"type", bestType,
"len", bestType + 4)
}
static _RemoteDiscoverBackOff(h, funcs, funcData, jumpOff) {
counts := Map()
for index, q in funcs {
linePtr := NumGet(funcData[index], jumpOff, "Ptr")
if linePtr <= 0x10000 or linePtr >= 0x7fffffffffff
continue
try
lineData := AhkMagic._RemoteRead(h, linePtr, 0x80)
catch
continue
loop lineData.Size // 8 {
bo := (A_Index - 1) * 8
if NumGet(lineData, bo, "Ptr") = q
counts[bo] := (counts.Has(bo) ? counts[bo] : 0) + 1
}
}
bestOff := 0
bestCount := 0
for off, count in counts {
if count > bestCount or (count = bestCount and off < bestOff) {
bestCount := count
bestOff := off
}
}
if !bestOff
throw Error("back-reference slot not found", -1)
return bestOff
}
static _DiscoverInProcStructs(gScript, arrPtr, oldCount, newFunc
, jumpOff) {
h := DllCall("OpenProcess", "UInt", 0x1F0FFF, "Int", 0
, "UInt", DllCall("GetCurrentProcessId"), "Ptr")
if !h
throw Error("OpenProcess(self) failed", -1)
try {
funcs := []
funcData := []
loop oldCount {
q := NumGet(arrPtr + (A_Index - 1) * 8, "Ptr")
if q <= 0x10000 or q >= 0x7fffffffffff
continue
funcs.Push(q)
data := Buffer(0x400)
DllCall("RtlMoveMemory", "Ptr", data.Ptr, "Ptr", q
, "UPtr", 0x400)
funcData.Push(data)
}
funcs.Push(newFunc)
data := Buffer(0x400)
DllCall("RtlMoveMemory", "Ptr", data.Ptr, "Ptr", newFunc
, "UPtr", 0x400)
funcData.Push(data)
backOff := AhkMagic._RemoteDiscoverBackOff(h, funcs, funcData
, jumpOff)
locOut := Buffer(64, 0)
NumPut("Ptr", AhkMagic.gScript, locOut, 0)
NumPut("Ptr", AhkMagic.finalizeExpr, locOut, 8)
NumPut("Ptr", AhkMagic.findOrAddVar, locOut, 16)
NumPut("Ptr", AhkMagic.crtFree, locOut, 24)
NumPut("UInt", AhkMagic.symInvalid, locOut, 32)
return AhkMagic._RemoteDiscoverStructs(h, locOut, funcs
, funcData, jumpOff, backOff)
} finally {
DllCall("CloseHandle", "Ptr", h)
}
}
static _RemoteDiscoverStructs(h, locOut, funcs, funcData, jumpOff
, backOff) {
rows := AhkMagic._RemoteLineRows(h, funcs, funcData, jumpOff, backOff)
if rows.Length < 1
throw Error("no ordinary functions for struct discovery"
, -1)
sentinel := NumGet(locOut, 32, "UInt")
postfixes := AhkMagic._RemoteCollectPostfixes(h, rows)
tokens := AhkMagic._RemoteDiscoverTokens(h, postfixes, sentinel)
header := AhkMagic._RemoteLineHeaderOff(rows)
arg := AhkMagic._RemoteLineArgOff(h, rows, tokens, sentinel)
argExpr := AhkMagic._RemoteArgExpressionOff(h, rows
, arg["line_arg"], arg["arg_postfix"], tokens, sentinel)
lineNext := AhkMagic._RemoteLineNextOff(h, rows, arg["line_arg"]
, backOff)
lineAttr := AhkMagic._RemoteLineAttributeOff(h, rows
, arg["line_arg"], lineNext, backOff, funcs)
deref := AhkMagic._RemoteDerefLayout(h, rows, arg["line_arg"])
return Map(
"line_action", header["action"],
"line_argc", header["argc"],
"line_arg", arg["line_arg"],
"line_attribute", lineAttr,
"line_next", lineNext,
"arg_expression", argExpr,
"arg_postfix", arg["arg_postfix"],
"arg_deref", deref["arg_deref"],
"token_stride", tokens["stride"],
"token_symbol", tokens["symbol"],
"token_usage", tokens["usage"],
"token_value", tokens["value"],
"token_var_symbol", tokens["var_symbol"],
"deref_stride", deref["stride"],
"deref_marker", deref["marker"],
"deref_var", deref["var"],
"deref_type", deref["type"],
"deref_len", deref["len"])
}
; Discover the mFuncs layout of a target. The validator needs live
; function entries, so a target whose script defines no functions
; (fresh/empty scripts) fails discovery. Inject two probe functions
; through the target's own loader and retry once; the probes stay in
; the target's mFuncs as harmless extras.
static _RemoteDiscoverLayoutWithProbe(h, secs, base, loc) {
try {
return AhkMagic._RemoteDiscoverLayout(h, secs, base, loc)
} catch as e {
if !InStr(e.Message, "dynamic function layout not found")
and !InStr(e.Message, "dynamic token layout not found")
throw e
tag := Format("{:x}", A_TickCount)
probe := "ahkHackLayoutProbeA" tag
. "(x) {`n return x + 1`n}`n"
. "ahkHackLayoutProbeB" tag
. "(y) {`n return y * 2`n}`n"
rc := AhkMagic._RemoteLoadScript(h, base, loc, probe)
if rc != 0
throw Error("probe injection for layout discovery failed rc="
. rc " (TextStream layout validation failed in target)"
, -1)
try {
; First pass discovers mFuncs/mjumpLine without structs.
layout := AhkMagic._RemoteDiscoverLayout(
h, secs, base, loc, false)
; The injected functions were loaded without preparse, so
; their lines have no postfix buffers yet. Preparse them
; through the target's own pipeline, then finish struct
; discovery on the now-complete probe rows.
savedCur := AhkMagic._RPtr(h, layout["g"] + layout["curr_off"])
for q in layout["funcs"] {
jump := AhkMagic._RPtr(h
, q + layout["mjump_line_off"])
if !jump
continue
AhkMagic._WPtr(h, layout["g"] + layout["curr_off"], q)
prc := AhkMagic._RemoteCall(h
, base + loc["preparse_rva"]
, [layout["gscript"], jump])
AhkMagic._WPtr(h, layout["g"] + layout["curr_off"], savedCur)
if prc != 1
throw Error("probe PreparseExpressions rc=" prc, -1)
}
layout["struct"] := AhkMagic._RemoteDiscoverStructs(
h, loc["loc_out"], layout["funcs"], layout["func_data"]
, layout["mjump_line_off"], layout["jump_back_off"])
layout["shape"] := AhkMagic._RemoteArgShape(h, layout
, layout["struct"])
return layout
} catch as e2 {
throw Error("layout discovery failed even after probe "
. "injection: " e2.Message, -1)
}
}
}
static _RemoteDiscoverLayout(h, secs, base, loc, doStructs := true) {
locOut := AhkMagic._RemoteInternalLocator(h, secs, base
, loc["postfix_rva"], loc["expand_rva"])
loc["loc_out"] := locOut
gscript := NumGet(locOut, 0, "Ptr")
gAddr := base + loc["gptr_rva"]
g := AhkMagic._RPtr(h, gAddr)
currOff := loc["curr_off"]
snap := AhkMagic._RemoteRead(h, gscript, 0x800)
bestScore := -1
best := Map("funcs_off", 0, "count_off", 0, "arr_ptr", 0
, "count", 0, "name_off", 0, "jump_off", 0
, "back_off", 0, "funcs", [], "func_data", [])
loop 0x200 // 8 {
off := (A_Index - 1) * 8
if off + 12 > snap.Size
continue
arrPtr := NumGet(snap, off, "Ptr")
count := NumGet(snap, off + 8, "Int")
if arrPtr <= 0x10000 or arrPtr >= 0x7fffffffffff
or count < 1 or count > 100000
continue
limit := Min(count, 16)
funcs := []
funcData := []
lineCache := Map()
valid := true
loop limit {
try
q := AhkMagic._RPtr(h, arrPtr + (A_Index - 1) * 8)
catch {
valid := false
break
}
if q <= 0x10000 or q >= 0x7fffffffffff {
valid := false
break
}
funcs.Push(q)
funcData.Push(AhkMagic._RemoteRead(h, q, 0x400))
}
if !valid
continue
jumpOff := 0
jumpMatches := 0
jumpBestSlot := 0
jumpSlotOff := 0
loop 0x200 // 8 {
jo := (A_Index - 1) * 8
matches := 0
slotCount := Map()
for index, q in funcs {
linePtr := NumGet(funcData[index], jo, "Ptr")
if linePtr <= 0x10000 or linePtr >= 0x7fffffffffff
continue
if lineCache.Has(linePtr) {
lineData := lineCache[linePtr]
} else {
try {
lineData := AhkMagic._RemoteRead(h, linePtr, 0x80)
lineCache[linePtr] := lineData
} catch {
continue
}
}
foundSlot := 0
loop lineData.Size // 8 {
bo := (A_Index - 1) * 8
if NumGet(lineData, bo, "Ptr") = q {
foundSlot := bo
break
}
}
if foundSlot {
matches += 1
slotCount[foundSlot] := (slotCount.Has(foundSlot)
? slotCount[foundSlot] : 0) + 1
}
}
bestSlot := 0
bestSlotCount := 0
for slot, count in slotCount {
if count > bestSlotCount or count = bestSlotCount {
bestSlotCount := count
bestSlot := slot
}
}
if bestSlotCount > jumpBestSlot
or (bestSlotCount = jumpBestSlot and jumpBestSlot
and jo < jumpOff) {
jumpMatches := matches
jumpBestSlot := bestSlotCount
jumpSlotOff := bestSlot
jumpOff := jo
}
}
minJump := limit < 2 ? 1 : 2
if jumpBestSlot < minJump
continue
; A real mFuncs array must have back-reference rows for a
; majority of its entries. Decoy arrays (other object lists in
; the Script object) can contain a few function-like pointers
; and win the name-based score; rejecting sub-majority
; back-reference counts keeps the scan on the real array.
if jumpBestSlot * 2 < limit
continue
nameOff := 0
nameMatches := 0
nameUnique := 0
loop 0x200 // 8 {
no := (A_Index - 1) * 8
matches := 0
seen := Map()
for index, q in funcs {
namePtr := NumGet(funcData[index], no, "Ptr")
if namePtr <= 0x10000 or namePtr >= 0x7fffffffffff
continue
try
name := AhkMagic._RemoteReadString(h, namePtr, 128)
catch
continue
if name = "" or StrLen(name) > 127
continue
if !RegExMatch(name, "^[A-Za-z_][A-Za-z0-9_.]*$")
continue
matches += 1
seen[name] := 1
}
if matches > nameMatches
or (matches = nameMatches and seen.Count > nameUnique) {
nameMatches := matches
nameUnique := seen.Count
nameOff := no
}
}
minName := limit < 2 ? 1 : 2
if nameMatches < minName
continue
score := nameMatches * 100000 + nameUnique * 1000
+ jumpMatches * 100 + limit
if score > bestScore {
bestScore := score
best["funcs_off"] := off
best["count_off"] := off + 8
best["arr_ptr"] := arrPtr
best["count"] := count
best["name_off"] := nameOff
best["jump_off"] := jumpOff
best["back_off"] := jumpSlotOff
best["funcs"] := funcs
best["func_data"] := funcData
}
}
if !best["funcs_off"] or !best["name_off"] or !best["jump_off"]
throw Error("dynamic function layout not found", -1)
loc["parser"] := AhkMagic._RemoteDiscoverParser(h, gscript
, loc["q_cmp"], loc["d_cmp"])
layout := Map(
"gscript", gscript,
"g", g,
"curr_off", currOff,
"mfuncs_off", best["funcs_off"],
"mfuncs_count_off", best["count_off"],
"mlast_line_off", 0,
"mjump_line_off", best["jump_off"],
"name_off", best["name_off"],
"jump_back_off", best["back_off"],
"funcs", best["funcs"],
"func_data", best["func_data"],
"parser", loc["parser"])
if doStructs {
layout["struct"] := AhkMagic._RemoteDiscoverStructs(h, locOut
, best["funcs"], best["func_data"], best["jump_off"]
, best["back_off"])
layout["shape"] := AhkMagic._RemoteArgShape(h, layout
, layout["struct"])
}
return layout
}
static _RPtr(h, addr) {
return NumGet(AhkMagic._RemoteRead(h, addr, 8), 0, "Ptr")
}
static _RInt(h, addr) {
return NumGet(AhkMagic._RemoteRead(h, addr, 4), 0, "Int")
}
static _WPtr(h, addr, val) {
buf := Buffer(8)
NumPut("Ptr", val, buf, 0)
AhkMagic._RemoteWrite(h, addr, buf)
}
static _WInt(h, addr, val) {
buf := Buffer(4)
NumPut("Int", val, buf, 0)
AhkMagic._RemoteWrite(h, addr, buf)
}
static _WByte(h, addr, val) {
buf := Buffer(1)
NumPut("UChar", val, buf, 0)
AhkMagic._RemoteWrite(h, addr, buf)
}
static _DeclaredFunctionNames(text) {
names := []
depth := 0
candidate := ""
for raw in StrSplit(text, "`n", "`r") {
line := Trim(raw)
if candidate = "" and line != "" and SubStr(line, 1, 1) != ";" {
if RegExMatch(line
, "^([A-Za-z_][A-Za-z0-9_]*)\s*\(", &m)
and !RegExMatch(line
, "^(if|else|for|while|switch|try|catch|finally"
. "|return|break|continue|global|local|static|class"
. "|new|throw|until)\b") {
candidate := m[1]
}
}
if candidate != "" and InStr(line, "{") {
found := false
for n in names {
if n = candidate {
found := true
break
}
}
if !found and depth = 0
names.Push(candidate)
candidate := ""
}
opens := 0
closes := 0
loop StrLen(line) {
c := SubStr(line, A_Index, 1)
if c = "{"
opens += 1
else if c = "}"
closes += 1
}
depth += opens - closes
}
return names
}
static RemoteEvalScript(hook, text) {
if !(hook is Map) or !hook.Has("pid")
throw TypeError("hook must be an AttachRemote result", -1)
if !(text is String) or Trim(text) = ""
throw TypeError("text must be a non-empty string", -1)
; Same protection as the in-process path, but validated with the
; TARGET's own interpreter when its path is known.
vpath := hook.Has("module") and hook["module"] != ""
? hook["module"] : A_AhkPath
AhkMagic._ValidateScript(text, vpath)
h := AhkMagic._RemoteOpen(hook["pid"], true)
try {
mod := AhkMagic._RemoteModuleBase(h, hook["pid"])
secs := AhkMagic._RemoteSections(h, mod["base"])
if hook.Has("script_loc") and hook.Has("script_layout") {
loc := hook["script_loc"]
layout := hook["script_layout"]
} else {
loc := AhkMagic._RemoteLocateEvalScript(secs, mod["base"])
layout := AhkMagic._RemoteDiscoverLayoutWithProbe(
h, secs, mod["base"], loc)
hook["script_loc"] := loc
hook["script_layout"] := layout
}
locOut := loc["loc_out"]
if !layout.Has("struct")
layout["struct"] := AhkMagic._RemoteDiscoverStructs(h, locOut
, layout["funcs"], layout["func_data"]
, layout["mjump_line_off"], layout["jump_back_off"])
if !layout.Has("shape")
layout["shape"] := AhkMagic._RemoteArgShape(h, layout
, layout["struct"])
s := layout["struct"]
gscript := layout["gscript"]
g := layout["g"]
currOff := layout["curr_off"]
for name in AhkMagic._DeclaredFunctionNames(text) {
if AhkMagic._RemoteFindUserFuncs(h, layout, name).Length
throw Error("function name already exists in target: "
. name, -1)
}
oldFuncCount := AhkMagic._RInt(h, gscript + layout["mfuncs_count_off"])
savedCur := AhkMagic._RPtr(h, g + currOff)
savedCurFunc := AhkMagic._RPtr(h, g + currOff)
parser := layout["parser"]
savedState := []
for key in ["mopen", "mpending_parent", "mline_parent"
, "mpending_related", "mlast_param_init", "mpending_hotkey"
, "mexpr_func", "mcurrent_module"]
if parser.Has(key)
savedState.Push([key, "Ptr"
, AhkMagic._RPtr(h, gscript + parser[key])])
for key in ["mexpr_func_index", "mclass_count"]
if parser.Has(key)
savedState.Push([key, "Int"
, AhkMagic._RInt(h, gscript + parser[key])])
for key in ["mnext_func_body", "mignore_block", "mbackcompat"]
if parser.Has(key)
savedState.Push([key, "UChar"
, NumGet(AhkMagic._RemoteRead(h, gscript + parser[key], 1)
, 0, "UChar")])
try {
for key in ["mopen", "mpending_parent", "mline_parent"
, "mpending_related", "mlast_param_init"
, "mpending_hotkey", "mexpr_func"]
if parser.Has(key)
AhkMagic._WPtr(h, gscript + parser[key], 0)
if parser.Has("mexpr_func_index")
AhkMagic._WInt(h, gscript + parser["mexpr_func_index"]
, 0x7fffffff)
if parser.Has("mnext_func_body")
AhkMagic._WByte(h, gscript + parser["mnext_func_body"], 0)
if parser.Has("mignore_block")
AhkMagic._WByte(h, gscript + parser["mignore_block"], 0)
if parser.Has("mbackcompat")
AhkMagic._WByte(h, gscript + parser["mbackcompat"], 1)
if parser.Has("mclass_count")
AhkMagic._WInt(h, gscript + parser["mclass_count"], 0)
AhkMagic._WPtr(h, g + currOff, 0)
rc := AhkMagic._RemoteLoadScript(h, mod["base"], loc, text)
if rc != 0
throw Error("LoadIncludedFile(memory) rc=" rc, -1)
funcsItem := AhkMagic._RPtr(h, gscript + layout["mfuncs_off"])
funcCount := AhkMagic._RInt(h, gscript + layout["mfuncs_count_off"])
if funcCount < oldFuncCount
or funcCount > oldFuncCount + 4096
throw Error("mFuncs count changed unexpectedly: "
. oldFuncCount " -> " funcCount, -1)
if funcCount > oldFuncCount {
loop funcCount - oldFuncCount {
idx := oldFuncCount + A_Index - 1
newFunc := AhkMagic._RPtr(h, funcsItem + idx * 8)
jump := AhkMagic._RPtr(h
, newFunc + layout["mjump_line_off"])
if !jump
continue
AhkMagic._WPtr(h, g + currOff, newFunc)
rc := AhkMagic._RemoteCall(h
, mod["base"] + loc["preparse_rva"]
, [gscript, jump])
if rc != 1
throw Error("PreparseExpressions rc=" rc, -1)
AhkMagic._WPtr(h, g + currOff, newFunc)
line := jump
lineSteps := 0
while line {
if lineSteps > 100000
throw Error("remote line chain too long; layout likely wrong", -1)
lineSteps += 1
lineSize := Max(0x40, s["line_next"] + 8
, s["line_attribute"] + 8, s["line_arg"] + 8)
lineData := AhkMagic._RemoteRead(h, line, lineSize)
action := NumGet(lineData, s["line_action"], "UChar")
attr := NumGet(lineData, s["line_attribute"], "Ptr")
argc := NumGet(lineData, s["line_argc"], "UChar")
arg := NumGet(lineData, s["line_arg"], "Ptr")
if action = 3 and attr
AhkMagic._WPtr(h, g + currOff, attr)
if argc and arg
and NumGet(AhkMagic._RemoteRead(h
, arg + s["arg_expression"], 1)
, 0, "UChar") {
postfix := AhkMagic._RPtr(h
, arg + s["arg_postfix"])
if postfix {
tokenSteps := 0
loop {
if tokenSteps > 100000
throw Error("remote postfix token chain too long; layout likely wrong", -1)
tokenSteps += 1
tokenSize := Max(s["token_stride"]
, s["token_symbol"] + 4
, s["token_usage"] + 8
, s["token_value"] + 8)
token := AhkMagic._RemoteRead(h
, postfix, tokenSize)
symbol := NumGet(token
, s["token_symbol"], "UInt")
if symbol = NumGet(locOut, 32, "UInt")
break
if symbol = s["token_var_symbol"]
and NumGet(token, s["token_usage"]
, "UInt") < 3 {
deref := NumGet(token
, s["token_value"], "Ptr")
if deref {
derefSize := Max(24
, s["deref_len"] + 4
, s["deref_type"] + 1
, s["deref_marker"] + 8
, s["deref_var"] + 8)
derefData := AhkMagic._RemoteRead(h
, deref, derefSize)
derefType := NumGet(derefData
, s["deref_type"], "UChar")
marker := NumGet(derefData
, s["deref_marker"], "Ptr")
derefLen := NumGet(derefData
, s["deref_len"], "UInt")
if derefType = 7 {
AhkMagic._WPtr(h, postfix
, AhkMagic._RPtr(h
, deref
+ s["deref_var"]))
} else if derefType = 0
and marker
and derefLen > 0
and derefLen <= 64 {
var := AhkMagic._RemoteCall(h
, NumGet(locOut, 16, "Ptr")
, [gscript, marker
, derefLen, 0x103])
if var
AhkMagic._WPtr(h
, postfix
+ s["token_value"]
, var)
}
}
}
postfix += s["token_stride"]
}
}
}
line := AhkMagic._RPtr(h
, line + s["line_next"])
}
AhkMagic._WPtr(h, g + currOff, savedCur)
rc := AhkMagic._RemoteCall(h
, mod["base"] + loc["preprocess_rva"]
, [gscript, newFunc])
if rc != 1
throw Error("PreprocessLocalVars rc=" rc, -1)
AhkMagic._WPtr(h, g + currOff, savedCurFunc)
}
}
AhkMagic._WPtr(h, g + currOff, 0)
last := ""
for raw in StrSplit(text, "`n", "`r") {
t := Trim(raw)
if t = ""
continue
if RegExMatch(t
, "^(if|else|for|while|loop|try|catch|finally|return|break|continue|class|static|global|local|throw)\b")
continue
if SubStr(t, -1) = "{"
continue
last := t
}
if last = ""
throw ValueError("no expression result found in script text")
return AhkMagic.RemoteEval(hook, last)
} finally {
for item in savedState {
if item[2] = "Ptr"
AhkMagic._WPtr(h, gscript + parser[item[1]], item[3])
else if item[2] = "Int"
AhkMagic._WInt(h, gscript + parser[item[1]], item[3])
else
AhkMagic._WByte(h, gscript + parser[item[1]], item[3])
}
AhkMagic._WPtr(h, g + currOff, savedCurFunc)
AhkMagic._WPtr(h, g + currOff, savedCur)
}
} finally {
DllCall("CloseHandle", "Ptr", h)
}
}
static RemoteReplaceFuncBody(hook, oldName, newName) {
if !(hook is Map) or !hook.Has("pid")
throw TypeError("hook must be an AttachRemote result", -1)
h := AhkMagic._RemoteOpen(hook["pid"], true)
try {
mod := AhkMagic._RemoteModuleBase(h, hook["pid"])
secs := AhkMagic._RemoteSections(h, mod["base"])
if hook.Has("script_layout") {
layout := hook["script_layout"]
} else {
loc := AhkMagic._RemoteLocateEvalScript(secs, mod["base"])
layout := AhkMagic._RemoteDiscoverLayoutWithProbe(
h, secs, mod["base"], loc)
hook["script_loc"] := loc
hook["script_layout"] := layout
}
oldList := AhkMagic._RemoteFindUserFuncs(h, layout, oldName)
newPtr := AhkMagic._RemoteFindUserFunc(h, layout, newName)
if !oldList.Length or !newPtr
throw Error("function object not found: " oldName " / " newName, -1)
jumpOff := layout["mjump_line_off"]
newJump := AhkMagic._RPtr(h, newPtr + jumpOff)
if !newJump
throw Error("new function body not found", -1)
patched := []
for oldPtr in oldList {
AhkMagic._WPtr(h, oldPtr + jumpOff, newJump)
patched.Push(oldPtr)
}
return Map("old", oldList, "new", newPtr, "jump_off", jumpOff
, "count", patched.Length)
} finally {
DllCall("CloseHandle", "Ptr", h)
}
}
static _RemoteFindUserFunc(h, layout, name) {
list := AhkMagic._RemoteFindUserFuncs(h, layout, name)
return list.Length ? list[1] : 0
}
static _RemoteFindUserFuncs(h, layout, name) {
gscript := layout["gscript"]
arr := AhkMagic._RPtr(h, gscript + layout["mfuncs_off"])
count := AhkMagic._RInt(h, gscript + layout["mfuncs_count_off"])
if count < 0 or count > 100000
throw Error("mFuncs count out of range: " count, -1)
result := []
shortName := name
dot := InStr(name, ".")
fullName := ""
if dot {
shortName := SubStr(name, dot + 1)
fullName := SubStr(name, 1, dot - 1)
. ".Prototype." shortName
}
loop count {
nf := AhkMagic._RPtr(h, arr + (A_Index - 1) * 8)
candidates := [name, shortName]
if fullName != ""
candidates.Push(fullName)
for want in candidates {
data := AhkMagic._RemoteRead(h, nf, 0x500)
found := false
loop data.Size // 2 - StrLen(want) {
off := (A_Index - 1) * 2
ok := true
loop StrLen(want) {
if NumGet(data, off + (A_Index - 1) * 2, "UShort")
!= Ord(SubStr(want, A_Index, 1)) {
ok := false
break
}
}
if ok and NumGet(data, off + StrLen(want) * 2, "UShort") = 0 {
found := true
break
}
}
if found
result.Push(nf)
}
}
return result
}
; Validate text with the interpreter's own /validate switch before it
; reaches the loader, so a syntax error becomes a clean library error
; carrying the real parse message instead of a dialog or an aborted
; host process (in-process EvalScript) or target (RemoteEvalScript).
; The validator is driven by its captured output and a completion
; marker file, never by process exit codes, and its process tree is
; killed the moment the error text lands so no build can leave a
; dialog on screen. When no signal arrives within six seconds the
; check is treated as inconclusive and the loader's own failure check
; remains the fallback guard.
static _ValidateScript(text, ahkPath) {
if !ahkPath or !FileExist(ahkPath)
return
tag := Format("{:x}", A_TickCount) "-" Format("{:x}", Random(1, 0x7fffffff))
vFile := A_Temp "\ahk_hack_validate_" tag ".ahk"
errFile := A_Temp "\ahk_hack_validate_" tag ".err"
batFile := A_Temp "\ahk_hack_validate_" tag ".bat"
doneFile := A_Temp "\ahk_hack_validate_" tag ".done"
try {
; #Warn off keeps warning text out of the captured stream so
; only real errors drive the throw below.
FileAppend "#Warn All, Off`n" text, vFile, "UTF-8"
; The command line is routed through a batch file because AHK's
; Run re-parses nested quotes and can silently drop switches;
; inside the batch, cmd.exe receives the arguments verbatim.
; Format builds the quoted line (no manual quote escaping), and
; the batch is written without a BOM (cmd would otherwise fold
; the BOM into the first token). /ErrorStdOut routes load
; errors to the captured stream so the validator never needs a
; dialog.
batLine := Format('"{1}" /validate /ErrorStdOut "{2}" >"{3}" 2>&1'
, ahkPath, vFile, errFile)
FileAppend batLine "`n", batFile, "UTF-8-RAW"
; The done marker is only written after the validator exits
; (cmd runs batch lines sequentially), so it is a reliable
; completion signal - process handles/PIDs are not, because
; AHK's Run may surface an intermediate shell whose PID is
; immediately reused.
FileAppend Format('echo done >"{1}"`n', doneFile)
, batFile, "UTF-8-RAW"
Run(Format('"{1}"', batFile), , "Hide", &vpid)
; The capture file is the primary signal: as soon as the error
; text lands, kill the whole validator tree (some builds still
; block on a dialog after writing it) and throw with the real
; parse message. The done marker means the validator finished
; with no captured error: valid text.
deadline := A_TickCount + 6000
loop {
size := FileExist(errFile) ? FileGetSize(errFile) : 0
if size > 0 {
RunWait(Format('taskkill /pid {1} /T /F', vpid)
, , "Hide")
detail := Trim(FileRead(errFile, "UTF-8"))
if StrLen(detail) > 300
detail := SubStr(detail, 1, 300) "..."
throw Error("script validation failed: " detail, -1)
}
if FileExist(doneFile)
break
if A_TickCount >= deadline {
; No error text and no completion marker: kill the tree
; and treat validation as inconclusive - the loader's
; own rc check still guards the real load.
RunWait(Format('taskkill /pid {1} /T /F', vpid)
, , "Hide")
return
}
Sleep 20
}
} finally {
; The validator tree may still be releasing its file handles
; when this runs (taskkill returns before handles close), so
; cleanup races must never mask the real validation error.
try {
if FileExist(vFile)
FileDelete(vFile)
if FileExist(errFile)
FileDelete(errFile)
if FileExist(batFile)
FileDelete(batFile)
if FileExist(doneFile)
FileDelete(doneFile)
} catch {
}
}
}
static EvalSubprocess(expr) {
AhkMagic.Init()
if !(expr is String)
throw TypeError("expr must be a string", -1)
if expr = ""
throw ValueError("expr must not be empty", -1)
if StrLen(expr) > 4096
throw ValueError("expr too long", -1)
tag := Format("{:x}", A_TickCount) "-" Format("{:x}", Random(1, 0x7fffffff))
scriptFile := A_Temp "\ahk_mcode_eval_" tag ".ahk"
resultFile := A_Temp "\ahk_mcode_eval_" tag ".txt"
quote := expr
script := "try {`n"
. " result := (" quote ")`n"
. " FileAppend result, `"" resultFile "`", `"UTF-8`"`n"
. "} catch as e {`n"
. " FileAppend `"ERROR: `" e.Message, `"" resultFile "`", `"UTF-8`"`n"
. " ExitApp 1`n"
. "}`n"
FileAppend script, scriptFile, "UTF-8"
try {
; Compiled exes built from the regular v2 runtime can re-enter
; interpreter mode with /script; the SC build does not support it.
cmd := A_IsCompiled
? Format('"{1}" /script /ErrorStdOut "{2}"', A_AhkPath, scriptFile)
: Format('"{1}" /ErrorStdOut "{2}"', A_AhkPath, scriptFile)
RunWait cmd, , "Hide"
if !FileExist(resultFile)
throw Error("eval child produced no result", -1)
result := FileRead(resultFile, "UTF-8")
if RegExMatch(result, "^ERROR: (.*)$", &m)
throw Error("eval failed: " m[1], -1)
return result
} finally {
try {
if FileExist(scriptFile)
FileDelete(scriptFile)
if FileExist(resultFile)
FileDelete(resultFile)
} catch {
}
}
}
}
MC_INTERNAL_LOCATOR_X64 := "4157415641554154565755534881ec38030000488bb424a8030000c744242c000000004885c90f94c04885d2410f94c24108c24d85c0410f94c34508d34885f6410f94c2b8010000004508da0f854a0200004c8b9c24a00300004929d10f92c04929d3410f92c24108c2b8020000000f85270200004d8d9100000200b8030000004d39c20f8712020000498d41074c39d00f87000200004801ca4881c10000010031ff4c89c8eb180f1f840000000000488d58014883c0084c39d04889d8777c803c024875ea488d1c02807b018d75e0807b020d75da4c6373034c01f34883c3074839cb72ca4189ff4531f685ff74154a399cf430010000740b49ffc64d39f775ee4589fe4139fe400f95c583ff40410f93c44108ec75134a899cfc3001000042c744bc3000000000ffc74139fe73804489f3ff449c30e974ffffff85ff0f845301000089fb89d883e00383ff040f832202000031ff4531f631c94885c074384a8d1cf44881c3300100004e8d34b44983c6304531ffeb100f1f84000000000049ffc74c39f87410438b2cbe39cd76f04a8b3cfb89e9ebe84885ff0f84f6000000b8050000004981f8000100000f82e900000031c94889d3eb20660f1f4400004c8d71014881c10101000048ffc34c39c14c89f10f87c2000000803c0a4475e0807c0a018975d9807c0a024c75d2807c0a032475cb807c0a042075c441be04000000eb106666662e0f1f8400000000004983c60242807c33fc66752442807c33fd83751c42807c33fe3a751442807c33ff0074386666662e0f1f8400000000004981fe000100000f8473ffffff42807c33fd6675bb42807c33fe8375b342807c33ff3a75ab42803c330075a44c8d72ff4885c97418410fb6040e3dc3000000740e3dcc000000740748ffc975e831c94801d14531ffeb2eb8040000004881c4380300005b5d5f5e415c415d415e415fc3498d5f014983c706b8080000004d39c74989df77d742803c3ae875e44a8d043a486358014801d84883c0054839c875d04d85ff740f4c89fb41803c1ecc740748ffcb75f431db488d83000100004c39c077ae4801d3b804000000eb044883c002807c03fc667515807c03fd83750e807c03fe3a7507807c03ff007427483d000100000f8478ffffff807c03fd6675cd807c03fe8375c6807c03ff3a75bf803c030075b94d39d34d0f42d3498d41104c39d00f866d0100004531f6488d4a064531ffe99100000083e3fc31ff4531f631c9eb1b66666666662e0f1f8400000000004983c6044c39f30f84befdffff428b6cb43039cd760a4a8bbcf43001000089e9428b6cb43439cd760a4a8bbcf43801000089e9428b6cb43839cd760a4a8bbcf44001000089e9428b6cb43c39cd76b14a8bbcf44801000089e9eba54d8d5f014983c70648ffc1b8060000004d39c74d89df0f8795feffff42803c3ae875dd4a8d043a4c6360014c01e04829d04883c0054c39c875c64d8d5f05498d470a4c39c0400f97c5498d47654939c3410f93c34108eb75a741bb050000004d29e34531e442807c21ffe8750d4e632c214b8d2c234c01ed75264f8d2c274983c5064939c50f8375ffffff4f8d2c274983c50b49ffc44d39c576cae960ffffff4c8d4c242c4889d54889d14c89c24d89d0e80f01000085c00f84f90000004c01ed4c01fd498d042c4883c00a48893e4c89760848895e10488946188b44242c89462031c0e9c8fdffff4d89ceeb14498d46014983c6114d39d64989c60f877afeffff42803c324875e542807c32018975dd42807c32025475d542807c32032475cd42807c32041075c542807c32055575bd42807c32065675b542807c32075775ad42807c32084175a542807c320954759d42807c320a41759542807c320b55758d42807c320c41758542807c320d560f8579ffffff42807c320e410f856dffffff4983fe020f8263ffffff42807c320f570f8557ffffff42807c32ffcc0f854bffffff42807c32fecc0f853fffffff4901d6e9c8fdffffb807000000e9f0fcffff0f1f4000498d80000800004839d0480f42d04983c00431c0eb0d662e0f1f84000000000049ffc04939d0772842807c01fc8375f042807c01fe1075e8460fb65401ff458d5ace4180fb3277d8458911b801000000c3"
MC_MEM_SCRIPT_X64 := "5657534883ec204c89c64989ca488b4c24684d85d20f94c04885d2410f94c04108c04d85c9410f94c34508c34885c9410f94c0b8010000004508d80f85c80100008b4424600f57c00f11010f1141100f1141200f1141300f1141400f1141500f1141600f1141700f1181800000000f1181900000000f1181a00000000f1181b00000000f1181c00000000f1181d00000000f1181e00000000f1181f00000004c8d81000100000f1181000100000f1181100100000f1181200100000f1181400100000f1181500100000f1181600100000f1181700100000f1181800100000f1181900100000f1181a00100000f1181b00100000f1181c00100000f1181d00100000f1181e00100000f1181f00100004c8d1d0a0100004c89194c8d1d300100004c8959084c8d1d350100004c8959104c8d1d5a0100004c8959184c8d1d9f0200004c8959204c8d1da40200004c8959284c8d1da90200004c8959304c8d1dae0200004c8959384889890001000048c781080100000c00000049bb00000000b00400004c899910010000c781180100000200000066c7812c01000000000f1181300100004c898940010000898148010000c6814c010000004c898950010000898158010000c6815c010000004c8989600100004885f674048b3eeb0231ff4885f60f94c34889d14c89c24189f841ffd289c131c083f9010f95c108cb740688c801c0eb06ffc7893e31c04883c4205b5f5ec36666666666662e0f1f84000000000048c7414000000000c7414800000000c6414c0048c7415000000000c7415800000000c6415c0048c7416000000000c390b001c3666666662e0f1f84000000000048c7414000000000c7414800000000c6414c0048c7415000000000c7415800000000c6415c0048c7416000000000c390565753488b41404c8b51504c8b4960448b59484c29d04c01d84589c34c39d8410f43c085c074214189c083f808410f92c34889d64c29d64883fe200f92c34408db740d4531dbeb7e4531c0e9e300000083f82073054531dbeb424589c34183e3e031f6666666662e0f1f840000000000410f100432410f104c32100f1104320f114c32104883c6204939f375e34539c30f849d00000041f6c018742a4c89de4589c34183e3f8662e0f1f840000000000498b3c3248893c324883c6084939f375ef4539c3746d4c89c74c89de4883e703741e4c89de66662e0f1f840000000000410fb61c32881c3248ffc648ffcf75f04d29c34983fbfc773a0f1f8000000000450fb61c3244881c32450fb65c320144885c3201450fb65c320244885c3202450fb65c320344885c32034883c6044939f075cd4d01c24c8951504d01c14c8949605b5f5ec366662e0f1f84000000000031c0c3666666662e0f1f84000000000031c0c3666666662e0f1f840000000000488b4150482b4140c30f1f80000000008b4148c3"
MC_INPROC_EVAL_X64 := "4157415641554154565755534881ecf80000004c8ba424b00100004c8bbc24680100004c8b94246001000048c78424e00000000000400048c78424c8000000000000004885c90f94c04885d2410f94c34108c34d85c00f94c04d85c90f94c308c34408db4d85d20f94c008d84d85ff410f94c34883bc2488010000000f94c34408db08c34883bc2498010000000f94c04883bc24a001000000410f94c34108c34d85e4400f94c7b8010000004408df4008df40f6c7010f85b10900004d8b6c2470498b7424604d8b7424684983fd100f92c04d8d5e044d39eb410f97c3488d7e084c39ef0f97c34408db08c3b80a0000000f85760900004c89b424980000004889b424b8000000498b7424784d8bb424900000004889b424c00000004c8d5e084d39eb0f97c3498bb424a0000000488d7ebf4883ffcf400f92c74c89b424a000000049ffc64939f6400f97c54d8b9c24980000004008dd498d5b0448897424504839f30f97c34008fb4008eb0f85fb0800004c895c2470488bbc2480010000488bb42478010000498b8424800000004889842480000000498b8424880000004889442478498d81000100004885ff4989fb4c0f44d8498d99800100004d8db1000300004c89b424d80000004c89b424f00000004885f6490f44f10f85980000000f57c0410f1101410f114110410f114120410f114130410f114140410f114150410f114160410f114170410f118180000000410f118190000000410f1181a0000000410f1181b0000000410f1181c0000000410f1181d0000000410f1181e0000000410f1181f00000004d8b342441c60436034d8b74240841c60436014d8b7424104d85f6740841c70436010000004d8b7424184d891c360f57c00f11030f1143100f1143200f1143300f1143400f1143500f1143600f1143706641833a00741e4531f66666662e0f1f8400000000006643837c7202004d8d760175f3eb034531f648898c24900000004885ff4c899c24880000004889b424b00000000f85e0020000498d890008100048894c24680f57c00f11000f1140100f1140200f1140300f1140400f1140500f1140600f114070498b44242042c6041800498b44242842c6041801498b44243046893418498b4424384e891418498b44245042c7041800010000498b44245842c70418000100004531db4c89d54889e931c00f1f40000fb73c0185ff0f841e02000083ff27743e83ff2274398d779f6683fe1a0f82070100006683ff5f0f84fd0000008d77a56683fee50f87f00000000fb7f781fe800000000f83e10000004883c002ebb1488d2c014883c502440fb7740102664585f6410f94c7664139fe400f94c64408fe753966666666662e0f1f840000000000664183fe60750b66837d020074044883c502440fb775024883c502664585f6410f94c77406664139fe75d54584ff4c8bbc24680100000f856e0100004181fbfe000000775c4489de480faf7424504803742468488d3c014883c7024d89de4c8b9c248000000049893c334c8b5c247841c60433014c8b9c24a00000006641c7043303014d89f34889ef4829cf4829c74883c7fe48d1ef488b442470893c3041ffc34883c502e9cdfeffff4c895c24584c8d3c014531f64c89fdeb100f1f4400000fb77d024883c50241ffc68d779f6683fe1a72ec8d77bf6683fe1a400f92c66683ff5f410f94c34108f375d4440fb7df4181fb80000000410f92c38d77c66683fef6400f92c64184f374b54d39d7742f488b7c245881ffff000000410f92c366837c01fe2e400f95c64484de4989fb4c8bbc24680100000f843afeffffeb1a4c8b5c24584181fbfe0000004c8bbc24680100000f871efeffff448b5c24584c0faf5c24504c035c24684801c1488b8424800000004a890c18488b4424784ac7041800000000488b8424a00000006642c704180000488b442470468934184c8b5c245841ffc3e9cdfdffff4585db4c89d84c8b9c2488000000488bb424b0000000742189c0488b4c2450480fafc84c8b5424684ac7041100000000498b4424404e8914184c894c2458488954246849c707010000000f57c0410f1147084d8b304c894424504989304c8d8424c80000004889f14c89daff94249000000089c5488b8c24c80000004885c97407ff9424a001000083fd010f85e30100004883bc247001000000488b5424500f84e3010000498b442448488b8c2488000000488b3c084885ff0f84de0100004c897424604c8b9c24a8010000488b8424980000008b0407be010000004439d8488b4c24584c8b8424900000004c8b5424700f85b5010000488b8424900100004885c00f94c24c39c0410f94c04108d0488bbc2488000000488bac24b000000075164889e94889faffd0488b4c245883f8010f85a8020000488b9424d800000048899424e80000004c8bb4249800000041c7840e80010000ffffffff488b8424c000000048c7840880010000ffffffff4989940d8001000049c7840d88010000000000004881c100033f0048894c2438488d8424e00000004889442430488d8424f00000004889442428488d8424e8000000488944242048c744244000003f004c8d8424ac0000004889e931d24989d9ff542468488b4c2450488b5424604889114885c00f84d601000041c70700000000418b0c1e41894f04488b8c24b8000000488b0c1949894f0849894f10498d4f204889ca4829da4883fa0f0f87ec010000ba05000000440fb64413fb44884411fb440fb64413fc44884411fc440fb64413fd44884411fd440fb64413fe44884411fe440fb64413ff44884411ff440fb60413448804114883c2064883fa3575b6e9ae010000488b4424504c893041c70702000000e9590200004c893248b80000000063000000498907e9440200004c893241c70708000000e9350200004c8b8c24c00000004a8d2c0f4c8b8c24b80000004e8d340f4c01ef4803bc249800000049b9010000000080ffff4981c1010001004c898c24d0000000eb234c8b4c24784a8b04084989068b074c01ed4d01ee4c01ef48ffc64439d80f84eafdffff4881fe010001000f84de0000003d011000000f83a600000083f80475cc837d000277c6498b064885c074be49b9010000000080ffff4901c14c3b8c24d0000000727c4c8b8c24a000000042803c0807748c468b0410488b8c2480000000488b1408488b8c248801000041b903010000ff9424980100004c8b9c24a80100004c8b5424704c8b842490000000488b4c2458488b5424504885c00f8548ffffff488b44246048890241c70705000000e92201000041c70703000000e916010000488b44246048890241c70709000000e902010000488b442450488b4c246048890841c70706000000e9e9000000488b44246048890241c70707000000e9d50000000f10030f11010f1043100f1141100f1043200f114120418b4f0483f905774aba270000000fa3ca7340498b442448488b043849894750498b44244848833c38000f8481000000490faff54885f674784881fe20010000b820010000480f42c64883fe01751231c9eb4841c747040000000049894710ebb289c281e2fe01000031c94d8b4424484d8b0438450fb604084588440f584d8b4424484d8b0438450fb64408014588440f594883c1024839ca75d0a8017412498b442448488b04380fb604084188440f588b8424ac000000418987c800000031c04881c4f80000005b5d5f5e415c415d415e415fc3"