Being a .NET core assembly, it was pretty easy to reverse:
So, what the Loop function basically does, is to allocate three buffers on the stack (nicely aligned) and provide us with 4 commands:
I : Increase ptr3
R : Reset ptr3 (to the value stored at the beginning of ptr)
P : Print the value ptr3 is currently pointing to
W : Read a value from the user in hex and write it to the address ptr3 is pointing to
Using the unsafe keyword for the Loop function, all of .NETs memory safety obviously goes to hell. Since it also has no boundary checking on the allocated buffers, we can easily “walk” outside of those buffers into the previous stack values.
The stack layout will be like this:
While I was trying to setup the docker environment in the background, I played around with it locally and it turned out pretty quick, that we don’t even need the right environment to be able to exploit it.
To debug it locally, you’ll need a runtimeconfig (thanks to pusher for preparing that).
myApp.runtimeConfig.json
Basic script to communicate with the service:
With this, I started to dump the stack to get an idea of the layout, and from where we could possibly leak some addresses.
So, this looks already promising. Having not worked on .NET binaries by now, my first thought was “Let’s search a libc leak and build a ropchain”, but since the CLR doesn’t seem to rely much on libc, there were none near to our stack (found some way down the stack, but moving the pointer there mostly crashed the application).
As it turned out, though, being in an unsafe context, it’s way easier to exploit this successfully.
But first things first. We can now read values from the stack and also change them, but while we’re stuck in the loop, we cannot do anything useful with it. As you can see in the dumped data, the loop variable is stored at offset 32, so we can just move the stack pointer there, and overwrite it with a 0 which will end the loop and return.
Since it was too bothersome, to follow the execution of the CLR, but knowing, it would have to “return” at some point, I just wrote some invalid values to the stack to see where it breaks :)
So, the first time it will segfault at offset 109.
This happens because at the previous offset (108) ptr3 itself is stored, and we just overwrote the pointer with 108. Let’s keep this in mind, might come in handy to be able to move ptr3 to an arbitrary address this way.
So we’ll ignore this index for now, and overwrite the following addresses, and break out of the loop by overwriting the loop variable with 0.
By overwriting offset 113 we gain rip control. Still no useful leaks by now, so I just checked, where it would normally return to.
Didn’t really look further into it and just assumed that this might be some JIT region from the .NET CLR.
And the loop function will return there, as soon as we break the loop. Well, perfect…
As we previously saw, ptr3 is stored at offset 108. We can just point it anywhere by overwriting the address at that offset (btw, it’s not a good idea to overwrite the original address stored in ptr and then reset, to move ptr3, since you won’t be able to get back into the current buffer again to reset the loop).
So, the plan is:
Leak the value at offset 113 to the jit region
Reset ptr3 and walk back to offset 108 and overwrite it with the jit return address (ptr3 now pointing into jit region at return address)
Write shellcode to the jit region
Reset ptr3 and walk to offset 32 to overwrite the loop variable to break the loop