Open CherryWorm opened 4 years ago
I'm not sure what there is here for squirrel to tackle. The real vulnerability is in the service running untrusted bytecode. Even if you stuffed the VM with all the sanity checks possible so that it spent 100% of time checking sanity and 0% of time doing useful work, someone will still find a way to exploit it. I think the source-only exploits will be more interesting, since it's reasonable to do sanity checks there as a precaution against programming mistakes even if you don't have any interest in making a mission critical server application.
I don't think it's unreasonable to have bound checks for things like the load instruction (there's a similar vulnerability for the move instruction), it's not like the vm is optimized to a point where this would impact performance in any way, and it could also prevent the exploitation of other bugs.
The way you put it makes it seem like it is unreasonable to assume that a vm won't have undefined behaviour no matter what kind of bytecode it executes, but I don't think most people would agree. Not only is it really not that hard to write decent, non-exploitable code that still performs well, most popular vm languages like for example Java actually guarantee this. Obviously a source-only vulnerability is more critical, but this should still either be fixed or there should be a visible warning on every function that may load bytecode that it might lead to exploitable undefined behaviour.
I think one solution would be to have a "sanitycheck" parameter in the load function that optionally boundchecks bytecode and looks for other possible exploits. I'd rather not have to do any sanity checks runtime and especially in an environment where the source is trusted. Squirrel's bytecode was never meant to be executed from an untrusted source but I can understand that someone might want it I think to make a "safe" bytecode java style is a quite ambitious task.
The squirrel VM has been featured in a recent German CTF (https://earth.2020.cscg.de/). The setup was as follows: a server reads squirrel files (source code or bytecode), and executes them. The goal was to pop a shell, however the systemlib had not been registered.
There have been numerous different solutions, exploiting multiple different 0days, some source-code only, some including some patches inside the bytecode. I'm sure other people will post their writeups in the coming days as well. I think all of them are worthy of being fixed. What follows is my writeup, as well as the poc squirrel script.
The vulnerability
I exploited the following vulnerability in sqvm.cpp:
Here, arg1 is an arbitrary signed integer, and there are no bounds checks for the array, so this allows us to load SQObjectPtr's from an arbitrary offset onto the stack.
General setup
I wrote a small script which patches the following function in a compiled squirrel script:
As hinted at in the comment, instead of loading the string "a" and returning it (which corresponds to the instruction LOAD 1 1 0 0), my script replaces arg1 with 0x1000, which triggers the out-of-bound read.
Exploit
I used the following trick to leak addresses:
Squirrel will print a SQObjectPtr with an unknown type in the format "(%s, 0x%p)", where %p is the second 8-bytes of this 16-byte structure. To illustrate, I use this to leak a heap-address:
The next thing I do is create a large, but not too large blob, which gets allocated on the heap somewhere behind the literal-array:
I tag every 8-byte bundle, to later identify, which index I load onto the stack with the LOAD-primitive. By calling addr, I get the second 8-byte bundle of the struct which points somewhere into the blob.
Now, if we overwrite the 8 bytes directly before this index, we control the type of the SQObjectPtr that we load onto the stack.
First of all, I use this to create a fake array, where the second 8 bytes will be the pointer the array struct is stored at:
The first address I want to leak is
heap_base + 0x1000 * 0x10
(the-0x38
is just the offset of the_size
field in the array struct, wihch I read by callinglen()
on the array). This is just some random address in the blob, but we know the actual absolute address (heap_base + 0x1000 * 0x10
), which allows us to calculate the address of the start of the array, by just subtracting the index we read there, times 8, because we're dealing with 8-byte chunks. We're going to need this address later on.We use the same trick again, but this time to leak a function pointer inside sqlstdlib, by reading the struct of a native_closure (in this case blob.tell). Then we add an offset to it, to get a pointer to the function
sqstd_register_systemlib
:Now we know the absolute address of our blob and we know the absolute address of
sqstd_register_systemlib
. This is now enough to craft our own NativeClosure.The layout of a SQObjectPtr to a NativeClosure is the following: the SQObjectPtr has to have the type of NativeClosure in its first 8 bytes, and a pointer to a NativeClosure in its second 8 bytes (this is what we use the absolute address of our blob for). In this struct there are a bunch of fields and a pointer to an actual c function, that gets called when we call the object in Squirrel. If all of those fields except the pointer are 0, we by default pass all checks before the actual call (this includes fields like the amount of parameters, which is why I chose to call
sqstd_register_systemlib
, it takes no arguments and allows arbitrary command execution afterwards). This is what the following code does:Now we loaded the entire system-standard-library, including the function
system
. Getting the flag is now as easy as this:POC Script