SiberSiaga 2023: Packed
The binary looks so messy. Get me the flag. Please :(
Last updated
The binary looks so messy. Get me the flag. Please :(
Last updated
Manually unpack the executable via debugging using dnSpy's
Module Breakpoints
feature.
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
:
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.
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
:
finaldotnet.exe (The application itself)
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.
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}