Deobfuscating AutoIt scripts

kao

Every once in a while, someone posts an interesting challenge concerning protected or obfuscated AutoIt scripts. Today I'd like to show some basic approaches to AutoIt deobfuscation. As a target I'll use a very simple protection called AutoGuardIt and the crackme from Tuts4You thread. If you don't have access to Tuts4You, here is the alternative download link: https://www.mediafire.com/?qs52emp7tkk472g

In general, there is nothing hard in decompiling AutoIt scripts. The Autoit script interpreter is designed in such a way that it's really easy to convert P-Code back to the script form. There's also a tidy.exe utility which takes ugly hand-written script and reformats it to make it really pretty. All of this makes writing deobfuscators much easier because you can start with well-formatted AutoIt script and your deobfuscator can consist of simple regexps and string replaces. It will not be very pretty code but it will work.

While I was preparing this blog post, SmilingWolf came up with a full-featured solution written in Python. It's a nice solution but it doesn't explain how or why it works. So, in this article I will explain how the protection works, show the basic techniques and sample source code to defeat each of the protection steps. Making a full-featured deobfuscator is left as an exercise for the reader.

Required tools

  • C# compiler. All my examples were tested under Visual Studio 2010 but any recent version should do
  • MyAutToExe. I'm using my personal modification of myAutToExe. You can download it from Bitbucket: https://bitbucket.org/kao/myauttoexe
  • Tool for testing regexps. I'm using http://regexr.com/
  • Some brains. You can't become a reverser if you can't think for yourself.

Decompiling the script

There are 2 public tools for extracting compiled AutoIt script: MyAutToExe and Exe2Aut.

Exe2Aut uses dynamic approach for obtaining script - it runs the file and gets decrypted and decompressed script from process memory. That's usually the easiest way but you really don't want to run the malware on your computer.

MyAutToExe uses static approach - it analyzes file and tries to locate, decrypt and decompress the script on its own. That's more safe approach but it's easier to defeat using different packers, modified script markers and so on. To extract script from this crackme, I used my own MyAutToExe (see "Required tools" section above).

Analyzing the obfuscation

Once the script is extracted and decompiled, it looks quite strange and unreadable:

NTDLLCONFIGEX()
Do
   If 88 - 87 Then
      Do
         Assign(Chr(Random(1, 3300 + SRandom(516534452), 1) - 849) & Chr(Random(1, 3867 + SRandom(952848153), 1) - 1209) & Chr(Random(1, 3308 + SRandom(472440744), 1) - 1113) & Chr(Random(1, 2981 + SRandom(204134073), 1) - 1912) & Chr(Random(1, 3216 + SRandom(754454926), 1) - 2967) & Chr(Random(1, 3490 + SRandom(169073304), 1) - 846) & Chr(2432 - Random(1, 2623 + SRandom(66569023), 1)) & Chr(Random(1, 2900 + SRandom(109480308), 1) - 2387) & Chr(Random(1, 2812 + SRandom(30309101), 1) - 854) & Chr(Random(1, 2739 + SRandom(329725379), 1) - 2104) & Chr(Random(1, 2338 + SRandom(751952368), 1) - 1847) & Chr(Random(1, 2255 + SRandom(979815258), 1) - 1586) & Chr(802 - Random(1, 2718 + SRandom(50238634), 1)) & Chr(Random(1, 2441 + SRandom(590667820), 1) - 1556) & Chr(Random(1, 3457 + SRandom(858383127), 1) - 74) & Chr(Random(1, 3062 + SRandom(462948094), 1) - 1332) & Chr(Random(1, 2724 + SRandom(591366195), 1) - 1024) & Chr(Random(1, 3620 + SRandom(28553399), 1) - 1346) & Chr(639 - Random(1, 2693 + SRandom(880254218), 1)) & Chr(Random(1, 2109 + SRandom(681224018), 1) - 1172) & Chr(1917 - Random(1, 2625 + SRandom(230020321), 1)) & Chr(Random(1, 3244 + SRandom(854266612), 1) - 2140) & Chr(Random(1, 2992 + SRandom(341248807), 1) - 1134) & Chr(Random(1, 3571 + SRandom(253330699), 1) - 260) & Chr(Random(1, 3087 + SRandom(999571281), 1) - 729) & Chr(Random(1, 3944 + SRandom(367148632), 1) - 2086) & Chr(Random(1, 2619 + SRandom(143608942), 1) - 1839) & Chr(Random(1, 2436 + SRandom(254454044), 1) - -72) & Chr(Random(1, 2984 + SRandom(138746714), 1) - 2225) & Chr(Random(1, 3920 + SRandom(148637653), 1) - 1816), Execute("UBound"))
      Until 1
   EndIf
Until 1
For $NTUSER32POWERSHELLN = 0 To 839.629607988521
   Do
      For $ASSEMBLYAPPLAUNCHLOADERA = 0 To 38.5529099325649
         If 147195648 / 96 - 76 = Random(1, 2207 + SRandom(781101274), 1) - -1532371 Then
            Assign(Chr(Random(1, 2361 + SRandom(696154655), 1) - 479) & Chr(Random(1, 2151 + SRandom(374165062), 1) - 1165) & Chr(Random(1, 3132 + SRandom(871835034), 1) - 2244) & Chr(Random(1, 2703 + SRandom(163537687), 1) - 2429) & Chr(Random(1, 2208 + SRandom(107173462), 1) - 873) & Chr(385 - Random(1, 3300 + SRandom(116179080), 1)) & Chr(Random(1, 2106 + SRandom(526159134), 1) - 537) & Chr(Random(1, 2809 + SRandom(61792761), 1) - 753) & Chr(1730 - Random(1, 3007 + SRandom(54581081), 1)) & Chr(2605 - Random(1, 2526 + SRandom(723252683), 1)) & Chr(Random(1, 2796 + SRandom(68738530), 1) - 2531) & Chr(Random(1, 2532 + SRandom(702895909), 1) - 156) & Chr(1706 - Random(1, 3920 + SRandom(666148063), 1)) & Chr(2785 - Random(1, 2685 + SRandom(367920941), 1)) & Chr(Random(1, 2804 + SRandom(63818035), 1) - 2601) & Chr(Random(1, 2325 + SRandom(643112190), 1) - 2216) & Chr(2320 - Random(1, 2824 + SRandom(115595137), 1)) & Chr(Random(1, 3283 + SRandom(530687995), 1) - 2657) & Chr(Random(1, 3088 + SRandom(175987883), 1) - 230) & Chr(2277 - Random(1, 2607 + SRandom(156297082), 1)) & Chr(Random(1, 2271 + SRandom(271295668), 1) - 1682) & Chr(1054 - Random(1, 2223 + SRandom(981650208), 1)) & Chr(Random(1, 3997 + SRandom(82629437), 1) - 841) & Chr(Random(1, 3586 + SRandom(291610090), 1) - 2993) & Chr(Random(1, 3231 + SRandom(555808060), 1) - 2567) & Chr(3496 - Random(1, 3435 + SRandom(51719875), 1)) & Chr(Random(1, 3235 + SRandom(201120626), 1) - 653) & Chr(Random(1, 2596 + SRandom(229290841), 1) - 1839), Execute("Tan"))
         EndIf
         ExitLoop
      Next
   Until 1
   ExitLoop
Next
Do
   For $NTHASHTABLEVOIDUSER32EX = 0 To 399.044101731619
      For $NTEXCEPTIONGETINFORMATIONPROCESSERRORSTDOUTEX = 0 To 663.803335762816
         Assign(Chr(Random(1, 2266 + SRandom(326424240), 1) - 494) & Chr(Random(1, 3011 + SRandom(93056167), 1) - 1475) & Chr(Random(1, 2899 + SRandom(455451109), 1) - 2318) & Chr(Random(1, 2457 + SRandom(204945), 1) - 928) & Chr(1714 - Random(1, 3307 + SRandom(105031094), 1)) & Chr(Random(1, 3418 + SRandom(925652849), 1) - 2130) & Chr(Random(1, 2311 + SRandom(396157406), 1) - 2200) & Chr(Random(1, 2311 + SRandom(666877116), 1) - -57) & Chr(393 - Random(1, 3921 + SRandom(49978531), 1)) & Chr(Random(1, 2475 + SRandom(494692917), 1) - 2281) & Chr(Random(1, 2353 + SRandom(558419780), 1) - 883) & Chr(Random(1, 3855 + SRandom(783264667), 1) - 620) & Chr(Random(1, 2135 + SRandom(650654129), 1) - 589) & Chr(1790 - Random(1, 3013 + SRandom(515405811), 1)) & Chr(Random(1, 2352 + SRandom(830623589), 1) - 2277) & Chr(Random(1, 3455 + SRandom(41889835), 1) - 2503) & Chr(Random(1, 3905 + SRandom(35527843), 1) - 1812) & Chr(Random(1, 3195 + SRandom(230223745), 1) - 2702) & Chr(Random(1, 3346 + SRandom(278453836), 1) - 2868) & Chr(Random(1, 2332 + SRandom(733194675), 1) - 12) & Chr(Random(1, 3919 + SRandom(779041546), 1) - 1362) & Chr(Random(1, 3754 + SRandom(373790451), 1) - 217) & Chr(Random(1, 3512 + SRandom(726368194), 1) - 2263), Execute("StringSplit"))
         ExitLoop
      Next
      ExitLoop
   Next
Until 1
If 1 Then
   For $IOBJECTTHREADENVIRONMENTBLOCKW = 0 To 938.744062930811
      For $NATIVECONVERSIONA = 0 To 505.457816104172
         While 1
            Assign(Chr(Random(1, 3747 + SRandom(783912334), 1) - 2281) & Chr(Random(1, 2627 + SRandom(33229718), 1) - 1343) & Chr(Random(1, 3539 + SRandom(186205954), 1) - 3346) & Chr(Random(1, 3739 + SRandom(341316792), 1) - 2933) & Chr(817 - Random(1, 3794 + SRandom(299375926), 1)) & Chr(2218 - Random(1, 3641 + SRandom(337121368), 1)) & Chr(Random(1, 2005 + SRandom(918455413), 1) - 30) & Chr(399 - Random(1, 2099 + SRandom(812497894), 1)) & Chr(1718 - Random(1, 3642 + SRandom(201640616), 1)) & Chr(Random(1, 2022 + SRandom(548220934), 1) - 1893) & Chr(Random(1, 2077 + SRandom(220340072), 1) - 1312) & Chr(2446 - Random(1, 3375 + SRandom(910541033), 1)) & Chr(Random(1, 3834 + SRandom(182516730), 1) - 1863) & Chr(2981 - Random(1, 3611 + SRandom(291650956), 1)) & Chr(Random(1, 2522 + SRandom(786912362), 1) - 1021) & Chr(1237 - Random(1, 2832 + SRandom(105804377), 1)), Execute("StringLen"))
            ExitLoop
         WEnd
         ExitLoop
      Next
      ExitLoop
   Next
EndIf
...

Let's look at each of the obfuscation techniques and see how it works and how it can be defeated.

Integer decomposition

AutoGuardIt takes constants and converts them to series of math operations. Example:

$VIRTUALIZATIONCIPHERBROWSERA = 71 / 4240 + 4303 + 3057 / 8461 / 4560 + 299 + 7624 * 7110 - 4262

Deobfuscator should be able to take the expression, evaluate it and replace the expression with the correct value.

The biggest problem here is the precedence of operations (multiply and divide should be processed before addition and subtraction), so you can't start from the beginning of line and do it one step at a time. This would be wrong:

$VIRTUALIZATIONCIPHERBROWSERA = 71 / 4240 + 4303 + 3057 / 8461 / 4560 + 299 + 7624 * 7110 - 4262
$VIRTUALIZATIONCIPHERBROWSERA = 0.0167... + 4303 + 3057 / 8461 / 4560 + 299 + 7624 * 7110 - 4262
$VIRTUALIZATIONCIPHERBROWSERA = 4303.0167... + 3057 / 8461 / 4560 + 299 + 7624 * 7110 - 4262
$VIRTUALIZATIONCIPHERBROWSERA = 7360.0167... / 8461 / 4560 + 299 + 7624 * 7110 - 4262   <---- wrong! 
...

After some thinking and few Google searches, I found a LoreSoft.MathExpressions library that does all the heavy lifting for me. 馃檪

The following C# code snippet will find all math expressions, extract them, evaluate them and replace expression with the actual value:

MathEvaluator eval = new MathEvaluator();
Regex regex2 = new Regex(@"(-)?\d+(( )+[-+*/]( )+([-+])?\d+)+");
for (int i = 0; i < lines.Length; i++)
{
   Match m2 = regex2.Match(lines[i]);
   while (m2.Success)
   {
      double d = eval.Evaluate(m2.Value);
      lines[i] = regex2.Replace(lines[i], d.ToString(), 1);
      m2 = m2.NextMatch();
   }
}

Pseudo-random integers

This is quite strange protection that relies on a fact that AutoIt's Random function is actually pseudo-random number generator. If you seed it with the same seed, you get the same results. Example:

If 194639540 / 20 = Random(1, 3342 + SRandom(753822096), 1) - -9731726 Then
   $HTTPDELPHIHEADERW = 951.668468197808
EndIf

In general, it's a very bad idea because there's no guarantee that random number generator will not change in the next version of AutoIt. But for now it works..

Since I was already using myAutToExe, I decided to use RanRot_MT.dll from the package.

[DllImport("ranrot_mt.dll", CallingConvention = CallingConvention.StdCall)]
private extern static UInt32 MT_Init(UInt32 seed);
[DllImport("ranrot_mt.dll", CallingConvention = CallingConvention.StdCall)]
private extern static UInt32 MT_GetI8();

private UInt32 Random(UInt32 from, UInt32 to, UInt32 dummy)
{
   UInt32 result = MT_GetI8() % (to - from + 1) + from;
   return result;
}

private UInt32 SRandom(UInt32 seed)
{
   MT_Init(seed);
   return 1;
}

// do SRandom/Random
Regex regex = new Regex(@"Random\(1, (\d+) \+ SRandom\((\d+)\), 1\)");
for (int i = 0; i < lines.Length; i++)
{
   Match m1 = regex.Match(lines[i]);
   while (m1.Success)
   {
      UInt32 expr1 = UInt32.Parse(m1.Groups[1].Value);
      UInt32 expr2 = UInt32.Parse(m1.Groups[2].Value);
      UInt32 rnd = Random(1, expr1 + SRandom(expr2), 1);
      lines[i] = regex.Replace(lines[i], rnd.ToString(), 1);

      m1 = m1.NextMatch();
   }
}

a = StringLen("xyz")

Small integers can be obfuscated by using function StringLen:

$AUTHENTICATIONREFERENCEN = StringLen("UJCzofdlRD")

To clean them up, a simple regex can be used:

Regex regex3 = new Regex(@"StringLen\(\""([A-Za-z]+)\""\)");
Match m3 = regex3.Match(lines);
while (m3.Success)
{
   string expr1 = m3.Groups[1].Value.ToString();
   lines = regex3.Replace(lines, String.Format("{0}", expr1.Length), 1);
   m3 = m3.NextMatch();
}

End result:

$AUTHENTICATIONREFERENCEN = 10

Chr(x)

Some strings in the executable are split into bytes, and each byte is then encoded as call to Chr function:

Assign(Chr(73) & Chr(68) & Chr(101) & Chr(99) & Chr(114) & Chr(121) & Chr(112) & Chr(116) & Chr(69) & Chr(120) & Chr(101) & Chr(99) & Chr(117) & Chr(116) & Chr(105) & Chr(111) & Chr(110) & Chr(67) & Chr(104) & Chr(97) & Chr(105) & Chr(110) & Chr(67) & Chr(105) & Chr(112) & Chr(104) & Chr(101) & Chr(114) & Chr(69) & Chr(120), Execute("UBound"))

Another simple regex will kill all of those:

Regex regex3 = new Regex(@"Chr\((\d+)\)");
Match m3 = regex3.Match(lines);
while (m3.Success)
{
   UInt32 expr1 = UInt32.Parse(m3.Groups[1].Value);
   lines = regex3.Replace(lines, String.Format("\"{0}\"", (char)expr1), 1);
   m3 = m3.NextMatch();
}

The result will be valid but still hardly readable string:

Assign("I" & "D" & "e" & "c" & "r" & "y" & "p" & "t" & "E" & "x" & "e" & "c" & "u" & "t" & "i" & "o" & "n" & "C" & "h" & "a" & "i" & "n" & "C" & "i" & "p" & "h" & "e" & "r" & "E" & "x", Execute("UBound"))

And one more simple search-replace will fix that:

lines = lines.Replace("\" & \"", "");

End result:

Assign("IDecryptExecutionChainCipherEx", Execute("UBound"))

If 1 Then

Once you remove the integer obfuscations, you'll see a lot of useless statements like this:

If 1 Then
   $LOADERHARDWAREIDNULLA = 336.785775121767
EndIf

This condition is always true, so we can remove both If and EndIf lines and improve readability.

The problem here is that If's can be nested and you can't just remove first EndIf that you encounter. Consider this example:

If 1 Then        <-- we start here.
   For $IBINARYDECLAREA = 0 To 826.403731859988
      If 1 Then
         ; do something useful
      EndIf      <-- this is first endif we see, but it's not the right one.
      ExitLoop
   Next
EndIf            <-- this is endif we're looking for

Taking all that into account, I came up with this ugly but working* code:

if (lines.Trim() == "If 1 Then")
{
   int level = 0;
   int idx = i+1;
   while (idx < lines.Length)
   {
      string s = lines[idx];
      if (s.Trim() == "EndIf")
      {
         if (level == 0)
         {
            // found the proper endif
            lines[i] = "";
            lines[idx] = "";
            break;
         }
         else
            level--;
      }
      else if (s.Contains("If "))
      {
         level++;
      }
      idx++;
   }
}

* - see below for some scenarios where this code might fail.

If a = a Then

Variation of previous protection. In such cases, using regexp is much more efficient than simple string comparison

If 1533212 = 1533212 Then
   ; do something
EndIf

Do Until 1

It's pretty much the same protection as If 1 Then and it can be defeated the exact same way.

Do
   ; do something
Until 1

While 1 / ExitLoop / WEnd

Another protection of the same kind, just uses 3 lines of code instead of 2. Same approach, just make sure you match the correct lines and remove all 3 of them.

While 1
   ; do something
   ExitLoop
WEnd

For $random=0 to 123.456 / ExitLoop / Next

Another protection, very similar to previous ones.

For $NTEXCEPTIONGETINFORMATIONPROCESSERRORSTDOUTEX = 0 To 663.803335762816
   Assign("ZwRing3LibraryAssemblyW", Execute("StringSplit"))
   ExitLoop
Next

Here one must be very careful not to remove the real for cycles from program, so it's better to use regexps. Apart from that, it's pretty much the same code again.

Regex regex4 = new Regex(@"For \$([A-Z0-9])+ = 0 To [0-9]+\.[0-9]+");
Match m4 = regex4.Match(lines);
while (m4.Success)
{
   int level = 0;
   int idx = i + 1;
   while (idx < lines.Length)
   {
      string s = lines[idx];
      if (s.Trim() == "Next")
      {
         if (level == 0)
         {
            // found the proper next
            if (lines[idx - 1].Trim() == "ExitLoop")
            {
               lines[i] = "";
               lines[idx - 1] = "";
               lines[idx] = "";
            }
            break;
         }
         else
            level--;
      }
      else if (s.Contains("For "))
      {
         level++;
      }
      idx++;
   }

   m4 = m4.NextMatch();
}

Assign/Execute

This type of protection relies on AutoIt's Assign function. First, an alias to a function is defined:

Assign("LicenseLoadRing3Ex", Execute("MsgBox"))

Later, alias is used to call the function:

$LICENSELOADRING3EX(1, BinaryToString(BinaryToString(BinaryToString(BinaryToString(BinaryToString(RING3LOADEREX()))))), BinaryToString(NTCONFIGLICENSELISTW()))

Deobfuscation is a simple operation: find all calls to Assign, extract the variable name and the function name, then replace all references to the variable with the function name:

MsgBox(1, BinaryToString(BinaryToString(BinaryToString(BinaryToString(BinaryToString(RING3LOADEREX()))))), BinaryToString(NTCONFIGLICENSELISTW()))

BinaryToString

As you can see in the example above, some strings in the script are replaced with calls to BinaryToString. Here's a another example of the same protection where part of code is replaced with BinaryToString + call to Execute.

Local $VIRTUALMACHINESAFECRITICALEX
$VIRTUALMACHINESAFECRITICALEX &= BinaryToString("0x4368722852616E646F6D28312C2033363530202B205352616E646F6D28343733333336303430292C203129202D20323130292026204368722852616E646F6D28312C2033323435202B205352616E646F6D28373636323730373731292C203129202D20373132292026204368722852616E646F6D28312C2033333139202B205352616E646F6D28363333383035313430292C203129202D20313832372920262043687228333931202D2052616E646F6D28312C2032353736202B205352616E646F6D28363434353336303331292C203129292026204368722852616E646F6D28312C2032353030202B205352616E646F6D28363538343038303535292C203129202D2032333731292026204368722833343536202D2052616E646F6D28312C2033393633202B205352616E646F6D28343731363230353433292C203129292026204368722852616E646F6D28312C2033363538202B205352616E646F6D28383834353332303635292C203129202D20343632292026204368722831303130202D2052616E646F6D28312C2032313835202B205352616E646F6D28393831353836353633292C203129292026204368722831333338202D2052616E646F6D28312C2033313535202B205352616E646F6D28323738373033303233292C203129292026204368722852616E646F6D28312C2032343835202B205352616E646F6D28353930353836323039292C203129202D20353632292026204368722833323139202D2052616E646F6D28312C2033383033202B205352616E646F6D28393337353439363534292C203129292026204368722831373831202D2052616E646F6D28312C2032303234202B205352616E646F6D28313432323539383133292C203129292026204368722852616E646F6D28312C2033373736202B205352616E646F6D28333737383435393830292C203129202D203239333529202620436872")
$VIRTUALMACHINESAFECRITICALEX &= BinaryToString("0x2852616E646F6D28312C2032343436202B205352616E646F6D28393034353431373635292C203129202D2031313535292026204368722852616E646F6D28312C2032383731202B205352616E646F6D28343739303733343431292C203129202D202D3135292026204368722852616E646F6D28312C2033303639202B205352616E646F6D28333639313136333235292C203129202D2031393037292026204368722852616E646F6D28312C2032353838202B205352616E646F6D283134313732323033292C203129202D2032323734292026204368722852616E646F6D28312C2033313539202B205352616E646F6D28393230333036303330292C203129202D2031393739292026204368722831303830202D2052616E646F6D28312C2032363237202B205352616E646F6D28333432353038373031292C203129292026204368722852616E646F6D28312C2032303130202B205352616E646F6D28333331363537373930292C203129202D20313939292026204368722831363735202D2052616E646F6D28312C2033313131202B205352616E646F6D28313238343533323334292C203129292026204368722852616E646F6D28312C2033363530202B205352616E646F6D283139373330383439292C203129202D2031333631292026204368722852616E646F6D28312C2032343731202B205352616E646F6D28383630383637373738292C203129202D20363537292026204368722852616E646F6D28312C2032393230202B205352616E646F6D28323833373631323238292C203129202D2031323437292026204368722852616E646F6D28312C2033383738202B205352616E646F6D28393136313339343131292C203129202D2031393430292026204368722852616E646F6D28312C2032383538202B205352616E646F6D283330323139363430292C203129202D2031393432292026204368722852616E")
$VIRTUALMACHINESAFECRITICALEX &= BinaryToString("0x646F6D28312C2033323133202B205352616E646F6D283930303133363136292C203129202D2031303530292026204368722852616E646F6D28312C2033353235202B205352616E646F6D28373139323732363431292C203129202D2032303736292026204368722832313634202D2052616E646F6D28312C2033323232202B205352616E646F6D28353933373831363831292C203129292026204368722852616E646F6D28312C2033383435202B205352616E646F6D28383036323630313036292C203129202D203637342920262043687228373237202D2052616E646F6D28312C2033373434202B205352616E646F6D28363835323338393533292C203129292026204368722852616E646F6D28312C2033353334202B205352616E646F6D28323835383436323133292C203129202D2031373835292026204368722852616E646F6D28312C2033313032202B205352616E646F6D28393530303831313437292C203129202D20323430292026204368722852616E646F6D28312C2032363631202B205352616E646F6D28373532373332373638292C203129202D20363633292026204368722831303733202D2052616E646F6D28312C2032313235202B205352616E646F6D28343838333633333735292C203129292026204368722852616E646F6D28312C2033353532202B205352616E646F6D28333930333938383535292C203129202D2031313137292026204368722852616E646F6D28312C2032363933202B205352616E646F6D28383535333633353035292C203129202D203138373429")
$IENUMERATORENGINENATIVECALLEX = Execute($VIRTUALMACHINESAFECRITICALEX)

Merging all 3 lines into one and converting hex strings to bytes gives us the following code:

$IENUMERATORENGINENATIVECALLEX = Chr(Random(1, 3650 + SRandom(473336040), 1) - 210) & Chr(Random(1, 3245 + SRandom(766270771), 1) - 712) & Chr(Random(1, 3319 + SRandom(633805140), 1) - 1827) & Chr(391 - Random(1, 2576 + SRandom(644536031), 1)) & Chr(Random(1, 2500 + SRandom(658408055), 1) - 2371) & Chr(3456 - Random(1, 3963 + SRandom(471620543), 1)) & Chr(Random(1, 3658 + SRandom(884532065), 1) - 462) & Chr(1010 - Random(1, 2185 + SRandom(981586563), 1)) & Chr(1338 - Random(1, 3155 + SRandom(278703023), 1)) & Chr(Random(1, 2485 + SRandom(590586209), 1) - 562) & Chr(3219 - Random(1, 3803 + SRandom(937549654), 1)) & Chr(1781 - Random(1, 2024 + SRandom(142259813), 1)) & Chr(Random(1, 3776 + SRandom(377845980), 1) - 2935) & Chr(Random(1, 2446 + SRandom(904541765), 1) - 1155) & Chr(Random(1, 2871 + SRandom(479073441), 1) - -15) & Chr(Random(1, 3069 + SRandom(369116325), 1) - 1907) & Chr(Random(1, 2588 + SRandom(14172203), 1) - 2274) & Chr(Random(1, 3159 + SRandom(920306030), 1) - 1979) & Chr(1080 - Random(1, 2627 + SRandom(342508701), 1)) & Chr(Random(1, 2010 + SRandom(331657790), 1) - 199) & Chr(1675 - Random(1, 3111 + SRandom(128453234), 1)) & Chr(Random(1, 3650 + SRandom(19730849), 1) - 1361) & Chr(Random(1, 2471 + SRandom(860867778), 1) - 657) & Chr(Random(1, 2920 + SRandom(283761228), 1) - 1247) & Chr(Random(1, 3878 + SRandom(916139411), 1) - 1940) & Chr(Random(1, 2858 + SRandom(30219640), 1) - 1942) & Chr(Random(1, 3213 + SRandom(90013616), 1) - 1050) & Chr(Random(1, 3525 + SRandom(719272641), 1) - 2076) & Chr(2164 - Random(1, 3222 + SRandom(593781681), 1)) & Chr(Random(1, 3845 + SRandom(806260106), 1) - 674) & Chr(727 - Random(1, 3744 + SRandom(685238953), 1)) & Chr(Random(1, 3534 + SRandom(285846213), 1) - 1785) & Chr(Random(1, 3102 + SRandom(950081147), 1) - 240) & Chr(Random(1, 2661 + SRandom(752732768), 1) - 663) & Chr(1073 - Random(1, 2125 + SRandom(488363375), 1)) & Chr(Random(1, 3552 + SRandom(390398855), 1) - 1117) & Chr(Random(1, 2693 + SRandom(855363505), 1) - 1874)

which using the methods described earlier can be deobfuscated as:

$IENUMERATORENGINENATIVECALLEX = "ZwConventionReflectionAuthenticationN"

Functions returning constants

Some strings are not only encoded using BinaryToString but also moved to a separate function.

Local $ASSIGNASSEMBLYEX = DllStructCreate(BinaryToString(DECRYPTSOCKETW()))
...
Func DECRYPTSOCKETW($IENUMERATORDECRYPTIONA = 9887.61287710024, $SESSIONUSER32APPLAUNCHEX = 4750.21296840813)
	Local $WINAPIUSERBASEN
	$DRIVEREXECUTIONCHAINDLLN = "0x75696E743B64776F7264"
	Return $DRIVEREXECUTIONCHAINDLLN
EndFunc

Deobfuscated code will look like this:

Local $ASSIGNASSEMBLYEX = DllStructCreate(BinaryToString("0x75696E743B64776F7264"))

It's quite tricky to find the correct return value for each function and replace function calls with correct values. In addition to that, regexes aren't really suitable for such tasks. 馃檪 The code I wrote is really ugly, so I'm not going to show it. Go, figure it out yourself! 馃檪

Switch control-flow obfuscation

This is actually the hardest one of them all. The example code looks like this:

$1375150359 = 727533448
...
Switch $1375150359
   Case 727533448
      OBFUSCATEDFREEMEMORYTHREADN($NTVARIABLENATIVECALLCONNNECTN)
      $1375150359 = 834946917
   Case 834946917
      Local $ASSIGNCIPHERALLOCATEEX = $BINARYPOINTERHASHTABLEN
      $1375150359 = 1646727737
   Case 1561719614
      Local $LIBRARYINTEX = $OBJECTDLLUSERBASEEX(NTASSEMBLYREFLECTIONBROWSERN(), $VOIDKERNELA(ICRYPTOGRAPHYENGINEN()), $VOIDKERNELA(IBYTEOBFUSCATIONMEMORYN()), $VOIDKERNELA(PROXYENGINEMEMBERN()), 0, $VOIDKERNELA(IESCALATIONVIRTUALIZATIONN()), 0, $VOIDKERNELA(IESCALATIONVIRTUALIZATIONN()), 0, $VOIDKERNELA(LICENSEDECRYPTIONEXECUTEN()), $ASSIGNCIPHERALLOCATEEX, $VOIDKERNELA(LICENSEDECRYPTIONEXECUTEN()), $LICENSELOADKERNEL32SOCKETEX)
      ExitLoop
   Case 1646727737
      $DECLAREWRAPPERPROXYENGINEA = $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 3169 + SRandom(653589833), 1) - 2528) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2474 + SRandom(380961127), 1) - 339) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 3978 + SRandom(827933013), 1) - 3839) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2462 + SRandom(36794804), 1) - 1794) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 3989 + SRandom(628400507), 1) - -107) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX(177 - $ZWLOADERHASHW(1, 2867 + SRandom(441699849), 1)) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2037 + SRandom(987330901), 1) - -36) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 3730 + SRandom(95099852), 1) - 3081) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2966 + SRandom(515060101), 1) - 432) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2583 + SRandom(701470155), 1) - 2082)
      $1375150359 = 1561719614
EndSwitch

You must find the initial value of the variable. Then you must find the correct start/end of the Switch, all the switch cases and all other assignments to the control variable. Then you'll be able to reorder all the code. It's a moderately hard problem for which I don't have a pretty solution. 馃檪

Here's my code which seems to work:

Dictionary assignedValues = new Dictionary();
Regex assignVal = new Regex(@"(\$[0-9]+) = ([0-9]+)");   // $1375150359 = 727533448
Regex switchVal = new Regex(@"Switch (\$[0-9]+)");   // Switch $1375150359
Regex caseVal = new Regex(@"Case ([0-9]+)");     // Case 1561719614
for (int i = 0; i < lines.Length; i++)
{
    Match m1 = assignVal.Match(lines[i]);
    if (m1.Success)
    {
        string s1 = m1.Groups[1].ToString();
        if (!assignedValues.ContainsKey(s1))
        {
            assignedValues.Add(s1, m1.Groups[2].ToString());
        }
        else
        {
            assignedValues[s1] = m1.Groups[2].ToString();
        }
    }
    else
    {
        Match m2 = switchVal.Match(lines[i]);
        if (m2.Success)
        {
            // found a switch. now find endswitch. create a list of cases.
            string switchVariableName = m2.Groups[1].ToString();
            int i2 = i + 1;
            Dictionary cases = new Dictionary();
            while (lines[i2].Trim() != "EndSwitch")
            {
                Match m3 = caseVal.Match(lines[i2]);
                if (m3.Success)
                {
                    cases.Add(m3.Groups[1].ToString(), i2);
                }
                i2++;
            }
            cases.Add("EndSwitch", i2);

            // reorder lines.
            List orderedLines = new List();
            Regex switchVarAssignment = new Regex(String.Format(@"\{0} = ([0-9]+)",switchVariableName));  //$1375150359 = 834946917
            string nextValue = assignedValues[switchVariableName];    // starting with the initial value
            while (nextValue != null)
            {
                int idx = cases[nextValue] + 1;
                nextValue = null;
                while (!cases.ContainsValue(idx))
                {
                    Match m4 = switchVarAssignment.Match(lines[idx]);
                    if (m4.Success)
                    {
                        nextValue = m4.Groups[1].ToString();
                    }
                    else
                    {
                        orderedLines.Add(lines[idx]);
                    }
                    idx++;
                }
            }

            // replace all the original lines with spaces..
            for (int k = i; k <= i2; k++)
            {
                lines[k] = "";
            }
            // overwrite first X lines with ordered ones..
            for (int k = 0; k < orderedLines.Count; k++)
            {
                lines[i + k] = orderedLines[k];
            }
        }
    }
}

After cleanup, the deobfuscated code looks like this:

OBFUSCATEDFREEMEMORYTHREADN($NTVARIABLENATIVECALLCONNNECTN)
Local $ASSIGNCIPHERALLOCATEEX = $BINARYPOINTERHASHTABLEN
$DECLAREWRAPPERPROXYENGINEA = $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 3169 + SRandom(653589833), 1) - 2528) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2474 + SRandom(380961127), 1) - 339) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 3978 + SRandom(827933013), 1) - 3839) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2462 + SRandom(36794804), 1) - 1794) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 3989 + SRandom(628400507), 1) - -107) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX(177 - $ZWLOADERHASHW(1, 2867 + SRandom(441699849), 1)) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2037 + SRandom(987330901), 1) - -36) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 3730 + SRandom(95099852), 1) - 3081) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2966 + SRandom(515060101), 1) - 432) & $THREADENVIRONMENTBLOCKINITIALIZATIONLIBRARYEX($ZWLOADERHASHW(1, 2583 + SRandom(701470155), 1) - 2082)
Local $LIBRARYINTEX = $OBJECTDLLUSERBASEEX(NTASSEMBLYREFLECTIONBROWSERN(), $VOIDKERNELA(ICRYPTOGRAPHYENGINEN()), $VOIDKERNELA(IBYTEOBFUSCATIONMEMORYN()), $VOIDKERNELA(PROXYENGINEMEMBERN()), 0, $VOIDKERNELA(IESCALATIONVIRTUALIZATIONN()), 0, $VOIDKERNELA(IESCALATIONVIRTUALIZATIONN()), 0, $VOIDKERNELA(LICENSEDECRYPTIONEXECUTEN()), $ASSIGNCIPHERALLOCATEEX, $VOIDKERNELA(LICENSEDECRYPTIONEXECUTEN()), $LICENSELOADKERNEL32SOCKETEX)
ExitLoop

Unused variable assignments

There are random variable assignments sprinkled all over the code.

$ZWPROGRAMCONVENTIONW = 37455182.0495048
$IVOIDARCHCONTROLFLOWA = "NtSSLReflectionNullEx"

They are only assigned once and never used. To clean them up, one can locate the assignments using regex, count how many times this variable appears in the code and remove it, if it's only assigned once. Something like this:

Regex assignFloat = new Regex(@"^\s*(\$[A-Z0-9]+) = \-?[0-9]+\.[0-9]+");
for (int i = 0; i < lines.Length; i++)
{
    Match m1 = assignFloat.Match(lines[i]);
    if (m1.Success)
    {
        string variableName = m1.Groups[1].ToString();
        int count = 0;
        for (int i2 = 0; i2 < lines.Length; i2++)
        {
            if (lines[i2].Contains(variableName))
               count++;
        }
        if (count == 1)
        {
            lines[i] = "";
      }
   }
}

You can remove string and integer assignments using very similar regex.

Lather, rinse, repeat

If you run each of the mentioned deobfuscations only once, you'll end up with half-deobfuscated code. Also, there isn't one specific order in which the deobfuscations should be applied. Of course, you could just run the entire loop 100 times, but it's just ugly.

Therefore, I prefer to run the code in the loop like this:

bool modified = true;
while (modified)
{
   modified = DeobIntegerDecomposition();
   modified |= DeobPseudoRandom(); 
   modified |= DeobChr(); 
   ...
}

It will run all the deobfuscations until there's nothing left to clean up. Then run tidy.exe on the output and you'll get a nicely readable script.. 馃檪

Possible problems and gotchas

Deobfuscators based on string matching are very easy to implement. However, one must be very careful to write correct regular expressions and string comparisons. My example code works very well on this specific crackme. However, it will mess up with code like this:

If 1 Then
   $A = "If at first you don't succeed, try, try again." <-- Cleaner will find string "If " and mess up counts.
EndIf               <-- So this EndIf will not be removed.
...                 <-- Cleaner will find another "EndIf" in code below, or just crash

You can work around such issues by using more specific regexes with anchors. During my research, I used http://regexr.com/ a lot, and I find it really helpful. Give it a try!

Conclusion

In this article I showed basic approaches that can be used to deobfuscate (not only) AutoIt scripts. They are extremely simple and work well for one-time tasks. For more complex tasks, deobfuscators based on abstract syntax trees or disassembled P-Code are much more efficient but more time-consuming to create.

Have fun!

23 thoughts on “Deobfuscating AutoIt scripts

  1. kao Hello! I tried to download the myAutToExe modified by you, but from the link you'd posted in the article I just downloaded zip-archive without the exe-file inside. Could you, please, re-upload it somewhere else?
    Best regards.

    1. That's normal and expected - Bitbucket repository contains only source code. If you want EXE file, you'll have to compile it yourself.. 馃檪

  2. I wish I were a skilled programmer...It took me a whole day searching for some free VBA compiler but as the result nothing except a lot of errors. Could you,please, share the compiled executable?

    1. I'm on a vacation right now. 馃槈 I could post a compiled EXE after I get back.

      However, I can assure that it's very easy to find a copy of Microsoft Visual Basic 6 on the net. "Portable" is one keyword that comes to mind..

      1. Another good keyword could be "kitfrends". The usual warning about files downloaded from 3rd party sources applies..

  3. Can't compile your mod, original MyAutToExe compiles fine, but while loading modified version portable vb6 (11 mb in size) failes to load some forms, eaven FrmAbout 馃檪 Useless error logs have no info at all. I've never used vb before, so it's hard for me to determine the reason, please help.

    1. Thanks for letting me know! 馃檪

      The issue is caused by git messing up CR/LF symbols in FRX files. Please fetch the latest source and try compiling again. I just checked in my VMWare and it compiled just fine. If it still fails for you, please email me with you git config and the exact steps you took to obtain my modified files.

      1. Thanks for quick fix, all is working good now! No git config, just downloaded sources via browser 馃榾

  4. pawel97
    Could you, please, share, the compilled MyAutToExe.exe?
    Sorry, but I didn't manage to compile ot from source code by myself...(((

  5. I may be late to the party here, but I can't get past the first step of getting your version of MyAutToExe to even give me a token file. I turned on verbose output, so I believe it tries to detect which version of AutoIt ObfuscatedFile.exe is using, fails all of those, doesn't extract a token file, then tries to deobfuscate on a 0 length file, and quits.

    1. Does the original MyAutToExe work? Are you trying to unpack that crackme I mentioned or some other exe?

      There are plenty of things that could go wrong and MyAutToExe doesn't handle (user) errors very well. If you still can't make it work, could you please send me as much information as you can, so that I can reproduce the issue and try to fix it?

      1. Yes, I'm trying the AutoIt crackme from Tuts4You thread. I tried both your modified MyAutToExe, and the one from SmilingWolf's "solution" zip, and both have the same problem. I thought it may be my VB6 package, but I tried a few different ones, and got the same problem.

        I did get a bit further using another pre-compiled (and probably modified) myAutToExe 2.09. When I run ObfuscatedFile.exe through it, it actually recognizes it as a Modified Script Type 3.2.5+, but when I click Yes for AutoIt Script, it starts looking at the pData, seems to find the beginning of it, tries, "Decrypting script data...", then just gives me an, "Out of memory" error, stops, and spits out the log.

        Let me know if there's more I can give you to help reproduce the issue. I'm perfectly happy using these tools (if they worked ...) but I'd also be interested in learning how to extract the obfuscated script blob from memory using a debugger like x64dbg or something, if that's easier (then I wouldn't have to rely on these tools, or, perhaps, write my own tool instead of relying on a modified version of an ancient VB6 tool =).

      2. Another thing I should mention is that I'm running myAutToExe from within VB6. I haven't been able to create a standalone myAutToExe.exe without VB6 crashing.

        1. Well, learning how to build proper EXE file would certainly help. MyAutToExe relies on external DLLs, so quite likely it will not work from within VB6.

          One thing I didn't mention is that crackme is packed with UPX. You do know that you should unpack UPX first, right?

          If you wish to extract tokens from running process memory (myAutToExe calls it .tok file), try this:
          1) load file in Olly;
          2) unpack UPX;
          3) find the sequence of bytes "81 E2 FF FF 01 00 88 04 0A". There should be just one occurrence in main module. That's function "JB01_Decompress::DecompressLoop_EA05". Find the end of the function (around 0xA0 bytes further):

          .text:00468DA8 xor     eax, eax
          .text:00468DAA pop     edi
          .text:00468DAB pop     esi
          .text:00468DAC pop     ebx
          .text:00468DAD mov     esp, ebp
          .text:00468DAF pop     ebp
          .text:00468DB0 retn
          

          Put breakpoint on that "xor eax, eax". When breakpoint hits, examine values [EDI] = pointer to buffer with tokens and [EDI+8] = size of that buffer. Dump it, detokenize it and then deobfuscate it.

  6. Tried the source from your bitbucket.
    But found malware/virus.
    Could you upload source code with no virus to your bitbucket?

    1. There is no virus. Get an antivirus that actually works.

      Furthermore, there is no pre-built myAutToExe binary on my Bitbucket. You have to compile it from the source - so you can always be sure you have a clean version.

  7. Bartosz W贸jcik

    Hi, what do you think about my obfuscator Kao?

    Here is a free code to play with it

    343E-162B-1484-663D

    link:

    {hidden link}

    I'm thinking about adding anti-regex obfuscations after I read your article, this looks like a weak spot of all autoit deobfuscators I've seen. Currently I'm upgrading the parser to handle object method calls (like COM/WMI etc.) and access to object properties (a few days).

    Hit me up with some good obfuscation ideas, I like to play with this toy project of mine.

    The biggest issue I have is the server side processing, while it's great to keep those nasty crackers away it prevents me from incorporating more computing intensive obfuscations like complex math transformations, especially those using floating point numbers (it's just too damn slow while processing complex AST structures).

    What do you think is the future of AutoIt obfuscation? Opcodes virtualization? Finite state machines?

    1. You want an honest opinion?

      In 10+ years I've seen hundreds of malwares and gamehacks written in AutoIt. 95% of them were protected with some sort of obfuscator. But I haven't seen a single useful, legitimate, commercial software written in AutoIt that would require obfuscator to protect an actual intellectual property.

      So yeah, you might have a very cool toy project. It's fun and games to push the limits of what can be achieved in the niche largely overlooked by everyone else.

      But who does benefit most from your work?

      1. Bartosz W贸jcik

        I have plenty of legitimate customers, from system administrators to TV station developers (seriously!). I could argue C++ was used plenty times to create malware too, would you blame Visual Studio team for this? Of course not.

        1. Oh, the good old "don't blame compiler/packer/protector for malware" argument. Classy move! 馃檪 There were some legitimate users of RLPack and MEW as well. Or EPL/FlyStudio. But the vast majority of protected software was malware, so the protection itself got blacklisted swiftly. Hell, some antiviruses even detect Confuser(Ex) by default.

          So, you're right, there might be some legitimate use cases for Autoit obfuscation, but I've just never seen one.

          As for your product - I googled for the first available AutoIt example code, namely a a snake game. It compiles and runs fine using standalone AutoIt 3.3.14.2 (no particular reason for that version, I just happened to have it installed). Your code, however stops with utterly unhelpful message

          An error occurred while applying obfuscation to the parsed code.

          Tried another example code, this time - calculator:

          An error occurred while parsing the input source code.

          That's equally unhelpful.

          So, if you want my suggestion - work on the user-friendliness of the interface. 馃檪

  8. Bartosz W贸jcik

    You can create malware in anything you want, so it's not a valid argument to blame one specific technology while you can find bad guys using almost everything. AutoIt forums has millions of posts and you would have to be ignorant to think most of it is about writing malware.

    Damn boy, you found a bug in my code!

    Remove the #cs comment section at the start in the calculator, I thought I got it covered. Will upgrade it for sure, thanks for the report 馃檪

    I'm adding anti-regex patterns to the obfuscation strategies right now, looks promising.

    I'm thinking about adding small, internal VM engines.

    What do you think would be good to prevent deobfuscation?

Leave a Reply

  • Be nice to me and everyone else.
  • If you are reporting a problem in my tool, please upload the file which causes the problem.
    I can`t help you without seeing the file.
  • Links in comments are visible only to me. Other visitors cannot see them.

3  +  two  =