Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

AHK_L Decompiler (Payload Method)


  • Please log in to reply
69 replies to this topic
IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007

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



Nostalgia
  • Guests
  • Last active:
  • Joined: --
I remember that back in the day you've been against "decompilers" for AHK... and now you create your own? :?

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007

Hi Nostalgia,

This is a tool like Exe2AHK, just for AHK_L.

Back then, Autohotkey Basic had a password functionality and stated public that it is "protected". I just took up a project which patched the Exe so that the Decompiler could not automatically find the password.

However, I've come to the conclusion, that it is nonsense to propagate security when none exists.

We are better off, accepting the fact that there is no protection in a interpreted scripting-language.

Nowadays, we have a more open system, by default the source is attached as resource.
Given the fact, that almost everything here is open source, I don't expect any harm to the public. If you rely on the belief that your compiled script is not visible to anyone, you potentially store passwords etc. in plaintext in your script. I think in knowing that extraction of the source is easy brings people away from storing such things in their script.

My motivation btw was, to check some suspicious files for malware. As this task comes very often over me, I wanted it automated. Et voilà!



  • Guests
  • Last active:
  • Joined: --
Very nice :)

  • Guests
  • Last active:
  • Joined: --
By the way, it works on ANSI version of Ahk_l, not Unicode.

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007

By the way, it works on ANSI version of Ahk_l, not Unicode.

Thanks, I had a little issue in the encoding. Its now fixed.

However, the first method - Resource-Loading - will not work, when the running decompiler is not in the same bit mode. 32bit Decompiler will not be able to decompile 64bit-Exe and vice-versa.
The generic Unpacker will work always at the cost of executing the target and consuming some processing time.

  • Guests
  • Last active:
  • Joined: --
Added it to faq <!-- m -->https://ahknet.autoh...html#protection<!-- m --> :wink:

guest
  • Guests
  • Last active:
  • Joined: --
Doesn't work on http://macro-bar.com/link/ref.php?id=6

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007

Added it to faq

Thanks. I think its very important that users are aware of the existence, so they will protect important data in better ways.

Doesn't work on

Actually, it does work with the current version. However, please do not ask for specific Tools to be supported by the decompiler. I may add enough configuration possibilities to help skilled people to manage it, but I decided to not include predefined configurations.

Anyway, you can always find the script in the minidump if you search for common keywords like "return".

  • Guests
  • Last active:
  • Joined: --

Added it to faq

Thanks. I think its very important that users are aware of the existence, so they will protect important data in better ways.

I agree - not sure if it has been added as I don't compile but I suggested to fincs to incorporate a warning "your script is NOT automatically protected by compiling it" in the compiler Gui. You see people posting compiled scripts with ftp / email passwords etc they should be aware it is might not be secure as they think.

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007
Posted Image

fragmannli
  • Guests
  • Last active:
  • Joined: --
nice one! while we're at it, how would you protect sensitive data like passwords or API keys in a script without relying on an external server? I only have some API keys which are currently encoded, but anyone with AHK knowledge could simply decode them.

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007

Hey fragman,

Well there are very diffrent usecases.

The script uses or generates sensitive User data

  • For integrity and authentication purposes, you can rely on hashes most time. You do not save the original pw, but a hash of it.
  • To protect high sensitive data, you encrypt it with a secure algorithm. The password must not be stored/cached somewhere, or its not secure. (All "remember my password" methods are mutually insecure)


The Script Author wants to protect something

  • Anything which is locally encrypted but will be decrypted automatically at runtime by the script itself is mutually insecure.
  • Accessing directly administrative services such as (S)FTP or Databases is insecure when the connection is not encrypted. Even if the password is obfuscated or a simple encryption is used, its very easy to sniff the data by man in the middle attacks and also manipulate the data. Do not access any administrative service directly.
  • Instead: Accept that the API is public accessible, and therefore design it as such and handle user authentication by your self.
  • Example: Use Server-Side code (PHP etc) as the only interface to your internal services like DBs

As you can see, as long as you store the API keys (accessing external services from 3th parties) local on the users hard drive, it is insecure as hell.
Obfuscation is very often easy to break - especally when the source is open accessible like here. On the other hand, even without the source, netowork sniffers offer an easy way to grab such keys.

You could either force the user to be responsible for his own key (each user has a unique key). If this is not possible, you are forced to use a proxy server, which will offer the same interface as the API but has a custom authentication system. If a user has authenticated successfully, you simply redirect requests to the real API service and using the key only server side.

This way, you can also deal with leaked user accounts - if a user account does access from multiple ips at the same time you can block them easily.

Personally, I would base every online system with server side authentication. This is the only secure way. Virtually any offline system can not be protected, you just can obfuscate things - refer to cracked Games etc.



fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
That's what I thought so far as well, I was just wondering if there were better local methods :(
I'm only concerned about some Google APIs and an image hoster API, but it's still bothersome...

IsNull
  • Moderators
  • 990 posts
  • Last active: May 15 2014 11:56 AM
  • Joined: 10 May 2007
I've added an native shellcode to search for the pattern in the dump. (Credits @ Mr. Laszlo)
If you are using 64bit AHK, the 64bit version of the shellcode doesnt work so well, thus for now a separate dll is required. (It will be downloaded automatically)

C++
__declspec(dllexport) long __cdecl PatternScan(unsigned char *targetBuffer, unsigned long targetSize, unsigned char *needle, unsigned long needleSize);


long PatternScan(unsigned char *targetBuffer, unsigned long targetSize, unsigned char *pattern, unsigned long patternSize){
	
	long pos = -1;
	int iPattern = 0;

	for(int i = 0; i < targetSize; i++)
	{
		if(targetBuffer[i] == pattern[iPattern])
		{
			
			// the byte matches our pattern
			// check if we are done (and found a match)

			if(patternSize == iPattern+1){
				return i;
			}else
				iPattern++;

		}else
			iPattern = 0; // reset the pattern
	}

	return pos;
}

The speed-improvement is enormous :)