This article was published on the 9th of October 2018. This article was updated on the 30th of April 2020.
The first high level language that will be covered is the .NET Framework. First, general information regarding the framework will be given. After that, a practical case is examined in which the used tooling is explained and a challenge is analysed step-by-step. The focus of this analysis is on the thought process, as well as the used techniques, since both are equally important to solve a challenge.
Table of contents
The framework’s structure
The .NET Framework (pronounced as Dot Net) has been developed by Microsoft and allows users to use the Framework Class Library (FCL). These libraries contain functionality regarding the system on which it is executed, as well as exception handling, networking, XML parsing and some additional libraries.
Apart from the libraries, the framework offers usability between all .NET programs. A Visual Basic .NET application can use a C# dynamic link library (DLL) and vice versa. All languages are compiled to the Common Intermediate Language (CIL), a form of bytecode. Previously, the Common Intermediate Language was known as the Microsoft Intermediate Language (MSIL).
Bytecode
Being somewhat similar to assembly language, the CIL consists of instructions with parameters. Although these instructions are more complex than most assembly language instructions, one must keep in mind that the bytecode is compiled, again, during the runtime of the program. The Just-In-Time (JIT) compiler compiles the CIL to the target platform’s architecture. This allows a .NET program to run on both the 32-bit (x86) and 64-bit (x86_64) architectures. As a result, the programmer only needs to code the requested functionality, ignoring the differences between the architectures. Do note here that the system on which the .NET binary is executed, requires the .NET Framework to be installed.
Common Language Runtime
Not only is the CIL (together with the JIT compiler) usable on multiple architectures, it also provides additional advantages for programmers. The JIT compiler resides inside the Common Language Runtime (CLR), which is a virtual environment that manages more than the architectural differences. Besides the above mentioned, memory management (and garbage collection), security (including type safety), exception handling and thread management are all handled by the CLR.
Memory management
Because the memory is handled by, and within, the CLR, there rarely is a reason to require direct memory access. In other (lower level) languages, direct memory access is common. An example of such a language is C. Within the .NET languages, it is possible to work with pointers with the use of the Interoperability, but it is not required for most of the tasks.
Within the .NET Framework, a user doesn’t need to worry about garbage collection and related vulnerabilities, such as use-after-free or double free. An example would be to use a list instead of an array, in which a stack overflow isn’t possible anymore: lists grow dynamically in size. There are, however, still limitations to the provided structures. These are handled with exceptions, instead of causing a segmentation fault. Uncaught exceptions are still a problem.
Do note that this does not guarantee safety, since the .NET framework implementation might contain a vulnerability. Generally, it is safe to use, but it is no guarantee. Additionally, when using the Interoperability, such vulnerabilities can occur.
Security
The framework has, besides the memory management security, additional features which provide safety. Private members of objects can not be read from outside the object itself, making it safe to pass such an object to other functions, unless the field is exposed indirectly, via a method or different field which links to the private field. Structures (or unions for that matter) in C do not possess such a property.
Exception handling
Whereas exceptions are generally caught manually in lower level languages, such as C or assembly, the .NET framework has a system in place to handle exceptions, including custom exceptions.
In C, one might return a negative value if an error occurred, as can be seen below.
int x; if (x == null) { return -1; }
This requires another check in the function which called this function, because the function will return properly, but the value indicates whether an error occurred or not. In C#, one can throw an exception, which also needs to be handled by the calling function, or within the function itself but does not returns the same way a normal value would. An example is given below.
int x; if (x == null) { throw new ArgumentNullException(); }
Thread management
Using the thread management provided by the .NET framework, one can use a readily available library which is optimised for performance (scheduling). This can be done manually as well, but the support is already included.
Mono
The Mono framework is similar to the .NET Framework, as it also includes C# and Visual Basic .NET in a cross-platform environment. Note that only the .NET Core functionality is cross platform. Upon creating a project, there is a distinct difference between the .NET Framework project and a .NET Core project. Although Mono’s specific implementation differs from the .NET Framework’s implementation, the concept is the same: a virtual machine with a JIT compiler, compiles the code for the target platform during runtime, after the C# or Visual Basic .NET code has been compiled to ECMA CIL. The ECMA CIL is an open standard, which leaves room for different implementations in the future.
Tooling
In this practical case, dnSpy will be used to decompile a .NET binary. Within dnSpy, the option to display the CIL, Visual Basic .NET or C# version of the code is available. Additionally, dnSpy supports binary patching, debugging and static analysis.
In the examples that are given below, dnSpy has been used to obtain the code. In this practical case, CIL and C# are the preferred languages, whereas Visual Basic .NET will only be used as an example.
CIL in practice
The first function that is called in the CIL, is the Main function in the Program class. An example is given below, in decompiled C# code. The decompiled application is a Windows Forms application.
using System; using System.Windows.Forms; namespace Chapter3 { // Token: 0x02000003 RID: 3 internal static class Program { // Token: 0x06000005 RID: 5 RVA: 0x0000213C File Offset: 0x0000033C [STAThread] private static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }
At first, the visual styles are enabled and the compatible text rendering default is set to false. After that, the call to the next class is made with the function Run: Application.Run(new Form1());. Often, the form (which is a class, but it inherits the class Form, making it a Windows Form) is the first part where the programmer’s code is located. It is, however, possible to include code in the Program.cs class.
One can compare this with the entry0 function in a native binary. Generally, the main function is the first function in which the programmer’s code is found, but it is not always the case.
The same function that is given in C# above, can be found in the CIL below.
// Token: 0x02000003 RID: 3 .class private auto ansi abstract sealed beforefieldinit Chapter3.Program extends [mscorlib]System.Object { // Methods // Token: 0x06000005 RID: 5 RVA: 0x0000213C File Offset: 0x0000033C .method private hidebysig static void Main () cil managed { .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) // Header Size: 1 byte // Code Size: 26 (0x1A) bytes .maxstack 8 .entrypoint /* 0x0000033D 00 */ IL_0000: nop /* 0x0000033E 282400000A */ IL_0001: call void [System.Windows.Forms]System.Windows.Forms.Application::EnableVisualStyles() /* 0x00000343 00 */ IL_0006: nop /* 0x00000344 16 */ IL_0007: ldc.i4.0 /* 0x00000345 282500000A */ IL_0008: call void [System.Windows.Forms]System.Windows.Forms.Application::SetCompatibleTextRenderingDefault(bool) /* 0x0000034A 00 */ IL_000D: nop /* 0x0000034B 7301000006 */ IL_000E: newobj instance void Chapter3.Form1::.ctor() /* 0x00000350 282600000A */ IL_0013: call void [System.Windows.Forms]System.Windows.Forms.Application::Run(class [System.Windows.Forms]System.Windows.Forms.Form) /* 0x00000355 00 */ IL_0018: nop /* 0x00000356 2A */ IL_0019: ret } // end of method Program::Main } // end of class Chapter3.Program
Here the similarity with the aforementioned assembly language can be seen. The instructions such as nop, call and ret serve a similar function, whereas the newobj (new object) instruction is not found in assembly language. The ldc.i4.0 instruction pushes the value 0 on the stack as a 32-bit integer. In assembly language this would be push 0 in a 32-bit binary.
Also note that the return types of functions exists in the CIL, as can be seen after the call instructions in the example above.
To gain further understanding, take a look at the decompiled C# code for Form1 in the Chapter3 application, which is given below.
using System; using System.ComponentModel; using System.Drawing; using System.Windows.Forms; namespace Chapter3 { // Token: 0x02000002 RID: 2 public class Form1 : Form { // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250 public Form1() { MessageBox.Show("Event \"Form Constructor\" is triggered!"); this.InitializeComponent(); } // Token: 0x06000002 RID: 2 RVA: 0x00002073 File Offset: 0x00000273 private void Form1_Load(object sender, EventArgs e) { MessageBox.Show("Event \"Load\" is triggered!"); base.Close(); } // Token: 0x06000003 RID: 3 RVA: 0x00002088 File Offset: 0x00000288 protected override void Dispose(bool disposing) { bool flag = disposing && this.components != null; if (flag) { this.components.Dispose(); } base.Dispose(disposing); } // Token: 0x06000004 RID: 4 RVA: 0x000020C0 File Offset: 0x000002C0 private void InitializeComponent() { base.SuspendLayout(); base.AutoScaleDimensions = new SizeF(6f, 13f); base.AutoScaleMode = AutoScaleMode.Font; base.ClientSize = new Size(800, 450); base.Name = "Form1"; this.Text = "Form1"; base.Load += this.Form1_Load; base.ResumeLayout(false); } // Token: 0x04000001 RID: 1 private IContainer components = null; } }
In this code, two messageboxes are shown. The first messagebox is found in the constructor of Form1. The second messagebox is found in the event handler of the Load function, named Form1_Load. After the second messagebox, the program terminates, as can be seen in the excerpt below.
base.Close();
This excerpt equals this.Close();, which closes the class. Since there are no other calls in the Program class, the binary exits normally.
In CIL, the Form1 class looks rather similar.
// Token: 0x02000002 RID: 2 .class public auto ansi beforefieldinit Chapter3.Form1 extends [System.Windows.Forms]System.Windows.Forms.Form { // Fields // Token: 0x04000001 RID: 1 .field private class [System]System.ComponentModel.IContainer components // Methods // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250 .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Header Size: 1 byte // Code Size: 34 (0x22) bytes .maxstack 8 /* 0x00000251 02 */ IL_0000: ldarg.0 /* 0x00000252 14 */ IL_0001: ldnull /* 0x00000253 7D01000004 */ IL_0002: stfld class [System]System.ComponentModel.IContainer Chapter3.Form1::components /* 0x00000258 02 */ IL_0007: ldarg.0 /* 0x00000259 281400000A */ IL_0008: call instance void [System.Windows.Forms]System.Windows.Forms.Form::.ctor() /* 0x0000025E 00 */ IL_000D: nop /* 0x0000025F 00 */ IL_000E: nop /* 0x00000260 7201000070 */ IL_000F: ldstr "Event \"Form Constructor\" is triggered!" /* 0x00000265 281500000A */ IL_0014: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string) /* 0x0000026A 26 */ IL_0019: pop /* 0x0000026B 02 */ IL_001A: ldarg.0 /* 0x0000026C 2804000006 */ IL_001B: call instance void Chapter3.Form1::InitializeComponent() /* 0x00000271 00 */ IL_0020: nop /* 0x00000272 2A */ IL_0021: ret } // end of method Form1::.ctor // Token: 0x06000002 RID: 2 RVA: 0x00002073 File Offset: 0x00000273 .method private hidebysig instance void Form1_Load ( object sender, class [mscorlib]System.EventArgs e ) cil managed { // Header Size: 1 byte // Code Size: 20 (0x14) bytes .maxstack 8 /* 0x00000274 00 */ IL_0000: nop /* 0x00000275 724F000070 */ IL_0001: ldstr "Event \"Load\" is triggered!" /* 0x0000027A 281500000A */ IL_0006: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string) /* 0x0000027F 26 */ IL_000B: pop /* 0x00000280 02 */ IL_000C: ldarg.0 /* 0x00000281 281600000A */ IL_000D: call instance void [System.Windows.Forms]System.Windows.Forms.Form::Close() /* 0x00000286 00 */ IL_0012: nop /* 0x00000287 2A */ IL_0013: ret } // end of method Form1::Form1_Load // Token: 0x06000003 RID: 3 RVA: 0x00002088 File Offset: 0x00000288 .method family hidebysig virtual instance void Dispose ( bool disposing ) cil managed { // Header Size: 12 bytes // Code Size: 43 (0x2B) bytes // LocalVarSig Token: 0x11000001 RID: 1 .maxstack 2 .locals init ( [0] bool ) /* 0x00000294 00 */ IL_0000: nop /* 0x00000295 03 */ IL_0001: ldarg.1 /* 0x00000296 2C0B */ IL_0002: brfalse.s IL_000F /* 0x00000298 02 */ IL_0004: ldarg.0 /* 0x00000299 7B01000004 */ IL_0005: ldfld class [System]System.ComponentModel.IContainer Chapter3.Form1::components /* 0x0000029E 14 */ IL_000A: ldnull /* 0x0000029F FE03 */ IL_000B: cgt.un /* 0x000002A1 2B01 */ IL_000D: br.s IL_0010 /* 0x000002A3 16 */ IL_000F: ldc.i4.0 /* 0x000002A4 0A */ IL_0010: stloc.0 /* 0x000002A5 06 */ IL_0011: ldloc.0 /* 0x000002A6 2C0E */ IL_0012: brfalse.s IL_0022 /* 0x000002A8 00 */ IL_0014: nop /* 0x000002A9 02 */ IL_0015: ldarg.0 /* 0x000002AA 7B01000004 */ IL_0016: ldfld class [System]System.ComponentModel.IContainer Chapter3.Form1::components /* 0x000002AF 6F1700000A */ IL_001B: callvirt instance void [mscorlib]System.IDisposable::Dispose() /* 0x000002B4 00 */ IL_0020: nop /* 0x000002B5 00 */ IL_0021: nop /* 0x000002B6 02 */ IL_0022: ldarg.0 /* 0x000002B7 03 */ IL_0023: ldarg.1 /* 0x000002B8 281800000A */ IL_0024: call instance void [System.Windows.Forms]System.Windows.Forms.Form::Dispose(bool) /* 0x000002BD 00 */ IL_0029: nop /* 0x000002BE 2A */ IL_002A: ret } // end of method Form1::Dispose // Token: 0x06000004 RID: 4 RVA: 0x000020C0 File Offset: 0x000002C0 .method private hidebysig instance void InitializeComponent () cil managed { // Header Size: 12 bytes // Code Size: 112 (0x70) bytes .maxstack 3 /* 0x000002CC 00 */ IL_0000: nop /* 0x000002CD 02 */ IL_0001: ldarg.0 /* 0x000002CE 281900000A */ IL_0002: call instance void [System.Windows.Forms]System.Windows.Forms.Control::SuspendLayout() /* 0x000002D3 00 */ IL_0007: nop /* 0x000002D4 02 */ IL_0008: ldarg.0 /* 0x000002D5 220000C040 */ IL_0009: ldc.r4 6 /* 0x000002DA 2200005041 */ IL_000E: ldc.r4 13 /* 0x000002DF 731A00000A */ IL_0013: newobj instance void [System.Drawing]System.Drawing.SizeF::.ctor(float32, float32) /* 0x000002E4 281B00000A */ IL_0018: call instance void [System.Windows.Forms]System.Windows.Forms.ContainerControl::set_AutoScaleDimensions(valuetype [System.Drawing]System.Drawing.SizeF) /* 0x000002E9 00 */ IL_001D: nop /* 0x000002EA 02 */ IL_001E: ldarg.0 /* 0x000002EB 17 */ IL_001F: ldc.i4.1 /* 0x000002EC 281C00000A */ IL_0020: call instance void [System.Windows.Forms]System.Windows.Forms.ContainerControl::set_AutoScaleMode(valuetype [System.Windows.Forms]System.Windows.Forms.AutoScaleMode) /* 0x000002F1 00 */ IL_0025: nop /* 0x000002F2 02 */ IL_0026: ldarg.0 /* 0x000002F3 2020030000 */ IL_0027: ldc.i4 800 /* 0x000002F8 20C2010000 */ IL_002C: ldc.i4 450 /* 0x000002FD 731D00000A */ IL_0031: newobj instance void [System.Drawing]System.Drawing.Size::.ctor(int32, int32) /* 0x00000302 281E00000A */ IL_0036: call instance void [System.Windows.Forms]System.Windows.Forms.Form::set_ClientSize(valuetype [System.Drawing]System.Drawing.Size) /* 0x00000307 00 */ IL_003B: nop /* 0x00000308 02 */ IL_003C: ldarg.0 /* 0x00000309 7285000070 */ IL_003D: ldstr "Form1" /* 0x0000030E 281F00000A */ IL_0042: call instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Name(string) /* 0x00000313 00 */ IL_0047: nop /* 0x00000314 02 */ IL_0048: ldarg.0 /* 0x00000315 7285000070 */ IL_0049: ldstr "Form1" /* 0x0000031A 6F2000000A */ IL_004E: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Text(string) /* 0x0000031F 00 */ IL_0053: nop /* 0x00000320 02 */ IL_0054: ldarg.0 /* 0x00000321 02 */ IL_0055: ldarg.0 /* 0x00000322 FE0602000006 */ IL_0056: ldftn instance void Chapter3.Form1::Form1_Load(object, class [mscorlib]System.EventArgs) /* 0x00000328 732100000A */ IL_005C: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int) /* 0x0000032D 282200000A */ IL_0061: call instance void [System.Windows.Forms]System.Windows.Forms.Form::add_Load(class [mscorlib]System.EventHandler) /* 0x00000332 00 */ IL_0066: nop /* 0x00000333 02 */ IL_0067: ldarg.0 /* 0x00000334 16 */ IL_0068: ldc.i4.0 /* 0x00000335 282300000A */ IL_0069: call instance void [System.Windows.Forms]System.Windows.Forms.Control::ResumeLayout(bool) /* 0x0000033A 00 */ IL_006E: nop /* 0x0000033B 2A */ IL_006F: ret } // end of method Form1::InitializeComponent } // end of class Chapter3.Form1
As stated before, both C# and Visual Basic .NET code is compiled to the CIL. It is therefore possible to obtain the Visual Basic .NET code of a C# class, since both can be derived from the CIL, as is shown in the example below.
Imports System Imports System.ComponentModel Imports System.Drawing Imports System.Windows.Forms Namespace Chapter3 ' Token: 0x02000002 RID: 2 Public Class Form1 Inherits Form ' Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250 Public Sub New() MessageBox.Show("Event ""Form Constructor"" is triggered!") Me.InitializeComponent() End Sub ' Token: 0x06000002 RID: 2 RVA: 0x00002073 File Offset: 0x00000273 Private Sub Form1_Load(sender As Object, e As EventArgs) MessageBox.Show("Event ""Load"" is triggered!") MyBase.Close() End Sub ' Token: 0x06000003 RID: 3 RVA: 0x00002088 File Offset: 0x00000288 Protected Overrides Sub Dispose(disposing As Boolean) Dim flag As Boolean = disposing AndAlso Me.components IsNot Nothing If flag Then Me.components.Dispose() End If MyBase.Dispose(disposing) End Sub ' Token: 0x06000004 RID: 4 RVA: 0x000020C0 File Offset: 0x000002C0 Private Sub InitializeComponent() MyBase.SuspendLayout() MyBase.AutoScaleDimensions = New SizeF(6F, 13F) MyBase.AutoScaleMode = AutoScaleMode.Font MyBase.ClientSize = New Size(800, 450) MyBase.Name = "Form1" Me.Text = "Form1" AddHandler MyBase.Load, AddressOf Me.Form1_Load MyBase.ResumeLayout(False) End Sub ' Token: 0x04000001 RID: 1 Private components As IContainer = Nothing End Class End Namespace
The amount of lines in the CIL is significantly more than the amount of lines in the C# or Visual Basic .NET counterparts. The purpose of these examples is to show the similarities and differences between the languages. In the rest of the case, needless blocks of code will be redacted to increase readability.
Practical case
This practical case revolves around the second challenge of the Flare-On 2018 CTF: UltimateMinesweeper. The binary can be downloaded here. Note that there are many possible ways to solve this challenge, it is therefore not the intention to present this as the only way to solve it, but rather as the way I solved it.
Examining the application
Upon obtaining the binary, the type is the first thing one should look for. This has multiple reasons.
Firstly, taking a different platform than the binary’s target platform during the static analysis phase prevents accidental execution of the binary. This is of utmost importance when analysing malicious binaries. During a CTF this is of less importance, although it can never harm to be too cautious.
Secondly, it provides information about the tool which should be used to analyse the binary. An example would be Radare2, which does not support the analysis of .NET Framework binaries. Therefore, another analysis tool needs to be used, if the binary in question is written in C# or Visual Basic .NET.
There are multiple options to retrieve this information. Firstly, one can use the GNU file binary. The output of the minesweeper binary is given below.
libra@laptop:~/Downloads/FlareOn/Challenge2$ file ./UltimateMinesweeper.exe ./UltimateMinesweeper.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows
Additionally, one can use Radare2 or Rabin2 (part of the Radare2 framework). Although the disassembly of a .NET binary is not supported in Radare2, it is able to recognise the binary type. Both examples are given below.
libra@laptop:~/Downloads/FlareOn/Challenge2$ r2 ./UltimateMinesweeper.exe [...] .NET Version: v4.0.30319 [...] libra@laptop:~/Downloads/FlareOn/Challenge2$ rabin2 -A ./UltimateMinesweeper.exe [...] .NET Version: v4.0.30319
Note that the […] replaces one or multiple omitted lines for the sake of readability.
Knowing that the binary is written in C# or Visual Basic .NET, brings the aforementioned dnSpy forward as a possible analysis tool.
Context
Executing the binary within a Windows 10 (x86_64) virtual machine provides safety for the analyst and provides insight in what the binary does. The binary itself is a legitimate game, one that nearly everybody knows: minesweeper.
In case someone does not know the game, the rules are rather simple. One should open (click on) all squares under which there is no mine. The numbers in the opened squares provide information about the amount of mines surrounding that specific square. However, this edition of the game is a bit different. The playing field is 30 by 30 squares in size, totaling 900 squares. There are three squares under which no mine is buried, whilst the other 897 squares are armed. If the player opens a single square with a mine beneath it, the game is over. If the user suspects that a certain square is actually a mine, one can right-click on the square to flag it. This avoids accidental clicks on the square (as those are blocked) and gives a clear overview where the mines likely are.
Upon clicking on a mine, the player has to wait for a bit and then a new screen is shown. It displays Failure in red letters, after which the application closes. After opening all squares without stepping on a mine, the game is won.
Without winning the game, there is no way to know what the winners screen looks like. Or is there?
Summary
The next step in the reverse engineering process is to summarise all facts that have been obtained and write them down in an organised manner. The specifics of such a manner differs per person and is therefore irrelevant in this course. One can use the provided method in this course or use a completely different way of working.
Note that this step is repeated often during the reverse engineering process. When you find yourself stuck, take a step back and summarise all facts again. One can include assumptions as they allow less restricted thinking. Although, one should be careful not to think in circles and block other potentially correct thoughts because one assumption seems more plausible than another.
The summary, in this case, is brief, as summaries should be.
- The application is a .NET application, written in C# or Visual Basic .NET
- Minesweeper is won if all non-mined squares are opened
- Out of the 900 squares, only three are safe to open
With the help of this summary, one can think of the next steps to take.
- Use dnSpy to open the binary
- Assumption: winning the game provides the flag
- Find out what information is displayed in the winning screen
So far, the next three steps are based upon the information that has been presented to us. It would be logical to assume that the final goal would be to win the game, but this might not be the case. Immediately trying to win the game might result in a quick success from time to time, but it might also be a complete waste of time. With a structured approach, one does not waste time on random guesses. Structuring the approach with rational decisions and educated guesses minimises the amount of mistakes, and therefore wasted time.
The goal must not be to get the correct answer the fastest once, but to always get the correct answer the fastest. This is achieved by reducing the time spent on guesses and increasing the time spent on certainties (or educational guesses, if you are stuck). One can create certainties by creating an overview of the available information and by creating a plan based on this information. In the long run, this provides the fastest and most accurate results.
Investigation
To find out what the winning screen displays, one has to look no further than the list of classes in the minesweeper binary. One of the classes within the binary is named FailurePopup and another class is named SuccessPopup. Opening the SuccessPopup in dnSpy does not provide the GUI a user would see after winning the game, but it does provide an overview of all used components in the GUI. This components are initialised in a function with a similar name: InitializeComponent. The C# version of the code is given below.
private void InitializeComponent() { ComponentResourceManager componentResourceManager = new ComponentResourceManager(typeof(SuccessPopup)); this.pictureBox1 = new PictureBox(); this.textBox1 = new TextBox(); [...] this.pictureBox1.Image = (Image)componentResourceManager.GetObject("pictureBox1.Image"); this.pictureBox1.Location = new Point(1, 2); this.pictureBox1.Name = "pictureBox1"; this.pictureBox1.Size = new Size(456, 533); this.pictureBox1.TabIndex = 0; this.pictureBox1.TabStop = false; this.textBox1.Font = new Font("Courier New", 12f, FontStyle.Regular, GraphicsUnit.Point, 0); this.textBox1.Location = new Point(463, 300); this.textBox1.Name = "textBox1"; this.textBox1.ReadOnly = true; this.textBox1.Size = new Size(344, 30); this.textBox1.TabIndex = 1; [...] this.label1.Text = "Congratulations!"; [...] this.label2.Text = "You have won the"; [...] this.label3.Text = "Ultimate Minesweeper Championship"; [...] this.label4.Text = "and nobody cares."; [...] this.label5.Text = "Here is your prize:"; [...] this.label6.Text = "And remember kids, Winners don't do drugs"; [...] base.Controls.Add(this.label6); base.Controls.Add(this.label5); base.Controls.Add(this.label4); base.Controls.Add(this.label3); base.Controls.Add(this.label2); base.Controls.Add(this.label1); base.Controls.Add(this.textBox1); base.Controls.Add(this.pictureBox1); [...] this.Text = "Winner's Circle"; [...] }
Looking at the excerpts of the initialisation of the class, the displayed message can be observed. There does not seem to be a flag in this form, however. Looking at the used objects is a way to see where the missing information might be.
Firstly, a picturebox object is used. Within this object, an image is loaded and displayed to the viewer. The flag could very well be residing in an image, this is something to add to the to-do list.
Secondly, a textbox is used but no text seems to be set in the textbox. This means that the textbox is either left empty or that the text is set somewhere else. This is the second item on the to-do list as of now.
The flag in an image
The images reside within the Resources part of the binary. Looking into the embedded files, there is an image with the name pictureBox1.image within the SuccessPopup class. Upon opening this image within dnSpy, an image with a couple of balloons is shown. This image is used as a celebration for those who manage to beat the game. The flag could be hidden inside an image using steganography, but given the nature of this challenge, this is a highly unlikely scenario. Moving on to the next step is the most efficient decision to make.
The textbox text
The empty textbox was the second possible lead. Within the SucessPopup class, the textbox1.Text field is set in the constructor, as can be seen in the C# code below.
public SuccessPopup(string key) { this.InitializeComponent(); this.textBox1.Text = key; }
The key, or flag if you will, is not generated in the SuccessPopup class, but only passed to it, in order to display it in the success screen. The next step is to find out where the key is generated.
In the MainForm, one can find a function named GetKey, which is given below.
private string GetKey(List<uint> revealedCells) { revealedCells.Sort(); Random random = new Random(Convert.ToInt32(revealedCells[0] << 20 | revealedCells[1] << 10 | revealedCells[2])); byte[] array = new byte[32]; byte[] array2 = new byte[] { 245, 75, 65, 142, 68, 71, 100, 185, 74, 127, 62, 130, 231, 129, 254, 243, 28, 58, 103, 179, 60, 91, 195, 215, 102, 145, 154, 27, 57, 231, 241, 86 }; random.NextBytes(array); uint num = 0u; while ((ulong)num < (ulong)((long)array2.Length)) { byte[] array3 = array2; uint num2 = num; array3[(int)num2] = (array3[(int)num2] ^ array[(int)num]); num += 1u; } return Encoding.ASCII.GetString(array2);
This function uses an array together with a seeded random and an xor-sequence to generate an ASCII encoded string. This looks like the generation of the flag, when provided with the correct values.
As a double check, the entry point of the minesweeper binary is, indeed, the Program, which then starts the MainForm, as can be seen below.
using System; using System.Windows.Forms; namespace UltimateMinesweeper { // Token: 0x02000006 RID: 6 internal static class Program { // Token: 0x06000034 RID: 52 RVA: 0x00003219 File Offset: 0x00001419 [STAThread] private static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } } }
Searching for the key
The next question that needs an answer, goes back to the GetKey function. The revealedCells argument that is passed on is generated somewhere. Looking in the fields of the MainForm class, one finds the RevealedCells field. Note the capital letter at the start, indicating that it is a different variable.
A quick search within the MainForm‘s code, reveals that a value is added to the RevealedCells field in the SquareRevealedCallback function, as can be seen below. Additionally, the SuccessPopup‘s key is calculated with the GetKey function, whose sole argument is the list of revealed cells.
private void SquareRevealedCallback(uint column, uint row) { if (this.MineField.BombRevealed) { this.stopwatch.Stop(); Application.DoEvents(); Thread.Sleep(1000); new FailurePopup().ShowDialog(); Application.Exit(); } this.RevealedCells.Add(row * MainForm.VALLOC_NODE_LIMIT + column); if (this.MineField.TotalUnrevealedEmptySquares == 0) { this.stopwatch.Stop(); Application.DoEvents(); Thread.Sleep(1000); new SuccessPopup(this.GetKey(this.RevealedCells)).ShowDialog(); Application.Exit(); } }
A further search, reveals that the RevealedCells field is instantiated in the constructor of the MainForm, as can be seen in the code below.
public MainForm() { this.InitializeComponent(); this.MineField = new MineField(MainForm.VALLOC_NODE_LIMIT); this.AllocateMemory(this.MineField); this.mineFieldControl.DataSource = this.MineField; this.mineFieldControl.SquareRevealed += this.SquareRevealedCallback; this.mineFieldControl.FirstClick += this.FirstClickCallback; this.stopwatch = new Stopwatch(); this.FlagsRemaining = this.MineField.TotalMines; this.mineFieldControl.MineFlagged += this.MineFlaggedCallback; this.RevealedCells = new List<uint>(); }
The third line of code draws attention in this function. The name AllocateMemory is odd, considering that the memory allocation is nearly always handled by the CLR. A closer look at the AllocateMemory provides more insight into its purpose.
private void AllocateMemory(MineField mf) { for (uint num = 0u; num < MainForm.VALLOC_NODE_LIMIT; num += 1u) { for (uint num2 = 0u; num2 < MainForm.VALLOC_NODE_LIMIT; num2 += 1u) { bool flag = true; uint r = num + 1u; uint c = num2 + 1u; if (this.VALLOC_TYPES.Contains(this.DeriveVallocType(r, c))) { flag = false; } mf.GarbageCollect[(int)num2, (int)num] = flag; } } }
There is no memory allocation to be found in this function, unlike the name suggests. There are, however, more clues within this function. The variable names r and c seem to refer to row and column respectively. Additionally, the constant variable VALLOC_NODE_LIMIT is used in both for-loops. The value of this constant is 30, which is both the height and the width of the playing field, when counted in squares. Lastly, there is a boolean named flag which is only set if a specific condition is met. This flag provides information if the given field is an armed square or an empty square. If the flag is set to false, the square is an empty square. In short, this function generates the minefield.
The final step
There are multiple ways to solve this challenge, and two of the possible solutions will be given in this practical case.
Summary
With all the gathered information, it is time to make a new summary.
- The application is a .NET application, written in C# or Visual Basic .NET
- Minesweeper is won if all non-mined squares are opened
- Out of the 900 squares, only three are safe to open
- The flag is generated based on the constant value of the unarmed squares
- The flag is shown on the screen when the game is won
The manual solution
By putting a breakpoint on the line where the flag variable is set to false (line 74 in the MainForm), one can observe the values of the variables r and c. These values correspond with the location on the field. Below, the coordinates are given in the format n(r,c).
- A(0x08, 0x1d)
- B(0x15, 0x08)
- C(0x1d, 0x19)
Converting these hexadecimal values to decimal values, one can manually open the squares at the provided coordinates. Again, the coordinates are provided in the format n(r,c).
- A(8d, 29d)
- B(21d, 8d)
- C(29d, 25d)
After opening the correct squares, the winning screen is shown. On this screen, the flag is also provided:
Ch3aters_Alw4ys_W1n@flare-on.com
The automated solution
As an alternate solution, one can automate the flag generation, based upon the AllocateMemory function, the minefield constants and the GetKey function. In the provided solution below, some parts of the copied code have been altered for the sole reason to avoid errors. Reconstructed decompiled code is, as a rule of thumb, not directly compilable.
The values of r and c in the callback method are both in a hexadecimal format, as one can see when adding a breakpoint within the callback method. The values or r and c during the generation of the playing field are given in a decimal value. Therefore, the values need to be converted. One way to convert a decimal integer to a hexadecimal integer in C# is to use the Parse function, as can be seen below. The code I modified, was originally posted by Gaven Miller on StackOverflow.
private static uint ConvertDecToHex(uint toConvert) { string hexValue = toConvert.ToString("X"); return uint.Parse(hexValue, System.Globalization.NumberStyles.HexNumber); }
The AllocateMemory function has also been altered. Firstly, the return type is now a List of unsigned integers instead of void. Instead of setting the flag variable to false, the constant value of the revealed cell is calculated and added in the list which is later returned. The code can be found below.
private static List<uint> AllocateMemory() { List<uint> revealedCells = new List<uint>(); for (uint num = 0u; num < VALLOC_NODE_LIMIT; num += 1u) { for (uint num2 = 0u; num2 < VALLOC_NODE_LIMIT; num2 += 1u) { uint r = num + 1u; uint c = num2 + 1u; if (VALLOC_TYPES.Contains(DeriveVallocType(r, c))) { r = ConvertDecToHex(r - 1); c = ConvertDecToHex(c - 1); uint revealedCellConstant = r * VALLOC_NODE_LIMIT + c; revealedCells.Add(revealedCellConstant); } } } return revealedCells; }
The reason why both r and c are reduced by 1 is for the sole reason that arrays (and lists) start at the index of zero, whilst humans start to count at one. This can also be verified by putting a breakpoint in the callback function. The value of the index is equal to the value of the row (or column) minus the base difference, which equals 1.
The DeriveVallocType function returns the inverse value of r * VALLOC_NODE_LIMIT +c, as can be seen in the source code below.
private static uint DeriveVallocType(uint r, uint c) { return ~(r * VALLOC_NODE_LIMIT + c); }
With the modified return type of the AllocateMemory function, it can directly be passed on to the GetKey function. This function returns the flag in the form of a string, which can be printed in the console.
The complete source code for the automatic flag generation program can be found below. The added notes are only added for the sake of completion, since all the required information has already been discussed above.
using System; using System.Collections.Generic; using System.Text; namespace FlareOn2018Challenge2Solver { class Program { //Field size (since it is a square, it is 30 by 30) private static uint VALLOC_NODE_LIMIT = 30u; //Open square location 1 private static uint VALLOC_TYPE_HEADER_PAGE = 4294966400u; //Open square location 2 private static uint VALLOC_TYPE_HEADER_POOL = 4294966657u; //Open square location 3 private static uint VALLOC_TYPE_HEADER_RESERVED = 4294967026u; //List with all open square locations private static List<uint> VALLOC_TYPES = new List<uint>() { VALLOC_TYPE_HEADER_PAGE, VALLOC_TYPE_HEADER_POOL, VALLOC_TYPE_HEADER_RESERVED }; /// <summary> /// Code originally taken from Gaven Miller (https://stackoverflow.com/questions/1139957/c-sharp-convert-integer-to-hex-and-back-again), slightly modified by myself /// </summary> /// <param name="toConvert">The value to be converted to a hexadecimal representation of the same value</param> /// <returns></returns> private static uint ConvertDecToHex(uint toConvert) { string hexValue = toConvert.ToString("X"); return uint.Parse(hexValue, System.Globalization.NumberStyles.HexNumber); } /// <summary> /// Calculates the constant values for the cells /// </summary> /// <returns>A list with the constant values of the open squares</returns> private static List<uint> AllocateMemory() { List<uint> revealedCells = new List<uint>(); for (uint num = 0u; num < VALLOC_NODE_LIMIT; num += 1u) { for (uint num2 = 0u; num2 < VALLOC_NODE_LIMIT; num2 += 1u) { //The 'r' stands for 'row' uint r = num + 1u; //The 'c' stands for 'column' uint c = num2 + 1u; if (VALLOC_TYPES.Contains(DeriveVallocType(r, c))) { //The row value is interpreted as a hexadecimal value, hence the need to convert the value //The '-1' is required because the top left square is on the location of (0,0), whereas most humans will see this as (1,1) r = ConvertDecToHex(r - 1); //The column value is, similar to the row value, a hexadecimal representation of a array which starts at 0 c = ConvertDecToHex(c - 1); uint revealedCellConstant = r * VALLOC_NODE_LIMIT + c; revealedCells.Add(revealedCellConstant); } } } return revealedCells; } private static uint DeriveVallocType(uint r, uint c) { return ~(r * VALLOC_NODE_LIMIT + c); } private static string GetKey(List<uint> revealedCells) { revealedCells.Sort(); Random random = new Random(Convert.ToInt32(revealedCells[0] << 20 | revealedCells[1] << 10 | revealedCells[2])); byte[] array = new byte[32]; byte[] array2 = new byte[] { 245, 75, 65, 142, 68, 71, 100, 185, 74, 127, 62, 130, 231, 129, 254, 243, 28, 58, 103, 179, 60, 91, 195, 215, 102, 145, 154, 27, 57, 231, 241, 86 }; random.NextBytes(array); uint num = 0u; while (num < (ulong)(array2.Length)) { byte[] array3 = array2; uint num2 = num; array3[(int)num2] = (byte)(array3[(int)num2] ^ array[(int)num]); num += 1u; } return Encoding.ASCII.GetString(array2); } /// <summary> /// The GetKey function returns the flag, but only when the argument that is provided, is the correct array of 'revealed cells'. This array contains the fixed value of a row and column, represented as a single number. These values are calculated when a square is revealed, in a hexadecimal value. Since arrays start at 0 and (most) humans start to count at 1, the row and column numbers need to be reduced by 1, otherwise the output is incorrect. /// </summary> /// <param name="args"></param> static void Main(string[] args) { //Print the flag Console.WriteLine("The flag is: " + GetKey(AllocateMemory())); //Keep the console screen open Console.ReadKey(); } } }
The output of the program can be found below.
The flag is: Ch3aters_Alw4ys_W1n@flare-on.com
To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], send me a PM on Reddit, or DM me on Twitter @Libranalysis.