Tuesday, February 23, 2016

Reversing LINQPad - Part 1 - Analysis

Introduction

I am always surprised that there are .NET developers that have not heard of a great .NET software utility called LINQPad. 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 we will not be distributing modifications to LINQPad, sub-licensing it, or circumventing any paid edition usage restrictions. In this series; however, we will be discussing what protections the software has and what methods we could take to bypass them.

What You Need

To complete this segment, we will need a .NET decompiler such as .NET Reflector, dotPeek, JustDecompile, or ILSpy.

Getting Started

In order to decompile, you will need to download and install LINQPad. I have opted to get the most recent version which is V5 (at the time of this writing). Once installed, open the .NET decompiler of choice and load the LINQPad executable installed.
Once decompiled, you will want to find the Entry point so we can see how the application starts up. The interesting part is that it appears as though we will be able to view the entire application from the decompiler!

You should see that LINQPad starts from Loader.Main with the following code:
[LoaderOptimization(LoaderOptimization.MultiDomainHost)]
[STAThread]
internal static void Main(string[] args)
{
    try
    {
        string str = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LINQPad");
        ProfileOptimization.SetProfileRoot(str);
        ProfileOptimization.StartProfile(string.Concat("JitProfile.46.", IntPtr.Size));
    }
    catch
    {
    }
    if (!Loader.AlreadyRun)
    {
        Loader.AlreadyRun = true;
        if (PermissionsCheck.Demand())
        {
            if ((typeof(int).GetType().BaseType.Name != "TypeInfo" ? false : VersionCheck.Is46Available()))
            {
                AppHost.Instance = new GuiAppHost();
                ProgramStarter.Run(args);
            }
            else
            {
                MessageBox.Show("LINQPad 5 requires .NET Framework 4.6 to run.", "LINQPad", MessageBoxButtons.OK, MessageBoxIcon.Hand);
            }
        }
    }
}
From here the program goes to ProgramStarter.Run which is just a wrapper for Program.Start.
public static void Start(string[] args)
{
    FirstChanceExceptionTracker.Install();
    InternalAssemblyResolver.AddLINQPadAssemblyResolver(true);
    if (!Debugger.IsAttached)
    {
        Application.ThreadException += new ThreadExceptionEventHandler(Program.Application_ThreadException);
        try
        {
            Program.Go(args);
        }
        catch (Exception exception)
        {
            Program.ProcessException(exception);
        }
        try
        {
            ProcessClient.Shutdown();
        }
        catch
        {
        }
        AutoSaver.Shutdown();
    }
    else
    {
        Program.Go(args);
        AutoSaver.Shutdown();
    }
}
In this method we can see some InternalAssemblyResolver logic, which will be very important to understand for the next post.
internal static void AddLINQPadAssemblyResolver(bool mainHostDomain = false)
{
    if (!InternalAssemblyResolver._linqPadAssemblyResolverAdded)
    {
        InternalAssemblyResolver._linqPadAssemblyResolverAdded = true;
        if (!mainHostDomain)
        {
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler((object sender, ResolveEventArgs args) => {
                Assembly assembly;
                if (args.Name.ToLowerInvariant().StartsWith("linqpad,"))
                {
                    string data = AppDomain.CurrentDomain.GetData("LINQPad.Path") as string;
                    if (!string.IsNullOrEmpty(data))
                    {
                        assembly = Assembly.LoadFrom(data);
                        return assembly;
                    }
                }
                assembly = ((args.Name == "Resources, Version=1.0.0.0, Culture=neutral, PublicKeyToken=21353812cd2a2db5" || args.Name == "Ionic.Zip.Reduced, Version=1.7.2.12, Culture=neutral, PublicKeyToken=791165b13cf84eca" || args.Name == "APS.SyntaxEditor.WinForms, Version=14.1.323.0, Culture=neutral, PublicKeyToken=21353812cd2a2db5" || args.Name == "APS.Shared.WinForms, Version=14.1.323.0, Culture=neutral, PublicKeyToken=21353812cd2a2db5" || args.Name == "NuGet, Version=3.3.0.212, Culture=neutral, PublicKeyToken=31bf3856ad364e35" || args.Name == "Microsoft.Web.XmlTransform, Version=2.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" || args.Name == "LINQPad.Debugger, Version=1.0.0.0, Culture=neutral, PublicKeyToken=21353812cd2a2db5" || args.Name == "LINQPad.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=21353812cd2a2db5" || args.Name == "ICSharpCode.NRefactory, Version=3.0.0.3630, Culture=neutral, PublicKeyToken=efe927acf176eea2" || args.Name == "FSharp.Compiler.Service, Version = 1.4.2.1, Culture = neutral, PublicKeyToken = 21353812cd2a2db5" || args.Name == "FSharp.Core, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" || args.Name == "Hyperlinq, Version=1.0.0.0, Culture=neutral, PublicKeyToken=21353812cd2a2db5" || args.Name == "Microsoft.CodeAnalysis, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" || args.Name == "Microsoft.CodeAnalysis.CSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" || args.Name == "Microsoft.CodeAnalysis.VisualBasic, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" || args.Name == "Microsoft.CodeAnalysis.Workspaces.Reduced, Version=1.0.0.0, Culture=neutral, PublicKeyToken=21353812cd2a2db5" || args.Name == "Microsoft.CodeAnalysis.Workspaces.Reduced.VB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=21353812cd2a2db5" || args.Name == "System.Collections.Immutable, Version=1.1.36.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" || args.Name == "System.Reflection.Metadata, Version=1.0.21.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" || args.Name == "Microsoft.Msagl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=640c57aa40e7ae7d" || args.Name == "Microsoft.Msagl.Drawing, Version=3.0.0.0, Culture=neutral, PublicKeyToken=8a3d7c21d5fa1306" || args.Name == "Microsoft.Msagl.GraphViewerGdi, Version=3.0.0.0, Culture=neutral, PublicKeyToken=fffc27ea4058b3a1" || args.Name == "ObjectListView, Version=2.8.2.5352, Culture=neutral, PublicKeyToken=b1c5bf581481bcd4" ? false : !AstoriaDynamicDriver.IsAstoriaAssembly(args.Name)) ? null : InternalAssemblyResolver.FindAssem(sender, args));
                return assembly;
            });
        }
        else
        {
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(InternalAssemblyResolver.FindAssem);
        }
    }
}
The internal assembly resolver is responsible for looping over the items in the app Resources where the name is one from a list of assemblies. If you go to the Resources section in your decompiler you should see a list of these. In the screenshot shown, they are the ones with the yellow warning symbol.
Resources Reference

Continue analyzing the assembly until you get to Program.Go, which is where the meat of the startup logic is stored. It is in this method that the arguments passed to LINQPad are checked (from the string[] args that got passed from the entry point) and the UI visual styles are set before calling Program.Run.

Another hefty method that does a lot of the setup and management needed for the UI. One of the interesting calls is the call to Program.InitWSAgent - which sets up the registration classes we will come back to later. WSAgent handles license activation, license removal, and hash information for the license. The code is the last section before the forms start appearing - you can see the logic used to determine if the splash form should be displayed before the main form. There is an else block that contains code to display a RegisterForm that may be of interest to us:
using (RegisterForm registerForm = new RegisterForm(activationCode, activateAll))
{
    registerForm.ShowDialog();
}
Since we have gotten to the main form launching, the next step to analyze is the RegisterForm (unless you want to know more about how LINQPad works). We know one of the constructors is called from Program.Run. If we look at that constructor, we see code similar to the following:
public RegisterForm(string code, bool allUsers) : this()
{
    this.txtCode.PasswordChar = '*';
    this.txtCode.Text = code.Trim();
    this.rbAll.Checked = allUsers;
    this.btnActivate_Click(this, EventArgs.Empty);
}
This constructor simulates the activate button being clicked and calls to the default constructor. The default constructor does a little more work with Initializing things and setting things up. Of particular note is the background worker setup:
public RegisterForm()
{
    this.InitializeComponent();
    this.txtCode.TextChanged += new EventHandler((object A_1, EventArgs A_2) => {
        if ((this.txtCode.Text.Trim().Length != 26 || this.txtCode.Text.Trim()[5] != '-' ? false : "+-".Contains(this.txtCode.Text.Trim()[11])))
        {
            this.txtCode.Text = this.txtCode.Text.Trim().Substring(0, 11);
        }
    });
    this._origText = this.Text;
    try
    {
        RadioButton radioButton = this.rbMe;
        radioButton.Text = string.Concat(radioButton.Text, " (", Environment.UserName, ")");
    }
    catch
    {
    }
    try
    {
        this.lblNeedsInternet.Font = new Font(this.lblNeedsInternet.Font, FontStyle.Bold);
    }
    catch
    {
    }
    this._worker.WorkerSupportsCancellation = true;
    this._worker.DoWork += new DoWorkEventHandler(this._worker_DoWork);
    this._worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(this._worker_RunWorkerCompleted);
}
Check out the btnActivate_Click handler to see why.
private void btnActivate_Click(object sender, EventArgs e)
{
    this._activating = true;
    this.Text = "Working...";
    this.EnableControls();
    base.Update();
    this._worker.RunWorkerAsync();
}
It uses the background worker to do some work - the method that is used is the one in the default constructor. Now looking at the DoWork event method assigned in the default constructor we will see that it calls into WSAgent again. By now we know that WSAgent is the class responsible for handling the license (e.g. determining validity and registering the application). This makes WSAgent a high priority target for our analysis, but the decompiler cannot seem to find it. If you keep analyzing the RegisterForm you will come across a method to authenticate offline.
private void lblNeedsInternet_MouseDown(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Right)
    {
        using (OfflineActivation offlineActivation = new OfflineActivation(this.rbAll.Checked))
        {
            offlineActivation.ShowDialog(this);
        }
        base.Close();
    }
}
From the code, we see that to get to the OfflineActivation form you must right click on the bold label in the RegisterForm.









The OfflineActivation has an activate button that when clicked performs the following logic:
private void btnActivate_Click(object sender, EventArgs e)
{
    Button button = this.btnActivate;
    TextBox textBox = this.txtCode;
    this.btnCancel.Enabled = false;
    textBox.Enabled = false;
    button.Enabled = false;
    base.Update();
    LicenseManager.Licensee = null;
    WSAgent.RegisterOffline(this.txtCode.Text, this._forAll);
    if (string.IsNullOrEmpty(LicenseManager.Licensee))
    {
        MessageBox.Show("Invalid Activation Code");
    }
    else
    {
        MessageBox.Show("Activation Successful");
        base.Close();
    }
    Button button1 = this.btnActivate;
    TextBox textBox1 = this.txtCode;
    this.btnCancel.Enabled = true;
    textBox1.Enabled = true;
    button1.Enabled = true;
}
Which again calls into WSAgent. Further emphasizing that this is the class that will need to be analyzed next in order to see how the license check works. We will look for the WSAgent class next time.

In this post, we analyzed the LINQPad executable. We found out that most of the license logic is stored in WSAgent. But where is WSAgent? The decompiler cannot find it! We will work on analyzing that assembly next time.

Summary

I was surprised to find that the author allows us to decompile the executable directly (meaning it is not obfuscated or protected in any way). I appreciate the author allowing us to decompile the executable as it allows me to learn new coding tricks (such as how the to use AppDomains to execute arbitrary code). The code is relatively clean, considering it has been interpreted by a decompiler and each class follows the Single Responsibility Principle fairly well. It is obvious 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