Fundamentals of .NET Decompilation With dnSpy

3 months ago 60
BOOK THIS SPACE FOR AD
ARTICLE AD

Joward

Recently, I’ve picked up an interest in testing compiled applications. Historically, I’ve had the most experience performing network pentesting and web application assessments/bug bounty, so I was interested in finding an approachable way to get acclimated to the concept. After doing some digging and testing, I found testing .NET applications to be exactly what I was looking for. There are tons of .NET applications to test that can be quickly decompiled to their near original form, making it an easier introduction to static and dynamic code analysis. Below, I’ve detailed some of the fundamentals I’ve learned while using dnSpy to test .NET apps.

Interested in discussing .NET app hacking? Find me on Twitter @JowardBince.

The fundamental components of the .NET ecosystem

A full explanation of .NET is beyond the scope of this article, but we’ll talk about it briefly for the uninitiated. As the .NET ecosystem is so large, discussing all of it’s components would be an article of it’s own. For those interested, I’d recommend Microsoft’s official overview here.

In short, the .NET ecosystem provides tools and packages for fast and efficient software development on numerous platforms in multiple languages. It provides compilers and runtime components to develop in C#, F#, and Visual Basic with multiple other languages providing their own support. It’s possible to write a piece of software in any of these languages and immediately compile it for use on desktop, mobile, web, and more.

Even better, it provides a ton of out of the box packages and templates for quick project setup. You can create a new desktop GUI app, an ASP.NET web app, or a mobile app straight from a template. The packages include Windows functionality and other common functions. What’s more, you can easily import other packages for use in your project. These features make .NET an extremely popular choice for all types of development and companies and thus a very rich target for pentesters and bug bounty hunters.

One of the things that make .NET interesting to work with and hack on is the ability to decompile to source. Decompilation is the opposite of compilation, where we take a compiled binary file and get source code from it.

When decompiling other compiled languages such as C, the source code is inferred from the assembly code present in the binary. Therefore, it requires a lot more leg work to get it back to an easily readable, near source format. However, because of the nature of the .NET framework, we can decompile a .NET DLL or executable back to it’s original source code; complete with package imports and everything. This makes it a great starting point for those interested in reverse engineering and binary exploitation.

Since my focus is offensive security research, we’ll use a vulnerable application for our tests and examples. We’ll be using the Damn Vulnerable C Sharp API. This has a number of different common .NET vulnerabilities that are great to poke around with.

For the actual task of decompilation, I prefer dnSpy. It’s feature rich, has an attractive UI (dark mode by default), and it’s totally open source. For those getting more advanced, JetBrains dotPeek provides a number of advanced features.

With dnSpy, decompilation is straight forward: open dnSpy, drag and drop your executable, and you’re good to go.

Decompiled source code for DVCSharp API

Most worthwhile decompilers will include a debugger. With this, we can run the binary we’re working with and review the code as we test features. dnSpy let’s you execute a loaded DLL through the .NET framework under Debug -> Start Debugging. You’d select your debug engine, the executable, the working directory, and any run time arguments.

Starting the dnSpy debugger

If it’s a web app, by default it will run on localhost:5000.

One of the most important features for testing .NET binaries is the ability to set tracepoints and breakpoints. Tracepoints log actions as they occur, and breakpointst pause the execution of the program when a certain line of code is reached.

With dnSpy, you can set tracepoints and breakpoints on the functionality of entire classes. dnSpy offers a rich logging suite that let’s us control exactly what’s being logged, including the name of a function or the values of local variables.

dnSpy class menu

By right clicking a class and selecting Add Class Tracepoint, we can define an entire suite of tracepoints and exactly what’s being logged.

It also provides all of the values to log different variables like the function name, memory addresses, process name, etc.

Logging variables and options

In DVCSharp API, we’ll set a class tracepoint on the ProductsController class and log the function name.

Setting a tracepoint

Now anytime we send a request to the API that calls a function in the ProductsController, we’ll see the function name in the log.

Requesting Products
dnSpy log displaying the function called

Breakpoints are very similar. The key difference is that, instead of just logging the function executed, it will actually pause the program execution as it hits a specific line of code. Breakpoints can be created through the same contextual menu on a class or by clicking in the left hand margin of the decompiled code.

A breakpoint set on a specific line of code
Hitting the breakpoint

When using a breakpoint to debug an application, we may want to inspect each instruction as it’s executed. We can step into or step over functions. Stepping into F11, will move on to the next sequential instruction in the function call. Stepping over F12 will execute the entire function and pause at the next function call. This lets you get extremely granular in your inspection of function execution.

Of course, using breakpoints to walk through code execution is great. However, you’ll usually want to see how variables are being manipulated at each step of execution. dnSpy allows us to easily view the values of local variables and objects at each step of execution. As an example, we’ll send a request to DVCSharp API to create a product, but create a breakpoint at the start of the Post() function.

Adding a product to hit a breakpoint

After sending the API request, the program execution pauses and we can inspect the product object in the Locals tab.

Inspecting local variable values

Better yet, we can even include these variables and their values in our logging messages. If we go back to modify our tracepoint or breakpoint and, for example, wanted to include the product name in the log message, we could do the following. This works exactly the same way for both tracepoints and breakpoints.

Modifying tracepoint logging

When inspecting the logs, we’d see the following.

Viewing custom tracepoint logging

Decompiling and testing .NET applications can be a a great introduction to testing and reverse engineering compiled applications. I personally have very little experience with either, and I’ve found this to be an approachable and digestible way to get acclimated with many of the core concepts of decompliation and debugging. dnSpy has a load of features such as modifying the source code, recompiling, and much more that are worth exploring. Hopefully, the above intro should be sufficient as an intro to the topic.

Read Entire Article