queenp's blog

Posted Tue 01 September 2015

Bsides MCR 2015 NCC crackme writeup

So, this is a .NET executable. Chucking it in your favourite .NET disassembler is the easiest way to see what’s going on.

Ideally the easiest is to find a tool that’ll a) translate it into C# and b) render the ints as hex rather than signed. I couldn’t find a nice tool that did that nicely which is a bit of a shame. If you’re happy just using text based tools, it’s still perfectly managable using monodis to render it as human-readable IL code and to use a python console as a running calculator.

Preliminary stuff

There’s a viable shortcut here incidentally: You can bypass the obfuscation altogether. The solution is split into three 32-bit ascii chunks, converted to integers, each \(\oplus \texttt{0xFFFFFFFF}\). Quickly scanning through the constants and \(\oplus\)ing them bytewise with \(\texttt{0xFF}\) and rearranging them so they made a sensible solution phrase is much less laborious than deobfuscating the code (I wish I’d done that).

A quick strings check turns up a base64 code which decoded reads

Real ninjas drink smoked porter!</redherring>

Alas, not the solution. There’s also a SHA-512 hash of the solution (so good luck cracking that [1]).

Disassembly

On disassembling it, there’s a form which (either by grabbing the enter key or clicking on the button) runs the input in a form field through the validateBeer() method. There’s a hash checker method. There’s the validateBeer() method. I guessed that validateBeer was probably where the flag was, but just to be sure I had a look at the form first.

There’s an interesting demonstration of the obfuscation method pretty much straight away:

IL_0011:  ldc.i4 260823854
IL_0016:  ldc.i4 641331575
IL_001b:  xor
IL_001c:  dup
IL_001d:  stloc.1
IL_001e:  ldc.i4.4
IL_001f:  rem.un
IL_0020:  switch (
            IL_0071,
            IL_0037,
            IL_0011,
            IL_0052
          )

Load 2 constants. \(\oplus\) them, store the result in a local variable, take their remainder \(\texttt{mod 4}\). Jump to the instruction indicated by a list in a switch statement (spoiler: the remainder is \(1\), ie the 2nd of 4 statements). Python is quite comfortable handling unsigned ints of this length and xoring them, but to avoid errors on signed ints, it’s probably an idea when you’re using the py console as a calculator to import uint32 from numpy and wrap any pasted constants in uint32() so that python does the right bitwise operations. [2]

Following a bit longer down this chain of events we see that input gets run (either via the buttonSubmit or keypress methods) through the validateBeer() method, which is in fact where the solution is to be found.

validateBeer(string theBeer)

IL_0000:  ldarg.1
IL_0001:  stloc.0
IL_0002:  ldloc.0
IL_0003:  callvirt instance int32 string::get_Length()
IL_0008:  ldc.i4.3
IL_0009:  bge.s IL_007b

So at this point I was getting to know .NET IL a bit better. Check theBeer’s length is \(\geq 3\).

IL_007b:  ldloc.0
IL_007c:  ldstr " "
IL_0081:  callvirt instance bool string::Contains(string)
IL_0086:  brfalse.s IL_0090

IL_0088:  ldc.i4 -1252282546
IL_008d:  dup
IL_008e:  br.s IL_0096

IL_0090:  ldc.i4 -1935870315
IL_0095:  dup
IL_0096:  pop
IL_0097:  br IL_0010

Check if the beer contains a space. If so, load 0xb55baf4e on the stack, else 0x8c9cf695, either way jump to IL_0010. I guess the intention is that you have no idea which path is the case being tested for inclusion.

IL_0010:  ldc.i4 -1025652129
IL_0015:  xor
IL_0016:  dup
IL_0017:  stloc.s 9
IL_0019:  ldc.i4.6
IL_001a:  rem.un
IL_001b:  switch (
            IL_004c,
            IL_003a,
            IL_009c,
            IL_007b,
            IL_000b,
            IL_005e
          )

Alrighty then. It’s that pattern again. loc_9 = stack[0]^stack[1]. Reduce it \(mod 6\). If theBeer has no space, jump to IL_004c (ldc.i4.0; ret, ie return False, not the condition we’re looking for) else jump to IL_005e.

IL_005e:  ldloc.0
IL_005f:  ldstr "!"
IL_0064:  callvirt instance bool string::Contains(string)
IL_0069:  brtrue.s IL_009e
...
IL_009e:  call class [mscorlib]System.Text.Encoding class [mscorlib]System.Text.Encoding::get_ASCII()
IL_00a3:  ldloc.0
IL_00a4:  callvirt instance unsigned int8[] class [mscorlib]System.Text.Encoding::GetBytes(string)
IL_00a9:  stloc.1
IL_00aa:  leave.s IL_00b5

Okay, so if the string contains !, get an ascii encoded array of bytes.

IL_00b5:  ldstr ""
IL_00ba:  stloc.2
IL_00bb:  ldc.i4 -1867669444
IL_00c0:  ldc.i4 -1025652129
IL_00c5:  xor
IL_00c6:  dup
IL_00c7:  stloc.s 9
IL_00c9:  ldc.i4.s 0x1d
IL_00cb:  rem.un
IL_00cc:  switch (

Starting at IL_00b5 is a much bigger switch table with 29 entries! (I’m not going to bother pasting so many things, as you might imagine). Scanning ahead we can see a lot of br IL_00c0, which is to say this is a loop, where the last value pushed on the stack is XORed with the constant at IL_00c0, saved (at loc_9) and used to decide the next path to take. We can also see a bunch of subroutines where 1 and 0 strings are concatenated onto another string (presumably the empty string we loaded on the stack at IL_00b5). We can see some bitshifting (shl) stuff, and a few “load constant, return to start of loop” actions, which are clearly forcing one path or another.

Without wanting to show my working for following this, that’s basically what I did. Following the logic is tortuous. The first jump is to the 8th address (ie, the remainder is 7). Which stores 0 in loc3, multiplies the previous XORed value with a constant, \(\oplus\)s the result with another constant back to 00c0. As such the value you're coming back into the loop with tracking control flow is dependent on having gotten your maths right previously. Doing this by hand (or at least by python console) is very brittle and it’s a good time to start liberally using the console’s (and maybe notepad’s) ability to store variables that may be depended on later because there’s no really good way to recover if you screw up your maths.

Working in that way through the control flow you find out first that there’s a test to make sure that the string is 12 bytes long, and second realise that all that bitshifting comes down to reading the input string bit by bit, flipping the bits one by one and building a flipped string representation. With that as a clue you might look ahead and spot that there are a few branches where a local variable is loaded, a ‘negative’ constant is loaded, followed by a beq.s instruction. Printable ascii only occurs in the 0x00 - 0x7f, when its bits are flipped (thanks to 2’s complement notation) interpreted as a signed int, all bytes will be negative. So let’s stop tracing the control flow for a minute and look at those value comparisons:

IL_0329:  ldloc.s 4
IL_032b:  ldc.i4 -1936551784
IL_0330:  beq.s IL_033a
uint32(-1936551784) ^ 0xFFFFFFFF = 'smog'
IL_03e5:  ldloc.s 5
IL_03e7:  ldc.i4 -544370532
IL_03ec:  beq.s IL_03f6
uint32(-544370532) ^ 0xFFFFFFFF = ' roc'
IL_04cd:  ldloc.s 6
IL_04cf:  ldc.i4 -1801810978
IL_04d4:  beq.s IL_04de
uint32(-1801810978) ^ 0xFFFFFFFF = 'ket!'

At that point it doesn’t take many guesses to see which order they should be part of the phrase. Punch it into the CrackMe.exe form and voila.

[1]Although again maybe you could. The passphrase was 2 short english words with one space and a terminating ’!’, pretty feasible, but only after you’ve reversed the initial part of validateBeer(). My computer when I did this was ancient. Not like yon fancy computers since I got a fancy computer job.
[2]Later edit: Actually it's better to learn how the struct module works properly and use it. If you're using Python to hack bytes you need it regularly as it is a pretty sweet swiss army knife for this sort of thing.
Category: writeups
Tags: reversing the hard way things you should smt-solve things-I-didn't-smt-solve crackme ctf