This challenge involves an old version of CS:GO VScript, which is vulnerable to a UAF bug and a type confusion bug.
The sort function of squirrel array is array_sort
in sqbaselib.cpp
, which will call _qsort
:
The r
index passed into _qsort
is fixed at the beginning, so by abusing array.resize
in compare function, we can retrieve dangling reference to freed objects through compare function parameters.
By freeing a string then overlap it with an array, the _len
field of the freed SQString
object will be overwritten by the _sharedstate
field of the newly created SQArray
. It’s a pointer so the value will be very large, and we can use the dangling string to do arbitrary reading over a large heap space after it.
_regexp_*
functions in sqstdstring.cpp
retrieve SQRex
object from the current object using SETUP_REX
macro:
The typetag
parameter is 0
, means that it will not check for type mismatch. So we can call _regexp_*
functions using any instance
object (examples: self-defined classes, external library classes like CS:GO script classes).
As we have a long string by using UAF bug above, we can just spray a lot of CScriptKeyValues
and find one of them using last 2 bytes of SQInstance::vtable
as they will not be affected by Windows ASLR, then use confusion to watch for changes to _userpointer
field. But there are other instance
objects too, and we have no way to be sure that it’s a CScriptKeyValues
object.
Fortunately, the tostring
method will return the type name and the address in memory of any object. For number and string it will just return the value. But we overlapped the freed string with an array, so we can get address of it by calling tostring
on the array. We can keep allocate new CScriptKeyValues
object until we get one that lies after our long string and in the range that we can read its data. I won’t go into detail of Source Engine heap in this writeup, but most of the time we will get a satisfied object without triggering Squirrel timeout watchdog.
By reading the CScriptKeyValues
object, we can get these values:
SQInstance::vtable
, which can be used to calculate vscript.dll
base address for ROP gadgets_userpointer
of that objectMy approach is to use a CS:GO script class, CScriptKeyValues
. Squirrel will panic if you attempt to modify the prototype after 1 instance of a class has been created. Since in map loading, there’re no instance of this class would be created, we can modify its prototype:
When we call any method of a CS:GO script class, CSquirrelVM::TranslateCall
in vsquirrel.cpp
will be called. It will access _userpointer
field of the object to get binding information:
_regexp_constructor
will create a new SQRex
class and store it in _userpointer
field. That means we can control pContext
. Below is InstanceContext_t
struct:
Below is SQRex
struct:
pClassDesc
field overlaps with _bol
field. When we call _regexp_search(str)
, _bol
field will be set to the beginning of str
. So we can craft a fake ScriptClassDesc_t
object using a string. Below is ScriptClassDesc_t
struct:
Below is IScriptInstanceHelper
interface:
We can craft a fake IScriptInstanceHelper
object to control the virtual method table.
Fortunately, Squirrel string is not null-terminated, so we don’t have to worry about null bytes.
In conclusion, the fake object will look like this:
Offset | Content |
0x0 | pivot gadget |
… | padding |
0x2c | _userpointer + 0x4 (_bol ) |
Thanks ALLES! team for organizing a great CTF with awesome challenges, and allowed late submission of 🔥 challenges.
Source Engine is a mature engine with a lot of functions, and use a lot of unsafe memory code. With the f.mdact that any people can host dedicated servers, it’s a huge attack surface. It’s sad that Valve never bothers fixing security bugs in the engine quickly. I really hoped that they will pick up the pace after secret club’s callout, but seems like they will never do that.
See POC here.
What do you think?