diff --git a/.github/workflows/kernelctf-submission-verification.yaml b/.github/workflows/kernelctf-submission-verification.yaml index 02a84290e..df19c3921 100644 --- a/.github/workflows/kernelctf-submission-verification.yaml +++ b/.github/workflows/kernelctf-submission-verification.yaml @@ -114,6 +114,11 @@ jobs: env: RELEASE_ID: ${{ matrix.target }} SUBMISSION_DIR: ${{ needs.structure_check.outputs.submission_dir }} + EXPLOIT_INFO: ${{ needs.structure_check.outputs[format('exploit_info_{0}', matrix.target)] }} + defaults: + run: + shell: bash + working-directory: ./kernelctf/repro/ steps: - name: Checkout repo content uses: actions/checkout@v3 @@ -133,12 +138,11 @@ jobs: uses: actions/download-artifact@v3 with: name: exploit_${{ env.RELEASE_ID }} - path: exp/ + path: ./kernelctf/repro/exp/ - name: Fetch rootfs run: | - wget https://storage.googleapis.com/kernelctf-build/files/rootfs_repro_v1.img.gz - mv rootfs_repro_v1.img.gz rootfs.img.gz + wget -O rootfs.img.gz https://storage.googleapis.com/kernelctf-build/files/rootfs_repro_v2.img.gz gzip -d rootfs.img.gz - name: Download bzImage @@ -146,56 +150,59 @@ jobs: if [ "$RELEASE_ID" == "mitigation-6.1" ]; then RELEASE_ID="mitigation-6.1-v2"; fi wget https://storage.googleapis.com/kernelctf-build/releases/$RELEASE_ID/bzImage + - name: List repro folder contents + run: ls -alR ./ + # ugly hack to make Github Actions UI to show repro logs separately in somewhat readable fashion - id: repro1 name: Reproduction (1 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 1 + run: ./repro.sh 1 - id: repro2 name: Reproduction (2 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 2 + run: ./repro.sh 2 - id: repro3 name: Reproduction (3 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 3 + run: ./repro.sh 3 - id: repro4 name: Reproduction (4 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 4 + run: ./repro.sh 4 - id: repro5 name: Reproduction (5 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 5 + run: ./repro.sh 5 - id: repro6 name: Reproduction (6 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 6 + run: ./repro.sh 6 - id: repro7 name: Reproduction (7 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 7 + run: ./repro.sh 7 - id: repro8 name: Reproduction (8 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 8 + run: ./repro.sh 8 - id: repro9 name: Reproduction (9 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 9 + run: ./repro.sh 9 - id: repro10 name: Reproduction (10 / 10) continue-on-error: true - run: ./kernelctf/repro.sh 10 + run: ./repro.sh 10 - name: Upload repro QEMU logs as an artifact uses: actions/upload-artifact@v3 @@ -208,4 +215,4 @@ jobs: STEPS: ${{ toJSON(steps) }} run: | echo $STEPS >> steps.json - ./kernelctf/repro_summary.py ${{ github.run_id }} + ../repro_summary.py ${{ github.run_id }} diff --git a/kernelctf/.gitignore b/kernelctf/.gitignore new file mode 100644 index 000000000..16d3c4dbb --- /dev/null +++ b/kernelctf/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/kernelctf/check-submission.py b/kernelctf/check-submission.py index 3996d0726..edeb3383c 100755 --- a/kernelctf/check-submission.py +++ b/kernelctf/check-submission.py @@ -9,12 +9,15 @@ import csv import io import hashlib +import time +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) PUBLIC_CSV_URL = "https://docs.google.com/spreadsheets/d/e/2PACX-1vS1REdTA29OJftst8xN5B5x8iIUcxuK6bXdzF8G1UXCmRtoNsoQ9MbebdRdFnj6qZ0Yd7LwQfvYC2oF/pub?output=csv" POC_FOLDER = "pocs/linux/kernelctf/" EXPLOIT_DIR = "exploit/" +CACHE_DIR = f"{BASE_DIR}/.cache" MIN_SCHEMA_VERSION = 2 -DEBUG = "--debug" in sys.argv +# DEBUG = "--debug" in sys.argv errors = [] warnings = [] @@ -80,10 +83,19 @@ def checkRegex(text, pattern, errorMsg): error(f"{errorMsg}. Must match regex `{pattern}`") return m -def fetch(url): +def fetch(url, cache_name, cache_time=3600): + cache_fn = f"{CACHE_DIR}/{cache_name}" + if cache_name and os.path.isfile(cache_fn) and (time.time() - os.path.getmtime(cache_fn) < cache_time): + with open(cache_fn, "rb") as f: return f.read().decode('utf-8') + response = requests.get(url) if response.status_code != 200: fail(f"expected 200 OK for request: {url}") + + if cache_name: + os.makedirs(CACHE_DIR, exist_ok=True) + with open(cache_fn, "wb") as f: f.write(response.content) + return response.content.decode('utf-8') def parseCsv(csvContent): @@ -93,7 +105,7 @@ def parseCsv(csvContent): argv = [arg for arg in sys.argv if not arg.startswith("--")] print(f"[-] Argv: {argv}") -mergeInto = argv[1] if len(argv) >= 2 else "origin/main" +mergeInto = argv[1] if len(argv) >= 2 else "origin/master" print(f"[-] Params: mergeInto = {mergeInto}") mergeBase = run(f"git merge-base HEAD {mergeInto}")[0] @@ -149,7 +161,7 @@ def parseCsv(csvContent): schemaVersion = MIN_SCHEMA_VERSION schemaUrl = f"https://google.github.io/security-research/kernelctf/metadata.schema.v{schemaVersion}.json" - schema = json.loads(fetch(schemaUrl)) + schema = json.loads(fetch(schemaUrl, f"metadata.schema.v{schemaVersion}.json")) metadataErrors = list(jsonschema.Draft202012Validator(schema).iter_errors(metadata)) if len(metadataErrors) > 0: @@ -161,11 +173,7 @@ def parseCsv(csvContent): submissionIds = [submissionIds] print(f"[-] Submission IDs = {submissionIds}") -if DEBUG: - with open("public.csv", "rt") as f: publicCsv = f.read() -else: - publicCsv = fetch(PUBLIC_CSV_URL) - +publicCsv = fetch(PUBLIC_CSV_URL, "public.csv") publicSheet = { x["ID"]: x for x in parseCsv(publicCsv) } # print(json.dumps(publicSheet, indent=4)) @@ -207,6 +215,8 @@ def parseCsv(csvContent): print(f"[-] Got flags for the following targets: {', '.join(flagTargets)}") checkList(flagTargets, lambda t: t in exploitFolders, f"Missing exploit for target(s)") checkList(exploitFolders, lambda t: t in flagTargets, f"Found extra exploit(s) without flag submission", True) +if schemaVersion >= 3: + checkList(flagTargets, lambda t: t in metadata["exploits"].keys(), f"Missing metadata information for exploit(s)") def ghSet(varName, content): varName = f"GITHUB_{varName}" @@ -224,8 +234,17 @@ def summary(success, text): if len(errors) > 0: summary(False, f"The file structure verification of the PR failed with the following errors:\n{formatList([f'❌ {e}' for e in errors], True)}") -ghSet("OUTPUT", "targets=" + json.dumps([f for f in exploitFolders if not f.startswith("extra-")])) +ghSet("OUTPUT", "targets=" + json.dumps([f for f in flagTargets])) ghSet("OUTPUT", f"submission_dir={subDirName}") +for target in flagTargets: + if schemaVersion >= 3: + exploit_info = metadata["exploits"].get(target) + if not exploit_info: continue + exploit_info = { key: exploit_info[key] for key in ["uses", "requires_separate_kaslr_leak"] if key in exploit_info } + else: + exploit_info = {} + ghSet("OUTPUT", f"exploit_info_{target}={json.dumps(exploit_info)}") + summary(True, f"✅ The file structure verification of the PR was successful!") diff --git a/kernelctf/repro/init/init.sh b/kernelctf/repro/init/init.sh new file mode 100755 index 000000000..740cf240d --- /dev/null +++ b/kernelctf/repro/init/init.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -ex +mount -t proc none /proc +mount -t sysfs none /sys + +mkdir /tmp/exp_ro +mount -t 9p exp /tmp/exp_ro + +mkdir /tmp/exp +chown user:user /tmp/exp +chmod a+rx /tmp/exp + +cp /tmp/exp_ro/* tmp/exp/ +chmod a+rx /tmp/exp/* + +CMD="/tmp/exp/exploit" +if [[ " $* " == *" kaslr_leak=1 "* ]]; then + KASLR_BASE=`head -n 1 /proc/kallsyms | cut -d " " -f1` + CMD="$CMD $KASLR_BASE" +fi + +echo "running exploit, cmd=$CMD" +su user -c "$CMD" \ No newline at end of file diff --git a/kernelctf/repro.sh b/kernelctf/repro/repro.sh similarity index 81% rename from kernelctf/repro.sh rename to kernelctf/repro/repro.sh index d8bd12e84..49c0a679f 100755 --- a/kernelctf/repro.sh +++ b/kernelctf/repro/repro.sh @@ -16,6 +16,9 @@ touch $QEMU_TXT START_TIME=$(date +%s) +CMDLINE="console=ttyS0 root=/dev/vda1 rootfstype=ext4 rootflags=discard ro init=/init hostname=repro" +if echo $EXPLOIT_INFO | jq -e '.requires_separate_kaslr_leak'; then CMDLINE="$CMDLINE -- kaslr_leak=1"; fi + expect -c ' set timeout -1 set stty_init raw @@ -27,8 +30,9 @@ expect -c ' -nic user,model=virtio-net-pci \ -drive file=rootfs.img,if=virtio,cache=none,aio=native,format=raw,discard=on,readonly=on \ -drive file=flag,if=virtio,format=raw,readonly=on \ - -virtfs local,path=exp,mount_tag=exp,security_model=none \ - -append "console=ttyS0 root=/dev/vda1 rootfstype=ext4 rootflags=discard ro init=/init hostname=repro" \ + -virtfs local,path=init,mount_tag=init,security_model=none,readonly=on \ + -virtfs local,path=exp,mount_tag=exp,security_model=none,readonly=on \ + -append "'"$CMDLINE"'" \ -nographic -no-reboot expect "# "