Deobfuscating AutoIt scripts, part 2
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. smile
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:
1 2 3 4 5 6 7 8 9 |
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? smile
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
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(); } } |
I tried it here and it failed on floating point numbers like this:
1 |
$_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? bigsmile
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
int bracketPos; bracketPos = lines[i].IndexOf('(', 0); while (bracketPos != -1) { // find the closing parenthesis int closingBracketPos = bracketPos + 1; int level = 1; while (level > 0) { switch (lines[i][closingBracketPos]) { case '(': level++; break; case ')': level--; break; } closingBracketPos++; } // extract the expression string expression = lines[i].Substring(bracketPos, closingBracketPos - bracketPos); // do something with expression // find next bracket. bracketPos = lines[i].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:
1 2 3 4 |
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:
1 |
$_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:
1 |
$_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:
1 |
$_1111L1LLL = $_1111L1LLL + $_11L1L1LL111(2, 3, 0.484222998499551) |
That doesn't look right! The original line was
1 |
$_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:
1 2 3 4 5 6 7 |
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:
1 2 3 4 5 6 7 8 9 10 |
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! smile
Function names
After solving math problems, methods are starting to look a bit better:
1 2 3 4 5 6 7 8 9 |
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:
1 2 3 4 5 |
Global ... $_L1111L1L11L ... $_L1111L1L11L = STRINGREVERSE $_11L1LLL1LL1 = $_L1111L1L11L(....) $_L1111L1L11L = STRINGREPLACE $_L1111L1L11L = GUICTRLCREATEBUTTON |
That's interesting. uneasy
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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:
1 2 3 4 5 |
Func _LL1111LL1L1L() $_111L1111L11 = ONAUTOITEXITREGISTER $_1L1LLLLLLLL1 = ASSIGN $_LL1L111LLLL = DRIVEGETDRIVE ... |
Then _LL1LLL1() reassigns some (or maybe all) of the variables:
1 2 3 4 5 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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. smile
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:
1 2 3 4 5 6 7 8 |
$_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:
1 2 3 4 |
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:
1 2 3 4 5 6 7 8 9 |
// 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[i] = lines[i].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! smile
Our test method now looks like this:
1 2 3 4 5 6 7 8 9 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Regex regex = new Regex(@"BITOR\((\d+)\, (\d+)\)"); 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 result = expr1 | expr2; lines[i] = regex.Replace(lines[i], 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.
There are several solutions possible, I just got rid of NextMatch and used a big while loop instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
do { changed = false; Regex regex = new Regex(@"BITOR\((\d+)\, (\d+)\)"); for (int i = 0; i < lines.Length; i++) { Match m1 = regex.Match(lines[i]); 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[i] = regex.Replace(lines[i], 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:
1 2 3 4 5 6 7 8 9 |
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. smile
String reverse
We're left with one final problem that is STRINGREVERSE function:
1 2 3 4 5 6 7 8 9 |
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:
1 2 3 4 5 6 7 8 9 |
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. smile
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/
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
Forgot to say ... this phrase 'We're just a few fuckups away from the solution! :)' made my day ;)
Reverse engineering comprises:
25% knowledge
25% time
25% persistence
25% coffee
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).
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.
Great read once again
Thank you for sharing your knowledge!
regards
cawk