Deobfuscating AutoIt scripts, part 2

kao

Almost 4 years ago, I wrote a blogpost about deobfuscating a simple AutoIt obfuscator. Today I have a new target which is using a custom obfuscator. 🙂

Update: This obfuscator is called ObfuscatorSG and can be downloaded from Github. Thanks Bartosz Wójcik!

Author had a very specific request about the methods used to solve the crackme:

If I'm allowed to be picky, I'm primarily interested in scripted efforts to RegEx analyze strings/integers. Very little effort (as in none) went into hiding the correct string. The script was merely passed-through a self-made obfuscator.

In this article I'll show the things I tried, where and how I failed miserably and my final solution for this crackme. I really suggest that you download the crackme from tuts4you and try replicating each step along the way, that way it will be much easier to follow the article.

So, let's get started!

Required tools

  • MyAutToExe. I'm using my personal modification of myAutToExe but even a standard version should work;
  • C# compiler. I used VS2017 but any version will do;
  • Some library that evaluates math expressions. Just like in my previous article, I used MathExpressions library from LoreSoft.Calculator project;
  • Tool for testing regexes. I'm using Regexr;
  • Some brains. Writing deobfuscators is like 80% thinking, 20% writing the actual code.

First steps

First steps are easy - unpack UPX, extract tokens and decompile. The process has been described numerous times, so just google for details.
Once decompiled, the code looks something like this:

Func _LL11LLLL11L()
	$_LL1L11L1 = $_L1111L1L11L($L($Q(27, $G($1($Q(72, 96), (28 * (10 * 36 / 90) - 96)), 98))) & $L($Q((28 * ((52 * ((11 * 6 - 60) * 11 - 64) / 26) * (12 * ((60 ^ 2 / 45 - 76) * 11 - 40) / 8) - 21) - 78), 110)) & $L($1(99, 66)) & $L($1($G($1($Q(8, 36), $G($G($1(42, 17), 52), $Q(29, 37))), $1((12 * (12 * (((5 * 12 - 51) * 8 - 69) * 24 - 67) - 57) - 33), 38)), 85)) & $L($Q(93, $Q($1(40, (3 * 32 - 88)), 26))) & $L($Q(80, $G($Q($1(9, 32), ((5 * 15 - 70) * 4 - 13)), $1((16 * 13 / 52), $G($Q(68, 97), $Q($1(8, 48), 5)))))) & $L($Q($Q($1(19, 48), 23), (4 * 34 / 34))) & $L($Q($1($Q(37, 14), $Q(3, $Q($1(62, 9), 29))), 69)) & $L($1(97, $1($1(2, 46), $Q(21, $Q(77, 115))))) & $L($1(77, $Q(99, 67))) & $L($1($1(40, $Q($Q(3, 34), 5)), 77)) & $L($1(78, 97)) & $L($1($G($1($1(2, 33), (24 * 4 - 95)), 62), 99)) & $L($Q(36, (6 * 56 / 84))) & $L($1(69, 36)) & $L($Q($1(34, 2), 74)) & $L($1($G($1((6 * (3 * 10 - 22) - 47), 48), $Q(98, 94)), 84)) & $L($Q(36, 4)) & $L($Q(86, $1($Q($1(7, 47), 29), (13 * (22 * 4 - 81) - 72)))) & $L($1(99, 82)) & $L($1(75, 36)) & $L($1(44, 76)) & $L($Q(36, 4)) & $L($1(48, 98)) & $L($Q(80, $1(27, $1($G(50, 58), 13)))) & $L($Q((13 * ((6 * 67 / 67) * (13 * 4 - 44) - 44) - 37), 97)) & $L($Q(36, 4)) & $L($Q($1(50, 22), 27)) & $L($Q(36, 4)) & $L($Q(43, 88)) & $L($1($1($Q(20, 51), 34), 99)) & $L($Q((5 * 11 - 40), 97)) & $L($1($Q(39, (4 * 23 - 78)), 97)) & $L($Q($Q(83, 19), $Q(29, $Q(36, (15 * 7 - 87))))) & $L($Q(36, 4)) & $L($Q($Q((6 * 17 - 85), 34), 91)) & $L($1(96, $G($1($1(48, 21), (12 * (3 * 15 - 41) / 8)), $G($Q(49, 5), $1(53, 35))))) & $L($1(40, 73)) & $L($Q(20, 99)) & $L($Q(36, 4)) & $L($Q(78, 37)) & $L($1(76, $Q((10 * 7 - 64), $1(6, $Q(6, $G($1(62, 20), $1($1(6, 32), 11))))))) & $L($Q($Q(20, 62), 75)) & $L($Q($G($1(33, (3 * 10 - 25)), $Q($G(59, 62), 11)), 86)) & $L($Q(36, 4)) & $L($Q(44, 94)) & $L($Q(((11 * 4 - 41) * 16 - 47), 78)) & $L($Q(36, 4)) & $L($1(36, 40)) & $L($1(68, $G($1(33, 17), 97))) & $L($Q(87, $Q(30, $1((10 * 11 - 98), 60)))) & $L($1(20, 96)) & $L($Q($G(84, 65), $1(48, 2))) & $L($Q(46, 71)) & $L($1($G(52, $1(34, 52)), 82)) & $L($Q(36, 4)) & $L($Q(92, 46)) & $L($1(64, $G($1(28, 47), $1(53, 36)))) & $L($Q(37, 74)) & $L($1(88, 33)) & $L($Q(36, 4)) & $L($Q(63, 79)) & $L($1(97, 4)) & $L($1(33, 69)) & $L($Q(81, $Q(44, 22))) & $L($Q(36, 4)) & $L($1(96, 4)) & $L($Q(99, (19 * ((11 * 4 - 39) * (6 * 11 - 64) / 2) - 82))) & $L($Q($Q(23, $Q(98, 79)), 91)) & $L($Q(36, 4)) & $L($1(83, $Q(25, $1((3 * 25 - 57), 42)))) & $L($Q(92, $1($G(37, 48), 56))) & $L($1($Q(21, 35), 99)) & $L($1($1(46, 43), $G(85, $Q((15 * 7 - 93), 76)))) & $L($Q(39, 85)) & $L($1(99, 99)) & $L($Q(36, 4)) & $L($Q(96, ((13 * 36 / 52) * 10 - 82))) & $L($Q(75, 63)) & $L($1(73, 32)) & $L($1(39, 87)) & $L($Q(36, 4)) & $L($1(35, 73)) & $L($Q(93, $Q(20, 37))) & $L($G(97, 99)) & $L($Q(54, 66)) & $L($Q(36, 4)) & $L($1(44, 78)) & $L($Q(12, 109)) & $L($1(99, 99)) & $L($Q(36, 4)) & $L($Q(50, 71)) & $L($1(76, 99)) & $L($1(88, 33)) & $L($Q(36, 4)) & $L($Q(69, $1($Q(27, 59), 35))) & $L($G(75, 77)))
	If $_LLLLL11LL == $_LL1L11L1 Then
		_1L11L111L11()
	Else
		$_LLLLLL1L1($_LLL1LLLLL, $_L1111L1L11L($L($Q($G($Q($1(40, 39), 24), $Q(73, 96)), (((((4 * 18 - 68) * 6 - 21) * 34 - 93) * (15 * 5 - 68) - 58) * 13 - 50))) & $L($Q($Q(7, 38), (14 * 7 - 83))) & $L($Q(33, 15)) & $L($1(78, $G($1((3 * 17 - 49), $Q(95, 99)), $Q($1($G($Q(33, 12), 59), 41), (4 * 16 - 52))))) & $L($1($G(81, 76), $Q($Q(88, 98), (14 * 7 - 79)))) & $L($Q($Q(((3 ^ 3 - 19) * (13 * 8 - 96) - 57), $1($1(36, 6), 20)), 80)) & $L($1(69, 98)) & $L($Q($1($Q(7, 39), $Q((((4 * 20 - 71) * 7 - 58) * (60 * (31 * 3 - 84) / 90) - 29), 34)), 66)) & $L($Q((12 * (20 * (15 ^ (3 * 21 - 61) / 3 - 71) - 71) - 94), $1((28 * ((16 * (3 * 33 - 96) - 41) * ((20 * 3 - 52) * 7 - 49) - 46) - 78), $G($G(44, 60), $G($Q(12, 52), 60))))) & $L($1(88, $G($Q(20, 37), $G($Q(32, 15), $1($G($Q($Q(85, 99), (11 * 8 - 77)), 60), (10 * 3 - 13)))))) & $L($1((9 * 7 - 47), 98)) & $L($G(84, 86))))
	EndIf
	$_1LL1LLL111L($_LLL1LLLLL, (31 * (15 * ((14 * 4 - 49) * (14 * 21 / 42) - 46) - 42) - 77))
EndFunc

Horrible, isn't it? 🙂

Cleaning up the math

So, let's get rid of the math expressions first! In my previous post, I used the following regex + math library to clean up the stuff:

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

I tried it here and it failed on floating point numbers like this:

$_1111L1LLL = $_1111L1LLL + $_LLL1LLLL111(-(52 * (11 * 3 - 31) / 26) + 0.5)

I fixed that and regex started to work. Sort of. There are evil parentheses everywhere and my regex doesn't handle them.

So, I added a second regex to support parentheses at beginning and the end of the expression. What could possibly go wrong? 😀

As I learned few hours later, a lot! See, for example, here:

First, regex matched stuff inside parentheses 4 * 21 - 80 and computed it. Then it matched expression 18 - 71 and computed that.

Well, it's already f*cked up, because that's not the correct order of operations. Multiplication has a higher precedence than subtraction!

At this point managing regexes was becoming so messy that I stopped. This is not going to work, I need a new approach!

Matching parentheses

If you want to read more about crazy regexes to find matching parentheses, this StackOverflow discussion is a good place to start. But I decided to keep it simple.

There are several algorithms, but the simplest one is just counting opening/closing parentheses until you find the correct one.

int bracketPos;
bracketPos = lines.IndexOf('(', 0);
while (bracketPos != -1)
{
    // find the closing parenthesis
    int closingBracketPos = bracketPos + 1;
    int level = 1;
    while (level > 0)
    {
        switch (lines[closingBracketPos])
        {
            case '(':  level++; break;
            case ')':  level--; break;
        }
        closingBracketPos++;
    }

    // extract the expression
    string expression = lines.Substring(bracketPos, closingBracketPos - bracketPos);
    // do something with expression

    // find next bracket.
    bracketPos = lines.IndexOf('(', bracketPos + 1);
}

Now I can take the expression I found, and pass it to the LoreSoft.MathExpressions. Right?

Wrong. Parentheses are also used in function definitions or when passing parameters to another function:

Func _LLLL1L111L(ByRef $_1111L1LLL)
...
Local $_111111LL1L1 = $QG($_1LLLL11111, $_11L1LL11L1, $_1LL11L)
...

So, I added another check to see if the extracted expression looks like a math expression. And it seemed to work.

Problematic minus signs

Next problem I encountered was LoreSoft.MathExpressions complaining about some expressions like these:

$_1111L1LLL = $_1111L1LLL + $_LLL1LLLL111(-(52 * (11 * 3 - 31) / 26) + 0.5)

Apparently, library can handle negative numbers when they are alone, but combination of negative sign and parentheses like "(-(1 + 2))" just confuses the hell out of it. Since there were only a few cases in the crackme, I manually edited them:

$_1111L1LLL = $_1111L1LLL + $_LLL1LLLL111(0-(52 * (11 * 3 - 31) / 26) + 0.5)

Another problem solved!

Fixing math library

To continue my journey of failures, some of the calculated expressions were really, really strange. For example:

$_1111L1LLL = $_1111L1LLL + $_11L1L1LL111(2, 3, 0.484222998499551)

That doesn't look right! The original line was

$_1111L1LLL = $_1111L1LLL + $_11L1L1LL111(((4 * 91 / 91) * 35 / 70), ((16 * 4 - 55) * (9 * 4 - 28) - 69), (77 ^ 1 / 11 - 1))

77 to the power of 1 equals 77. Divided by 11 equals 7. Minus 1 equals 6. So the result should definitely be 6. Why the hell we have 0.48422...?

It turns out that LoreSoft.MathExpressions is buggy and "raise to power" operator doesn't have the correct precedence. See the source:

private static int Precedence(string c)
{
    if (c.Length == 1 && (c[0] == '*' || c[0] == '/' || c[0] == '%'))
        return 2;

    return 1;
}

Raise to power doesn't have any special handling, so it's handled after the division or multiplication. Which is terribly wrong but really easy to fix:

private static int Precedence(string c)
{
    if (c.Length == 1 && (c[0] == '^'))
        return 3;

    if (c.Length == 1 && (c[0] == '*' || c[0] == '/' || c[0] == '%'))
        return 2;

    return 1;
}

Finally, the math problems are solved! 🙂

Function names

After solving math problems, methods are starting to look a bit better:

Func _LL11LLLL11L()
	$_LL1L11L1 = $_L1111L1L11L($L($Q(27, $G($1($Q(72, 96), 16), 98))) & $L($Q(6, 110)) & $L($1(99, 66)) & $L($1($G($1($Q(8, 36), $G($G($1(42, 17), 52), $Q(29, 37))), $1(3, 38)), 85)) & $L($Q(93, $Q($1(40, 8), 26))) & $L($Q(80, $G($Q($1(9, 32), 7), $1(4, $G($Q(68, 97), $Q($1(8, 48), 5)))))) & $L($Q($Q($1(19, 48), 23), 4)) & $L($Q($1($Q(37, 14), $Q(3, $Q($1(62, 9), 29))), 69)) & $L($1(97, $1($1(2, 46), $Q(21, $Q(77, 115))))) & $L($1(77, $Q(99, 67))) & $L($1($1(40, $Q($Q(3, 34), 5)), 77)) & $L($1(78, 97)) & $L($1($G($1($1(2, 33), 1), 62), 99)) & $L($Q(36, 4)) & $L($1(69, 36)) & $L($Q($1(34, 2), 74)) & $L($1($G($1(1, 48), $Q(98, 94)), 84)) & $L($Q(36, 4)) & $L($Q(86, $1($Q($1(7, 47), 29), 19))) & $L($1(99, 82)) & $L($1(75, 36)) & $L($1(44, 76)) & $L($Q(36, 4)) & $L($1(48, 98)) & $L($Q(80, $1(27, $1($G(50, 58), 13)))) & $L($Q(15, 97)) & $L($Q(36, 4)) & $L($Q($1(50, 22), 27)) & $L($Q(36, 4)) & $L($Q(43, 88)) & $L($1($1($Q(20, 51), 34), 99)) & $L($Q(15, 97)) & $L($1($Q(39, 14), 97)) & $L($Q($Q(83, 19), $Q(29, $Q(36, 18)))) & $L($Q(36, 4)) & $L($Q($Q(17, 34), 91)) & $L($1(96, $G($1($1(48, 21), 6), $G($Q(49, 5), $1(53, 35))))) & $L($1(40, 73)) & $L($Q(20, 99)) & $L($Q(36, 4)) & $L($Q(78, 37)) & $L($1(76, $Q(6, $1(6, $Q(6, $G($1(62, 20), $1($1(6, 32), 11))))))) & $L($Q($Q(20, 62), 75)) & $L($Q($G($1(33, 5), $Q($G(59, 62), 11)), 86)) & $L($Q(36, 4)) & $L($Q(44, 94)) & $L($Q(1, 78)) & $L($Q(36, 4)) & $L($1(36, 40)) & $L($1(68, $G($1(33, 17), 97))) & $L($Q(87, $Q(30, $1(12, 60)))) & $L($1(20, 96)) & $L($Q($G(84, 65), $1(48, 2))) & $L($Q(46, 71)) & $L($1($G(52, $1(34, 52)), 82)) & $L($Q(36, 4)) & $L($Q(92, 46)) & $L($1(64, $G($1(28, 47), $1(53, 36)))) & $L($Q(37, 74)) & $L($1(88, 33)) & $L($Q(36, 4)) & $L($Q(63, 79)) & $L($1(97, 4)) & $L($1(33, 69)) & $L($Q(81, $Q(44, 22))) & $L($Q(36, 4)) & $L($1(96, 4)) & $L($Q(99, 13)) & $L($Q($Q(23, $Q(98, 79)), 91)) & $L($Q(36, 4)) & $L($1(83, $Q(25, $1(18, 42)))) & $L($Q(92, $1($G(37, 48), 56))) & $L($1($Q(21, 35), 99)) & $L($1($1(46, 43), $G(85, $Q(12, 76)))) & $L($Q(39, 85)) & $L($1(99, 99)) & $L($Q(36, 4)) & $L($Q(96, 8)) & $L($Q(75, 63)) & $L($1(73, 32)) & $L($1(39, 87)) & $L($Q(36, 4)) & $L($1(35, 73)) & $L($Q(93, $Q(20, 37))) & $L($G(97, 99)) & $L($Q(54, 66)) & $L($Q(36, 4)) & $L($1(44, 78)) & $L($Q(12, 109)) & $L($1(99, 99)) & $L($Q(36, 4)) & $L($Q(50, 71)) & $L($1(76, 99)) & $L($1(88, 33)) & $L($Q(36, 4)) & $L($Q(69, $1($Q(27, 59), 35))) & $L($G(75, 77)))
	If $_LLLLL11LL == $_LL1L11L1 Then
		_1L11L111L11()
	Else
		$_LLLLLL1L1($_LLL1LLLLL, $_L1111L1L11L($L($Q($G($Q($1(40, 39), 24), $Q(73, 96)), 15)) & $L($Q($Q(7, 38), 15)) & $L($Q(33, 15)) & $L($1(78, $G($1(2, $Q(95, 99)), $Q($1($G($Q(33, 12), 59), 41), 12)))) & $L($1($G(81, 76), $Q($Q(88, 98), 19))) & $L($Q($Q(7, $1($1(36, 6), 20)), 80)) & $L($1(69, 98)) & $L($Q($1($Q(7, 39), $Q(1, 34)), 66)) & $L($Q(14, $1(6, $G($G(44, 60), $G($Q(12, 52), 60))))) & $L($1(88, $G($Q(20, 37), $G($Q(32, 15), $1($G($Q($Q(85, 99), 11), 60), 17))))) & $L($1(16, 98)) & $L($G(84, 86))))
	EndIf
	$_1LL1LLL111L($_LLL1LLLLL, 16)
EndFunc

Now we need to get rid of those obfuscated variable names like $_L1111L1L11L and replace them with a proper function names. But what exactly is $_L1111L1L11L? I ran a simple grep, and there are 11 references in the code - 1 declaration of variable, 7 uses of variable and 3 assignments:

Global ... $_L1111L1L11L ...
$_L1111L1L11L = STRINGREVERSE
$_11L1LLL1LL1 = $_L1111L1L11L(....)
$_L1111L1L11L = STRINGREPLACE
$_L1111L1L11L = GUICTRLCREATEBUTTON

That's interesting. :/

First of all, AutoIt allows to do this weird thing where you assign a function to a variable. Then you can use this variable to call a function. Crackme that I solved in my previous post used combination of Assign + Execute methods for the same purposes.

Second, you can have several assignments to the same variable. But which one is the correct one? First one? Last one? A random one?

There is no magic solution here, you just need to go through the script and see the execution flow. In AutoIt, anything that's not inside a function is considered to be main code and will be executed starting from the top. So, I went through the script and left only the interesting parts:

Global ... $_11L1LL1 = 1
_LL1111LL1L1L()
_LL1LLL1()
Switch $_11L1LL1
	Case 5
		_LL1111LL1L1L()
		_LL11LLL1111()
	Case 4
		_11111L1LLL1()
	Case 3
		_LL11LLL1111()
	Case 2
		_LL1111LL1L1L()
		_LL1LLL1()
	Case 1
		_111LL111LL()
		_11111L1LLL1()
EndSwitch
_LL1111L1L1()

This is the order in which the functions will be called. First _LL1111LL1L1L() and then _LL1LLL1() will be executed. Then inside the Switch we'll take Case 1 because that's the value of global variable $_11L1LL1. So, that will call _111LL111LL() and _11111L1LLL1(). Finally, _LL1111L1L1() will be called.

Method _LL1111LL1L1L() does the first assignments:

Func _LL1111LL1L1L()
	$_111L1111L11 = ONAUTOITEXITREGISTER
	$_1L1LLLLLLLL1 = ASSIGN
	$_LL1L111LLLL = DRIVEGETDRIVE
...

Then _LL1LLL1() reassigns some (or maybe all) of the variables:

Func _LL1LLL1()
	$_LLL111L1 = WINLIST
	$_LLLLLLLLL = SQRT
	$_L1L1L11LL1 = VARGETTYPE
...

And so on..

I'm too lazy to analyze all of the assignments, so I just reimplemented all 3 methods in my code.

private void UpdateDictionary1()
{ 
    AddOrUpdate("$_111L1111L11", "ONAUTOITEXITREGISTER");
    AddOrUpdate("$_1L1LLLLLLLL1", "ASSIGN");
    AddOrUpdate("$_LL1L111LLLL", "DRIVEGETDRIVE");
...
}
private void UpdateDictionary2()
{
    AddOrUpdate("$_LLL111L1", "WINLIST");
    AddOrUpdate("$_LLLLLLLLL", "SQRT");
    AddOrUpdate("$_L1L1L11LL1", "VARGETTYPE");
...
}
UpdateDictionary1(); //_LL1111LL1L1L()
UpdateDictionary2(); //_LL1LLL1()
UpdateDictionary3(); //_111LL111LL()

Of course, I did not type all the assignments manually. Simple regex "search and replace" created C# code from the AutoIt code. 🙂

Now I have a dictionary of variable names and the actual function names. Let's just run a simple search and replace!

...and we'll fuck up again.

See for example here:

$_11L1L1 = @LogonDomain
$_11L1L1L = SRANDOM
$_11L1L1LL11 = DEC
...
Func _1111111L111(ByRef $_1111L1LLL)
	$_1111L1LLL = $_1111L1LLL + $_11L1L1LL11(3966)
	Return $_1111L1LLL
EndFunc

If you start from the first string and do dumb search-and-replace, you'll replace a wrong substring and get a result like this:

Func _1111111L111(ByRef $_1111L1LLL)
	$_1111L1LLL = $_1111L1LLL + @LogonDomainLL11(3966)
	Return $_1111L1LLL
EndFunc

For the exact same reason, you should avoid touching local variable names.

My final search-and-replace solution looked like this:

// start with the longest name and work back to the shortest names.
// "(" ensures the we replace only function calls, not variables or locals.
foreach (KeyValuePair<string, string> kvp in m_dictionary.OrderByDescending(x => x.Key.Length))
{
    for (int i = 0; i < lines.Length; i++)
    {
        lines = lines.Replace(kvp.Key + "(", kvp.Value + "(");
    }
}

Bit operations

All the hard stuff is done, I promise! We're just a few fuckups away from the solution! 🙂

Our test method now looks like this:

Func _LL11LLLL11L()
	$_LL1L11L1 = STRINGREVERSE(CHR(BITXOR(27, BITAND(BITOR(BITXOR(72, 96), 16), 98))) & CHR(BITXOR(6, 110)) & CHR(BITOR(99, 66)) & CHR(BITOR(BITAND(BITOR(BITXOR(8, 36), BITAND(BITAND(BITOR(42, 17), 52), BITXOR(29, 37))), BITOR(3, 38)), 85)) & CHR(BITXOR(93, BITXOR(BITOR(40, 8), 26))) & CHR(BITXOR(80, BITAND(BITXOR(BITOR(9, 32), 7), BITOR(4, BITAND(BITXOR(68, 97), BITXOR(BITOR(8, 48), 5)))))) & CHR(BITXOR(BITXOR(BITOR(19, 48), 23), 4)) & CHR(BITXOR(BITOR(BITXOR(37, 14), BITXOR(3, BITXOR(BITOR(62, 9), 29))), 69)) & CHR(BITOR(97, BITOR(BITOR(2, 46), BITXOR(21, BITXOR(77, 115))))) & CHR(BITOR(77, BITXOR(99, 67))) & CHR(BITOR(BITOR(40, BITXOR(BITXOR(3, 34), 5)), 77)) & CHR(BITOR(78, 97)) & CHR(BITOR(BITAND(BITOR(BITOR(2, 33), 1), 62), 99)) & CHR(BITXOR(36, 4)) & CHR(BITOR(69, 36)) & CHR(BITXOR(BITOR(34, 2), 74)) & CHR(BITOR(BITAND(BITOR(1, 48), BITXOR(98, 94)), 84)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(86, BITOR(BITXOR(BITOR(7, 47), 29), 19))) & CHR(BITOR(99, 82)) & CHR(BITOR(75, 36)) & CHR(BITOR(44, 76)) & CHR(BITXOR(36, 4)) & CHR(BITOR(48, 98)) & CHR(BITXOR(80, BITOR(27, BITOR(BITAND(50, 58), 13)))) & CHR(BITXOR(15, 97)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(BITOR(50, 22), 27)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(43, 88)) & CHR(BITOR(BITOR(BITXOR(20, 51), 34), 99)) & CHR(BITXOR(15, 97)) & CHR(BITOR(BITXOR(39, 14), 97)) & CHR(BITXOR(BITXOR(83, 19), BITXOR(29, BITXOR(36, 18)))) & CHR(BITXOR(36, 4)) & CHR(BITXOR(BITXOR(17, 34), 91)) & CHR(BITOR(96, BITAND(BITOR(BITOR(48, 21), 6), BITAND(BITXOR(49, 5), BITOR(53, 35))))) & CHR(BITOR(40, 73)) & CHR(BITXOR(20, 99)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(78, 37)) & CHR(BITOR(76, BITXOR(6, BITOR(6, BITXOR(6, BITAND(BITOR(62, 20), BITOR(BITOR(6, 32), 11))))))) & CHR(BITXOR(BITXOR(20, 62), 75)) & CHR(BITXOR(BITAND(BITOR(33, 5), BITXOR(BITAND(59, 62), 11)), 86)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(44, 94)) & CHR(BITXOR(1, 78)) & CHR(BITXOR(36, 4)) & CHR(BITOR(36, 40)) & CHR(BITOR(68, BITAND(BITOR(33, 17), 97))) & CHR(BITXOR(87, BITXOR(30, BITOR(12, 60)))) & CHR(BITOR(20, 96)) & CHR(BITXOR(BITAND(84, 65), BITOR(48, 2))) & CHR(BITXOR(46, 71)) & CHR(BITOR(BITAND(52, BITOR(34, 52)), 82)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(92, 46)) & CHR(BITOR(64, BITAND(BITOR(28, 47), BITOR(53, 36)))) & CHR(BITXOR(37, 74)) & CHR(BITOR(88, 33)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(63, 79)) & CHR(BITOR(97, 4)) & CHR(BITOR(33, 69)) & CHR(BITXOR(81, BITXOR(44, 22))) & CHR(BITXOR(36, 4)) & CHR(BITOR(96, 4)) & CHR(BITXOR(99, 13)) & CHR(BITXOR(BITXOR(23, BITXOR(98, 79)), 91)) & CHR(BITXOR(36, 4)) & CHR(BITOR(83, BITXOR(25, BITOR(18, 42)))) & CHR(BITXOR(92, BITOR(BITAND(37, 48), 56))) & CHR(BITOR(BITXOR(21, 35), 99)) & CHR(BITOR(BITOR(46, 43), BITAND(85, BITXOR(12, 76)))) & CHR(BITXOR(39, 85)) & CHR(BITOR(99, 99)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(96, 8)) & CHR(BITXOR(75, 63)) & CHR(BITOR(73, 32)) & CHR(BITOR(39, 87)) & CHR(BITXOR(36, 4)) & CHR(BITOR(35, 73)) & CHR(BITXOR(93, BITXOR(20, 37))) & CHR(BITAND(97, 99)) & CHR(BITXOR(54, 66)) & CHR(BITXOR(36, 4)) & CHR(BITOR(44, 78)) & CHR(BITXOR(12, 109)) & CHR(BITOR(99, 99)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(50, 71)) & CHR(BITOR(76, 99)) & CHR(BITOR(88, 33)) & CHR(BITXOR(36, 4)) & CHR(BITXOR(69, BITOR(BITXOR(27, 59), 35))) & CHR(BITAND(75, 77)))
	If $_LLLLL11LL == $_LL1L11L1 Then
		_1L11L111L11()
	Else
		GUICTRLSETDATA($_LLL1LLLLL, STRINGREVERSE(CHR(BITXOR(BITAND(BITXOR(BITOR(40, 39), 24), BITXOR(73, 96)), 15)) & CHR(BITXOR(BITXOR(7, 38), 15)) & CHR(BITXOR(33, 15)) & CHR(BITOR(78, BITAND(BITOR(2, BITXOR(95, 99)), BITXOR(BITOR(BITAND(BITXOR(33, 12), 59), 41), 12)))) & CHR(BITOR(BITAND(81, 76), BITXOR(BITXOR(88, 98), 19))) & CHR(BITXOR(BITXOR(7, BITOR(BITOR(36, 6), 20)), 80)) & CHR(BITOR(69, 98)) & CHR(BITXOR(BITOR(BITXOR(7, 39), BITXOR(1, 34)), 66)) & CHR(BITXOR(14, BITOR(6, BITAND(BITAND(44, 60), BITAND(BITXOR(12, 52), 60))))) & CHR(BITOR(88, BITAND(BITXOR(20, 37), BITAND(BITXOR(32, 15), BITOR(BITAND(BITXOR(BITXOR(85, 99), 11), 60), 17))))) & CHR(BITOR(16, 98)) & CHR(BITAND(84, 86))))
	EndIf
	GUICTRLSETSTATE($_LLL1LLLLL, 16)
EndFunc

I decided to use regex loops from my old article:

Regex regex = new Regex(@"BITOR\((\d+)\, (\d+)\)");
for (int i = 0; i < lines.Length; i++)
{
   Match m1 = regex.Match(lines);
   while (m1.Success)
   {
      UInt32 expr1 = UInt32.Parse(m1.Groups[1].Value);
      UInt32 expr2 = UInt32.Parse(m1.Groups[2].Value);
      UInt32 result = expr1 | expr2;
      lines = regex.Replace(lines, result.ToString(), 1);
      m1 = m1.NextMatch();
   }
}

...and it failed. Some of the calculated numbers just didn't make any sense.

This issue is a little bit tricky. To figure it out, you need to read the documentation for each method used:

Match.NextMatch:
Returns a new System.Text.RegularExpressions.Match object with the results for the next match, starting at the position at which the last match ended (at the character after the last matched character).

Regex.Replace:
In a specified input string, replaces a specified maximum number of strings that match a regular expression pattern with a specified replacement string.

Can you see a problem here? I couldn't. So, I spent ~20 minutes debugging it in VisualStudio.

Here's an image for you:

There are several solutions possible, I just got rid of NextMatch and used a big while loop instead.

do
{
    changed = false;

    Regex regex = new Regex(@"BITOR\((\d+)\, (\d+)\)");
    for (int i = 0; i < lines.Length; i++)
    {
        Match m1 = regex.Match(lines);  
        if (m1.Success)  // process only 1st match
        {
            UInt32 expr1 = UInt32.Parse(m1.Groups[1].Value);
            UInt32 expr2 = UInt32.Parse(m1.Groups[2].Value);
            UInt32 result = expr1 | expr2;
            lines = regex.Replace(lines, result.ToString(), 1);  // process only 1st match
            changed = true;
        }
    }
}
while (changed);  // and do so until nothing matches.

TL;DR - DO NOT combine Match.NextMatch with Regex.Replace. It will bite you in the butt one day!

Chr() and string concatenation

Now we're getting somewhere! Code is looking better and better:

Func _LL11LLLL11L()
	$_LL1L11L1 = STRINGREVERSE(CHR(59) & CHR(104) & CHR(99) & CHR(117) & CHR(111) & CHR(116) & CHR(32) & CHR(110) & CHR(111) & CHR(109) & CHR(109) & CHR(111) & CHR(99) & CHR(32) & CHR(101) & CHR(104) & CHR(116) & CHR(32) & CHR(101) & CHR(115) & CHR(111) & CHR(108) & CHR(32) & CHR(114) & CHR(111) & CHR(110) & CHR(32) & CHR(45) & CHR(32) & CHR(115) & CHR(103) & CHR(110) & CHR(105) & CHR(107) & CHR(32) & CHR(104) & CHR(116) & CHR(105) & CHR(119) & CHR(32) & CHR(107) & CHR(108) & CHR(97) & CHR(119) & CHR(32) & CHR(114) & CHR(79) & CHR(32) & CHR(44) & CHR(101) & CHR(117) & CHR(116) & CHR(114) & CHR(105) & CHR(118) & CHR(32) & CHR(114) & CHR(117) & CHR(111) & CHR(121) & CHR(32) & CHR(112) & CHR(101) & CHR(101) & CHR(107) & CHR(32) & CHR(100) & CHR(110) & CHR(97) & CHR(32) & CHR(115) & CHR(100) & CHR(119) & CHR(111) & CHR(114) & CHR(99) & CHR(32) & CHR(104) & CHR(116) & CHR(105) & CHR(119) & CHR(32) & CHR(107) & CHR(108) & CHR(97) & CHR(116) & CHR(32) & CHR(110) & CHR(97) & CHR(99) & CHR(32) & CHR(117) & CHR(111) & CHR(121) & CHR(32) & CHR(102) & CHR(73))
	If $_LLLLL11LL == $_LL1L11L1 Then
		_1L11L111L11()
	Else
		GUICTRLSETDATA($_LLL1LLLLL, STRINGREVERSE(CHR(46) & CHR(46) & CHR(46) & CHR(110) & CHR(105) & CHR(97) & CHR(103) & CHR(97) & CHR(32) & CHR(121) & CHR(114) & CHR(84)))
	EndIf
	GUICTRLSETSTATE($_LLL1LLLLL, 16)
EndFunc

Cleaning up the CHR calls and string concatenation was easy. Regex and string replace from my previous article worked without any issues. 🙂

String reverse

We're left with one final problem that is STRINGREVERSE function:

Func _LL11LLLL11L()
	$_LL1L11L1 = STRINGREVERSE(";hcuot nommoc eht esol ron - sgnik htiw klaw rO ,eutriv ruoy peek dna sdworc htiw klat nac uoy fI")
	If $_LLLLL11LL == $_LL1L11L1 Then
		_1L11L111L11()
	Else
		GUICTRLSETDATA($_LLL1LLLLL, STRINGREVERSE("...niaga yrT"))
	EndIf
	GUICTRLSETSTATE($_LLL1LLLLL, 16)
EndFunc

We can use a simple regex loop to fix those. Just like the one we used for bit operations.

The end result

And this is how the serial check looks like after deobfuscation:

Func _LL11LLLL11L()
	$_LL1L11L1 = "If you can talk with crowds and keep your virtue, Or walk with kings - nor lose the common touch;"
	If $_LLLLL11LL == $_LL1L11L1 Then
		_1L11L111L11()
	Else
		GUICTRLSETDATA($_LLL1LLLLL, "Try again...")
	EndIf
	GUICTRLSETSTATE($_LLL1LLLLL, 16)
EndFunc

Sure, there is a lot of useless code left in the crackme. Variables are not renamed. I could spend half-hour more and clean up all that mess. But I wasn't interested in that, I just wanted to solve the crackme. 🙂

Final thoughts

In this post I documented all my mistakes and fuckups while solving a rather simple crackme, so that others can learn from them. Reverse engineering is not an easy process and making mistakes is a huge part of it.

I have not failed. I've just found 10,000 ways that won't work. /Thomas A. Edison/

14 thoughts on “Deobfuscating AutoIt scripts, part 2

  1. Thanks a lot kao for your detailed and step-by-step (try-fail-retry) walktrough ! It's very very interesting.

    Preparing code blocks and images for the post I'm sure took you lots of time, but the result is great and helps understanding the tricky parts!

    So thank you for the analysis of this 'rather simple crackme' (simple for you! LoL) ...

    Best Regards,
    Tony

    1. Forgot to say ... this phrase 'We're just a few fuckups away from the solution! :)' made my day 😉

  2. Bartosz Wójcik

    This output is from recent {hidden link}

    I would like you to examine my work {hidden link} 🙂

    I did read your article about deobfuscation and incorporated some techniques to make it harder to deobfuscate the code, like anti-regex patterns.

    I didn't know about the function to variable assignment possibility within AutoIt and it looks like a cool thing to add.

    The floating point obfuscation looks like a good idea to make deobfuscation harder, I have tried it that once but the overall speed of floating point numbers processing put the entire process to ground (damn slow within PHP FPU calculations).

    1. Thank you for that info, I will update my post with the obfuscator name and link. 🙂

      As for your tool - in 3-4 years when my next AutoIt-related blogpost comes, I might look at it. But not today.

  3. some code in the new version can not be de-obfuscated as your method upper, could you please update some ideas for updating? do you have a developed or new app to do that? if yes, kindly share that by email (frombinhdinh@gmail.com). thank you so much!!!

  4. {hidden link}

    generally, it was compiled and obfuscated in the same way, but I can not find out method to de-code correctly. I have tried to use myautoexe 2.15, but it could not be decoded.

    1. Thank you, I'll try to look at it over the weekend.

      EDIT: oops, I can't download your file:

      The file you requested has been blocked for a violation of our Terms of Service.

      1. just sent you the file by wetransfer. pls let me know if you still cant get it.
        {hidden link}

        1. Thank you, I was able to download your archive. Now I just need to find some free time to look at it...

        2. So, what exactly is your problem?

          Extracting and decompiling the script? There are plenty of new tools that work really well, so you do not need to use myAutToExe. Try for example, UnAutoIt (originally written by x0r119x91). It is slow but works on all your files.

          Deobfuscating the script? For AT2D.exe and AT2D6C.exe you only need to fix 2 things. First, string concatenation:

          Global $24746578743229 = "4E68" & "E1BAAD702" & "054C3A0" & "69204B686FE" & "1BAA36E"

          Should be deobfuscated to:

          Global $24746578743229 = "4E68E1BAAD702054C3A069204B686FE1BAA36E"

          It's a simple search and replace operation.

          Then, you will need to decode the data. For example:

          _001321ptt($24746578743229)

          should be replaced with

          _001321ptt("4E68E1BAAD702054C3A069204B686FE1BAA36E")

          and then with hex-decoded version of the string:

          Nhập Tài Khoản

          (my apologies in case the Vietnamese characters got broken on WordPress).

          It also downloads some important data from the web server. You either can get those data and replace the values, or not. But that's not a problem of obfuscation.


          Your V4-Auto.exe, V6-Auto.exe and the rest use another obfuscator. I believe, it's the old obfuscator from AutoIt Script Editor package.

          For those files, you will need to do 4 things:
          #1 - constant initialization is done by this function on AutoIt startup:

          #OnAutoItStartRegister "A5700001F26_"

          You'll need to process it, and figure out all values of $os array.
          #2 - once you know values of $os array, you can process the huge globals block at the start of the file:

          Global $a59cbc03c10 = a5700001f26($os[0x3aa]), $a3ccbe0405c = a5700001f26($os[0x3ab])...

          Here, you'll need to replace $os[0x3aa] with the correct value, and then replace a5700001f26(...) with the hex-decoded value of that string.
          #3 - once you know all values of the globals, you can replace all of them in the code.

          Local $a2b37f01c54[Number($a454700543a)] = [DllOpen($a4447101747)]

          will become

          $a2b37f01c54[Number(" 2 ")] = [DllOpen("kernel32.dll")]

          #4 - then clean up all these Number(...) obfuscations and get this:

          $a2b37f01c54[2] = [DllOpen("kernel32.dll")]

          I think that covers it all. Easy, isn't it? 🙂

          1. Thanks for your support
            All of them need to be approved by the response.text from a website to start the process. The utility also uses some text in the content response to work. Now it is hard to know what things are in the response text.

            for the file xxAuto, I think that they were obfuscated by the way that used data from a stream file. they only de-obfuscated when the stream file connected to the App.
            Also hard to know the value of all the Globals, it is too much and has some risk if the declaration doesn't match the function code lower.

            Anyway, many thanks for your efforts.

  5. oh, a giant variable, globals, and array need to be found exacted value. It takes too much time to do it, will revert to this later. check if you have good ideas for the project, and please share them with me.

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.

three  −   =  two