In this post, I will be analyzing an assembly called LINQPad Server that is used used to register old versions of LINQPad. The goal of this post is to see how a malicious party could bypass the security included in LINQPad to determine whether it matches the inferences made in the previous posts.
Introduction
LINQPad is a code snippet IDE that instantly executes C#, F# and VB expressions, statement blocks or programs dynamically - e.g. without a Visual Studio project or solution. There are paid versions that enhance the functionality by providing the user with Intellisense and a debugger. You can find out more information about the application on the website.Disclaimer
I am not responsible for your use of the information provided here. At the time of this writing, the following information is not against the End User License Agreement as I will not be distributing modifications to LINQPad, sub-licensing it, or circumventing any paid edition usage restrictions. In this series; however, I will be discussing what protections the software has and what methods could be taken to bypass them.What You Need
To complete this segment, you will need a .NET decompiler such as .NET Reflector, dotPeek, JustDecompile, or ILSpy. You may want to check out dnSpy as well as it has better editing capabilities (thank you XenocodeRCE@rtn-team!). You will also need a deobfuscator that supports Confuser such as NoFuserEx. Finally, you will need the target, called LINQPad Server (or just follow along, because I will not provide this). If you do look for this, make sure you get it from a reputable site (as it could be packaged with malware otherwise).Getting Started
The README for LINQPad Server is as follows:LINQPad was written by Joseph Albahari.
He's given a lot of time to code this piece of software and protect it, so please give him support by buying LINQPad.
http://www.linqpad.com/
===================================
ONLINE ACTIVATION
-----------------------------------
- Click 'Activate autocompletion' or go to Help -> Upgrade to LINQPad
- Click Specify Web Proxy Server
- Select Manual Specific Proxy and put the following values:
- Proxy Address: http://127.0.0.1
- Proxy Port: 8080 (default)
- Click OK and run LINQPadServer as Administrator
- Put any activation code and wait for it to get activated! Disconnect your internet connection if you want to make sure that it doesn't connect to the real LINQPad server.
- Restore your old proxy details. This is for Nuget to work.
===================================
OFFLINE ACTIVATION
-----------------------------------
- Click 'Activate autocompletion' or go to Help -> Upgrade to LINQPad
- Disconnect your internet and put any activation code
- Click activate
- After failing to connect to the server, your 'Machine Activation Data' should be in your clipboard.
- Open LINQPadServer and click 'Offline Activation'
- Paste your activation data in the activation data textbox
- Go back to LINQPad and RIGHT-click on the text that says: 'Activation requires Internet connectivity.'
- Paste your activation code and click OK
How It Works
Running LINQPad Server starts an application that looks like this:This application serves two purposes.
First, it allows the user to set a port on the local server that the LINQPad application can connect to. This will send the data seen from Part 3. The response it gives will follow the same logic found in Part 4.
Or, it allows the user to disconnect from the internet and provide the failed error message to the application to provide a valid authorization code that can be copied to the RegisterOffline section found in Part 1. This will also follow the same logic found in Part 4.
This application, for all intents and purposes, replaces the actual LINQPad server using the proxy settings available within LINQPad. Which may explain how the application is named.
Reversing the Application
Loading the application in a decompiler fails, because the application is obfuscated by Confuser. I found a deobfuscator that supports Confuser called NoFuserEx. It takes a little while to run, but once completed I am able to load the assembly in a decompiler. I noticed that strings are still obfuscated a little but I was able to piece together what they were for the most part - given what I learned about the LINQPad Authorization Code structure from the previous posts. I start out by going to the Entry Point which looks like a typical WinForms application. It sets some defaults and then starts a form as the main form.[STAThread] private static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form0()); }Now this form has a static constructor that will execute first to set up a delegate method for decrypting the strings used in this class.
static Form0() { Class11.smethod_39(typeof(Form0)); Form0.array_0 = new string[] { Form0.GetString_14.Vmethod_35(2493), Form0.GetString_14.Vmethod_35(2510), Form0.GetString_14.Vmethod_35(2527), Form0.GetString_14.Vmethod_35(2548) }; }A quick look at this will show how the Form gets the delegate to decrypt strings.
public static void smethod_39(Type Param_94) { FieldInfo[] fields = Param_94.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField); for (int i = 0; i < (int)fields.Length; i++) { FieldInfo fieldInfo = fields[i]; try { if ((object)fieldInfo.FieldType == (object)typeof(Class9)) { string empty = string.Empty; Type type = typeof(string); Type[] typeArray = new Type[] { typeof(int) }; DynamicMethod dynamicMethod = new DynamicMethod(empty, type, typeArray, Param_94.Module, true); ILGenerator lGenerator = dynamicMethod.GetILGenerator(); lGenerator.Emit(OpCodes.Ldarg_0); MethodInfo[] methods = typeof(Class14).GetMethods(BindingFlags.Static | BindingFlags.Public); int num = 0; while (true) { if (num < (int)methods.Length) { MethodInfo methodInfo = methods[num]; if ((object)methodInfo.ReturnType == (object)typeof(string)) { lGenerator.Emit(OpCodes.Ldc_I4, fieldInfo.MetadataToken & 16777215); lGenerator.Emit(OpCodes.Sub); lGenerator.Emit(OpCodes.Call, methodInfo); break; } else { num++; } } else { break; } } lGenerator.Emit(OpCodes.Ret); fieldInfo.SetValue(null, dynamicMethod.CreateDelegate(typeof(Class9))); break; } } catch { } } }The static constructor passed the Form0 type so that this method could reflect off of it to set a static field that inherits from a particular class. Form0 only has one of these:
[NonSerialized] internal static Class9 GetString_14;Class9 is a delegate:
public delegate string Class9(IAsyncResult Param_92);If smethod_39 finds this field it creates a DynamicMethod:
Defines and represents a dynamic method that can be compiled, executed, and discarded. Discarded methods are available for garbage collection.The code then creates these virtual methods for each static public method in Class14 that returns a string.
if ((object)methodInfo.ReturnType == (object)typeof(string)) { lGenerator.Emit(OpCodes.Ldc_I4, fieldInfo.MetadataToken & 16777215); lGenerator.Emit(OpCodes.Sub); lGenerator.Emit(OpCodes.Call, methodInfo); break; }You may want to brush up on your opcodes, but this new delegate method looks something like this:
private string GetStringFromResources(int offset) { return Class14.smethod_40(offset - (fieldMetaDataToken & 16777215)); }Conveniently, Class14 only has one method smethod_40, but it also has a static constructor.
static Class14() { Class14.string_4 = "0"; Class14.string_5 = "232"; Class14.array_1 = null; Class14.Boolean_24 = false; Class14.int_2 = 0; if (Class14.string_4 == "1") { Class14.Boolean_24 = true; Class14.Dictionary = new Dictionary<int, string>(); } Class14.int_2 = Convert.ToInt32(Class14.string_5); using (Stream manifestResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("{9f2f3e9c-b74c-4a28-8320-a2229492f7c3}")) { int num = Convert.ToInt32(manifestResourceStream.Length); byte[] numArray = new byte[num]; manifestResourceStream.Read(numArray, 0, num); Class14.array_1 = Class15.smethod_44(numArray); numArray = null; manifestResourceStream.Close(); } }The static constructor sets some static fields, and reads a resource file. Presumably, which holds all of the strings for the assembly. It sets a static byte array in this class to the result of a method call that jumps around, contains nested classes, and performs a lot of mathematics that I could have a write-up dedicated to that alone. Since I do not really need to know any of the strings at this time, I will not be covering this.
public static string smethod_40(int Param_95) { string str; string str1; Param_95 = Param_95 - Class14.int_2; if (Class14.Boolean_24) { Class14.Dictionary.TryGetValue(Param_95, out str); if (str != null) { return str; } } int array1 = 0; int param95 = Param_95; int num = param95; param95 = num + 1; int array11 = Class14.array_1[num]; if ((array11 & 128) == 0) { array1 = array11; if (array1 == 0) { return string.Empty; } } else if ((array11 & 64) != 0) { int num1 = param95; param95 = num1 + 1; byte[] numArray = Class14.array_1; int num2 = param95; param95 = num2 + 1; byte[] numArray1 = Class14.array_1; int num3 = param95; param95 = num3 + 1; array1 = ((array11 & 31) << 24) + (Class14.array_1[num1] << 16) + (numArray[num2] << 8) + numArray1[num3]; } else { int num4 = param95; param95 = num4 + 1; array1 = ((array11 & 63) << 8) + Class14.array_1[num4]; } try { byte[] numArray2 = Convert.FromBase64String(Encoding.UTF8.GetString(Class14.array_1, param95, array1)); string str2 = string.Intern(Encoding.UTF8.GetString(numArray2, 0, (int)numArray2.Length)); if (Class14.Boolean_24) { try { Class14.Dictionary.Add(Param_95, str2); } catch { } } str1 = str2; } catch { str1 = null; } return str1; }The method called, uses the array from the static constructor minus one of the other static values to create the UTF8 string. Which supports the hypothesis that the resources file contains the strings used. The original method then creates a delegate so that the rest of the assembly can retrieve strings and assigns it to the field found earlier.
Finally, back to the default non-static constructor of Form0.
public Form0() { this.method_26(); }method_26 must be the InitializeComponent() call setup by WinForms applications. From this method I can determine what obfuscated methods correlate to the events for the controls.
- button_0.Click = method_23
- CheckBox_13.CheckChanged = method_20
- text_box_1.TextChanged = method_19
- Vmethod_18 = OnFormClosed
- Vmethod_25 = Dispose(bool disposing)
- method_24 = SetToolStripText(string text)
private void method_21(HttpListenerContext Param_52) { this.method_24(Form0.GetString_14.Vmethod_35(1744)); string text = this.textBox_0.Text; Class4 class4 = new Class4(Param_52, text); try { class4.Main(); this.method_24(string.Concat(Form0.GetString_14.Vmethod_35(1785), text, Form0.GetString_14.Vmethod_35(1334))); } catch (Exception exception1) { Exception exception = exception1; this.method_24(string.Concat(Form0.GetString_14.Vmethod_35(1830), exception.Message)); } } private void method_22(HttpListenerContext Param_54) { this.method_24(Form0.GetString_14.Vmethod_35(1855)); (new Class7(Param_54)).Main(); this.method_24(Form0.GetString_14.Vmethod_35(1904)); }
Register Online
Since I know the button click event, that seems like a good place to start to track down what happens next.private void method_23(object Param_56, EventArgs Param_57) { //... }When the button is clicked, it checks to see if the program is listening on the specified port
if (this.HttpListener_10.IsListening) { this.HttpListener_10.Stop(); this.method_24(Form0.GetString_14.Vmethod_35(1945)); this.textBox_0.Enabled = true; this.button_0.Text = Form0.GetString_14.Vmethod_35(1966); return; }If the program is, it resets the state of the controls to what they would be if it was not listening.
if (string.IsNullOrWhiteSpace(this.textBox_0.Text)) { MessageBox.Show(Form0.GetString_14.Vmethod_35(1510), Form0.GetString_14.Vmethod_35(1563), MessageBoxButtons.OK); return; }If it is not, it checks that the user has entered a valid license user. Once there is a valid name, it checks that the user has selected a valid port:
try { num = Convert.ToInt32(this.textBox_3.Text); } catch { num = 0; } if (num <= 0 || num > 65535) { MessageBox.Show(Form0.GetString_14.Vmethod_35(1983)); return; }If not, it displays another message to the user informing what a valid port range is. If everything is well up to this point, it calls to the method responsible for listening on the port specified and sets the controls display values to the proper text.
this.method_17(( from Param_69 in Form0.array_0 select string.Concat(new object[] { class9.Vmethod_35(2642), Param_69, class9.Vmethod_35(2655), num, class9.Vmethod_35(2660) })).ToArray<string>()); this.button_0.Text = Form0.GetString_14.Vmethod_35(2028); this.method_24(Form0.GetString_14.Vmethod_35(2045)); this.textBox_0.Enabled = false;Some additional checking is performed before setting up the HttpListener.
if (!HttpListener.IsSupported) { Console.WriteLine(Form0.GetString_14.Vmethod_35(1400)); return; } if (Param_42 == null || Param_42.Length == 0) { throw new ArgumentException(Form0.GetString_14.Vmethod_35(1497)); }If the HttpListener is supported and there are prefixes passed to the method, it assigns the prefixes and starts the listener.
this.HttpListener_10.Prefixes.Clear(); string[] param42 = Param_42; for (int i = 0; i < (int)param42.Length; i++) { string str = param42[i]; this.HttpListener_10.Prefixes.Add(str); } this.HttpListener_10.Start();To handle requests, the application queues a WorkItem onto the ThreadPool.
ThreadPool.QueueUserWorkItem((object Param_64) => { try { if (this.HttpListener_10.IsListening) { ThreadPool.QueueUserWorkItem((object Param_66) => { HttpListenerContext param66 = Param_66 as HttpListenerContext; string leftPart = param66.Request.Url.GetLeftPart(UriPartial.Path); if (leftPart.EndsWith(Form0.GetString_14.Vmethod_35(2569), StringComparison.OrdinalIgnoreCase)) { this.method_21(param66); } else if (leftPart.EndsWith(Form0.GetString_14.Vmethod_35(2602), StringComparison.OrdinalIgnoreCase)) { this.method_22(param66); } else { param66.Response.StatusCode = 200; } param66.Response.Close(); }, this.HttpListener_10.GetContext()); } } catch (HttpListenerException httpListenerException) { if (httpListenerException.ErrorCode != 995) { throw; } } });Before processing, it checks that the Listener is listening and then checks the URI to determine which method to use. I do not believe method_22 is viable at this time. The class it creates calls to the base, whose abstract Main method does not perform any logic. So method_21 is probably the one used with a proxy address as mentioned in the README.
this.method_24(Form0.GetString_14.Vmethod_35(1744)); string text = this.textBox_0.Text; Class4 class4 = new Class4(Param_52, text);Class4 is created with the HttpListenerContext and the licensee name from the text box. Looking at the class, it passes teh HttpListenerContext to the base class which is Class1.
internal Class4(HttpListenerContext Param_26, string Param_27) : base(Param_26) { this.property_4 = Param_27; }Then a call is made to the Class4 Main method to actually perform some logic related to the license. Any exceptions that may occur by these operations are displayed on the StatusStripLabel.
try { class4.Main(); this.method_24(string.Concat(Form0.GetString_14.Vmethod_35(1785), text, Form0.GetString_14.Vmethod_35(1334))); } catch (Exception exception1) { Exception exception = exception1; this.method_24(string.Concat(Form0.GetString_14.Vmethod_35(1830), exception.Message)); }The main method calls another private main method that returns a tuple with byte[].
internal override void Main() { Tuple<byte[], byte[]> tuple = this.Main(this.Main()); Tuple<string, string, string, string> tuple1 = this.Main(tuple.Item1, tuple.Item2); this.Main((new Class3() { property_3 = this.property_4 }).method_14(tuple1.Item3)); }The public main method is a series of calls to private main methods with various arguments and return types. The first call returns a byte[] using the data from the HttpListenerRequest.
private new byte[] Main() { byte[] array; using (MemoryStream memoryStream = new MemoryStream()) { base.property_1.InputStream.CopyTo(memoryStream); array = memoryStream.ToArray(); } return Convert.FromBase64String(Encoding.UTF8.GetString(array).Trim()); }This looks a little similar to the code found in the LINQPad executable.
The second call to main returns a tuple of two byte arrays.
private Tuple<byte[], byte[]> Main(byte[] Param_36) { short num = BitConverter.ToInt16(Param_36, 6); byte[] numArray = new byte[num]; byte[] numArray1 = new byte[(int)Param_36.Length - (8 + num)]; Array.Copy(Param_36, 8, numArray, 0, num); Array.Copy(Param_36, 8 + num, numArray1, 0, (int)numArray1.Length); return Tuple.Create<byte[], byte[]>(numArray1, numArray); }Looking back at the RegisterOnline analysis, it will become apparent how this method works.
byte[] numArray1 = new byte[16]; RandomNumberGenerator.Create().GetBytes(numArray1); byte[] array = WSAgent.smethod_5(memoryStream.ToArray(), numArray1, numArray1); using (RSACryptoServiceProvider rSACryptoServiceProvider = WSAgent.smethod_0(Convert.FromBase64String(PublicRsaKey))) { byte[] numArray2 = rSACryptoServiceProvider.Encrypt(numArray1, true); byte[] bytes = Encoding.UTF8.GetBytes("SymKey"); byte[] bytes1 = BitConverter.GetBytes((ushort)((int)numArray2.Length)); array = bytes.Concat<byte>(bytes1).Concat<byte>(numArray2).Concat<byte>(array).ToArray<byte>(); }The author skips the first six bytes because that is the length of the bytes array built from
Encoding.UTF8.GetBytes("SymKey");The author skips this value and reads the length which is the value of the bytes1 array:
BitConvert.GetBytes((ushort)((int)numArray2.Length));numArray1 is the encrypted data that contains the information needed to generate the activation license. The length is calculated from the six bytes of SymKey and the two bytes that represent the size value of the numArray stored in the stream and the actual length of the numArray.
The data is then copied into the corresponding array (using the same logic as before to calculate what data should be transferred) and stored in the tuple that gets returned. These two array are then passed into the next private main method that returns a tuple of four strings.
private Tuple<string, string, string, string> Main(byte[] Param_33, byte[] Param_34) { byte[] numArray; Tuple<string, string, string, string> tuple; using (RSACryptoServiceProvider rSACryptoServiceProvider = Class2.smethod_9()) { numArray = rSACryptoServiceProvider.Decrypt(Param_34, true); } using (Aes ae = Aes.Create()) { byte[] numArray1 = numArray; using (ICryptoTransform cryptoTransform = ae.CreateDecryptor(numArray1, numArray1)) { using (MemoryStream memoryStream = new MemoryStream(Param_33)) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Read)) { using (BinaryReader binaryReader = new BinaryReader(cryptoStream)) { string str = binaryReader.ReadString(); string str1 = binaryReader.ReadString(); string str2 = binaryReader.ReadString(); string str3 = binaryReader.ReadString(); tuple = Tuple.Create<string, string, string, string>(str, str1, str2, str3); } } } } } return tuple; }First, the data must be decrypted. A utility function similar to those seen in the LINQPad application is used.
internal static RSACryptoServiceProvider smethod_9() { byte[] numArray = Convert.FromBase64String(Class2.GetString_8.Vmethod_35(243)); RSACryptoServiceProvider rSACryptoServiceProvider = new RSACryptoServiceProvider(); try { rSACryptoServiceProvider.ImportCspBlob(numArray); } catch { throw; } return rSACryptoServiceProvider; }It is a little different - converts the value in the method instead of passing it in as a parameter.
Now the values can be read. Looking back at part 3, in smethod_3, 5 values are the values written to the stream:
MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream)) { binaryWriter.Write(code); binaryWriter.Write(Class11.smethod_5() ?? (Class11.smethod_4() ?? (Class11.smethod_1() ?? ""))); binaryWriter.Write(Environment.MachineName); binaryWriter.Write(""); binaryWriter.Write(5); }Perhaps the 5 is a new addition, or maybe it just is not needed. Who knows. A Class3 instance is created with the license name from the Windows form and the third value from the generated tuple (which from part 3 is the Environment.MachineName) is used as an argument to a method of this class.
internal byte[] method_14(string Param_23) { byte[] array; byte[] numArray = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(Param_23)); using (MemoryStream memoryStream = new MemoryStream()) { using (Aes ae = Aes.Create()) { byte[] numArray1 = numArray; using (ICryptoTransform cryptoTransform = ae.CreateEncryptor(numArray1, numArray1)) { using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Write)) { using (BinaryWriter binaryWriter = new BinaryWriter(cryptoStream)) { binaryWriter.Write(this.property_3); binaryWriter.Write(Class3.GetString_9.Vmethod_35(1316)); binaryWriter.Write(-1); binaryWriter.Write(-1); binaryWriter.Write(Class3.GetString_9.Vmethod_35(1321)); binaryWriter.Write(Class3.GetString_9.Vmethod_35(1334)); binaryWriter.Write(Class3.GetString_9.Vmethod_35(1351)); binaryWriter.Write(Guid.Empty.ToByteArray()); binaryWriter.Write(DateTime.Now.AddYears(10).Ticks); binaryWriter.Write((byte)5); binaryWriter.Write(1); binaryWriter.Write(Class3.GetString_9.Vmethod_35(1368)); binaryWriter.Write(Class3.GetString_9.Vmethod_35(1373)); binaryWriter.Flush(); binaryWriter.Close(); array = memoryStream.ToArray(); } } } } } return array; }The machine name is used to generate a hasharray to encrypt the values that are going to be written so the RegisterOffline function can decrypt them properly. Then the values needed for the activation license are written to the stream and converted to a byte[] which is returned. It is interesting that the author uses an empty guid, and a DateTime ten years from now.
The ensuing byte[] is passed to the final private main method.
private void Main(byte[] Param_39) { byte[] numArray = Class3.smethod_12(Param_39, Class0.Class0_0); base.property_2.ContentEncoding = Encoding.UTF8; byte[] bytes = base.property_2.ContentEncoding.GetBytes(Convert.ToBase64String(numArray)); base.property_2.OutputStream.Write(bytes, 0, (int)bytes.Length); }This method writes the byte[] back over the HttpListener so LINQPad can read.
Register Offline
The form class uses the CheckBox Changed and the TextBox TextChanged events to determine whether or not the user wants to use the RegisterOffline mode.private void method_19(object Param_46, EventArgs Param_47) { string str; Class0 class0; if (string.Equals(this.string_3 ?? string.Empty, this.textBox_1.Text.Trim(), StringComparison.OrdinalIgnoreCase)) { return; } this.string_3 = this.textBox_1.Text.Trim(); if (string.IsNullOrWhiteSpace(this.textBox_0.Text)) { MessageBox.Show(Form0.GetString_14.Vmethod_35(1510), Form0.GetString_14.Vmethod_35(1563), MessageBoxButtons.OK); return; } try { str = Class3.smethod_13(this.string_3, out class0); } catch { this.method_24(Form0.GetString_14.Vmethod_35(1584)); return; } byte[] numArray = (new Class3() { property_3 = this.textBox_0.Text }).method_14(str); this.textBox_2.Text = Convert.ToBase64String(Class3.smethod_12(numArray, class0)); this.textBox_2.Focus(); this.textBox_2.SelectAll(); this.method_24(Form0.GetString_14.Vmethod_35(1617)); }The first check is to see if the Offline Machine Activation Data textbox value matches the internal value representing the text box value. If they do it returns because the activation code has already been generated. If they do not, it sets the internal value to the textbox value that is checked against. If the user has not set a licensee name, it displays a message, just like the RegisterOnline version.
The next code section determines what type of machine is registering the activation code. As seen in part 4:
int num1 = numArray[0]; if (num1 != 0) { string machineName = Environment.MachineName; if (numArray[1] == 1) { string2 = null; } else { if (numArray[1] == 7) { str = Class11.smethod_5(); } else if (numArray[1] == 6) { str = Class11.smethod_4(); } else if (numArray[1] == 5) { str = Class11.smethod_3(); } else if (numArray[1] == 4) { str = Class11.smethod_2(); } else { str = (numArray[1] == 3 ? Class11.string_2 : Class11.string_1); } machineName = str; if (!string.IsNullOrEmpty(machineName)) { goto Label2; } flag = false; return flag; } }The last section creates the activation message in a new Class3 instance and calling the method_14 seen in the RegisterOnline section. The value is converted to a Base64String and the value is set to a textbox.
Does It Work?
Now that the analysis is complete, the question is - does it even work? I loaded up a virtual machine to give it a try (just in case there was some unwanted "additions"). I replaced teh LINQPad version with the one provided (it has some patches done to it) and generated an offline activation license. The problem was, as soon as I relaunched LINQPad after activating my internet connection - LINQPad updated which revoked the license (Good for you Joseph :-)).Summary
In this post, I analyzed a program that cracks the LINQPad activation license. I saw how it worked (mostly) and verified it did activate a previous version of LINQPad. In this way, I verified that there are at least two ways to bypass the LINQPad license. I found it interesting that not having strings available in the assembly was but a minor inconvenience. I was surprised that the LINQPad Server application utilized obfuscation software. I did not think that a executable created to bypass an applications licensing mechanism would need to be obfuscated. It did not really hinder my analysis however, just slowed it down slightly.I did not really understand why the OfflineRegistration had to use the LINQPad generated message. The only reason I can see is to use the MachineName that is embedded in the string when the RegisterOnline fails. I do not believe this is necessary and will cover this more in the next post. In the next post, I will summarize my findings and describe some of the various ways I see that the LINQPad licensing system can be bypassed.
It is obvious the author of LINQPad has put a lot of time and care into cultivating a quality product. If you find LINQPad useful please support the author by purchasing a license.
No comments:
Post a Comment