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 Register method again.
public static void Register(string code, bool allUsers, DoWorkEventArgs e)
{
e.Result = WSAgent.smethod_3(code, allUsers, e);
}
As seen before, this calls into smethod_3 (which I will call RegisterLicenseOnline from now on) passing the arguments through. The result of smethod_3 is an object that is stored as the result of the DoWorkEventArgs parameter.
The first thing the smethod_3 method does is write some information into a memory 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);
}
Some of the values make sense, like the code from the textbox in the RegisterForm and MachineName. There are others that just do not make sense without additional analysis, such as the second Write statement. The code makes several calls to various methods if the result of the previous call was null. If all of these values are null it will just write an empty string to the stream. Follow the methods to figure out what they return.
internal static string smethod_5()
{
string str;
try
{
string str1 = Class11.smethod_22();
if (str1 != null)
{
str = string.Concat("7 ", str1);
}
else
{
str = null;
}
}
catch (Exception exception)
{
Log.Write(exception);
str = null;
}
return str;
}
This appears to be some kind of getter method - but there is not quite enough information to know of what just yet. All that is known is that it makes a call to another method for a value. If that value is not null then it concatenates the value "7 " with it. This is probably some kind of code which may be used in some kind of switch statement. So take a look at method referenced to see what this represents.
private static string smethod_22()
{
string str;
try
{
Assembly assembly = Assembly.LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime");
if (assembly == null)
{
str = null;
}
else if (Convert.ToBase64String(assembly.GetName().GetPublicKey()) != PublicKeyValue)
{
str = null;
}
else if ((bool)assembly.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment").GetProperty("IsAvailable").GetValue(null, null))
{
PropertyInfo property = assembly.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment").GetProperty("CurrentRoleInstance");
object value = property.GetValue(null, null);
object obj = value.GetType().GetProperty("Role").GetValue(value, null);
str = obj.GetType().GetProperty("Name").GetValue(obj, null).ToString();
}
else
{
str = null;
}
}
catch (Exception exception)
{
Log.Write(exception);
str = null;
}
return str;
}
The method loads the Microsoft.WindowsAzure.ServiceRuntime assembly (ironically using the deprecated method LoadWithPartialName). If it is unable to load the assembly or an exception occurs then the string value is null. If it is able to load the assembly there are two additional if blocks that are checked. The first checks for the public key to see if it matches a hard-coded value (removed for brevity). Otherwise, the other else block attempts to use reflection to get the name of a property value several levels deep.
The first step is to figure out what this assembly is for by looking at the MSDN for this assembly. The RoleEnvironment "provides information about the configuration, endpoints, and status of running role instances". Going a level deeper to the CurrentRoleInstance property, it "gets a RoleInstance object that represents the role instance in which the code is currently running." This means that this is probably checking to see if LINQPad is being run on a WindowsAzure instance.
As I am not running on a Azure instance this is null for me. You can verify this on your machine by writing an application (or LINQPad query) that attempts to load this assembly using the logic from this method. Since the value is null for me, I will continue to the next method call.
internal static string smethod_4()
{
string str;
try
{
string str1 = Class11.smethod_21();
if (str1 != null)
{
str = string.Concat("6 ", str1);
}
else
{
str = null;
}
}
catch (Exception exception)
{
Log.Write(exception);
str = null;
}
return str;
}
Similar to the previous one, this method will make a call to another method and check the value. One difference is the concatenation value of "6 " instead of "7 ". This supports the theory that the server will use this value to tell what kind of registration is being used.
private static string smethod_21()
{
string str;
ServiceController serviceController = ((IEnumerable<ServiceController>)ServiceController.GetDevices()).FirstOrDefault<ServiceController>((ServiceController d) => d.ServiceName == "vmbus");
if ((serviceController == null ? false : serviceController.Status == ServiceControllerStatus.Running))
{
NetworkInterface networkInterface = ((IEnumerable<NetworkInterface>)NetworkInterface.GetAllNetworkInterfaces()).FirstOrDefault<NetworkInterface>((NetworkInterface n) => (n.OperationalStatus != OperationalStatus.Up || n.NetworkInterfaceType != NetworkInterfaceType.Ethernet || !n.Supports(NetworkInterfaceComponent.IPv4) || n.GetIPProperties() == null || n.GetIPProperties().GetIPv4Properties() == null ? false : n.GetIPProperties().GetIPv4Properties().IsDhcpEnabled));
if (networkInterface != null)
{
string dnsSuffix = networkInterface.GetIPProperties().DnsSuffix;
if (dnsSuffix != null)
{
dnsSuffix = dnsSuffix.ToLowerInvariant();
if (dnsSuffix.EndsWith(".cloudapp.net"))
{
str = string.Concat(dnsSuffix.Split(new char[] { '.' }).First<string>(), ".cloudapp.net");
}
else
{
str = null;
}
}
else
{
str = null;
}
}
else
{
str = null;
}
}
else
{
str = null;
}
return str;
}
There is quite a bit going on in this method but we will follow the same procedure. Check the MSDN for ServiceController "represents a Windows service and allows you to connect to a running or stopped service, manipulate it, or get information about it." This method is looking for a service called "vmbus". You can search for what this service is for but it is used to identify if LINQPad is running in a VM.
This makes sense as LINQPad's license structure allows you to register a single license on 3 vms as can be found on the purchase page.
"We know that you’ll want to use LINQPad on your work machine, home machine and laptop, so we’ve allowed a single-user license to activate up to three machines at once for your personal use. You also get an additional three activations for virtual machines running VMWare, MS Virtual PC, Hyper-V, Azure Roles, or Azure VMs. And when they’re all used, you can transfer your activations - up to 6 times a year - through an automated web app."Unless you are registering LINQPad in a VM this should return null so I am moving on to the next method.
internal static string smethod_1()
{
if (Class11.string_2 == null)
{
Thread.Sleep(1000);
}
return Class11.string_2;
}
This one is interesting because it waits a period of time using Thread.Sleep (also generally a bad practice) before returning a volatile string value. I checked the usages and it appears the only time the string is set is during smethod_20 (which is the RegisterLicenseOffline method). As we have not run smethod_20 yet, I would expect this value to be null initially as well. The value it uses is a concatenation of 2 with some other values that should be uncovered during analysis of the offline method.
In short, it appears an empty string is written into the memory stream for most cases - the special cases being when the machine is an Azure instance or a VirtualMachine.
The next string written to the memory stream is the machine name. This makes sense so that LINQPad can track and manage what machines have the license registered.
The next unusual entry is the empty string and the value "5". Perhaps the empty string is a value that is no longer used. I can only guess that the "5" is the version number of LINQPad (as I am decompiling LINQPad5). It is pretty simple to verify that theory by installing LINQPad4 and decompiling it. It may also shed some light on the empty string value if it was included in that version.
This is the corresponding section in LINQPad4:
MemoryStream memoryStream = new MemoryStream();
BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
try
{
binaryWriter.Write(string_0);
binaryWriter.Write(Class6.smethod_5() ?? (Class6.smethod_4() ?? (Class6.smethod_1() ?? GClass0.smethod_0(""))));
binaryWriter.Write(Environment.MachineName);
binaryWriter.Write(GClass0.smethod_0(""));
}
finally
{
if (binaryWriter != null)
{
((IDisposable)binaryWriter).Dispose();
}
}
The assembly uses an unknown obfuscator so it is possible something got lost in translation. I did not go through the trouble of finding the proper de-obfuscator since I only wanted to look at this snippet and it appears to be relatively similar. I do not think there is any additional information so on to the next snippet of code.
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>();
}
This snippet performs encryption. It first generates 16 random bytes that are passed into the smethod_5 twice along with the contents of the memory stream.
internal static byte[] smethod_5(byte[] byte_1, byte[] byte_2, byte[] byte_3)
{
byte[] numArray;
using (Aes ae = Aes.Create())
{
using (ICryptoTransform cryptoTransform = ae.CreateEncryptor(byte_2, byte_3))
{
numArray = WSAgent.smethod_7(byte_1, cryptoTransform);
}
}
return numArray;
}
smethod_5 just encrypts the data using Aes.
private static byte[] smethod_7(byte[] byte_1, ICryptoTransform icryptoTransform_0)
{
MemoryStream memoryStream = new MemoryStream();
using (Stream cryptoStream = new CryptoStream(memoryStream, icryptoTransform_0, CryptoStreamMode.Write))
{
cryptoStream.Write(byte_1, 0, (int)byte_1.Length);
}
return memoryStream.ToArray();
}
Or rather, smethod_7 does using the AES encryptor generated in smethod_5.
Once the program has the AES encrypted byte array we encrypt the result using RSA with a hard-coded public key blob. This is decent for security purposes as we do not know the private key. Assuming the LINQPad registration server can be spoofed, a patch will be required to the Resouces assembly to change the public key blob seen here to a new public key blob that will match the private key of the attacker. The keys can be generated easily enough using the RSACryptoServiceProvider, as can be seen by the following lines.
RSACryptoServiceProvider rsaCryptoServiceProvider = new RSACryptoServiceProvider(1024);
string xmlString = rsaCryptoServiceProvider.ToXmlString(false);
Console.WriteLine(string.Format("Private Key: {0}", xmlString));
byte[] publicBlob = rsaCryptoServiceProvider.ExportCspBlob(false);
Console.WriteLine(string.Format("Public Blob: {0}", Convert.ToBase64String(publicBlob)));
Console.WriteLine();
// Make a new one to show that it is not the same instance.
rsaCryptoServiceProvider = new RSACryptoServiceProvider(1024);
// Load the old xmlString
rsaCryptoServiceProvider.FromXmlString(xmlString);
xmlString = rsaCryptoServiceProvider.ToXmlString(false);
Console.WriteLine(string.Format("Private Key: {0}", xmlString));
publicBlob = rsaCryptoServiceProvider.ExportCspBlob(false);
Console.WriteLine(string.Format("Public Blob: {0}", Convert.ToBase64String(publicBlob)));
Or you can check out the following online version.
I imagine the server component will need the XML string to be able to generate the private key that will be used with the RSACryptoServiceProvider. Since LINQPad is a web service it is probably tucked away in a config file somewhere on the server.
internal static RSACryptoServiceProvider smethod_0(byte[] byte_1)
{
RSACryptoServiceProvider rSACryptoServiceProvider = new RSACryptoServiceProvider();
try
{
rSACryptoServiceProvider.ImportCspBlob(byte_1);
}
catch
{
rSACryptoServiceProvider = new RSACryptoServiceProvider(new CspParameters()
{
Flags = CspProviderFlags.UseMachineKeyStore
});
rSACryptoServiceProvider.ImportCspBlob(byte_1);
}
return rSACryptoServiceProvider;
}
smethod_0 is a factory method to generate an RSACryptoServiceProvider. If an exception occurs with the key, a backup plan is to use the UseMachineKeyStore - meaning a certificate should be installed on the local machine.
Gets or sets a value indicating whether the key should be persisted in the computer's key store instead of the user profile store.The generated provider then encrypts random numbers. The second parameter set to true performs direct RSA encryption using OAEP padding (only available on a computer running Microsoft Windows XP or later); otherwise, false to use PKCS#1 v1.5 padding according to MSDN. The encrypted random numbers, the value "SymKey" in bytes, and the length of the encrypted random numbers (in byte form) is concatenated together to be sent to the server component.
Now web requests are made to the server component if the user has not cancelled the procedure. LINQPad uses three different WebClients and two different web urls.
try
{
try
{
numArray = WSAgent.smethod_1(WSAgent.FastWebClientFactory(), array, false);
}
catch
{
try
{
numArray = WSAgent.smethod_1(WSAgent.BackupWebClientFactory(), array, false);
}
catch (Exception exception1)
{
exception = exception1;
numArray = WSAgent.smethod_1(WSAgent.WebClientFactory(), array, true);
}
}
}
smethod_1 has a check for the boolean that looks like two separate web services are available - one ends with FB which I can speculate means "Fast/Backup" and explains the boolean logic associated with determining which URL should be used. Should any of these WebClients fail or throw an exception the next one is attempted.
private static byte[] smethod_1(WebClient webClient_0, byte[] byte_1, bool bool_0)
{
string str = (bool_0 ? "http://www.linqpad.com/licensing/GetLicenseDataPageFB.aspx" : "http://www.linqpad.net/licensing/GetLicenseDataPage.aspx");
byte[] numArray = webClient_0.UploadData(str, Encoding.UTF8.GetBytes(string.Concat(Convert.ToBase64String(byte_1), "\r\n")));
return Convert.FromBase64String(Encoding.UTF8.GetString(numArray));
}
The WebClient response is stored in a byte array in the RegisterLicenseOffline method. There are two different paths that can be taken now. One is followed if a connection could successfully be established and data was sent. The other is done if a connection could not be established. The analysis thus far can be found here.
Connection Success
First assume no exception occurs to see what happens when the program receives a response.
if (numArray.Length == 0)
{
obj = "Invalid response from licensing server";
}
else if ((int)numArray.Length >= 10)
{
Class11.smethod_16(numArray, bool_0);
Thread thread = new Thread(new ParameterizedThreadStart(Class11.smethod_18))
{
Name = "FLD",
IsBackground = true
};
thread.Start(true);
obj = ActivationResult.OK;
}
else
{
switch (numArray[0])
{
case 1:
{
obj = "The licensing server returned an error: this has been logged. Please try again later.";
break;
}
case 2:
{
obj = "Invalid activation key.";
break;
}
case 3:
{
obj = "Cannot obtain machine information.";
break;
}
case 4:
{
obj = "Activation key has expired.";
break;
}
case 5:
{
obj = ActivationResult.TooManyActivations;
break;
}
case 6:
{
obj = ActivationResult.OldVersion;
break;
}
default:
{
obj = "An internal error has occurred. Please try again or contact support.";
break;
}
}
}
It checks the length of the response - it must be at least 10 bytes long to be considered successful. If the length of the response is any other value other than 0 it checks the first byte to display a corresponding message. When the response meets the proper criteria the middle if statement executes.
else if ((int)numArray.Length >= 10)
{
Class11.smethod_16(numArray, bool_0);
Thread thread = new Thread(new ParameterizedThreadStart(Class11.smethod_18))
{
Name = "FLD",
IsBackground = true
};
thread.Start(true);
obj = ActivationResult.OK;
}
This logic is the exact same as the RegisterOffline code so it will be covered in a later post.
Connection Error
When an exception occurs while attempting to communicate via the WebClient different logic is performed. This generates the code that must be used to RegisterOffline. This could be important if we can use the RegisterOffline as the attack vector.
if (WSAgent.byte_0 == null)
{
WSAgent.byte_0 = new byte[1];
RandomNumberGenerator.Create().GetBytes(WSAgent.byte_0);
}
It first generates a single random byte. This byte is used to help generate some strings in the next snippet.
string str = WSAgent.byte_0[0].ToString("X2").Replace("0", "Y").Replace("1", "Z");
string str1 = string.Concat(Environment.MachineName.Substring(0, 1), Environment.MachineName.Substring(Environment.MachineName.Length - 1, 1));
string str2 = string.Concat((
from b in (IEnumerable)Encoding.UTF8.GetBytes(str1)
select (byte)(b ^ WSAgent.byte_0[0]) into b
select b.ToString("X2").Replace("0", "Y").Replace("1", "Z")).ToArray<string>());
The first string is the byte in hexadecimal format. The second string is the first and last characters of your computer name. The third string does some magic manipulation using the two previous strings to generate a 4 character string. I find it interesting that he replaces the values "0" with "Y" and "1" with "Z". I am not sure what the significance of this may be for as they are random values.
The next part generates two additional strings using methods that we will have to check out - one of which we have seen already.
string str3 = Class11.smethod_2(); string str4 = Class11.smethod_3();smethod_2() needs the volatile string to be set and be greater than 4 characters long. We will still assume this is null for now. smethod_3() grabs the machine name and computes a hash off of it and then performs some arithmetic in smethod_27.
internal static string smethod_3()
{
string machineName = Environment.MachineName;
byte[] numArray = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(machineName));
return string.Concat("5 ", Class11.smethod_27(numArray, 4));
}
smethod_27 only generates 4 characters.
internal static string smethod_27(byte[] byte_0, int int_4)
{
char[] chrArray = new char[int_4];
for (int i = 0; i < int_4; i++)
{
byte byte0 = (byte)(byte_0[i] & 31);
byte0 = (byte0 >= 8 ? (byte)(byte0 + 57) : (byte)(byte0 + 50));
char chr = (char)byte0;
if (chr == 'O')
{
chr = 'Y';
}
else if (chr == 'I')
{
chr = 'Z';
}
chrArray[i] = chr;
}
return new string(chrArray);
}
Back in smethod_3() the FailedRegBlob used to copy to the clipboard is set to the code used and some checks are performed on it.
WSAgent.FailedRegBlob = string_0;
if (WSAgent.FailedRegBlob.Length > 5)
{
WSAgent.FailedRegBlob = string.Concat(WSAgent.FailedRegBlob.Substring(0, 5), "-", WSAgent.FailedRegBlob.Substring(5));
}
WSAgent.FailedRegBlob = string.Concat(WSAgent.FailedRegBlob, (string.IsNullOrEmpty(str3) ? "+" : "-"));
string str5 = ((string.IsNullOrEmpty(str3) ? str4 : str3)).Substring(2);
If the code is greater than 5 characters a hyphen is placed after 5 characters. Since str3 was null (for now at least) a '+' is appended to the end of the string as well. And str5 is set to "5 " because str3 is null (at this time) as well.
string[] failedRegBlob = new string[] { WSAgent.FailedRegBlob, null, null, null, null, null };
char chr = str[0];
failedRegBlob[1] = chr.ToString();
failedRegBlob[2] = str5;
failedRegBlob[3] = "-";
chr = str[1];
failedRegBlob[4] = chr.ToString();
failedRegBlob[5] = str2;
WSAgent.FailedRegBlob = string.Concat(failedRegBlob);
WSAgent.FailedRegBlob = string.Concat(WSAgent.FailedRegBlob, "-", Class11.smethod_27(SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(WSAgent.FailedRegBlob.Replace("-", "").Replace("+", ""))), 2));
This section of code generates the new blob to be copied to the clipboard. The first character is the first value of the hexadecimal bytes. The next characters are "5 -". The next character is the second value of the hexadecimal byte character as a string. Then the string value that is generated using the LINQ statement with the random hexadecimal byte and first and last characters of the machine name. Finally a "-" and two characters of the current blob are used with the smethod_27 covered above.
We can see the majority of the code in action here. There are still a few methods or parameters that are unknown but it generates a code that for the time being we can assume matches what should be provided to the server (this relies on the code from the assembly with little to no interference).
The last steps is to generate the message to display to the user why the request failed.
WebException webException = exception as WebException;
if (webException != null)
{
str6 = string.Concat(str6, ":\r\n", webException.Message);
if ((webException.Status != WebExceptionStatus.ProtocolError || !(webException.Response is HttpWebResponse) ? false : ((HttpWebResponse)webException.Response).StatusCode == HttpStatusCode.ExpectationFailed))
{
str6 = string.Concat(str6, "\r\nIf you're behind an HTTP proxy, click 'Specify Web Proxy' and verify your proxy server details.");
}
}
obj = str6;
return obj;
Summary
That was a little more convoluted than I would have liked it to be. I analyzed a lot of code for both when the message is being generated to the message generated when the registering fails and succeeds. The important part out of this is we figured out what the message sent to the LINQPad server should look like and how we would have to modify the assembly to generate our own messages that can be decrypted. While analyzing the code it seems one attack vector would be to spoof a LINQPad server via the proxy settings with a third party program that would return a valid response using the private key we generate. It will truly depend on what logic occurs in the RegisterOffline code as the RegisterOnline code calls into the same code after it gets a response from the server that is larger than 10 characters. We are getting a better picture of what would be required to break the licensing system. In the next post I will analyze the RegisterOffline logic to see how it works and if this is a valid attack vector.Conclusion
The security is standing up well. I have had to guess at what the server is doing with the data I am sending it. I am able to generate my own license message. This means if I am able to have a man in the middle acting as the LINQPad server the communication can be faked and thus the registration compromised. But only if the public RSA key blob is changed - otherwise the fake server will not be able to decrypt the message. There still is some analysis required of the RegisterOffline code to determine what data is being sent back from the LINQPad server. Depending on the data received a faked response may be able to be generated that could be valid to the application - although I suspect more patches to the public RSA key will appear.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