SiberSiaga 2023: Packed

The binary looks so messy. Get me the flag. Please :(

TL;DR

Manually unpack the executable via debugging using dnSpy's Module Breakpoints feature.

Initial Analysis

Analyzing the executable file via Detect It Easy shows a .NET executable that is heavily obfuscated using 3 different packers.

We can attempt to unpack the executable iteratively via de4dot:

 ┌──(kali💀JesusCries)-[~/…/CTF/SiberSiaga2023 (Finals)/Rev/Packed]
 └─$ de4dot finaldotnet.exe -p cr      
 de4dot v3.1.41592.3405 Copyright (C) 2011-2015 de4dot@gmail.com
 Latest version and source code: https://github.com/0xd4d/de4dot
 Detected Confuser v1.9 (r76186+) (/home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet.exe)
 Cleaning /home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet.exe
 Renaming all obfuscated symbols
 Saving /home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet-cleaned.exe
 ​
 ┌──(kali💀JesusCries)-[~/…/CTF/SiberSiaga2023 (Finals)/Rev/Packed]
 └─$ de4dot finaldotnet-cleaned.exe -p df 
 de4dot v3.1.41592.3405 Copyright (C) 2011-2015 de4dot@gmail.com
 Latest version and source code: https://github.com/0xd4d/de4dot
 Detected Dotfuscator (/home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet-cleaned.exe)
 Cleaning /home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet-cleaned.exe
 Renaming all obfuscated symbols
 Saving /home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet-cleaned-cleaned.exe                                                                                                                         
 
 ┌──(kali💀JesusCries)-[~/…/CTF/SiberSiaga2023 (Finals)/Rev/Packed]
 └─$ de4dot finaldotnet-cleaned-cleaned.exe -p go 
 de4dot v3.1.41592.3405 Copyright (C) 2011-2015 de4dot@gmail.com
 Latest version and source code: https://github.com/0xd4d/de4dot
 Detected Goliath.NET (/home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet-cleaned-cleaned.exe)
 Cleaning /home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet-cleaned-cleaned.exe
 Renaming all obfuscated symbols
 Saving /home/kali/Desktop/CTF/SiberSiaga2023 (Finals)/Rev/Packed/finaldotnet-cleaned-cleaned-cleaned.exe

Verify this against Detect It Easy shows that the executable is now fully unpacked.

However, this results in no avail. As verified in dnSpy, the decompiled code remains obfuscated despite de4dot claiming to have successfully unpack it. This is likely due to the use of a custom/unknown packer.

Manual Unpacking

We can expect this challenge to be solved with a similar approach as Vacine - that is: by debugging the executable until we reach the real entry point of the program.

By default, all assemblies in .NET (regardless of it's .EXE or .DLL extension) will be loaded directly into memory, which makes the debugging of external assemblies practically impossible.

To fix this, go to Debug -> Windows -> Module Breakpoints and change the name field to an asterisk wildcard *:

Start debugging the program by hitting Continue - F5 with the default configurations, and notice how dnSpy places a breakpoint on the first ever module loaded mscorlib.dll:

As mscorlib.dll is a common dependency of .NET applications for bootstrapping the Common Language Runtime (CLR) environment, this isn't something of our interest. For the same reason, we will also skip through all subsequent assemblies that looks benign with Continue - F5:

  1. finaldotnet.exe (The application itself)

  2. Microsoft.VisualBasic.dll, System.Core.dll, System.dll

A while later, we notice that Sevenziplib is loaded. This suggests that the executable is utilizing some kind of compression to obfuscate the application.

Regardless of that, Continue - F5 one step further shows that finaldotnet (without the .EXE extension) is now loaded. This breakpoint also corresponds to the function RuntimeAssembly.nLoadImage(rawAssembly), which confirms that finaldotnet.exe is loading finaldotnet - like an inception.

To verify what comes before the loading of finaldotnet, hit Step Out - Shift + F11. This brings us to the Assembly.Load(sLcn.Ptpi(array)) function, which sounded like the Caller of our previous function:

  • Caller: Assembly.Load(sLcn.Ptpi(array)) <- High Level Function

  • Callee: RuntimeAssembly.nLoadImage(rawAssembly) <- Low Level Function

Step Out - Shift + F11 again leads us to Assembly assembly = sLcn.lFRS(). Taking a wild guess, this line of code is most probably referencing the actual assembly that will be loaded, which in this case points tofinaldotnet.

To sum up, the reason for Stepping Out twice, is because there are 3 layers of Abstraction implemented. Therefore, we need to go back 2 levels to reach the top-most level Caller.

A few lines below (Line 99) the current statement (Line 42), indicates the the entry point entryPoint.Invoke(null, parameters) of our unpacked executable. Place a breakpoint on it.

Afterwards, hit Continue - F5 until the program execution stops at the entry point.

After stopping at the entry point, hit Step Over - F10 to start exploring the unpacked executable.

Recall that we configured dnSpy to set a breakpoint every time an external assembly is loaded. As we are now in the bootstrapping stage of the unpacked executable, it makes sense for it to be loading a tons of dependencies it requires. This also means that we have a long way to go (Step Over - F10 x28 times) until we will see some sensible lines of code.

Continue to Step Over - F10 repeatedly until the current statement reaches Line 36. This will allow a bunch of string variables to be initialized and populated.

BEFORE:

AFTER:

At this point onwards, when trying to run the executable normally, it will go to sleep indefinitely thanks to the code in Line 37. To bypass the sleep function, as well as other insignificant comparison statements marked under the giant cross mark, we need to Set Next Statement - CTRL + SHIFT + F10 on Line 66.

Line 66 onwards is our point of interest because it seems to be comparing our user input to a base64-decoded string. With that said, this is most probably a flag-checker function.

 using (uNPH.NvYX())
 {
     string jzvx = text6;
     string s = text7;
     byte[] wTdt = Convert.FromBase64String(s);
     byte[] bytes = uNPH.GBrL(wTdt, jzvx);
     string @string = Encoding.UTF8.GetString(bytes);
     Console.Write(value2);
     string a = Console.ReadLine();
     if (a == @string)
     {
         Console.WriteLine(value3);
         Console.WriteLine(value4);
     }
     else
     {
         Console.WriteLine(value5);
     }
 }

By the looks of it, the decrypted flag will be stored in variable @string. Hence, we can place a breakpoint on the comparison statement, and try to read the value of variable @string:

Finally, keep on Stepping Over - F10 from Line 66 until Line 73 for the values to be populated.

Flag: sibersiaga{2d50972fcecd376129545507f1062089}

Last updated