Objective and method
The goal is to analyze AutoHotkey64.exe as a live interpreter and extract enough structure to implement expression evaluation, script loading from memory, remote hooking, and live function tracing. The method is a repeatable loop: read the source tree to identify candidate fields, inspect process memory to confirm their actual offsets, encode the stable logic as position-independent machine code, then validate every result against structural invariants.
This page documents that loop in detail.
PE parsing and section mapping
Every x64 AutoHotkey v2 executable starts with an MZ DOS header. The PE signature is at the offset stored in e_lfanew. The optional header uses PE32+ magic 0x20B. The section table immediately follows the optional header.
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")
}
The scanner stores each section as {rva, size, ptr} and classifies it by name. Three sections matter:
| Section | Role | Invariant |
|---|---|---|
.text | executable code | function pointers must land here |
.rdata | read-only strings | UTF-16 names usually live here |
.data | writable globals | interpreter tables live here |
For packed builds, section names are unreliable. The scanner falls back to content-based classification: non-.rsrc code-like sections are treated as executable, and readable non-executable sections are treated as data/string storage.
Interpreter table discovery
The interpreter keeps three tables in the image. Their record strides are stable on x64:
| Table | Contents | Record stride |
|---|---|---|
g_BIF | script-visible built-ins such as Abs and Sin | 0x20 |
sMdFunc | typed native functions such as MsgBox | 0x28 |
g_BIV_A | built-in variables beginning with A_ | 0x18 |
The scanner does not hardcode addresses. It walks pointer-aligned slots and accepts a record only when:
- the first qword resolves to a printable UTF-16 string;
- the second qword resolves into
.text; - neighboring records are sorted by name;
- a known anchor is present, such as
Abs,BlockInput, orAhkPath.
The longest valid run becomes the table. The same algorithm runs as x64 machine code, so the scan speed and the scan logic do not depend on the AHK version.
Source-to-memory validation
Source code gives names and intent, not final offsets. The compiler, build flags, and debugger fields move members. The workflow therefore uses source to form hypotheses and memory to confirm them.
For UserFunc in script.h, the source declares:
class UserFunc : public Func
{
public:
int mInstances = 0;
Line *mJumpToLine = nullptr;
FuncParam *mParam = nullptr;
Object *mClass = nullptr;
...
VarList mVars {};
VarList mStaticVars {};
};
The memory probe confirms the actual x64 layout on two representative builds:
| Field | v2.1-alpha.30 | v2.0.26 |
|---|---|---|
mName | 0x28 | 0x20 |
mJumpToLine | 0x48 | 0x40 |
mParam | 0x50 | 0x48 |
mVars | 0x80 | 0x70 |
The offsets differ, so the library discovers them at runtime and does not maintain a version table.
Machine-code extraction pipeline
The stable scanner is written in C under lib/mcode/. The build uses clang to produce a freestanding, position-independent COFF object:
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 performs four checks:
- extract
.textfrom the COFF object; - decode every relocation record;
- accept
IMAGE_REL_AMD64_REL32only when the target stays inside.text; - reject any blob with an escaping relocation.
The final hex string is embedded into ahk_hack_single.ahk. MCode() decodes it into a Buffer, changes protection to executable, and returns it for DllCall.
EvalNative: expression evaluation analysis
AutoHotkey does not evaluate expression strings directly. During script load, Line::ExpressionToPostfix compiles an expression into an ExprTokenType postfix array. Line::ExpandExpression later evaluates that array.
EvalNative() constructs the same temporary Line and ArgStruct the parser would have built, then calls those two functions. This reuses the interpreter's own grammar and variable resolution.
The internal locator machine code discovers:
| Symbol | Source | Discovery |
|---|---|---|
ExpressionToPostfix | error string "Missing operand." | RIP-relative ref + prologue walk |
ExpandExpression | error string "Error evaluating expression." | RIP-relative ref + prologue walk |
gScript | most frequent lea rcx,[rip+disp] target | frequency filter |
FindOrAddVar | six-argument prologue | signature match |
SYM_INVALID | postfix terminator | immediate inside evaluator |
The token layout used by the evaluator is:
| Field | Offset | Type |
|---|---|---|
| value | 0x00 | pointer/int64/double union |
| marker/usage | 0x08 | pointer or VarRefUsageType |
| symbol | 0x10 | 32-bit SymbolType |
| stride | 0x18 | total struct size |
#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: parser and in-memory loading
Expression eval cannot define functions or classes. EvalScript() feeds multi-line text through the interpreter's include pipeline from an in-memory TextStream.
The relevant pipeline is:
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;
...
}
Before loading, the library saves and later restores:
mOpenBlock/mLineParent;mPendingRelatedLine;mLastParamInitializer;mPendingHotkey;mExprFuncIndex;mNextLineIsFunctionBody;mIgnoreNextBlockBegin;mBackCompatMode;mClassObjectCount;mCurrentModuleandmLastModule.
Parser-state anchors are read from the LoadIncludedFile prologue:
q: the offset ofmOpenBlock/mLineParent;d: the offset ofmClassObjectCount.
The TextStream object is built with both possible mData regions populated, so the same object works for 2.0 and 2.1 layouts; every load is validated by its result (rc == 1 plus a function-count delta for the probe).
Remote introspection
AttachRemote(pid) opens the target with OpenProcess, reads its PE image, scans its interpreter tables, and returns a hook map.
Remote eval injects a thread stub plus the locator and evaluator blobs:
| Step | Operation |
|---|---|
| 1 | allocate a scratch block with VirtualAllocEx |
| 2 | write stub, locator, evaluator, expression, output buffers |
| 3 | create a remote thread with CreateRemoteThread |
| 4 | wait and read the result buffer |
| 5 | free the scratch block |
RemoteEvalScript() uses the target's own LoadIncludedFile and PreparseExpressions. The injected thread sets g->CurrentFunc before preparse, so class methods and Super parse correctly.
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
Function body replacement swaps mJumpToLine:
AhkMagic.RemoteEvalScript(hook, "
(
NewA() {
return 42
}
NewA()
)")
AhkMagic.RemoteReplaceFuncBody(hook, "A", "NewA")
Send("{F9}")
The target writes A()=42 after the original output was A()=1.
Dynamic offset discovery
The remote layout scanner derives mFuncs, mFuncsCount, the function name field, and mJumpToLine from live memory:
- candidate array/count pairs are validated by reading each
Funcpointer; mNameis a pointer to a valid identifier string;mJumpToLinepoints to aLinethat contains a back-reference to its owningFunc;- the back-reference slot is consistent across ordinary functions.
The struct validator then discovers:
| Struct | Validated fields |
|---|---|
Line | action, argc, arg pointer, attribute, next line |
ArgStruct | is_expression, postfix pointer, deref pointer |
ExprTokenType | stride, symbol offset, usage offset, value offset |
DerefType | marker, var, type, length |
Token arrays are validated by walking until SYM_INVALID and requiring a SYM_VAR token before the terminator. DerefType is read from ArgStruct.deref arrays that already exist in the running process.
TraceFunction: clone-based tracing
Function tracing is harder than function replacement. The original implementation renamed the target function and injected a wrapper. That broke because:
- function names live in
Func.mNameand globalVar.mName; mFuncsandVarListare sorted arrays;- renaming one side breaks binary search;
- body replacement alone does not rebind parameter
Varobjects.
The stable fix reads UserFunc::Call:
bool UserFunc::Call(ResultToken &aResultToken, ExprTokenType *aParam[], int aParamCount)
{
...
for (j = 0; j < mParamCount; ++j)
{
FuncParam &this_formal_param = mParam[j];
...
}
}
The call path writes through mParam[i].var. Therefore two function objects can share the same parameter Var objects, and both bodies see the same values.
The final design:
- clone the
UserFuncobject; - create a
VAR_CONSTANTglobal alias for the clone; - inject a wrapper that calls the clone;
- alias the wrapper's parameter
Varobjects to the original parameters; - replace the original function body pointer;
- on untrace, restore the original
mJumpToLine.
The wrapper logs to a target-side global string, so RemoteEval can read the trace without relying on file I/O inside the evaluator thread.
Verification matrix
tools/verify_all_runtimes.ps1 runs thirteen tests against every runtime found on this machine:
| Test | Covers |
|---|---|
ahk_live_cross_smoke | attach, snapshot, inventory, replace, watch |
ahk_live_test | full trace/replace path |
ahk_live_product_test | session and patch rollback |
ahk_live_six_test | observability, fault, desktop, forensics, agent, CSV |
ahk_live_benchmark | eval/snapshot/list timings |
remote_hook_test | remote eval, script, class, function replacement |
evalscript_inproc | in-process script loading |
evalscript_repeat | repeated function/class loading |
evalscript_class | class definitions |
eval_object_syntax_test | object return values and syntax-error survival in-process |
remote_empty_object_test | remote object results and syntax-error survival |
remote_empty_target_test | probe-injection retry on targets with no user functions |
remote_deep_redirect_test | deep redirect through direct and dynamic call chains |
Current result:
| Runtime family | Builds | Result |
|---|---|---|
| 2.0 beta | beta.9, beta.10, beta.12, beta.13, beta.15 | 13/13 each |
| 2.0 rc | rc.1, rc.3 | 13/13 each |
| 2.0 stable | 0.0, 0.2, 0.3, 0.4, 0.26 | 13/13 each |
| 2.1 alpha | alpha.1, alpha.4, alpha.13, alpha.16, alpha.30 | 13/13 each |
| local dev builds | two 2.0-beta binaries | 13/13 each |
Total: 19 runtimes, all 13/13.
11.1 Regression coverage: error capture
The GUI converts every AhkLiveResult into a visible log line. OnError() appends exception fields to both the GUI log and %TEMP%\ahk_live_gui_error.log; SafeStr() serializes Map and Array values so a second error cannot break the logger. Watch and observability errors are forwarded to the callback as Error objects. --shot selects a tab and writes a ready marker for full-window screenshot automation.
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)
The regression test tests\ahk_live_regression.ahk covers three failure modes found while building the GUI:
EvalScriptfollowed byTraceFunction, which requires the cachedscript_layoutto backfillfind_varfromscript_loc;- snapshot expressions that contain commas inside parentheses, which must not be split as spec separators;
- duplicate top-level function names, which are rejected before
LoadIncludedFileruns:
for name in AhkMagic._DeclaredFunctionNames(text) {
if AhkMagic._RemoteFindUserFuncs(h, layout, name).Length
throw Error("function name already exists in target: " name, -1)
}
The new regression tests pass on 2.1-alpha.30 and 2.0.26. The 19-runtime thirteen-test matrix — now including the four robustness regressions above — remains the full compatibility gate, and all 19 runtimes pass 13/13.
Security and boundaries
This library allocates executable memory, executes machine code, and writes interpreter memory in the current or target process. It is not a sandbox.
Rules:
- only attach to processes you own;
- use disposable targets for repeated experiments;
- restore patches in
finallyor throughAhkLivePatchSession.Rollback(); - never eval untrusted input;
- treat locator failures as errors, never as a reason to guess.
Packed or hostile builds can rename sections or remove strings. The content-based scanner handles common packers, but a hostile build can still defeat discovery.
Failure behavior
Every path that reads interpreter memory or applies a patch is designed to fail explicitly:
| Area | Behavior |
|---|---|
| Table and export scans | counts are capped against the scanner contract and every name pointer is memory-map checked before use; unsupported layouts throw instead of reading out of bounds |
| Syntax errors | EvalScript/RemoteEvalScript pre-check the text with the interpreter's own /validate (the target's executable for the remote path); the validator runs hidden, captures its output (no dialogs), and kills its process tree on timeout |
| Remote timeouts | a hung evaluation throws after 10 s and leaks its injected block, because freeing memory the target is still executing would corrupt it |
| Object results | EvalNative returns live Func/Class objects; RemoteEval returns the target-side object address |
| Empty targets | RemoteEvalScript injects randomized probe functions and retries layout discovery when the target has no user functions |
| Patches | PatchBifObject and RemoteDeepRedirect commit atomically with rollback; the discovery probe restores its interpreter snapshot in finally |
Further reading
- Repository: MonoEven/ahk-hack-library
- Single-file core: ahk_hack_single.ahk
- Machine-code sources: lib/mcode
- AhkLive layer: lib/ahk_live
- Full matrix report: reports
Appendix: single-file core
The copy below is generated from the repository file by the build script.
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"