I’ve solved this little crackme quite some time ago, but I haven’t had time to publish the results. Besides this, protection wasn’t too hard, so I wasn’t sure if there is really anything to publish. Crackme was published on 14 January 2010 on crackmes.de, difficulty was set to 3 (Getting harder). Honestly speaking, without IronPython I would say that difficulty of this crackme is 1 (Very easy, for newbies, in the terms of crackmes.de scale), but with IronPython… well, it proved to be hard enough for me. Below analysis will shed some light on IronPython internals, there will be also part about .NET (as IronPython is just .NET Python), I’ll also cover the protection part, but it will not take too much space.

IronPython

I’ll not describe IronPython in general, but this specific case, so things may vary for different executables. Crackme is shipped as a zip package and contains 9 files:

CrackMe1.dll CrackMe1.exe IronPython.dll IronPython.xml Microsoft.Dynamic.dll Microsoft.Scripting.Core.dll Microsoft.Scripting.Debugging.dll Microsoft.Scripting.dll Microsoft.Scripting.ExtensionAttribute.dll

Microsoft.*.dlls are part of Dynamic Language Runtime (DLR) that runs on top of the .NET framework, IronPython.dll is IronPython runtime (with some .xml config). Crackme itself is split into two parts, executable and dll. Executable is just a host application that runs the proper module from the DLL:

// PythonMain [ STAThread ] public static int Main ( ) { return PythonOps . InitializeModule ( Assembly . LoadFile ( Path . GetFullPath ( "CrackMe1.dll" ) ) , "Program" , new string [ ] { "IronPython, Version=2.6.10920.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" , "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" , "System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" , "System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" , "System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" , "System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" , "System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" } ) ; } // PythonMain [STAThread] public static int Main() { return PythonOps.InitializeModule(Assembly.LoadFile(Path.GetFullPath("CrackMe1.dll")), "Program", new string[] { "IronPython, Version=2.6.10920.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" }); }

So, analysis will be done only on CrackMe1.dll. It contains only one class called DLRCachedCode:

As you can see names are not obfuscated, which seems very promising, especially function names like CheckKey$17() or KeyEncrypt$16(). Unfortunately code generated by IronPython compiler is more than enough in the term of obfuscation. All functions and objects are accessed through System.Runtime.CompilerServices.CallSite template class, for example:

//Part of the KeyEncrypt$16() function CallSite < Func < CallSite, object , CodeContext, object >> arg7 ; object arg6 = ( arg7 = ( CallSite < Func < CallSite, object , CodeContext, object >> ) strongBox . Value [ 284 ] ) . Target ( arg7, arg4, globalContext ) ; line = 126 ; CallSite < Func < CallSite, CodeContext, object , string , object >> arg9 ; CallSite < Func < CallSite, object , CodeContext, object >> arg10 ; object arg8 = ( arg9 = ( CallSite < Func < CallSite, CodeContext, object , string , object >> ) strongBox . Value [ 285 ] ) . Target ( arg9, globalContext, ( arg10 = ( CallSite < Func < CallSite, object , CodeContext, object >> ) strongBox . Value [ 286 ] ) . Target ( arg10, arg2, globalContext ) , "09887778" ) ; line = 127 ; CallSite < Func < CallSite, object , object , object >> arg11 ; CallSite < Func < CallSite, CodeContext, object , string , object >> arg12 ; CallSite < Func < CallSite, object , CodeContext, object >> arg13 ; ( arg11 = ( CallSite < Func < CallSite, object , object , object >> ) strongBox . Value [ 287 ] ) . Target ( arg11, arg4, ( arg12 = ( CallSite < Func < CallSite, CodeContext, object , string , object >> ) strongBox . Value [ 288 ] ) . Target ( arg12, globalContext, ( arg13 = ( CallSite < Func < CallSite, object , CodeContext, object >> ) strongBox . Value [ 289 ] ) . Target ( arg13, arg2, globalContext ) , "redbeansredbeans" ) ) ; //Part of the KeyEncrypt$16() function CallSite<Func<CallSite, object, CodeContext, object>> arg7; object arg6 = (arg7 = (CallSite<Func<CallSite, object, CodeContext, object>>)strongBox.Value[284]).Target(arg7, arg4, globalContext); line = 126; CallSite<Func<CallSite, CodeContext, object, string, object>> arg9; CallSite<Func<CallSite, object, CodeContext, object>> arg10; object arg8 = (arg9 = (CallSite<Func<CallSite, CodeContext, object, string, object>>)strongBox.Value[285]).Target(arg9, globalContext, (arg10 = (CallSite<Func<CallSite, object, CodeContext, object>>)strongBox.Value[286]).Target(arg10, arg2, globalContext), "09887778"); line = 127; CallSite<Func<CallSite, object, object, object>> arg11; CallSite<Func<CallSite, CodeContext, object, string, object>> arg12; CallSite<Func<CallSite, object, CodeContext, object>> arg13; (arg11 = (CallSite<Func<CallSite, object, object, object>>)strongBox.Value[287]).Target(arg11, arg4, (arg12 = (CallSite<Func<CallSite, CodeContext, object, string, object>>)strongBox.Value[288]).Target(arg12, globalContext, (arg13 = (CallSite<Func<CallSite, object, CodeContext, object>>)strongBox.Value[289]).Target(arg13, arg2, globalContext), "redbeansredbeans"));

Static analysis of this code is pretty much impossible, only some partial information can be gathered (code flow, strings, names of .NET objects). There is also one cool feature that gives some information about the original python code. In the above snippet there are assignments like “line = 126;”, those are line numbers from the original python script. Quick look at the #US (user string heap) stream from .NET image reveals some useful informations:

00000CF8: #US ... You must register to fully enjoy this software. ... Later, I'm Too Poor ... RedBeanSoup ... Thank you! Accepted - Registered to Please check your registration information try again. Invalid Key ... 09887778 redbeansredbeans ... UTF8Encoding RijndaelManaged CryptoStream CryptoStreamMode ... blocksize key text crypt /+= - Fully Registered Version finalkey ... BlockSize GetBytes IV translate strip CreateEncryptor ... Serial ToBase64String ... iv ToUpper ...

Judging only by those strings, I can tell that there will be Rijndael (“RijndaelManaged”, “BlockSize”, “IV”) and Base64 (“ToBase64String”) algorithms involved. There are also some fancy strings like “09887778” or “redbeansredbeans”, that might be somehow related to the serial number generation. At that point I’ve stuck, so I had to switch to dynamic analysis.

.NET Profiling

Having some bad experience with .NET bytecode debuggers in the past, I decided to give a try to .NET profiling API. There is a nice article about this topic written by Matt Pietrek and dated back to 2001 year:

http://msdn.microsoft.com/en-us/magazine/cc301725.aspx

It describes all the basic stuff behind .NET profiling and as a bonus it contains source code of simple profiler called DNProfiler. Link to the source code is broken, but I managed to put my dirty hands on it :) so here is the link to the original package:

Hood0112.zip

As those sources are 12 years old, they’re a bit outdated. Sample from the article is using ICorProfilerCallback while there is already ICorProfilerCallback4 (since .NET 4). Crackme is compiled for .NET 2.0, so I’ve updated this project to use ICorProfilerCallback2. .NET profiling API enables user to trace all entries and exits from every function (user defined as well as .NET runtime). Mentioned project doesn’t implement those hooks, but I’ve added it to gather some information about execution path. Below you can find patches that I’ve made to the project:

ProfilerCallback.patch

Amount of data gathered by this tool is unbelievable, when all hooks are enabled (enter/leave for every function) there is also huge slowdown. It would be possible to skip generation of some data, and filter output to collect only information useful from the crackme solver point of view, but honestly speaking it overwhelmed me a bit so I dropped that idea. Sample output:

ModuleLoadFinished: X:\xxx\RbsCrackMe1\CrackMe1.dll ModuleAttachedToAssembly: CrackMe1 AssemblyLoadFinished: CrackMe1 Status: 00000000 ObjectAllocated: array of System.String JITCompilationStarted: IronPython.Runtime.Operations.PythonOps::.cctor AssemblyLoadStarted ModuleLoadStarted ObjectAllocated: array of System.Object HandleCreated HandleCreated HandleCreated HandleCreated ObjectAllocated: System.Reflection.Assembly FunctionEnter: System.Reflection.Assembly::IsAssemblyUnderAppBase FunctionEnter: System.Reflection.Assembly::GetLocation FunctionEnter: System.Reflection.Assembly::get_InternalAssembly ObjectAllocated: System.String

Due to lack of time and other stuff that was waiting I’ve stopped playing with this profiling stuff and decided to move on.

ILSpy Debugger

ILSpy debugger worked surprisingly well on this crackme, there was one minor issue, namely it wasn’t working on x64 Windows, so I had to use it inside VMWare. I was debugging on C#-level as it was way more convenient than IL-level. There are basically two functions that needs to be traced: KeyEncrypt$16() and CheckKey$17(). KeyEncrypt$16() triggers first, debugging this code might be a bit complicated, but the bottom line is just about looking at the values/objects returned from all those cryptic CallSites<> calls. During debugging there are a lot of references to the array of objects called strongBox.Value[n], this array is initialized inside MainForm$7() function. It is easier to check indexes in this array from the Reflector, because ILSpy decompiles it as a proper array initialization and Reflector generates assignment operator for every array element:

//ILSpy: object [ ] value = new object [ ] { //... CallSite < Func < CallSite, object , CodeContext, object >>. Create ( PythonOps . MakeGetAction ( $globalContext, "BlockSize" , false ) ) , CallSite < Func < CallSite, CodeContext, object , string , object >>. Create ( PythonOps . MakeInvokeAction ( $globalContext, new CallSignature ( 1 ) ) ) , CallSite < Func < CallSite, object , CodeContext, object >>. Create ( PythonOps . MakeGetAction ( $globalContext, "GetBytes" , false ) ) , CallSite < Func < CallSite, object , object , object >>. Create ( PythonOps . MakeSetAction ( $globalContext, "IV" ) ) , //... } ; ( ( StrongBox < object [ ] > ) array [ 0 ] ) . Value = value ; //Reflector object [ ] objArray3 = new object [ 0x15a ] ; objArray3 [ 0x11c ] = CallSite < Func < CallSite, object , CodeContext, object >>. Create ( PythonOps . MakeGetAction ( $globalContext, "BlockSize" , false ) ) ; objArray3 [ 0x11d ] = CallSite < Func < CallSite, CodeContext, object , string , object >>. Create ( PythonOps . MakeInvokeAction ( $globalContext, new CallSignature ( 1 ) ) ) ; objArray3 [ 0x11e ] = CallSite < Func < CallSite, object , CodeContext, object >>. Create ( PythonOps . MakeGetAction ( $globalContext, "GetBytes" , false ) ) ; objArray3 [ 0x11f ] = CallSite < Func < CallSite, object , object , object >>. Create ( PythonOps . MakeSetAction ( $globalContext, "IV" ) ) ; object [ ] objArray2 = objArray3 ; ( ( StrongBox < object [ ] > ) objArray [ 0 ] ) . Value = objArray2 ; //ILSpy: object[] value = new object[] { //... CallSite<Func<CallSite, object, CodeContext, object>>.Create(PythonOps.MakeGetAction($globalContext, "BlockSize", false)), CallSite<Func<CallSite, CodeContext, object, string, object>>.Create(PythonOps.MakeInvokeAction($globalContext, new CallSignature(1))), CallSite<Func<CallSite, object, CodeContext, object>>.Create(PythonOps.MakeGetAction($globalContext, "GetBytes", false)), CallSite<Func<CallSite, object, object, object>>.Create(PythonOps.MakeSetAction($globalContext, "IV")), //... }; ((StrongBox<object[]>)array[0]).Value = value; //Reflector object[] objArray3 = new object[0x15a]; objArray3[0x11c] = CallSite<Func<CallSite, object, CodeContext, object>>.Create(PythonOps.MakeGetAction($globalContext, "BlockSize", false)); objArray3[0x11d] = CallSite<Func<CallSite, CodeContext, object, string, object>>.Create(PythonOps.MakeInvokeAction($globalContext, new CallSignature(1))); objArray3[0x11e] = CallSite<Func<CallSite, object, CodeContext, object>>.Create(PythonOps.MakeGetAction($globalContext, "GetBytes", false)); objArray3[0x11f] = CallSite<Func<CallSite, object, object, object>>.Create(PythonOps.MakeSetAction($globalContext, "IV")); object[] objArray2 = objArray3; ((StrongBox<object[]>) objArray[0]).Value = objArray2;

Below there are some most interesting values from this array:

strongBox.Value[284] -> "BlockSize" strongBox.Value[286] -> "GetBytes" strongBox.Value[287] -> "IV" strongBox.Value[289] -> "GetBytes strongBox.Value[291] -> "_textBox1" strongBox.Value[293] -> "translate" strongBox.Value[295] -> "strip" strongBox.Value[297] -> "_textBox1" strongBox.Value[299] -> "GetBytes" strongBox.Value[301] -> "_textBox1" strongBox.Value[304] -> "CreateEncryptor" strongBox.Value[305] -> "IV" strongBox.Value[311] -> "FlushFinalBlock" strongBox.Value[314] -> "ToBase64String" strongBox.Value[317] -> "iv" strongBox.Value[320] -> "CheckKey" strongBox.Value[322] -> "ToUpper" strongBox.Value[325] -> "translate"

There is also second global array that is worth to note (let’s call it globalArray):

PythonGlobal [ ] globalArrayFromContext = PythonOps . GetGlobalArrayFromContext ( globalContext ) ; PythonGlobal[] globalArrayFromContext = PythonOps.GetGlobalArrayFromContext(globalContext);

Having all those information ready I can start proper debugging. KeyEncrypt$16() creates new RijndaelManaged object, and sets explicitly BlockSize to 128. Next it converts “09887778” string to byte array (GetBytes). Following line reference “IV” and “GetBytes” from the strongBox array and “redbeansredbeans” string from the #US stream, I’ve assumed that it sets the initialization vector IV to “redbeansredbeans”. In the next step crackme performs strip() and translate() operations from python runtime on the “_textBox1” which contains entered user-name. All gathered information are put into CreateEncryptor function (from RijndalManaged object) and written into CryptoStream object. Below code represents what is happening (it is part of the keygen, so it is not 1:1 code from the crackme):

System. Text . ASCIIEncoding ascenc = new System. Text . ASCIIEncoding ( ) ; RijndaelManaged rm = new RijndaelManaged ( ) ; rm . BlockSize = 128 ; rm . KeySize = 256 ; ICryptoTransform ict = rm . CreateEncryptor ( ascenc . GetBytes ( "09887778" ) , ascenc . GetBytes ( "redbeansredbeans" ) ) ; string name = ( 0 == args . Length ) ? "ReWolf" : args [ 0 ] ; byte [ ] result ; using ( MemoryStream msEncrypt = new MemoryStream ( ) ) { using ( CryptoStream csEncrypt = new CryptoStream ( msEncrypt, ict, CryptoStreamMode . Write ) ) { using ( StreamWriter swEncrypt = new StreamWriter ( csEncrypt ) ) { swEncrypt . Write ( name ) ; } result = msEncrypt . GetBuffer ( ) ; } } System.Text.ASCIIEncoding ascenc = new System.Text.ASCIIEncoding(); RijndaelManaged rm = new RijndaelManaged(); rm.BlockSize = 128; rm.KeySize = 256; ICryptoTransform ict = rm.CreateEncryptor(ascenc.GetBytes("09887778"), ascenc.GetBytes("redbeansredbeans")); string name = (0 == args.Length) ? "ReWolf" : args[0]; byte[] result; using (MemoryStream msEncrypt = new MemoryStream()) { using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, ict, CryptoStreamMode.Write)) { using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) { swEncrypt.Write(name); } result = msEncrypt.GetBuffer(); } }

The last thing before call to CheckKey$17() is conversion of encrypted stream to base64 (Convert.ToBase64String()). CheckKey$17() is fairly easy to analyse, at first it makes generated base64 string uppercase (ToUpper), next it calls python runtime function called translate() which strips base64 output from this three characters “/+=”. CheckKey$17() gets only 16 characters from the output string. Those characters are split into four groups and separated with “-“. This new string is compared to entered serial number, yes the whole protection is just strcmp… Below you can find link to the keygen (written in C#):

rwf_rbs_keygen.cs

rbs_keygen.zip

I hope you enjoyed this analysis even though it was just plain strcmp.