# DotNetMe - VolgaCTF CTF 2020 (Reverse, 250p, 34 solved)
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/task.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/task.png)
## Introduction
DotNetMe is obviously a Windows reverse task.
Two binaries are provided:
- A PE32+ executable file (console)- Another PE32+ DLL
Both binary are .NET binaries are expected and are heavily obfuscated.
## Technical details
At first, we could see that both of the binaries are obfuscated and hard to read:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/obfuscated.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/obfuscated.png)
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/obfuscated2.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/obfuscated2.png)
In fact, the symbol names are displayed in some kind of Unicode and all the logic is behind a "state machine". Each code scope is executed depending on the value of a global 32-bit integer.
The first thing I did to avoid burning my eyes was to run **de4dot** ([https://github.com/0xd4d/de4dot](https://github.com/0xd4d/de4dot)) against the two binary. This tool is helping us have simple method names like method_1, method_2, etc.
So we just drag and drop the two binaries on the `de4dot.exe` executable file and our two binaries get cleaned:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/de4dot.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/de4dot.png)
Now, let's import these into dnSpy ([https://github.com/0xd4d/dnSpy](https://github.com/0xd4d/dnSpy) **OpenSource .NET debugger and assembly editor**):
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/cleaned_with_de4dot.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/cleaned_with_de4dot.png)
dnSpy is a cool tool that allows us to put breakpoint and see scoped local variables. Unfortunately, in this case, for some reason (Antidebugger tricks I guess), we could not see any locals ("Internal debugger error"):
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/debugger_internal_error.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/debugger_internal_error.png)
You can see line 28 that the program is displaying some text on the screen. The function returning the string to display is highly obfuscated and might be hard to reverse. Here, the text returned is displayed to the screen, so we just need to step over to see which string is returned.
The Main() method of the first binary is just looping infinitely and wait for a "flag" to be sent:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/cli.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/cli.png)
We can see that the interesting method called from main is located in the provided DLL:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/check_flag.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/check_flag.png)
Since we can't debug anything here, I decided to rewrite the code into **Visual Studio** so that we can debug the whole binary without any issue.
This way, a lot of variables and functions could be renamed and while debugging, all the scope variables can be read:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/vs.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/vs.png)
I went further and saw that the key has to respect a specific format (num variable represent the state of the function - Branching condition):
> Six '-' separated blocks
```csharpnum = (((GClass1.StringSplit(user_input, new char[]{'-'}).Length == 6) ? 683502988U : 1473095498U) ^ num2 * 1113113859U);```
> Each block has to contain 4 characters
```csharpnum = (GClass1.StringLength(input_parts[num3]) == 4) ? 161800768U : 267634710U;```
> The whole flag has to be 29 characters long
```csharpnum = (((GClass1.StringLength(user_input) != 29) ? 650567720U : 925404683U) ^ num2 * 2388823709U);```
> Finally, all of these characters have to be between 0x20 and 0x7f
```csharpcase 9U:{	num = ((c < '\u007f') ? 3945937441U : 3956362675U) ^ num2 * 3337894887U;	continue;}case 10U:{	num = (((c < ' ') ? 2328726507U : 3166744977U) ^ num2 * 414064944U);	continue;}```
From these conditions we can build a simple flag:
```abcd-efgh-ijkl-mnop-qrst-uvwx```
Another function is checking for "integrity". In fact, this function that we called `computeHash` will hash all the characters and then check for the returned value to be equal to the second parameter:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/computeHash.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/computeHash.png)
We can build a simple python function from this C# code:
```pythondef computeHash(flag):    i = 0    ret = ord(flag[0])    while i < len(flag) - 1:        ret = ord(flag[i + 1]) ^ ret        i += 1    return ret
# computeHash('abcd-efgh-ijkl-mnop-qrst-uvwx')```
In this case, the hash is computed and then compared to 41:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/hash_check.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/hash_check.png)
To match the first condition and go further we can simply replace the last 'x' with a 'd'. The message "Invalid flag format" will disappear:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/no_invalid_format.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/no_invalid_format.png)
Let's go further, a last operation is performed on our user input with an unknown string. At the end of this function, a check is done on the flag (the same with computeHash), but this time, checked against '74' and with the third parameter to **false** (this boolean make the function check or not check the input format: 0x20 > c < 0x7f, etc.).
In this capture, we can see that the string used "key" is still unknown (because of the heavily obfuscated function returning string from int):
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/unknown_string.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/unknown_string.png)
Earlier, we saw that string was retrieved through a "heavily obfuscated function.” Only one string remained unknown to me until I asked **XeR** to look into it. In fact, since the .NET standard library does not contain any antidebugger tricks, we could see all locals normally when going into it:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/extracted_key.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/extracted_key.png)
So we replaced the key, we go further with our special RE skills and write a python code from this cool function:
```pythondef last_step(flag):    out = ""    for i, c in enumerate(flag):        num4 = ord("*") * ord(c)
        a = (num4 >> 5) + (num4 >> 4) & 127        b = num4 + ord(c) & 127        c = ord(flag[len(flag) - 1 - i])
        out += chr(a ^ b ^ b)    return out```
> Here is the complete python script I made for the challenge "emulation"
```python#!/usr/bin/env python# @SakiiR
import sys
KEY = "3cD1Z84acsdf1caEBbfgMeAF0bObA"SUM1 = 41SUM2 = 74
def computeHash(flag):    i = 0    ret = ord(flag[0])    while i < len(flag) - 1:        ret = ord(flag[i + 1]) ^ ret        i += 1    return ret
def checkFormat(flag, hsh):    if len(flag) != 29:        print(f"Bad format: bad length {len(flag)}")        return False    if len(flag.split("-")) != 6:        print(f"Bad format: count split {len(flag.split('-'))}")        return False    for c in flag:        if ord(c) < 20 or ord(c) > 0x7F:            print(f"Bad format: Bad chr '{c}'")            return False    hsh_check = computeHash(flag) == hsh    if not hsh_check:        print(f"Bad format: Bad checksum '{computeHash(flag)}' != {hsh}")
    return hsh_check
def last_step(flag):    out = ""    for i, c in enumerate(flag):        num4 = ord("*") * ord(c)
        a = (num4 >> 5) + (num4 >> 4) & 127        b = num4 + ord(c) & 127        c = ord(flag[len(flag) - 1 - i])
        out += chr(a ^ b ^ b)    return out
def main(argv):    if len(argv) < 2:        print(f"USAGE: {argv[0]} FLAG")        return
    flag = argv[1]    if not checkFormat(flag, SUM1):        print(f"Bad Flag {flag}")        return False
    hsh = computeHash(last_step(flag))    if hsh != SUM2:        print(f"Bad Flag {flag}: Invalid hash '{hsh}' != {SUM2}")        return False
    print(f"Flag {flag} Ok !")
if __name__ == "__main__":    main(sys.argv)```
Note that later, there will be a check on the output of this function. The output's hash has to match the int "74". This is not very convenient since this is the second hash computed for the same output. We are going to need some z3 skills to solve these constraints.
Hopefully, **BitK** helped with that and provided us a cool script generating a valid key:
```pythonfrom z3 import *
solver = Solver()
secret = b"3cD1Z84acsdf1caEBbfgMeAF0bObA"key = [BitVec(f"input-{x}", 32) for x in range(29)]
def apply_format(arr):    for c in arr:        solver.add(c > 0x20)        solver.add(c < 0x7F)
    for x in range(4, 29, 5):        solver.add(arr[x] == ord("-"))
out = []nums = []for x in range(29):    num4 = 42 * key[x]    a = (LShR(num4, 6) + LShR(num4, 5)) & 127    b = (num4 + secret[x]) & 127    c = secret[28 - x]
    o = BitVec(f"out-{x}", 32)    solver.add(o == (a ^ b ^ c))    out.append(o)
def make_xor(arr):    out = arr[0]    for c in arr[1:]:        out = out ^ c    return out
solver.add(make_xor(key) == 41)solver.add(make_xor(out) == 74)apply_format(key)
if str(solver.check()) != "sat":    exit(":(")
model = solver.model()
print("".join(chr(model[c].as_long()) for c in key))```
The script outputs `!sfc-glZa-6|/z-~pNd-p3oK-kTss` which is valid against my script and the actual binary:
![https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/congratulation.png](https://raw.githubusercontent.com/TFNS/writeups/master/2020-03-29-VolgaCTF/dotnetme/congratulation.png)
Also work as a flag: `VolgaCTF{!sfc-glZa-6|/z-~pNd-p3oK-kTss}`
You can find my sources in the [sources](https://github.com/TFNS/writeups/tree/master/2020-03-29-VolgaCTF/dotnetme/sources/) Directory.
``` - @SakiiR
 Thanks to @BitK and @XeR```