This tool helps to extract the source from a compiled AHK_L Script. It uses a payload (injected dll) which is able to extract any resource. This method was discovered by fincs, and the payload dll bases upon his code. (thx!) By default, the resource-name gets patched, so the AHK Script cannot be executed.
AHK Basic compiled executables are NOT supported nor is any support planned.
There are two common use-cases:
You have lost your Source. This Tool will help you.
You have to check the Contents of a compiled AHK Program for malware. The script-contents will not be executed by the payload method.
In theory, most packers/crypter are tricked by this method.
The following packers are known to be bypassed:
- UPX
- Mpress
- XPack/XComp
- Engima
AHK_L Decompiler
/* * AHK_L Decompiler / Source Extractor * Written by IsNull 2012 * the payload method bases on fincs injection example * * This Script suports generic resource extraction * * * This version is based upon a dll injection which can extract the resources. This method was discovered by fincs, so credits go to him! * */ global DEBUG := false global POSSIBLE_RESOURCE_NAMES := [">AHK WITH ICON<", ">AUTOHOTKEY SCRIPT<"] global PATCHED_RESOURCE_NAMES := [">UHK WITH ICON<", ">UUTOHOTKEY SCRIPT<"] global ExtractionDir := A_ScriptDir "\ExtractionTemp" ;EXTERNAL RESOURCES: global DecompilerPayload_URL := "http://dl.securityvision.ch/tools/decompiler_payload.dll" global DecompilerPayload64_URL := "http://dl.securityvision.ch/tools/decompiler_payload_64.dll" global DecompilerPayload_BIN := A_ScriptDir "\payload.dll" global DecompilerPayload64_BIN := A_ScriptDir "\payload64.dll" global CurrentPayload_BIN := ExtractionDir "\winmm.dll" Gui, new, +Resize Gui, Color, White Gui, add, Edit, w800 h20 gExtractNow vAHKExe, [drag your compiled exe here] Gui, Font,, Consolas Gui, add, Edit, +ReadOnly vMyLog w800 h200 Gui, add, Edit, +ReadOnly vScriptSource w800 h500 c4A708B Gui, Font Gui, show,, AHK_L Decompiler by IsNull LogLn("<Running on: AHK Version "A_AhkVersion " - " (A_IsUnicode ? "Unicode" : "Ansi") " " (A_PtrSize == 4 ? "32" : "64") "bit>") return GuiClose: ExitApp GuiSize: Anchor("AHKExe","w") Anchor("MyLog","w") Anchor("ScriptSource","wh") return ExtractNow: Gui,submit, nohide LogClear() GuiCodeSet("") if(PrepareFile(AHKExe, preparedFile)) { ; file was prepared successfull if(EnsurePayloadIsPresent()) { LogLn("<Injecting payload...>") PlacePayload(preparedFile) RunWait, % preparedFile, % ExtractionDir, UseErrorLevel if(ErrorLevel == "ERROR") { LogLn("<The Target could not be started. Is this a valid PE Executable?>") }else{ SplitPath, preparedFile, OutFileName, OutDir, OutExtension, OutNameNoExt tmpCodePath = %OutDir%\%OutNameNoExt%-uncompiled.ahk try { WaitForFile(tmpCodePath, 10000) ; wait maximum 10secs for the file, this should be enough for even very slow harddisks. }catch e{ LogLn("<" e.Message ">") LogLn("<Missing: " tmpCodePath ">") } if(FileExist(tmpCodePath)) { LogLn("<Payload succeeded. Recovering Script.>") FileRead, script, % tmpCodePath if(!DEBUG) FileDelete, % tmpCodePath GuiCodeSet(script) }else{ LogLn("<Script could not be extracted.>") } if(!DEBUG) FileDelete, % CurrentPayload_BIN ; remove the winmm.dll as it causes trouble if it get accedintially injected.^^ } }else{ LogLn("<Missing payload. Aborting now.>") return } }else{ LogLn("<File seems not to be a valid compiled AHK Script or it uses an unknown protection.>") } if(!DEBUG) FileDelete, % preparedFile return GuiDropFiles: GuiControl,,AHKExe, % A_GuiEvent return /* * Waits until the file is present * can be aborted by the timeout * * file File-Path to check * timeout Timeout in Milliseconds (Max waittime) */ WaitForFile(file, timeout=5000){ start := A_TickCount while(!FileExist(file)) { if((A_TickCount - start) > timeout) throw Exception("TimeoutException: File was not present whithin the expected time.") } } PlacePayload(preparedFile){ success := false SplitPath, preparedFile, OutFileName, OutDir if(Is64BitAssembly(preparedFile)) { LogLn("<Target Application is 64bit.>") payloadSrc := DecompilerPayload64_BIN }else{ LogLn("<Target Application is 32bit.>") payloadSrc := DecompilerPayload_BIN } if(FileExist(payloadSrc)) { FileCopy, % payloadSrc, % CurrentPayload_BIN, 1 success := true } return success } Is64BitAssembly(appName){ static GetBinaryType := "GetBinaryType" (A_IsUnicode ? "W" : "A") static SCS_32BIT_BINARY := 0 static SCS_64BIT_BINARY := 6 ret := DllCall(GetBinaryType ,"Str", appName ,"int*", binaryType) return binaryType == SCS_64BIT_BINARY } EnsurePayloadIsPresent(){ if(!FileExist(DecompilerPayload_BIN)) { URLDownloadToFile, % DecompilerPayload_URL, % DecompilerPayload_BIN LogLn("<" DecompilerPayload_BIN " downloaded.>") } if(!FileExist(DecompilerPayload64_BIN)) { URLDownloadToFile, % DecompilerPayload64_URL, % DecompilerPayload64_BIN LogLn("<" DecompilerPayload64_BIN " downloaded.>") } return FileExist(DecompilerPayload_BIN) } PrepareFile(fileToPrepare, byref preparedFile){ success := false if(!FileExist(ExtractionDir)) FileCreateDir, % ExtractionDir if(FileExist(fileToPrepare)) { LogLn("<Recover Source for " fileToPrepare ">") binaryTarget := ExtractionDir "\patched.exe" preparedFile := binaryTarget FileCopy, % fileToPrepare, % binaryTarget, 1 LogLn("<Starting file analysis...>") ;########## ofile := FileOpen(binaryTarget, "rw") VarSetCapacity(buffer, ofile.Length) bytesRead := ofile.RawRead(buffer, ofile.Length) LogLn("<Readed " bytesRead " bytes from file.>") if(HasPEHeaderMagic(buffer)) { LogLn("<Seems to be a valid PE File.>") for i, resName in POSSIBLE_RESOURCE_NAMES { ahkResourceName := StringToUTFByteArray(POSSIBLE_RESOURCE_NAMES[i]) patch := StringToUTFByteArray(PATCHED_RESOURCE_NAMES[i]) LogLn("<Searching for " ByteArrayToHex(ahkResourceName) " in " bytesRead "bytes.>") if(pos := FindMagic(buffer, bytesRead, ahkResourceName)) { if(PatchBinary(ofile, pos, patch)){ LogLn("<Patched successfull>") success := true break } } } ofile.Close() ; Flush }else{ LogLn("<Whatever you dragged here, this is NOT a valid PE file.>") } ;########## }else{ LogLn("<File Not Found!>") } return success } HasPEHeaderMagic(ByRef buffer){ return (NumGet(buffer,0,"UChar") == 77 && (NumGet(buffer,1,"UChar") == (A_IsUnicode ? 90 : 82))) } PatchBinary(targetfile, pos, byteArrayReplacement){ written := false if(!IsObject(targetfile)){ throw "targetfile: must be a valid file instance" } if(pos != -1) { LogLn("<Found Resource-Name @" pos ">") LogLn("<Patching Resource-Name...>") targetfile.Seek(pos) size := ByteArrayToBuffer(byteArrayReplacement, patched) written := targetfile.RawWrite(patched, size) LogLn("<PatchBinary: Written " written " bytes.>") }else{ LogLn("<Could not find pattern: " ByteArrayToHex(arr) ">") } return written } global mylogData := "" LogLn(line){ global mylogData .= line "`n" GuiControl,,MyLog, % mylogData } LogClear(){ global mylogData := "" GuiControl,,MyLog, % mylogData } GuiCodeSet(scriptcode){ global GuiControl,,ScriptSource, % scriptcode } StringToUTFByteArray(str){ bufSize := StringToUTFBUffer(str, buf) return BufferToByteArray(buf, bufSize) } StringToUTFBUffer(str, byref buf){ ;size := StrPut(str, "UTF-16") ; seems the size is not calculated correctly for UTF-16 Strings... size := (StrPut(str, "UTF-16") - 1) * 2 VarSetCapacity( buf, size, 0x00) StrPut(str, &buf, size, "UTF-16") return size } BufferToHex( ptr, size ) { myhexdmp := "" SetFormat, integer, hex Loop, % size { byte := NumGet(ptr+0, A_index-1,"UChar") + 0 myhexdmp .= byte } return myhexdmp } ByteArrayToHex(arr){ s := "" SetFormat, integer, hex for each, byte in arr { byte += 0 s .= (StrLen(x := SubStr(byte, 3)) < 2 ? "0" x : x ) " " } SetFormat, integer, dez StringUpper, s, s return s } PrintArr(obj) { str := "" for i, val in obj str .= "[" i "] -> " val "`n" return str } PrintArrAsStr(obj) { str := "" for each, val in obj str .= val "(" (val != 0 ? chr(val) : "null") ")" "`n" return str } ToByteArray(str){ bytes := [] Loop, parse, str bytes[A_index] := asc(A_LoopField) return bytes } ByteArrayToBuffer(byteArray, byref buf){ bufferSize := byteArray.MaxIndex() VarSetCapacity(buf, bufferSize, 0x00) for each, byte in byteArray NumPut(byte, buf, A_Index-1, "uchar") return bufferSize } BufferToByteArray(byref buffer, size){ arr := [] loop, % size arr[A_index] := NumGet(buffer, A_Index-1, "UChar") return arr } /************************************************ * FindMagic * * Search in binary data for a given byte-Pattern * * buffer binary data * size size of the buffer * magic Byte-Array of the pattern to search * offset start offset to skip * * returns the position where the found magic starts * -1 indicates that no match was found ************************************************* */ FindMagic(byref buffer, size, magic, offset=0){ magicLen := ByteArrayToBuffer(magic, magicBuffer) magicByte := magic[1] searchPtr := &buffer + offset searchEnd := &buffer + size - magicLen + 1 ; First byte must precede searchEnd. if(searchPtr >= searchEnd) return -1 while searchPtr := DllCall("msvcrt\memchr", "ptr", searchPtr, "int", magicByte , "ptr", searchEnd - searchPtr, "ptr"){ if !DllCall("msvcrt\memcmp", "ptr", searchPtr, "ptr", &magicBuffer, "ptr", magicLen) return searchPtr - &buffer ; I think this is what the script expects... ++searchPtr ; Resume search at the next byte. } return -1 } /* Function: Anchor by Polyethene Defines how controls should be automatically positioned relative to the new dimensions of a window when resized. See http://www.autohotkey.com/community/viewtopic.php?t=4348 */ Anchor(i, a = "", r = false) { static c, cs = 12, cx = 255, cl = 0, g, gs = 8, gl = 0, gpi, gw, gh, z = 0, k = 0xffff If z = 0 VarSetCapacity(g, gs * 99, 0), VarSetCapacity(c, cs * cx, 0), z := true If (!WinExist("ahk_id" . i)) { GuiControlGet, t, Hwnd, %i% If ErrorLevel = 0 i := t Else ControlGet, i, Hwnd, , %i% } VarSetCapacity(gi, 68, 0), DllCall("GetWindowInfo", "UInt", gp := DllCall("GetParent", "UInt", i), "UInt", &gi) , giw := NumGet(gi, 28, "Int") - NumGet(gi, 20, "Int"), gih := NumGet(gi, 32, "Int") - NumGet(gi, 24, "Int") If (gp != gpi) { gpi := gp Loop, %gl% If (NumGet(g, cb := gs * (A_Index - 1)) == gp) { gw := NumGet(g, cb + 4, "Short"), gh := NumGet(g, cb + 6, "Short"), gf := 1 Break } If (!gf) NumPut(gp, g, gl), NumPut(gw := giw, g, gl + 4, "Short"), NumPut(gh := gih, g, gl + 6, "Short"), gl += gs } ControlGetPos, dx, dy, dw, dh, , ahk_id %i% Loop, %cl% If (NumGet(c, cb := cs * (A_Index - 1)) == i) { If a = { cf = 1 Break } giw -= gw, gih -= gh, as := 1, dx := NumGet(c, cb + 4, "Short"), dy := NumGet(c, cb + 6, "Short") , cw := dw, dw := NumGet(c, cb + 8, "Short"), ch := dh, dh := NumGet(c, cb + 10, "Short") Loop, Parse, a, xywh If A_Index > 1 av := SubStr(a, as, 1), as += 1 + StrLen(A_LoopField) , d%av% += (InStr("yh", av) ? gih : giw) * (A_LoopField + 0 ? A_LoopField : 1) DllCall("SetWindowPos", "UInt", i, "Int", 0, "Int", dx, "Int", dy , "Int", InStr(a, "w") ? dw : cw, "Int", InStr(a, "h") ? dh : ch, "Int", 4) If r != 0 DllCall("RedrawWindow", "UInt", i, "UInt", 0, "UInt", 0, "UInt", 0x0101) ; RDW_UPDATENOW | RDW_INVALIDATE Return } If cf != 1 cb := cl, cl += cs bx := NumGet(gi, 48), by := NumGet(gi, 16, "Int") - NumGet(gi, 8, "Int") - gih - NumGet(gi, 52) If cf = 1 dw -= giw - gw, dh -= gih - gh NumPut(i, c, cb), NumPut(dx - bx, c, cb + 4, "Short"), NumPut(dy - by, c, cb + 6, "Short") , NumPut(dw, c, cb + 8, "Short"), NumPut(dh, c, cb + 10, "Short") Return, true }
This project is more an experiment than anything else, but I think it could be interesting for some fellows here.
Cheers