Another approach to crackmes solving, this time it is .NET crackme written by |sas0|. I’ve found it on crackmes.de, it was published on 27 November 2012, difficulty was set to 3 – Getting harder. I’ve decided to give it a try as I don’t have much experience with .NET targets. It took me 3 days to solve it, but I consider those three days as a good time investment, because I had a chance to learn a few new things. So, here is my story:
Initial analysis
My weapon of choice for .NET analysis is Reflector. Quick look at the class tree reveals that crackme is not obfuscated:
Main() function looks pretty straight forward:
private static void Main(string[] args) { Console.Write("\t\t===[ Welcome to TheGame ]===\n\nEnter name: "); string str = Console.ReadLine(); Console.Write("Enter serial: "); string str2 = Console.ReadLine(); try { TheGame g = new TheGame(); g.Init(); g.AddName(str); g.AddSerial(str2); new Result(g).Process(); } catch (GameProtectionException exception) { Console.WriteLine("GameProtectionException: {0}\n\n{1}", exception.Message, exception.StackTrace); } catch (Exception exception2) { Console.WriteLine("{0}", exception2.Message); } } |
There is object called TheGame, that is responsible for all calculations. Sounds easy ? Well, it would be easy, but all important functions from this object (and not only from this object, but from all objects) are transformed into byte array and launched through reflection. Mechanism is the same for every function, I’ll describe it basing on TheGame.Init() method:
public void Init() { byte[] buffer = new byte[] { 0x16, 10, 2, 2, 0x1f, 9, 0x25, 13, 0x7d, 100, 0, 0, 4, 9, 0x7d, 0x63, 0, 0, 4, 0x16, 11, 0x2b, 0x12, 2, 0x7b, 0x62, 0, 0, 4, 7, 0x91, 0x2d, 4, 6, 0x17, 0x58, 10, 7, 0x17, 0x58, 11, 7, 2, 0x7b, 0x62, 0, 0, 4, 0x8e, 0x69, 50, 0xe3, 2, 6, 0x8d, 0x3e, 0, 0, 1, 0x7d, 0x65, 0, 0, 4, 0x16, 10, 0x16, 12, 0x2b, 0x1b, 2, 0x7b, 0x62, 0, 0, 4, 8, 0x91, 0x2d, 13, 2, 0x7b, 0x65, 0, 0, 4, 6, 0x25, 0x17, 0x58, 10, 8, 0x9e, 8, 0x17, 0x58, 12, 8, 2, 0x7b, 0x62, 0, 0, 4, 0x8e, 0x69, 50, 0xda, 0x2a }; Type[] arguments = new Type[] { typeof(int), typeof(int), typeof(int), typeof(int) }; Type[] resTypes = new Type[] { typeof(TheGame), typeof(TheGame), typeof(TheGame), typeof(TheGame), typeof(int), typeof(TheGame), typeof(TheGame), typeof(TheGame), typeof(TheGame) }; string[] resVars = Transformer.Change(new string[] { "+Nfs6fzp1+vn5P3l5g==", "+Nfs6fzp1/rn/w==", "+Nfs6fzp", "+Nfs6fzp", "web8u7o=", "+Nfh5uzt8A==", "+Nfs6fzp", "+Nfh5uzt8A==", "+Nfs6fzp" }); Type[][] funParamTypes = new Type[][] { new Type[0], new Type[0], new Type[0], new Type[0], new Type[0], new Type[0], new Type[0], new Type[0], new Type[0] }; int[] index = new int[] { 9, 15, 0x19, 0x2c, 0x37, 60, 0x48, 0x52, 100 }; int maxStackSize = 4; try { DynamicMethod method = new DynamicMethod("", typeof(void), new Type[] { typeof(TheGame) }, true); DynamicILInfo dynamicILInfo = method.GetDynamicILInfo(); SignatureHelper localVarSigHelper = SignatureHelper.GetLocalVarSigHelper(); localVarSigHelper.AddArguments(arguments, null, null); TokenResolver.Resolve(ref buffer, index, resTypes, resVars, funParamTypes, dynamicILInfo); dynamicILInfo.SetCode(buffer, maxStackSize); dynamicILInfo.SetLocalSignature(localVarSigHelper.GetSignature()); InitD td = (InitD) method.CreateDelegate(typeof(InitD), this); td(); } catch (Exception exception) { throw new GameProtectionException(exception.Message, exception); } } |
- buffer – byte array that represents raw bytecode of the original function
- arguments – types of local variables used by the original function
- resTypes – types of used tokens
- resVars – names of used tokens (usually encoded in base64)
- funParamTypes – types of parameters for tokens that represents functions (readability ftw!)
- index – offsets of each token in the bytecode buffer
Above arrays are used to properly setup DynamicMethod object. The most important part is hidden in TokenReslover.Resolve() function, which is responsible for adjusting tokens inside bytecode array. When bytecode is ready, crackme calls SetCode() & SetLocalSignature() methods and call dynamically generated function.
Now I had a few ideas on how to get proper code from those bytecode arrays:
-
Find some simple .NET disassembler that can handle raw binary buffer – This was the simplest plan, but I’ve failed at it. Few days after I’ve solved this crackme one of my friends told me that CFF Explorer has such feature. Well it’s a pity. Disadvantage of this method would be lack of resolved tokens, so I wouldn’t be able to easily deduce what is really happening.
-
Try to debug crackme with some IL-level debugger, it should hopefully step into dynamically generated code. I’ve tried ILSpy debugger and PEBrowse debugger, both failed at stepping into dynamic functions.
-
Third idea was somehow between 1st and 2nd, I’ve found add-on for Visual Studio which is able to disassemble bytecode from DynamicMethod objects, it is called IL Visualizer and next paragraph will describe it a bit more.
IL Visualizer
IL Visualizer is a Debugger Visualizer component for Visual Studio written by Haibo Luo back in 2005. As I said before, it enables debugger to disassemble bytecode from DynamicMethod objects, more information and screen-shots can be found in linked article. To make use of this little beast I had to recompile my version of the crackme. As the code wasn’t obfuscated I’ve tried FileGenerator plugin for Reflector. It even has option to create Visual Studio project, which worked really well and I had working and compilable source code of the entire crackme. I’ve also compiled IL Visualizer from sources and tried it with the decompiled crackme. It wasn’t working :) I’ve spent few hours on resolving this issue, but finally I came up with a solution that worked. Solution is probably not so nice from the .NET developer point of view, but sufficient from my point of view, and taking into consideration that I knew basically nothing about this topic (C#, System.Reflection etc) before, I’m proud of it :D The main problem with IL Visualizer was, that it uses DynamicMethod.GetILGenerator() function to obtain information about generated code. Crackme is using DynamicILInfo instead of ILGenerator to create dynamic method, in that case m_ilGenerator field from DynamicMethod object is set to null and IL Visualizer fails at acquiring bytecode and resolving tokens. To fix this thing I had to introduce changes in two files:
- DynamicMethodILProvider.cs
- DynamicScopeTokenResolver.cs
Changes in both files are similar, in places where original code tries to get ILGenerator from DynamicMethod object (GetILGenerator()), I’ve put additional code that first checks if m_code field from DynamicILInfo (GetDynamicILInfo()) is not empty and if it is, it will use data form DynamicILInfo instead of ILGenerator. Diff patches for both files as well as fully patched VS2010 solution can be found below:
- DynamicMethodILProvider.cs.patch
- DynamicScopeTokenResolver.cs.patch
- ILVisualizer2010_rewolf_patch.zip
Bytecode analysis
Further analysis was done in Visual Studio debugger and notepad, I’ve set breakpoints on every call to dynamic methods and when each breakpoint was hit, I was dumping disassembly from IL Visualizer to notepad. At the beginning I’ve skipped TheGame constructor and TheGame.Init() method, so I’ve proceeded directly to TheGame.AddName() function:
IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: newobj Void .ctor(System.String)/Game.Name IL_0008: stfld Game.Name p_name/Game.TheGame IL_000d: ldarg.0 IL_000e: ldfld Game.Name p_name/Game.TheGame IL_0013: callvirt Void Init()/Game.Name IL_0018: nop IL_0019: ldarg.0 IL_001a: ldfld Game.Name p_name/Game.TheGame IL_001f: ldarg.0 IL_0020: callvirt Void Process(Game.TheGame)/Game.Name IL_0025: nop IL_0026: ret |
Above code creates new object called Game.Name, and calls two methods (except .ctor) Game.Name.Init() and Game.Name.Process(). Full IL-dasm of those functions can be found here: analysis.txt. It contains IL dumped from most of important functions with comments and some pseudo C representation for each function. It’s a bit chaotic and not clear but I’m putting it here just as a reference to show how low level IL analysis might looks like. Description of opcodes can be found in ECMA-335 standard in the section III. To explain mechanics of each function I’ll use python snippets, as it will greatly improve readability of the whole post. First analysed function is Name.Init():
dig = hashlib.sha1(username).digest() nameSHA1 = [] for b in dig: nameSHA1.append(ord(b)) name_p_data = [] lsh = len(nameSHA1) for i in xrange(lsh): tb = ((nameSHA1[i] & 0xF0) >> 4) tb |= ((nameSHA1[i] & 0xF) << 4) if i < lsh - 1: tb ^= nameSHA1[i + 1] name_p_data.append(tb) |
It calculates SHA1 from username, and then transform it in a simple loop that swaps bits inside each byte and xor it with the next byte:
SHA1("rewolf"): 7B D5 3E 89 5D 0A 53 70 04 D5 C2 45 34 9C 7F 37 4C 63 AF 4F After 'swap-xor' loop: 62 63 6A C5 DF F3 45 03 95 9F 69 60 DF B6 C0 3F A7 99 B5 F4 |
Name.Process() is a bit more complicated and uses one array from TheGame object obtained by Game.TheGame.GetIndex() function, I’ll call this array game_p_index:
j = 0 while j + 15 < len(game_p_index): for i in xrange(len(name_p_data)): b0 = (name_p_data[i] & 0xF0) >> 4 b1 = name_p_data[i] & 0xF b2 = game_p_index[b0 + j] game_p_index[b0 + j] = game_p_index[b1 + j] game_p_index[b1 + j] = b2 j += 5 |
Above loop shuffle values in game_p_index array according to values from name_p_data.
game_p_index before shuffling: 00 01 03 05 06 0A 0B 0C 0E 0F 10 13 14 15 16 19 1A 1C 1D 1F 20 22 23 24 25 27 2B 2C 2D 2E 31 32 33 34 36 37 3A 3B 3C 3D 40 41 42 44 45 46 4A 4B 4D 4F 50 game_p_index after shuffling: 0A 01 0B 19 00 0C 13 0F 20 15 03 1A 16 27 1D 05 22 1F 31 24 0E 2B 25 37 2D 06 32 2E 40 34 10 3A 36 46 3C 14 41 3D 50 44 4D 4A 2C 33 42 1C 45 23 3B 4F 4B |
At this point there is one missing input array: game_p_index. It is generated by TheGame.Init() function:
game_p_index = [] for i in xrange(len(game_p_data)): if game_p_data[i] == 0: game_p_index.append(i) |
Quick look at this function shows that game_p_index contains indexes to game_p_data entries equal to zero. game_p_data array is filled at the beginning (TheGame.ctor):
game_p_data = [ 0, 0, 9, 0, 8, 0, 0, 4, 3, 6, 0, 0, 0, 9, 0, 0, 0, 8, 5, 0, 0, 0, 0, 3, 6, 0, 0, 4, 0, 0, 7, 0, 0, 9, 0, 0, 0, 0, 7, 0, 5, 9, 8, 0, 0, 0, 0, 5, 3, 0, 0, 0, 0, 1, 0, 0, 3, 6, 0, 0, 0, 0, 2, 7, 0, 0, 0, 4, 0, 0, 0, 6, 1, 8, 0, 0, 3, 0, 4, 0, 0 ] |
In the next step, crackme converts entered serial number with Convert.FromBase64String() function to binary data (serial_p_data) and calls Serial.Process() method:
v4 = 0 v5 = 0 v6 = True while v5 < len(serial_p_data): loc0 = (serial_p_data[v5] & 0xF0) >> 4 loc1 = serial_p_data[v5] & 0xF game_p_data[game_p_index[v4]] = loc0 v4 += 1 if v5 >= len(serial_p_data): v6 = False else: v6 = v4 < len(game_p_index) if (not v6): break game_p_data[game_p_index[v4]] = loc1; v4 += 1 v5 = v4 / 2 v6 = True |
Above code gets each 4 bits from serial_p_data and puts it into game_p_data array at indexes taken from game_p_index table. When game_p_data is filled, crackme runs first check routine TheGame.Check():
p_data_column = 9 p_data_row = 9 loc2 = 0 error = False while loc2 < p_data_column: loc1 = 0 loc0 = 0 loc3 = p_data_column * loc2 while loc3 < (loc2 + 1) * p_data_row: loc1 = (1 << (game_p_data[loc3] & 0x1F)) >> 1 if loc1 > 256: error = True break if loc1 == loc0 & loc1: error = True break loc0 |= loc1 loc3 += 1 if error: break loc1 = ~(loc0 ^ -512) if loc1 != 0: error = True break loc2 += 1 |
For each row, it calculates loc0 value and compares 0 to ~(loc0 ^ -512), or after simplification it just checks if loc0 is equal to 0x1FF. To calculate loc0, it takes each value from the row, compute bitmask of the value (e.g. 1 -> 0x0001, 2 -> 0x0002, 3 -> 0x0004, 4 -> 0x0008 etc) and OR it with loc0. It also checks if the specific bit was not set already. It is now clear that each row should be filled with values from 1 to 9 (does it ring a bell ? ;p). After this first check, there is call to second function called TheGame.CheckC(). I didn’t analysed it fully, as it was very similar to TheGame.Check(), the only difference was, that it checks columns instead of rows, and ending check is a bit different (comparison to 0x12C8 xor 0x1337). There is also third check called TheGame.Check33(), but I haven’t looked at it at all, as I’ve guessed that protection scheme is based on Sudoku.
Writing keygen
Fortunately I don’t need to write sudoku solving algorithm to code a proper keygen. There are many online sudoku solvers out there, I’ve used one of those to get a final game_p_data table:
output_b = [ 2, 1, 9, 5, 8, 6, 7, 4, 3, 6, 3, 4, 2, 9, 7, 5, 1, 8, 5, 7, 8, 4, 1, 3, 6, 2, 9, 4, 6, 1, 7, 2, 8, 9, 3, 5, 3, 2, 7, 1, 5, 9, 8, 6, 4, 8, 9, 5, 3, 6, 4, 2, 7, 1, 9, 4, 3, 6, 7, 5, 1, 8, 2, 7, 5, 2, 8, 4, 1, 3, 9, 6, 1, 8, 6, 9, 3, 2, 4, 5, 7 ] |
I’ve put solved sudoku to second array (output_b), because original game_p_data is used to generate initial values in game_p_index. Snippet that shuffles game_p_index with name_p_data (sha1 computed for given username) was already described. To generate serial number for the given username I just need to reverse Serial.Process() function, which was responsible for filling empty places in sudoku square. It was rather staright forward:
out_serial = [] for i in xrange(len(game_p_index)/2): b1 = output_b[game_p_index[2*i]] b1 <<= 4 b1 |= output_b[game_p_index[2*i+1]] out_serial.append(b1) out_serial.append(output_b[game_p_index[len(game_p_index) - 1]] << 4) |
Basically it creates 8 bits values from pairs of 4 bits values pointed by two consecutive indexes from game_p_index. After out_serial is generated, it should be encoded to base64. Fully working keygen (written in python) can be found here: keygen.py
Thanks for reading, if you spot any errors or obvious mistakes feel free to leave a comment.
Nice tutorial! Keep it going ;)
I’m glad that You liked it :)
Hi,
This is a very good .NET crackme writeup :-)
Thank you for not excluding any of your steps. Even the ones where you failed.
.NET Debugging is a topic which is unfortunately not covered enough in the “reversing media”.
Thanks again for this.
P.S. Your blog is awesome :-)
Thanks!