Join the discord

7Script.exe - Reverse engineering the license scheme

15 Mar, 2016 14:01
You know what's stupid in multiplayer games? Cheating.
You know what's even worse? Selling cheat tools to assholes who cant play.

So, in the next "few" lines, I'm about to take a look at a cheat tool for the game 7 Days To Die that not only ruin everyone's gaming experience, but costs only 15 Euro.



The target


The thing I'm talking about is called 7Script and provides the following functionalities to the "player":
Automine - automatically clicks the mouse button for you, so you can furiously masturbate in the meantime;
Bhop - or "Bunny Hopping", if you remember that from the good old days of Counter Strike 1.0, when you could run faster if you constantly jump;
Code Cracker - a bruteforcer for code protected stuff (crates, doors, etc.), that is actually pretty good idea that I was also considering about implementing, but for my education purposes only;
Crosshair - changes your default crosshair or add it where it's usually not present;
Duplicate Items - the thing that most of the time ruins your fun;
Drop (afk bot) - a something-something that helps you farm levels;
Creative Mode - that according to the authors of this tool doesn't work in the latest versions;

And that's all.
The authors claim their tool is undetectable by EasyAntiCheat protected games, because it's "not a hack, but a script".
If that's true, though, and your local XML files are used by the server in any way different than local engine skinning and texturing, that's a black hole so massive, that the CERN scientists should install 7 Days To Die on their Collider computers and observe that, instead slamming subatomic particles to each other.



Initial analysis


Ok, enough foreplay - here's what we got:


The tool is written in .NET and protected with Confuser.
A great tool for deobfuscating Confuser apps is de4dot and it's latest version 3.1 had absolutely no problems dealing with the 7Script executable.

Having the deobfuscated version I can now step up to the "How to buy premium" window.
dnSpythis.TextBox1.Location = new Point(11, 425);
this.TextBox1.Multiline = true;
this.TextBox1.Name = "TextBox1";
this.TextBox1.Size = new Size(32, 39);
this.TextBox1.TabIndex = 26;
this.TextBox1.Text = "Fuck off!\r\nStop reverse eneneering my software!";
this.TextBox1.Visible = false;
Oh, c'mon! Just a little bit... ;)

To find out what this HWID is I went here:
dnSpy// Token: 0x0600004A RID: 74 RVA: 0x0000229F File Offset: 0x0000049F
private void buy_Load(object sender, EventArgs e)
{
   this.TextBox1.Text = WindowsIdentity.GetCurrent().User.Value;
}

so it's just the current user's Windows Identity value from here:


Let's move on to the place where this value is used to determine if my license is valid or not:
dnSpytry
{
   WebClient webClient3 = new WebClient();
   Stream stream5 = webClient3.OpenRead("http://th17323-web361.server6.vorschauseite.eu/license/" + 
      WindowsIdentity.GetCurrent().User.Value + ".txt");
   StreamReader streamReader3 = new StreamReader(stream5);
   this.license = streamReader3.ReadToEnd();
}
catch (Exception expr_487)
{
   ProjectData.SetProjectError(expr_487);
   this.Additem("No license found.", Color.Silver);
   this.Additem("** Trial version **", Color.Silver);
   ProjectData.ClearProjectError();
   goto IL_A34;
}
So it's a web validation check using a user ID file. Alright.

Moving on:
dnSpytry
{
   if (Operators.CompareString(this.license, "", false) == 0)
   {
      this.Additem("No license found.", Color.Silver);
      this.Additem("** Trial version **", Color.Silver);
   }
   else if (Operators.CompareString(this.license, "Banned", false) == 0)
   {
      this.Additem("HWID got banned.", Color.Red);
      Interaction.MsgBox("Your HardwareID got banned from our system.\r\nYou're a bad guy...", MsgBoxStyle.Critical, "Banned!");
      ProjectData.EndApp();
   }
   else
   {
   // More code...
If the file is empty the license is invalid and if it contains the string "Banned" I'm considered as a bad guy.

Funny thing is the only way the file could be empty is when the authors specifically added an empty file with the user's ID. In every other case, for example, when the file with that name doesn't exist on their server, the result of streamReader3.ReadToEnd() will be the 404 NOT FOUND server message, in this case:
HTML<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /license/S-1-5-21-1255132489-119165214-112439472-1003.txt was not found on this server.</p>
</body></html>
which will automatically ignore the whole if (this.license == "") {... statement and fell into the else at the end.

So, let's take a look in there then:
dnSpyelse
{
   string key = "lootstreetcrew1337";
   this.keyfile.Text = sevenscript.Decrypt(this.license, key).ToString();
   if (Operators.CompareString(this.keyfile.Lines[0].ToString(), WindowsIdentity.GetCurrent().Use.Value, false) == 0)
   {
      this.Label7.Text = "Welcome, " + this.keyfile.Lines[1].ToString() + "!";
      this.Label7.Font = new Font("Kristen ITC", 10f, FontStyle.Bold);
      this.Additem("License found (" + this.keyfile.Lines[1].ToString() + ")", Color.Silver);
      // More code...
Seems like the file contents is key encrypted in a way, and to consider it as valid license, the first line should equal to my ID.

Here's the Decrypt() procedure:
dnSpy// Token: 0x04000040 RID: 64
private static TripleDESCryptoServiceProvider DES = new TripleDESCryptoServiceProvider();

// Token: 0x04000041 RID: 65
private static MD5CryptoServiceProvider MD5 = new MD5CryptoServiceProvider();

// _7_Script.sevenscript
// Token: 0x0600008B RID: 139 RVA: 0x000056EC File Offset: 0x000038EC
public static string Decrypt(string encryptedString, string key)
{
   string @string;
   try
   {
      sevenscript.DES.Key = sevenscript.MD5Hash(key);
      sevenscript.DES.Mode = CipherMode.ECB;
      byte[] array = Convert.FromBase64String(encryptedString);
      @string = Encoding.ASCII.GetString(sevenscript.DES.CreateDecryptor().TransformFinalBlock(array, 0, array.Length));
   }
   catch (Exception expr_44)
   {
      ProjectData.SetProjectError(expr_44);
      MessageBox.Show("Invalid Key", "Decryption Failed", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
      ProjectData.ClearProjectError();
   }
   return @string;
}

// _7_Script.sevenscript
// Token: 0x0600008A RID: 138 RVA: 0x000056C8 File Offset: 0x000038C8
public static byte[] MD5Hash(string value)
{
   return sevenscript.MD5.ComputeHash(Encoding.ASCII.GetBytes(value));
}

The decryption is quite simple:
1. read the license file
2. Base64 decode the data
3. Triple DES decrypt the result, using the MD5 result of "lootstreetcrew1337" as a key

It's a good start, so let's see what else is used from that key file:
dnSpythis.Label7.Text = "Welcome, " + this.keyfile.Lines[1].ToString() + "!";
this.Label7.Font = new Font("Kristen ITC", 10f, FontStyle.Bold);
this.Additem("License found (" + this.keyfile.Lines[1].ToString() + ")", Color.Silver);
while (true)
{
   try
   {
      if (Operators.CompareString(this.keyfile.Lines[2].ToString(), "Lifetime", false) == 0)
      {
         this.premium = true;
         this.Label23.Text = "Premium for Lifetime";
         this.hack3.ForeColor = Color.Silver;
         this.hack5.ForeColor = Color.Silver;
         this.hack6.ForeColor = Color.Silver;
         this.hack7.ForeColor = Color.Gray;
         this.hack9.Text = "Thx 4 Buying";
         this.status9.Text = ":)";
         this.Additem("License is Premium!", Color.Gold);
         goto IL_A34;
      }
      // More code...
   }
   // More code...
}
// More code...
Line two of the license file is the Username of the license holder, and line three is the validity period.
If the validity period is just a "Lifetime" string, we are considered as Lifetime license holder (duh?) and if it's a timestamp, a further parsing and validations are made to determine if our license is still valid or not.

Premium license is fine, so I've ignored the timestamp parsing.

With that information I can build my license data:
raw licenseS-1-5-21-1255132489-119165214-112439472-1003
XpoZed
Lifetime

next, Triple DES encrypt it with key md5("lootstreetcrew1337") and Base64 encode the result:
completed licensexwvHbwZo/6aViENQk3s1dhRseBd86YNZtsj5JnDmHMHv7IFlLu7huDQN3CqEr4WYOfZmV9GTv17RHFLYJyndBg==
Done!

To verify my research so far, I've did some super secret l33t h4x0r tinkering with the HTTP GET requests... that in reality was just to redirect th17323-web361.server6.vorschauseite.eu to my localhost and reply with my forged license.


But let's not stop just here, shall we?



Overcomplicating my life


I'm well known of my urge to overcomplicate things, so I've decided to write some more solid solution, that doesn't involve l33t h4x wizardy.

What I am thinking of is to interfere the HTTP/TCP send-receive process and swap the receive result with my forged license.

How can I accomplish this, though?
First, we are dealing with .NET Framework, which is (in big portion, and don't quote me on that) a wrapper over the core OS functions.
Knowing that, we can also assume (and it's true) that somewhere in the lower level, the HTTP transfer for the license validation is done with WinSock2 send() and recv().
So, I can just hook recv() and swap the result, but there's a slide problem here.

The program makes multiple HTTP requests (to check for new version, download ReadMe.txt file, etc.) so I cannot just change every recv() result with my license data.
What i need here is to know which recv() is the one for the license data.

By it's design, there is no way to get the requested URL from recv() parameters, but send() holds the requested URL, so i'll obviously have to hook this one too.

On logic level, things should look like this:

- hook the send() function, and every time the app calls it, I'll store the requested URL in a global variable;
- hook the recv(), check if the requested URL matches the license one, and swap the result if they match;

Sounds like a plan, now how to hook them?
The right way is to use a process watcher (loader) app, that waits for 7Script.exe to be executed, and injects it with a hooking DLL.
This means I'll need two more files - the loader and the DLL, and instead of running the 7Script.exe I'll have to run the loader instead.
Well I don't like that.

So here's my new plan. Instead of using the Loader+DLL method, I'll use a single DLL and load it without any magical tricks or application patches!

Every time we run an executable, depending on the way it's built and the compiler used, the executable will look for certain helper libraries in its base directory, core directories like System32, etc.
Sometimes these libraries doesn't exist in the base dir (they are usually in System32) and that's ok. If you remember the pre-dotNET era and Visual Basic back then, by default it looked for localisation files and .HLP files in the application's base directory.

.NET acts similarly, so I've fire up Process Monitor, filtered the files that is trying to access in its base directory but doesn't physically exist there, and take the .DLL ones only:
Process Monitor
11:45:58.4535001 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\CRYPTBASE.dll	NAME NOT FOUND
11:45:58.5540916 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\bcrypt.dll	NAME NOT FOUND
11:45:58.6182895 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\CRYPTSP.dll	NAME NOT FOUND
11:45:59.3361694 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\uxtheme.dll	NAME NOT FOUND
11:45:59.4639385 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\RpcRtRemote.dll	NAME NOT FOUND
11:45:59.5444419 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\version.dll	NAME NOT FOUND
11:45:59.5604811 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\Secur32.dll	NAME NOT FOUND
11:45:59.5623680 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\SSPICLI.DLL	NAME NOT FOUND
11:45:59.5707427 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\api-ms-win-downlevel-advapi32-l2-1-0.dll
11:45:59.6006220 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\ntmarta.dll	NAME NOT FOUND
11:45:59.7587578 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\rasapi32.dll	NAME NOT FOUND
11:45:59.7593883 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\rasman.dll	NAME NOT FOUND
11:45:59.7603530 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\rtutils.dll	NAME NOT FOUND
11:45:59.7621996 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\winhttp.dll	NAME NOT FOUND
11:45:59.7629514 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\webio.dll	NAME NOT FOUND
11:45:59.7641875 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\credssp.dll	NAME NOT FOUND
11:45:59.7650070 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\IPHLPAPI.DLL	NAME NOT FOUND
11:45:59.7655713 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\WINNSI.DLL	NAME NOT FOUND
11:45:59.7662524 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\dhcpcsvc6.DLL	NAME NOT FOUND
11:45:59.7854460 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\dhcpcsvc.DLL	NAME NOT FOUND
11:46:14.0265172 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\DNSAPI.dll	NAME NOT FOUND
11:46:14.0502997 AM	7Script.exe	3384	CreateFile	C:\Users\revbox\Desktop\rasadhlp.dll	NAME NOT FOUND
Plenty of fish in the sea, right?
After some tinkering with which one to pick, rasadhlp.dll was the right choice. It's "Remote Access AutoDial Helper", it should be located elsewhere and it's not used by the application.

So, that's everything I need to build my license rig DLL, considering the following things:

1 - Implement a Triple DES encryption, which is easy using Windows WinCrypt and Crypt32. However, there was a little pit hole here. In .NET, the original code was using MD5() to create a key for the Triple DES encryption.
MD5 can produce 128bit (16 bytes) key only, while Triple DES requires 192bit (24 bytes) one. What .NET does by default is to append the first 16bits (8 bytes) from the MD5 hash to its end.
That's something WinCrypt wont do for you and instead will append to the 16 bytes long key with either 0x00 or random bytes, depending on the way you initialized the buffer for the MD5 hash.

2 - Build a HOOK code for the Wsock32 send() function, that will store the "GET" packet. Of course, that's done in inline assembly:
InlineSssemblyvoid hookSend() {
   __asm__ __volatile__(
      ".intel_syntax noprefix;"
      "PUSHAD;"                             // Preserve registers
      "PUSHFD;"                             // Preserve flags
      "MOV EAX,DWORD PTR SS:[ESP+0x2C];"    // char *buf
      "LEA EDX,[_szTempPacket];"
      "PUSH EAX;"                           // const char *strSource
      "PUSH EDX;"                           // char *strDestination
      "CALL _strcpy;"
      "ADD ESP,0x08;"                       // STDCALL stack fix
      "POPFD;"                              // Restore flags
      "POPAD;"                              // Restore registers
      "MOV EDI,EDI;"                        // Restore send() EP
      "PUSH EBP;"                           //
      "MOV EBP,ESP;"                        //
      "JMP [_retSend];"                     // Continue send() codeflow
   );
}


3 - Build a HOOK code for the Wsock32 recv() function, that will check and eventually swap the license data:
InlineSssemblyvoid hookRecv() {
   __asm__ __volatile__(
      ".intel_syntax noprefix;"
      "PUSHAD;"                             // Preserve registers
      "PUSHFD;"                             // Preserve flags
      "LEA EAX,[_szTempPacket];"            // Previously sent request, used as haystack
      "LEA ECX,[_szNeedle];"                // Needle
      "PUSH ECX;"                           // const char *strSearch
      "PUSH EAX;"                           // const char *str
      "CALL _strstr;"
      "ADD ESP,0x08;"                       // STDCALL stack fix
      "CMP EAX,0x00;"
      "JE dont_patch;"
      "MOV EAX,DWORD PTR SS:[ESP+0x2C];"    // char *buf
      "MOV ECX,[_szForgedPacket];"          // My forged packet
      "PUSH ECX;"                           // const char *strSource
      "PUSH EAX;"                           // char *strDestination
      "CALL _strcpy;"
      "ADD ESP,0x08;"                       // STDCALL stack fix
      "POPFD;"                              // Restore flags
      "POPAD;"                              // Restore registers
      "MOV EAX,[_szForgedPacket];"          // My forged packet
      "PUSH EAX;"                           // const char *str
      "CALL _strlen;"                       // Get the forged packet length
      "ADD ESP,0x04;"                       // STDCALL stack fix
      "RET;"                                // Exit recv()
      "dont_patch:;"
      "POPFD;"                              // Restore flags
      "POPAD;"                              // Restore registers
      "MOV EDI,EDI;"                        // Restore recv() EP
      "PUSH EBP;"                           //
      "MOV EBP,ESP;"                        //
      "JMP [_retRecv];"                     // Continue recv() codeflow
   );
}

And that's pretty much everything.
A complete working code can be downloaded from here - http://nullsecurity.org/download/8dc0b08d0319ab80f9fc81cd97d9840a

Comments

* You have an opinion? Let us all hear it!

Guest 06 Mar, 2017 02:23
Got as far as getting the 7 script cleaned.exe and downloading dnspy, but couldn't find an .exe. Something about needing Python? In the absence of any other trainer, this very detailed article was the best hope but this assembly code stuff is really way beyond me. Back to Google I guess.
Guest 22 Dec, 2016 01:51
Hola amigo Podrias hacer un tutorial de forma basica para personas q no comprenden mucho del tema.
Guest 15 Jul, 2016 09:37
many thanks for this
XpoZed 14 Jul, 2016 09:18
Yep, it is correct.
hector666 13 Jul, 2016 01:21
Really nice tutorial, but what the heck is that byte-shifting doing?

I tried this one, and Decoding is telling me, that key is invalid.
My md5 hash is: 188e869dffdbe156d977d0bf67dfa427
so i added the first 8 Bytes from md5-hashed value and used
md5hash: 188e869dffdbe156d977d0bf67dfa427188e869dffdbe156

Is that correct ?
© nullsecurity.org 2011-2024 | legal | terms & rules | contacts