Reverse Engineering - Part 1

INTRODUCTION

At Improsec we have a desire to share our knowledge with the outside world in an attempt to improve worldwide security. In that regard, we have decided to create an introductory mini-series on Reverse Engineering of various types of software. Through this effort, we hope to motivate aspiring security specialists or guide people who wish to have a look into the world of reverse engineering.

In this specific section, namely ‘part 1’, we will be touching upon the topic of reverse engineering .NET applications written in C#. Everything introduced in this part is, by all means, perceived as introductory level, so we hope that most of you are able to understand what is going on - if not, we are probably doing a poor job at explaining it properly :)

TOOLKIT

Before we get into what tools you need to get started, it might be wise to explain why. Contrary to low-level coding languages such as C, C++, Rust, Delphi, etc., C# .NET does not compile to native machine code. Instead, it compiles to what is known as CIL (Common Intermediate Language), previously known as MSIL (Microsoft Intermediate Language). For a high-level explanation, this means that the code is simply parsed into structures that explain what the code is supposed to do, and this code is then read by an interpreter (somewhat similar to Java).

Since the code is stored more or less as-is, we are able to restore an almost 1:1 reflection of how the code looked before compilation, simply by parsing the CIL code. There are many tools that can do this for us, but we recommend using dnSpy - this tool is the be-all and end-all tool for C# .NET reverse engineering.

Challenges

We have collected 4 distinct CTF binaries written in C# .NET from around the internet, and present them here in ascending order based on which challenges we found to be most “difficult”. Bear in mind that even so, they are all considered introductory level, and are therefore fairly trivial to solve.

In CTF binaries you are supposed to find a flag that is hidden somewhere within the binaries. Flags presented in CTF contests are usually of the format:

  • ctf_name{flag_goes_here}.

Throughout this section, we will be walking through each of the 4 binaries, showing how to dissect the binaries, how to navigate their code, and eventually how to find the flag(s) hidden within them.

Without further ado, let us get knee-deep in some C# .NET reverse engineering!

Challenge #1 - cracknet

Our first CTF binary is the cracknet challenge collected from here.

As with any C# .NET challenge, we start off by loading it into dnSpy for further inspection.

Reverse engineering .NET applications written in C# by Improsec

Once we have loaded the executable into dnSpy, we notice a class called Crypto and another called Program inside the ctf.sectalks_bne.crackme namespace. As per convention in C# .NET console applications, the Main-function of the program is usually found inside the Program-class, so let us navigate there.

Reverse engineering .NET applications written in C# by Improsec

As expected, the Main-function was to be found inside the Program-class. If we examine the function, we see that the program outputs “Enter password: “ to the console and then reads input from the console into the string variable named input. This input is then compared to an AES-decrypted version of a Base64-encoded string. If the two are equal, it outputs an AES-decrypted version of another Base64-encoded string, which we assume is the flag that we are after.

There is now a multitude of ways to retrieve the password, but the easiest way is to simply run the program inside the dnSpy debugger and read the password from memory.

Reverse engineering .NET applications written in C# by Improsec

We start by toggling a breakpoint at the desired access point. In our case, we want the program to break (effectively pause) at the if-statement after the password has been decrypted so that we can read the decrypted (plaintext) password from memory. Once we have toggled the breakpoint, we can go ahead and start the application by clicking the ‘Start’ button at the top menu in the dnSpy application.

Reverse engineering .NET applications written in C# by Improsec

Once our program runs, it will expect an input. However, since we are interested in the runtime decrypted password, and not in the password we enter into the application, we can feed it any arbitrary string we want - in this case, we fed it “test”. Once the input has been received by the application, we should notice the if-statement being highlighted inside dnSpy - this is because our program execution has reached our breakpoint at that point in the binary, and is now in a paused state from which we can read its memory, local variables, in-memory modules and much more.

Reverse engineering .NET applications written in C# by Improsec

By navigating to the Locals window we can see an overview of locally defined variables. If you are unable to locate the Locals window, it can be accessed from the top menu: Debug -> Windows -> Locals. Amongst the local variables seen in the Locals window is our input variable containing the string “test” as well as the AES-decrypted password variable containing the string “the high ground”.

Reverse engineering .NET applications written in C# by Improsec

Now we simply press the ‘Continue’ button at the top menu in the dnSpy application, so that the binary continues execution, and we can then enter the correct password into the password prompt. This should pass the if-statement and reach the code branch that outputs what we assume is the flag. As can be seen in the illustration above, this assumption was correct, and we have now solved the challenge and retrieved the hidden flag.

Challenge #2 - Memecat Battlestation

Our second CTF binary is the FlareOn-2019 MemeCat Battlestation challenge collected from here.

As with any C# .NET challenge, we start off by loading it into dnSpy for further inspection.

Reverse engineering .NET applications written in C# by Improsec

In the overview we notice two distinct classes of interest, namely Stage1Form and Stage2Form. However, we do not know what we are after yet, so let us open the program to get a general idea of the code flow.

Reverse engineering .NET applications written in C# by Improsec

As seen in the above illustration, we are in search of a valid “Weapon Arming Code”, that can be used to eliminate the marauding tabby frigate. Let us take a look at the Stage1Form class.

Reverse engineering .NET applications written in C# by Improsec

Inside the class, we find the FireButton_Click event handler as shown in the illustration above. It validates the weapon arming code by comparing it to “RAINBOW”.

Reverse engineering .NET applications written in C# by Improsec

We enter the weapon arming code for the stage and press “Fire!”.

Reverse engineering .NET applications written in C# by Improsec

We have now advanced to stage 2, and will, therefore, have to look into the Stage2Form class, rather than the previously analyzed Stage1Form class. Inside the Stage2Form class, we find a function as detailed in the illustration below:

Reverse engineering .NET applications written in C# by Improsec

This function is very similar to the same function from the previous stage, but instead of comparing it to a string literal, it validates the weapon arming code by invoking the isValidWeaponCode function. We can now analyze that function to devise how to construct a valid weapon arming code.

Reverse engineering .NET applications written in C# by Improsec

As we can see in the illustration above, the isValidWeaponCode function uses the xor-operator to encode every character in our string, by xor’ing it with ‘A’ (the value 65). The function then compares the encoded string with the array shown above, consisting of seemingly random characters.

Luckily for us, the bit-wise xor operation is a self-reversible arithmetic operation, just like bit-wise negation and bit-wise not. In other words, any value xor’ed with a key, can be transformed back into its original value by simply applying another xor with the key. For example:

  • Encoding: ‘A’ xor ‘B’ = 65 xor 66 = 3

  • Decoding: 3 xor ‘B’ = 3 xor 66 = 65 = ‘A’

With that knowledge in mind, we can construct a simple python script to decode the expected string back into its original form, as shown in the illustration below:

Reverse engineering .NET applications written in C# by Improsec

In this script, we use the ord-definition to convert the ASCII literals to their decimal numerals (e.g. ‘A’ = 65), so that we can perform arithmetic operations (such as xor) on the characters of the array. We then transform it back into a character using the chr-definition.

Reverse engineering .NET applications written in C# by Improsec

When running the python script, we receive the output as shown in the illustration above. This is the weapon arming code. We can now enter it into stage 2 form of the application.

Reverse engineering .NET applications written in C# by Improsec

We enter the weapon arming code for the stage and press “Fire!”.

Reverse engineering .NET applications written in C# by Improsec

As can be seen in the illustration above, the weapon arming code working, and we have now reached the victory form, solved the challenge and retrieved the flag (in this case, of an atypical email-like form).

Challenge #3 - hideinpLainsight

Our third CTF binary is a challenge collected from a BSidesTLV event.

As with any C# .NET challenge, we start off by loading it into dnSpy for further inspection.

Reverse engineering .NET applications written in C# by Improsec

The binary consists of a single class, Sanchez, so there is only one place to go for further analysis.

Reverse engineering .NET applications written in C# by Improsec

Inside the Sanchez class we find the Main-function, which seems to present some anti-debugging tricks at the very beginning. The image has been cropped, as showing the lengthy il array is of no importance.

Reverse engineering .NET applications written in C# by Improsec

Later in the Main-function, we find the code as shown in the above illustration. What this code does, is basically construct a dynamically defined .NET module (i.e. a .NET assembly) called DoofusRick, and assigning it a class called J19Zeta7. Inside this class, a method called gimmedeflag is defined, whose method body is constructed from the byte-code presented in the il array seen at the start of the Main-function.

Reverse engineering .NET applications written in C# by Improsec

Since we’re using dnSpy, which should not trigger the Debugger.IsAttached condition (dnSpy uses a native debugger rather than a managed debugger), we can ignore that part. However, the next condition, which requires a random number to be subject to certain criteria might pose a problem for us. As we can see in the above illustration, when evaluating this condition, it resolves to true, effectively returning from the function prematurely.

In order to combat this, we desire to remove this condition or somehow circumvent it.

Reverse engineering .NET applications written in C# by Improsec

However, there’s a catch. If we look at the code in the above illustration, we can see that the current executable queries the byte-code (Intermediate Language) of the Main-function itself, which is later passed to the dynamically defined gimmedeflag function. We, therefore, cannot make too large structural changes to the Main-function, or we risk messing up the data necessary to decode the flag.

Reverse engineering .NET applications written in C# by Improsec

We can modify a function via. dnSpy, using the “Edit Method (C#)…” entry from the right-click context menu. This allows us to edit the function via. the presented code.

Reverse engineering .NET applications written in C# by Improsec

Since we are interested in passing the criteria while making as few changes to the underlying IL as possible, we decided to go for a very minor adjustment - changing the less than to an equal to. If we always pass the condition when it is less than, then we should never pass the condition when it is equal to.

Reverse engineering .NET applications written in C# by Improsec

We can now go ahead and save our changes to the module so that they will be effective when we debug the application further.

Reverse engineering .NET applications written in C# by Improsec

After having saved our changes, we can attempt to debug the application again, and will notice that the if-statement is now being skipped, as the criteria resolves to false. We can now attempt to step into the later flow of the Main-function.

Reverse engineering .NET applications written in C# by Improsec

In the above illustration, we can see that the program allocates a local object of the dynamically defined type, and invokes the first method inside of it. This effectively means that a dynamically defined type from the dynamically defined DoofusRick module will be invoked.

Reverse engineering .NET applications written in C# by Improsec

We want to be able to analyze the method, without simply invoking the function directly. To this end, we step into the CreateType method and step until the end of the function. When we reach the end of the CreateType method, the type should have been created, and the CitadelOfRicks assembly should exist in the current process.

Reverse engineering .NET applications written in C# by Improsec

We can check this, by navigating to the Modules view to see if we can find the module in question.

Reverse engineering .NET applications written in C# by Improsec

As we can see, the CitadelOfRicks assembly is present in the current process, as a result of having generated a type from the dynamically defined module.

Reverse engineering .NET applications written in C# by Improsec

We can right-click the module and click “Go To Module”, to open the module in dnSpy for further inspection.

Reverse engineering .NET applications written in C# by Improsec

As we can see, the module looks as expected. There’s a DoofusRick module with a J19Zeta7 class. Let us take a further look at the class.

Reverse engineering .NET applications written in C# by Improsec

Here we see the gimmedeflag method, which was constructed from the il array in the Main-function of the executable. This looks like a simple XOR-encryption, and we expect the function to decode the flag for us.

Reverse engineering .NET applications written in C# by Improsec

By setting a breakpoint after the for-loop, right at the if-statement, we can break the program after the flag has been decoded. Since the conditions of the if-statement does not resolve to true, the flag array is never returned from the function, but generally we do not really care, as we can simply read it from memory.

Reverse engineering .NET applications written in C# by Improsec

We can do this by navigating to the Locals window, in order to take a look at the A_0 local variable, which should contain the decoded flag.

Reverse engineering .NET applications written in C# by Improsec

The A_0 variable is defined as a byte array, so we cannot read it as a string directly, but if we look at the bytes, it should be fairly obvious that these are ASCII characters, as they all fall in the range [0x20; 0x7F].

Reverse engineering .NET applications written in C# by Improsec

We can instead right-click on the variable and click Show in Memory Window, in order to view the memory directly. The memory viewer is accompanied by a string-view, that allows us to read the string representation of raw bytes in memory.

Reverse engineering .NET applications written in C# by Improsec

As can be seen in the illustration above, the bytes encoded in the variable directly translates to the flag that we were looking for, and we have now solved the challenge.

Challenge #4 - maldropper

Our fourth CTF binary is an easyctf challenge collected from here.

As with any C# .NET challenge, we start off by loading it into dnSpy for further inspection.

Reverse engineering .NET applications written in C# by Improsec

As can be seen in the illustration above, the binary consists of a single class, Program, so there is only one place to go for further analysis.

Reverse engineering .NET applications written in C# by Improsec

As we can see, the Main-function does something quite malicious. The function reads the bytes from its own executable, i.e. maldrop.exe, and splits the array into multiple parts using the string “[SPLITERATOR]" as a delimiter. The array is split into 3 parts.

The program then dynamically loads part 2 (array index 1), and invokes the entry-point using part 3 as a parameter for the entry-point function. This is very similar to how many real malware unpacks embedded binaries and reflectively loads them into memory.

Reverse engineering .NET applications written in C# by Improsec

By setting a breakpoint at the Assembly.Load line, we can effectively step into the Load function, similar to how we stepped into the CreateType-function in the previous challenge.

Reverse engineering .NET applications written in C# by Improsec

Inside this function, we wish to step to the very end, so that we have the program paused after the binary has been loaded. However, the last line here is the line that actually loads the file, so instead we “step over” this line.

Reverse engineering .NET applications written in C# by Improsec

We should now return to the Main-function, after the Load-function has been called, but before the Invoke-function has been called.

Reverse engineering .NET applications written in C# by Improsec

We can now go to the Modules view and notice that a new assembly named payload has appeared. Like in the previous challenge, we right-click the module and click “Go To Module”.

Reverse engineering .NET applications written in C# by Improsec

Inside the payload binary, we see also just one single class, Program, so there is only one place to go for further analysis.

Reverse engineering .NET applications written in C# by Improsec

As we can see in the illustration above, the Main-function in the payload binary consists of yet another stage. This time, the binary uses gzip to decompress the first argument passed to the function, which as we saw in the Main-function of the primary executable, is part 3 of the binary array.

The contents of the decompressed gzip buffer are then once again loaded reflectively.

Reverse engineering .NET applications written in C# by Improsec

Once again, we will step into- and out of the Assembly.Load-function, in order to load the next stage into the memory space of the current process.

Reverse engineering .NET applications written in C# by Improsec

In the modules view, we now see a third assembly named flagbuilder. Once again, let us go analyze the assembly (Right-click => Go To Module).

Reverse engineering .NET applications written in C# by Improsec

Once again there is only a single class, Program, in the binary. Let us take a further look at the class.

Reverse engineering .NET applications written in C# by Improsec

This seems to be the final stage of the application. In this Main-function, the Random-class is instantiated with a constant seed, effectively forcing the pseudo-random number generator to generate the same “random” numbers every time. Six of these “random” numbers are then added together to form the contents of the easyctf{…} flag.

Reverse engineering .NET applications written in C# by Improsec

We can simply breakpoint at the end of the function when the text variable containing the final flag has been constructed.

Reverse engineering .NET applications written in C# by Improsec

Finally, we can read the flag string out of the text variable using the Locals view, and we have now solved the challenge.

CONCLUSION

Thus concludes part 1 of our reverse engineering mini-series.

We hope these small challenge samples and walkthroughs inspired you to get your hands dirty with some reverse engineering of any type. Stay tuned for further updates to the reverse engineering mini-series!