This first post will be documenting the method taken to unpack the initial .NET layers of this Oski Stealer binary, which is the first part of the challenge. The next post that will come out will cover the second part of the challenge, which is to develop a script that utilises API within a disassembler to automate string decryption.
So, let’s go ahead and get unpacking!
As the challenge stated it, we know the sample is packed with .NET. So, first things first we want to open it up in dnSpy.
Checking out the entry point, we can see there is a call to ttJiDM(), however this contains no visible code. However, a new instance of the gc() form is created, which takes us to the main function.
We can see near the end of the function block a call to methodInfo.Invoke(), which is a point of interest for us. Similar to setting breakpoints on VirtualAlloc() and VirtualProtect() when unpacking native binaries (C/C++), breaking on Invoke() within .NET can be very helpful in locating the unpacked code – especially if the internal payload is .NET. The reason for this is Invoke() will commonly be used to execute further assembly, and potential API calls. By breaking on Invoke(), we can examine any arguments to extract any .NET stages, or even native binaries passed as arguments.
We want to set a breakpoint on the methodInfo.Invoke() line, rather than stepping into the Invoke() function itself, as methodInfo appears to be resolved on runtime, and Invoke() may not be within a standard library. After setting this breakpoint, we can go ahead and run the debugger (make sure to use a 32-bit version, as it is a 32 bit binary).
Sure enough, we hit the breakpoint immediately. Stepping into the Invoke() function twice, we come to a function that will parse the arguments and proceed to call UnsafeInvokeInternal(), which is what we want to step into. Our goal is to find the lowest Invoke() call before it actually executes whatever it will execute, and we will set a breakpoint on that lowest call so we can skip through the unpacking.
Inside UnsafeInvokeInternal() we can see two calls to InvokeMethod() – we need to put a breakpoint on both calls, as attempting to view this will only take us to a definition.
Running the debugger, we hit the second breakpoint, and stepping into this takes us to a new module; FuncAttribute. From here we are going to want to simply run the debugger, and watch Process Hacker to make sure nothing unusual occurs like process spawning.
While there appears to be some kind of sleep call, we do eventually hit the next call to InvokeMethod(). Stepping into this takes us to yet another .NET module, named DotNetZipAdditionalPlatforms. Knowing Oski is developed in C++, we can be certain this isn’t the final payload. So, let’s go ahead and run the debugger again.
The next InvokeMethod() call we hit actually has some interesting parameters – specifically the this variable, which points to System.Reflection.Assembly.Load(). This will, as the name suggests, load in some .NET assembly, which is likely stored within the parameters object. It does contain a byte array with 0x71400 bytes, which, if you right click and view in the memory window, you can see is indeed an executable file. I decided to not dump this out to save some time, as the Assembly.Load() function will typically load .NET assemblies, and as we’re looking for a C++ compiled binary, this likely is not the binary we’re looking for.
Running the debugger again, we hit another breakpoint on InvokeMethod(), this time calling GetRuntimeDirectory(). This doesn’t seem to interesting, so we can go ahead and run the debugger again.
At this point, you might notice a strange pop-up talking about sqlite3.dll having an error, and then a process crashing, before the process being debugged terminates itself; this tells us that Oski Stealer is likely injected into a spawned process, and considering the spawned process has the same name as the payload we are currently debugging, that GetRuntimeDirectory() was most likely getting a pointer to the current payload name.
So, what we want to do now is restart and jump back to where GetRuntimeDirectory() was called.
Now we’ve reached that breakpoint, we want to simply step out of the functions until we are in the user-code. We eventually make it to the function veMSOMDjV(), which takes us back into a larger function. This has multiple calls to veMSOMDjV() (though from different classes), and stepping over one or two functions results in some of the variables being propagated with strings. There appears to be some flags referenced within the function, however none of the flags are set aside from the very first one, and so we soon hit the function return.
At this point due to the amount of obfuscation and weird function calls (functions called within arguments aren’t too easy to spot when its being debugged), I’m relying on the Step-Into button to hopefully get to a function that contains more than one line of code.
However, roughly 2 Step-Into’s later, I end up in the function evIlDKwwfOqtAVPasvA.veMSOMDjV(), that consists of a call to A_2() with two parameters – \u0020 and \u0020 – confusing I know! The first shown in the locals tree is a file name, to be exact it is the file name of the sample currently being debugged. The second is another byte array of around 0x32000 bytes in size. Another thing to look out for when unpacking .NET binaries if you didn’t know already is large byte arrays, as they can contain large encrypted string blobs, executables, shellcode, and are usually very helpful in the unpacking/analysis process.
Viewing the array in memory, it is clear it is an executable, so lets go ahead and dump it out.
Opening it up in PEStudio, we can see it is a Microsoft Visual C++ binary, and checking out the strings we can also see a large number of seemingly Base64 encoded strings and some HTTP request linked strings too.
At this point I’m pretty confident this is the core Oski Stealer payload, and so we will be analysing this in the next part and decrypting the strings!