Jump to content
Tuts 4 You

Poetic AutoIt (string/integer obfuscation)


SimplyMercurial

Recommended Posts

SimplyMercurial

Poetic AutoIt (string/integer obfuscation)


This is a simple CrackMe consisting of an input. The correct input is two stanzas of a particular poem and will reveal a label of the poem title and author upon success. Otherwise, "Try again..." will be displayed.

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.

Full disclosure:  It contains rudimentary debugger and compilation checks which, if failed, falls into a while loop with a one second sleep (harmless). But, as stated above, this isn't the focus of what I'm after.

 

 


 

Link to post
SHADOW_UA

Short way: search for memory (could be complex if you don't know what to search)

Spoiler

1.png.641389a71272c59a430a1ab40d9a8d74.png

Longer way: Unpack with UPX, use AutoIt Decompiler on the file. If we launch decompiled script, it will freeze. That is because there is a certain check that prevents us from running.

$_1111111ll11 = @Compiled
; ...
If NOT $_1111111ll11 Then _ll11lll1111() ; we have to comment this to be able to run

A correct "good boy" password can be obtained without analyzing how strings are stored as we can always print any variable to a messagebox.

Spoiler

2.png.b0f06e992d5f9ba8d5b6299ff341d054.png

 

  • Like 1
Link to post
SimplyMercurial

Nicely done, though that wasn't particularly what I was requesting.

Link to post
  • 4 weeks later...
kao

Regexps are not particularly efficient here and simple string operations work much better. :) 

 

Anyways, I made a writeup on my blog (https://lifeinhex.com/deobfuscating-autoit-scripts-part-2/) and made a copy-paste below. Unfortunately, all the hyperlinks are gone and I just can't be bothered to go through each and every one of them.

Also - it refers a lot to my old solution of another AutoIt crackme, so I really suggest to check that writeup as well: 

 

 

---------

 

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

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

Quote

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[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:

$_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:
spacer.png
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[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:

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 _LL1111LL1L1L()
    $_111L1111L11 = ONAUTOITEXITREGISTER
    $_1L1LLLLLLLL1 = ASSIGN
    $_LL1L111LLLL = DRIVEGETDRIVE
...

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 f*ck 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[i] = lines[i].Replace(kvp.Key + "(", kvp.Value + "(");
    }
}

Bit operations

All the hard stuff is done, I promise! We're just a few f*ckups away from the solution! smile

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(@"BITAND\((\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:

Quote

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).

Quote

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:

spacer.png

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(@"BITAND\((\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:

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 f*ckups 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.

Quote

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

 

 

 

  • Like 5
Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...