Friday, March 11, 2016

Reversing LINQPad - Part 4 - Analyzing WSAgent.RegisterOffline

In the previous post, I analyzed the WSAgent.Register function more thoroughly to obtain a deeper understanding of it's inner workings. I was able to use this information to generate a spoofed request that could be sent to the LINQPad license servers. I noticed one possible way to defeat the licensing scheme would be to have a man in the middle via the Proxy options LINQPad offers.

In this post, I am going to be taking a closer look at the RegisterOffline function to see if there is a valid attack vector. The goal is to gain a better understanding of what licensing information is being sent from the LINQPad server to the LINQPad application and how it processes it. With this knowledge it will be easier to determine if the software is vulnerable to an attack.

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

Getting Started

I will start from the RegisterOffline method in the WSAgent class of the Resources assembly retrieved from Part 2.
public static void RegisterOffline(string code, bool allUsers)
{
    try
    {
        code = code.Replace(" ", "").Replace("\r", "").Replace("\n", "").Replace("\t", "");
        Class11.smethod_16(Convert.FromBase64String(code), allUsers);
        Thread thread = new Thread(new ParameterizedThreadStart(Class11.smethod_18))
        {
            Name = "FLD",
            IsBackground = true
        };
        Thread thread1 = thread;
        thread1.Start(true);
        thread1.Join(3000);
    }
    catch (Exception exception)
    {
        Log.Write(exception);
    }
}
First the method trims any whitespace from the code. Using this new code it calls into smethod_16 and passes allUsers into it as well. Once that has finished it starts a new thread with smethod_18.
internal static void smethod_16(byte[] byte_0, bool bool_0)
{
    using (IsolatedStorageFile isolatedStorageFile = Class11.smethod_15(bool_0))
    {
        using (IsolatedStorageFileStream isolatedStorageFileStream = new IsolatedStorageFileStream("LastWindowPosition", FileMode.Create, isolatedStorageFile))
        {
            isolatedStorageFileStream.Write(byte_0, 0, (int)byte_0.Length);
        }
    }
    if (bool_0)
    {
        Class11.smethod_17(false);
    }
}
Here the code writes the code to IsolatedStorageFile. IsolatedStorageFile represents an isolated storage area containing files and directories. smethod_15 is a factory method to build the IsolatedStorageFile.
private static IsolatedStorageFile smethod_15(bool bool_0)
{
    IsolatedStorageScope isolatedStorageScope = IsolatedStorageScope.Domain | IsolatedStorageScope.Assembly;
    isolatedStorageScope = (IsolatedStorageScope)(6 | (bool_0 ? 16 : 1));
    return IsolatedStorageFile.GetStore(isolatedStorageScope, typeof(StrongName), typeof(StrongName));
}
I am not sure if the IsolatedStorageScope is messed up because of the decompilation process or if this is how the author intended it to look. IsolatedStorageScope.Domain is 2 and IsolatedStorageScope.Assembly is 4 so by default it should be 6 in the first statement. In the second statement it takes that value and if allUsers is true it adds IsolatedStorageScope.Machine (which is 16) otherwise it adds IsolatedStorageScope.User (which is 1). I am not sure why the hard-coded values are there.

Back to smethod_16 - the IsolatedStorageFileStream sets the path to "LastWindowPosition". The MSDN documents call it the relative path of the file within isolated storage. I am not sure why "LastWindowPosition" is the name chosen.

If allUsers is set, the code makes a call to smethod_17 with a value of false.
internal static void smethod_17(bool bool_0)
{
    byte[] numArray = new byte[234];
    (new Random()).NextBytes(numArray);
    numArray[0] = 0;
    numArray[1] = 0;
    try
    {
        using (IsolatedStorageFile isolatedStorageFile = Class11.smethod_15(bool_0))
        {
            try
            {
                using (IsolatedStorageFileStream isolatedStorageFileStream = new IsolatedStorageFileStream("LastWindowPosition", FileMode.Create, isolatedStorageFile))
                {
                    isolatedStorageFileStream.Write(numArray, 0, (int)numArray.Length);
                }
            }
            catch
            {
                isolatedStorageFile.DeleteFile("LastWindowPosition");
            }
        }
    }
    catch
    {
    }
}
smethod_17 starts of by generating 232 random bytes (the first two are set to 0) before re-writing the IsolatedStorageFile we just wrote with the random bytes. Bizzarre, maybe the motive will become clear later in the analysis.

This brings us back to the RegisterOffline function where we will start analyzing smethod_18. The method given to the thread to execute. The thread's initial parameter is true which will be passed to smethod_18 as object_0.
internal static void smethod_18(object object_0)
{
    try
    {
        if (Class11.smethod_20(object_0, false))
        {
            return;
        }
    }
    catch (Exception exception)
    {
        Log.Write(exception);
    }
    if (Class11.string_1 == null)
    {
        try
        {
            Class11.smethod_20(object_0, true);
        }
        catch
        {
        }
    }
}
The thread tries to validate the LicenseRegistration first by passing object_0 (which is the value true) and false. If that fails and the string_1 was not assigned to then it tries again using object_0 (still true) and true. This probably means that string_1 is some kind of error code that gets set in certain situations where the code should not register the license. This should become more clear in smethod_20. A huge method so I will be dividing it into parts as much as possible.

The first thing it does is sets string_4 to "IntelliPrompt". I think this is the type of registration that is being done because IntelliSense is done for both paid license types.
Class11.string_4 = "IntelliPrompt";
Next it reads from the "LastWindowPosition" IsolatedStorageFile written to before swallowing all exceptions that may occur.
byte[] numArray = null;
try
{
    using (IsolatedStorageFile isolatedStorageFile = Class11.smethod_15(false))
    {
        if (isolatedStorageFile.GetFileNames("LastWindowPosition").Length != 0)
        {
            using (IsolatedStorageFileStream isolatedStorageFileStream = new IsolatedStorageFileStream("LastWindowPosition", FileMode.Open, isolatedStorageFile))
            {
                using (BinaryReader binaryReader = new BinaryReader(isolatedStorageFileStream))
                {
                    numArray = binaryReader.ReadBytes(10000);
                }
            }
        }
    }
}
catch
{
}
It then checks the contents of what was read. First it checks for null, then checks that it is a valid size (must be greater than or equal to 20). Finally it checks if the first value is 0 - which it is if allUsers was set to true. If any of these cases are true it re-reads the isolated storage again, but the IsolatedStorageFile generated has different IsolatedStorageScope properties to account for all users.
if (numArray == null || numArray.Length < 20 || numArray[0] == 0)
{
    try
    {
        using (IsolatedStorageFile isolatedStorageFile1 = Class11.smethod_15(true))
        {
            if (isolatedStorageFile1.GetFileNames("LastWindowPosition").Length != 0)
            {
                using (IsolatedStorageFileStream isolatedStorageFileStream1 = new IsolatedStorageFileStream("LastWindowPosition", FileMode.Open, isolatedStorageFile1))
                {
                    using (BinaryReader binaryReader1 = new BinaryReader(isolatedStorageFileStream1))
                    {
                        numArray = binaryReader1.ReadBytes(10000);
                    }
                }
            }
        }
    }
    catch
    {
    }
}
Because different IsolatedStorageScopes were used the contents of the IsolatedStorageFile are kept in tact. The first read retrieves the random bytes one that tells the program to read it again with the proper IsolatedStorageScope for allUsers.

After this it does another check. It does the same ones as the previous one and adds in a new one. It checks that the second byte read is not equal to 1. If the numArray is larger than 20 characters and the 0 index is not 0 and the 1 index is 1 we can skip this block. When we try to generate our own we will probably try to do that because there is a lot of logic in here to break down.
if ((bool_0 || numArray == null || (int)numArray.Length < 20 || numArray[0] == 0 || numArray[1] != 1))
The first snippet checks if the LocalApplicationData folder (%localappdata%) has a file called CacheToken in the LINQPad folder.
string str4 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LINQPad\\CacheToken");
bool flag3 = false;
try
{
    flag3 = File.Exists(str4);
}
catch
{
}
The code then attempts to retrieve the serial number of the motherboard from the Win32_BaseBoard class.
for (int i = 0; i < 2; i++)
{
    try
    {
        EnumerationOptions enumerationOption = new EnumerationOptions();
        EnumerationOptions enumerationOption1 = enumerationOption;
        if (i == 0)
        {
            obj1 = 2;
        }
        else
        {
            obj1 = (flag3 ? 2 : 15);
        }
        enumerationOption1.Timeout = TimeSpan.FromSeconds((double)obj1);
        EnumerationOptions enumerationOption2 = enumerationOption;
        str1 = (new ManagementObjectSearcher(null, "Select * from Win32_BaseBoard", enumerationOption2)).Get().OfType().First().GetPropertyValue("SerialNumber").ToString().Trim();
        try
        {
            if ((!File.Exists(str4) ? true : File.ReadAllText(str4) != str1))
            {
                if (!Directory.Exists(Path.GetDirectoryName(str4)))
                {
                    Directory.CreateDirectory(Path.GetDirectoryName(str4));
                }
                File.WriteAllText(str4, str1);
            }
        }
        catch
        {
        }
        break;
    }
    catch
    {
    }
}
It reads the CacheToken file from before if it exists to see if the contents match the serial number retrieved. If it is not or the directory does not exist it creates it and writes the SerialNumber to it. It only gives it two tries to read this value. The first is a delay of only 2 seconds and the second is a delay of 2 seconds if the file exists otherwise it is a delay of 15 seconds - to prevent timeouts from occurring.

If the string value was null when retrieving the serial but the file exists it assigns the contents of the file to the string.
if (str1 == null)
{
    try
    {
        if (File.Exists(str4))
        {
            str1 = File.ReadAllText(str4);
        }
    }
    catch
    {
    }
}
str1 is the serial of the motherboard, what other information does LINQPad look for?
ManagementClass managementClass = new ManagementClass("Win32_Processor");
ManagementObject managementObject = managementClass.GetInstances().OfType().FirstOrDefault();
if (managementObject != null)
{
    try
    {
        str2 = managementObject.Properties["ProcessorId"].Value.ToString().Trim();
    }
    catch
    {
    }
}
if (managementObject != null)
{
    str3 = managementObject.Properties["Name"].Value.ToString().Trim();
}
LINQPad retrieves the ProcessorId and Name from the ManagementClass. The members of this class enable you to access WMI data using a specific WMI class path.

If LINQPad was able to retrieve the serial number of the motherboard and the ProcessorName it does some string logic to set some class variables.
if (((str1 ?? "").Trim().Length != 0 ? true : (str3 ?? "").Trim().Length != 0))
{
    Class11.string_1 = string.Concat("1 ", str1 ?? "?", " ~ ", str2 ?? "?");
    Class11.string_2 = string.Concat("2 ", str1 ?? "?", " ~ ", str3 ?? "?").Trim();
    if ((str3 == null ? true : str3 == "Intel Pentium III Xeon processor"))
    {
        string value = null;
        try
        {
            value = (string)Registry.LocalMachine.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0\\").GetValue("ProcessorNameString");
        }
        catch
        {
        }
        if ((value == null ? false : value.Trim().Length > 0))
        {
            string2 = Class11.string_2;
            Class11.string_2 = string.Concat("2 ", str1 ?? "?", " ~ ", value.Trim());
        }
    }
    else if (str1 == null)
    {
        string2 = string.Concat("2  ~ ", (str3 ?? "?").Trim());
    }
    else if ((str1 == "" ? true : str1 == "None"))
    {
        string2 = string.Concat("2 ? ~ ", (str3 ?? "?").Trim());
    }
}
else
{
    string str5 = "";
    Class11.string_2 = str5;
    Class11.string_1 = str5;
}
This is where the special string used in the RegisterOnline function was referenced (string_2) in smethod_1. The method that contained the Thread.Sleep to wait for the string to be populated. It sets string_2 to the motherboard serial number and the processor name. The value "2 ? ~ ?" is used if it was not able to retrieve this information. It sets string_1 to "1 ? ~ ?" if it was not able to read the motherboard serial number and processor id.

If the processor name was not read or is "IntelPentium III Xeon processor" LINQPad goes to the registry to get the processor name. If the value is not empty it assigns the string_2 to the member variable string2 and overwrites the string_2 value to "2 ? ~ [value]". Other logic is used to set a member variable string2 if the motherboard serial number is empty or "None".

If the initial if statement is false it sets string_2 and string_1 to empty strings.
if (!bool_0)
{
    goto Label1;
}
If the boolean value passed to the method is false it continues with the logic at Label1 otherwise it returns.
flag1 = (numArray == null ? true : (int)numArray.Length < 20);
The first time this method is called the boolean value passed in is false so we can continue to Label1.

The next section checks the first value in the numArray - it cannot be 0 or it returns out of the method without registering the application. Shortly after it checks the second value - which looks like having the second value be 1 makes the logic easier. Otherwise there are a series of methods that could be called if the value is 7, 6, 5, or 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;
    }
}
else
{
    flag = false;
}
Interesting, those values look the same as the values seen in the RegisterOnline function. The method for 7 is the Azure instance, the method for 6 is the Service controller and the value 5 is the hashed machine name. The new one is smethod_2.
internal static string smethod_2()
{
    string str;
    string string2 = Class11.string_2;
    if ((string.IsNullOrEmpty(string2) ? false : string2.Length >= 5))
    {
        byte[] numArray = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(string2));
        str = string.Concat("4 ", Class11.smethod_27(numArray, 4));
    }
    else
    {
        str = null;
    }
    return str;
}
smethod_2 checks the string_2 value and makes sure it is greater than 5 in order to create a hash of it. It reuses the smethod_27 method seen previously to restrict it to 4 characters. In short, try to keep the value at the second index to be 1 if possible. If everything goes according to plan Label2 is the next destination - and this is where the code is decrypted.
DateTime now = DateTime.Now;
int num2 = numArray[2] + numArray[3] * 256;
int num3 = numArray[4] + numArray[5] * 256;
byte[] array = numArray.Skip<byte>(num1).Take<byte>(num2).ToArray<byte>();
byte[] array1 = numArray.Skip<byte>(num1 + num2).Take<byte>(num3).ToArray<byte>();
It starts out by dividing separating the numArray into the registration code and the signature that will be used to validate the data matches. The third and fourth indexes relate to the registration code. The third is the remainder and the fourth is the number of clean 256 bytes should be attributed to the license message received. The same thing is done for the signature but in the fifth and sixth index locations.
using (RSACryptoServiceProvider rSACryptoServiceProvider = WSAgent.smethod_0(Convert.FromBase64String("KEY")))
{
    if (!rSACryptoServiceProvider.VerifyData(array, SHA1.Create(), array1))
    {
        flag = false;
        return false;
    }
}
It validates the data - if it fails it sets the flag to false and returns out. The first parameter of the VerifyData is the registration license message, the second is a SHA1 and the third parameter is the signature array. This means that the array1 may be the SHA1 representation of array.
MemoryStream memoryStream = new MemoryStream(array);
try
{
    if (CryptoConfig.AllowOnlyFipsAlgorithms)
    {
        typeof(CryptoConfig).GetField("s_fipsAlgorithmPolicy", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, false);
    }
}
catch
{
}
This sets the alrorithmPolicy to null. Searches suggest that this may disable the FIPS compliant algorithm policy.
byte[] numArray1 = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(machineName));
try
{
    using (Aes ae = Aes.Create())
    {
        using (ICryptoTransform cryptoTransform = ae.CreateDecryptor(numArray1, numArray1))
        {
            (new BinaryReader(new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Read))).ReadString();
        }
    }
}
catch
{
    if (string2 != null)
    {
        numArray1 = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(string2));
    }
}
Here it reads a string from the memory stream containing the contents of the RegistrationLicense. I believe this is to verify the string can be read because the exception attempts to manipulate the numArray1 using the value of string2 which we have seen throughout before the MemoryStream is read from again.
memoryStream.Position = 0L;
using (Aes ae1 = Aes.Create())
{
    using (ICryptoTransform cryptoTransform1 = ae1.CreateDecryptor(numArray1, numArray1))
    {
        using (Stream cryptoStream = new CryptoStream(memoryStream, cryptoTransform1, CryptoStreamMode.Read))
        {
            using (BinaryReader binaryReader2 = new BinaryReader(cryptoStream))
            {
                str6 = binaryReader2.ReadString();
                string str7 = binaryReader2.ReadString();
                Class11.int_1 = binaryReader2.ReadInt32();
                Class11.int_2 = binaryReader2.ReadInt32();
                string str8 = binaryReader2.ReadString();
                Class11.string_5 = binaryReader2.ReadString();
                Class11.string_6 = binaryReader2.ReadString();
                Class11.guid_0 = new Guid(binaryReader2.ReadBytes(16));
                DateTime dateTime = new DateTime(binaryReader2.ReadInt64());
                try
                {
                    try
                    {
                        LicenseManager.LicenseKind = binaryReader2.ReadByte();
                    }
                    catch
                    {
                    }
                    int num4 = binaryReader2.ReadInt32();
                    if ((!(AutocompletionManager.ProgramVersion != null) || AutocompletionManager.ProgramVersion.Major < 5 || LicenseManager.LicenseKind != 1 ? false : !string.IsNullOrEmpty(str6)))
                    {
                        if (num4 == 1)
                        {
                            str6 = string.Concat(str6, " (single-user license)");
                        }
                        else if (num4 <= 20)
                        {
                            str6 = string.Concat(new object[] { str6, " (", num4, "-user license)" });
                        }
                    }
                    string str9 = binaryReader2.ReadString();
                    if ((string.IsNullOrEmpty(str9) ? false : !str9.Contains("V5")))
                    {
                        if (DateTime.Now < new DateTime(2015, 8, 23))
                        {
                            flag2 = true;
                        }
                        else
                        {
                            flag = false;
                            return flag;
                        }
                    }
                    Class11.string_0 = binaryReader2.ReadString();
                }
                catch
                {
                }
                int int2 = Class11.int_2;
                TimeSpan timeSpan = dateTime - now;
                Class11.int_3 = int2 / Math.Max(0, timeSpan.Days + 2);
                Class11.string_3 = str7;
                Class11.string_4 = str8;
            }
        }
    }
}
The memory stream is reset to the beginning and data is read from it. This is my analysis of the values read from the stream:
  • Licensee
  • Unknown - needs at least 2 characters
  • Unknown - some kind of queue counter - set to 10 in constructor by default
  • Unknown - but set to "IntelliPrompt" by default at the start of the method
  • Unknown - needs 4 characters for operations found elsewhere with the next string
  • Unknown - needs 5 characters for operations found elsewhere with the previous string
  • Possibly a GUID Identifier for the license
  • Possibly an expiration time - comparison done later
  • License Kind - whether this should be for one license or more
  • Number of licenses
  • Needs to contain "V5" - assumed to be the version of LINQPad compatible with
  • LINQPad license edition - comparisons found elsewhere have it "PREM", "DEV", or "PRO"
if (true.Equals(object_0))
{
    try
    {
        LicenseManager.Register(str6);
    }
    catch
    {
    }
}
If there was a licensee in the contents it attempts to register the License with LicenseManager.
public static void Register(string licensee)
{
    try
    {
        string str = LicenseManager.smethod_0();
        LicenseManager.string_1.GetHashCode();
        byte[] array = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(string.Concat(licensee, LicenseManager.string_0)));
        array = ((IEnumerable)array).Select((byte b, int i) => (byte)(b ^ LicenseManager.Seed[i % (int)LicenseManager.Seed.Length])).ToArray();
        LicenseManager.Licensee = licensee;
        XElement xElement = new XElement("license", new object[] { new XElement("Licensee", licensee), new XElement("Code", Convert.ToBase64String(array)) });
        if (LicenseManager.bool_0)
        {
            xElement.AddAnnotation("IsLicensed");
        }
        string directoryName = Path.GetDirectoryName(str);
        if (!Directory.Exists(directoryName))
        {
            Directory.CreateDirectory(directoryName);
        }
        xElement.Save(str);
    }
    catch
    {
    }
}
This stores the contents in a file in a file found in LicenseManager.smethod_0.
internal static string smethod_0()
{
    return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LINQPad\\license.xml");
}
The file comes from the following directory: %localappdata%\LINQPad\license.xml.

If you have attempted to register this file may already exist. We see the hashcode is retrieved of "JSA8AJAPEEEO" (but is not stored). The next line of code creates a hash representation using SHA1 encryption of the licensee and "32'9v3p-9!82714rvg'36". Some bitwise encryption is done to the hash before being stored in an XElement. This is an XML node that is then written to the license.xml file.
while (true)
{
    activeForm = Form.ActiveForm;
    if ((activeForm == null ? false : activeForm.Name == "MainForm"))
    {
        break;
    }
    Thread.Sleep(100);
}
Thread.Sleep(100);
Waiting for the activeForm to be "MainForm" before continuing.
MethodInfo method = activeForm.GetType().GetMethod("DisplayMessage", BindingFlags.Instance | BindingFlags.NonPublic);
if (method != null)
{
    FieldInfo field = activeForm.GetType().GetField("_edition", BindingFlags.Instance | BindingFlags.NonPublic);
    if (field != null)
    {
        FieldInfo fieldInfo = field;
        Form form = activeForm;
        if (Class11.string_0 == null || Class11.smethod_6() == "PREM")
        {
            num = 3;
        }
        else
        {
            num = (Class11.string_0 == "DEV" ? 2 : 1);
        }
        fieldInfo.SetValue(form, num);
        if ((Class11.string_0 == null ? true : Class11.smethod_6() == "PREM"))
        {
            Type type = activeForm.GetType().Assembly.GetType("LINQPad.ExecutionModel.Debugging.FuncEval");
            if (type != null)
            {
                field = type.GetField("_funcEvalLog", BindingFlags.Static | BindingFlags.NonPublic);
                if (field != null)
                {
                    field.SetValue(null, (object obj) => {
                        if (Class11.methodInfo_0 == null)
                        {
                            Class11.methodInfo_0 = obj.GetType().GetMethod("Go");
                        }
                        if (Class11.methodInfo_0 != null)
                        {
                            Class11.methodInfo_0.Invoke(obj, null);
                        }
                    });
                }
            }
        }
    }
    method.Invoke(activeForm, new object[] { string.Concat("Licensed to ", str6), false, false, flag2 });
    flag = true;
}
Some reflection to go into the activeForm and call some methods. The first is "DisplayMessage". Additionally it sets the _edition property to the number representation of the edition found in the activation code. "PREM" is 3, "DEV" is 2, and "PRO" is 1. If the value was "PREM" the debugging capabilities are also set. Finally the DisplayMessage is activated telling the user the code has been licensed.

There is still a lot that could be tracked down - like how are the Class11.String_* used and where. This would give additional insight into what the values could be.

Summary

Generating a license the program will see as valid seems possible - assuming a patch can be made to prevent the code from exiting prematurely at the RSACryptoServiceProvider.VerifyData call. The second problem is figuring out what the best values for the unknown properties would be. While I was impressed at first with the security of the application, this does seem to be the weakest point - both online and offline activations come through here. This also contains the code that sets the edition and determines whether debugging should be allowed or not. Perhaps the author thought the RSACryptoServiceProvider.VerifyData call was enough.

I feel as though this could be made more secure if the author were to encrypt the activation message using the RSA key that the program can then decrypt and then perform the analysis on. This would ensure the data came from the proper source. While there are steps that could be taken to bypass this (coming up with your own public/private key pair and substituting it for the actual one) it could make the process a little more difficult for those seeking to bypass the security measures. Maybe the author has reasons for not doing this. This would make the reversing of the message very difficult as the code is in an embedded assembly. A malicious party would have to dump memory after it has been decrypted.

In the next post, I will attempt to reverse a public crack of the LINQPad executable that I have found on the internet to see how it works. It simulates the LINQPad server and the Proxy settings must be set to connect to it. It is for an old version of LINQPad and thus only works for that version.

It is apparent the author 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