diff --git a/kernelctf/server/kernelctf.service b/kernelctf/server/kernelctf.service new file mode 100644 index 00000000..5a97c8ab --- /dev/null +++ b/kernelctf/server/kernelctf.service @@ -0,0 +1,10 @@ +[Unit] +Description=kernelCTF + +[Service] +ExecStart=/home/poprdi/service.sh +User=kernelctf +Group=kernelctf + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/kernelctf/server/qemu.sh b/kernelctf/server/qemu.sh new file mode 100755 index 00000000..56897798 --- /dev/null +++ b/kernelctf/server/qemu.sh @@ -0,0 +1,22 @@ +#!/bin/bash +if [ $# -ne 3 ]; then echo "Usage: $0 "; exit 1; fi + +RELEASE_PATH=$1 +FLAG_FN=$2 +INIT=$3 +RELEASE=$(basename "$RELEASE_PATH") + +HARDENING="" +if [[ "$RELEASE" == "mitigation-v3"* ]]; then + HARDENING="sysctl.kernel.dmesg_restrict=1 sysctl.kernel.kptr_restrict=2 sysctl.kernel.unprivileged_bpf_disabled=2 sysctl.net.core.bpf_jit_harden=1 sysctl.kernel.yama.ptrace_scope=1"; +fi + +exec qemu-system-x86_64 -m 3.5G -nographic -no-reboot \ + -monitor none \ + -enable-kvm -cpu host -smp cores=2 \ + -kernel $RELEASE_PATH/bzImage \ + -initrd ramdisk_v1.img \ + -nic user,model=virtio-net-pci \ + -drive file=rootfs_v3.img,if=virtio,cache=none,aio=native,format=raw,discard=on,readonly \ + -drive file=$FLAG_FN,if=virtio,format=raw,readonly \ + -append "console=ttyS0 root=/dev/vda1 rootfstype=ext4 rootflags=discard ro $HARDENING init=$INIT hostname=$RELEASE" diff --git a/kernelctf/server/refresh_slots.py b/kernelctf/server/refresh_slots.py new file mode 100755 index 00000000..db5bca5e --- /dev/null +++ b/kernelctf/server/refresh_slots.py @@ -0,0 +1,38 @@ +#!/usr/bin/env -S python3 -u +import csv +import io +import json +import os +import requests + +def fail(msg): + print("\n[!] [FAIL] " + msg.replace('\n', '\n ')) + os._exit(1) + +def parseCsv(csvContent): + columns, *rows = list(csv.reader(io.StringIO(csvContent), strict=True)) + return [{ columns[i]: row[i] for i in range(len(columns)) } for row in rows] + +def fetch(url): + response = requests.get(url) + if response.status_code != 200: + fail(f"expected 200 OK for request: {url}") + return response.content.decode('utf-8') + +print("Fetching public spreadsheet...\n") +publicCsv = fetch("https://docs.google.com/spreadsheets/d/e/2PACX-1vS1REdTA29OJftst8xN5B5x8iIUcxuK6bXdzF8G1UXCmRtoNsoQ9MbebdRdFnj6qZ0Yd7LwQfvYC2oF/pub?output=csv") +publicSheet = parseCsv(publicCsv) + +slots = {} +for row in publicSheet: + for slot in [row["LTS slot"], row["COS slot"]]: + if slot != "" and not slot.startswith("("): + slots[slot] = row["ID"] +slots = dict(sorted(slots.items())) + +print("Taken slots:") +for slot in slots: + print(f" - {slot}: {slots[slot]}") + +print("\nSaving to slots.json") +with open("slots.json", "wt") as f: f.write(json.dumps(slots, indent=4)) \ No newline at end of file diff --git a/kernelctf/server/releases.yaml b/kernelctf/server/releases.yaml new file mode 100644 index 00000000..33cecad6 --- /dev/null +++ b/kernelctf/server/releases.yaml @@ -0,0 +1,129 @@ +lts-6.1.76: + release-date: 2024-02-20T12:00:00Z +cos-97-16919.450.16: + release-date: 2024-02-20T12:00:00Z +cos-105-17412.294.23: + release-date: 2024-02-20T12:00:00Z + +lts-6.1.77: + release-date: 2024-02-09T12:00:00Z +cos-97-16919.450.6: + release-date: 2024-02-09T12:00:00Z +cos-105-17412.294.10: + release-date: 2024-02-09T12:00:00Z + +lts-6.1.74: + release-date: 2024-01-26T12:00:00Z +cos-97-16919.404.34: + release-date: 2024-01-26T12:00:00Z +cos-105-17412.226.68: + release-date: 2024-01-26T12:00:00Z + +lts-6.1.72: + release-date: 2024-01-19T12:00:00Z +cos-97-16919.404.33: + release-date: 2024-01-19T12:00:00Z +cos-105-17412.226.67: + release-date: 2024-01-19T12:00:00Z + +lts-6.1.70: + release-date: 2024-01-12T12:00:00Z +cos-105-17412.226.52: + release-date: 2024-01-12T12:00:00Z +cos-97-16919.404.26: + release-date: 2024-01-12T12:00:00Z + +lts-6.1.67: + release-date: 2023-12-18T12:00:00Z +cos-105-17412.226.43: + release-date: 2023-12-18T12:00:00Z +cos-97-16919.404.21: + release-date: 2023-12-18T12:00:00Z + +lts-6.1.63: + release-date: 2023-12-01T12:00:00Z + +lts-6.1.61: + release-date: 2023-11-17T12:00:00Z +cos-105-17412.226.28: + release-date: 2023-11-17T12:00:00Z +cos-97-16919.404.13: + release-date: 2023-11-17T12:00:00Z + +lts-6.1.60: + release-date: 2023-11-03T12:00:00Z +cos-105-17412.226.18: + release-date: 2023-11-03T12:00:00Z +cos-97-16919.404.4: + release-date: 2023-11-03T12:00:00Z + +mitigation-v3-6.1.55: + release-date: 2023-10-21T12:00:00Z + +lts-6.1.58: + release-date: 2023-10-20T12:00:00Z +cos-105-17412.156.69: + release-date: 2023-10-20T12:00:00Z +cos-97-16919.353.53: + release-date: 2023-10-20T12:00:00Z + +lts-6.1.54: + release-date: 2023-09-29T12:00:00Z +cos-105-17412.156.59: + release-date: 2023-09-29T12:00:00Z +cos-97-16919.353.46: + release-date: 2023-09-29T12:00:00Z + +lts-6.1.52: + release-date: 2023-09-13T12:00:00Z +cos-105-17412.156.30: + release-date: 2023-09-13T12:00:00Z + +lts-6.1.47: + release-date: 2023-08-30T12:00:00Z +cos-105-17412.156.23: + release-date: 2023-08-30T12:00:00Z +cos-97-16919.353.23: + release-date: 2023-08-30T12:00:00Z + +lts-6.1.36: + release-date: 2023-06-30T13:35:00Z +cos-105-17412.101.42: + release-date: 2023-07-19T00:00:00Z +cos-101-17162.210.48: + release-date: 2023-07-19T00:00:00Z + deprecated: true +cos-97-16919.294.48: + release-date: 2023-07-19T00:00:00Z +cos-93-16623.402.40: + release-date: 2023-07-19T00:00:00Z + deprecated: true +mitigation-6.1-v2: + release-date: 2023-07-08T17:20:00Z + available-until: 2023-10-21T12:00:00Z + +lts-6.1.35: + release-date: 2023-06-30T14:05:00Z + +lts-6.1.31: + release-date: 2023-06-14T16:00:00Z + vmlinux: false +cos-105-17412.101.17: + release-date: 2023-06-14T16:00:00Z + vmlinux: false +cos-101-17162.127.42: + release-date: 2023-06-14T16:00:00Z + deprecated: true + vmlinux: false +cos-97-16919.294.28: + release-date: 2023-06-14T16:00:00Z + vmlinux: false +cos-93-16623.341.29: + release-date: 2023-06-14T16:00:00Z + deprecated: true + vmlinux: false +mitigation-6.1-broken: + release-date: 2023-06-14T16:00:00Z + deprecated: true + vmlinux: false + available-until: 2023-07-08T17:20:00Z \ No newline at end of file diff --git a/kernelctf/server/server.py b/kernelctf/server/server.py new file mode 100755 index 00000000..a7090363 --- /dev/null +++ b/kernelctf/server/server.py @@ -0,0 +1,206 @@ +#!/usr/bin/env -S python3 -u +import os +import re +import traceback +import tempfile +import sys +import hmac +import hashlib +import server_secrets +import time +import subprocess +import json +from datetime import datetime, timezone + +RELEASES_YAML = 'releases.yaml' +SLOTS_JSON = 'slots.json' + +sys.path.append('/usr/local/lib/python3.9/dist-packages') +from httplib2 import Http +import yaml + +os.chdir(os.path.dirname(__file__)) +isDevel = os.path.basename(__file__) == 'server_devel.py' or '--devel' in sys.argv +now = datetime.now(timezone.utc) +release_dir = './releases_new' if isDevel else './releases' + +def chat_msg(msg, mention=False): + if mention: + msg = ' ' + msg + + if isDevel: + print('chat_msg: ' + msg) + return + + Http().request(uri=server_secrets.webhook_url, method='POST', headers={'Content-Type': 'application/json; charset=UTF-8'}, body=json.dumps({'text': msg})) + +def warning(msg): + if isDevel: + print(f'[WARNING] {msg}') + +def get_releases(): + with open(RELEASES_YAML, 'r') as f: releases = yaml.safe_load(f) + + target_latest = {} + for release_id, release in list(releases.items()): + if not os.path.exists(f'{release_dir}/{release_id}'): + warning(f'release {release_id} not found in the {release_dir} folder') + del releases[release_id] + continue + + m = re.match(r'(?Plts|mitigation(-v3)?|cos-\d+)-(?P\d+(\.\d+)+)', release_id) + if m is None: + warning(f'release {release_id} does not match regex') + del releases[release_id] + continue + + released = release['release-date'] <= now + + if release.get('available-until', now) < now: + release['deprecated'] = True + + target = m.group('target') + + if released and not release.get('deprecated', False): + if not target in target_latest or target_latest[target]['release-date'] < release['release-date']: + target_latest[target] = release + + release['id'] = release_id + release['released'] = released + release['target'] = target + + for release in releases.values(): + release['latest'] = target_latest.get(release['target']) == release + if not release['released']: + release['status'] = 'future' + elif release['latest']: + release['status'] = 'latest' + else: + release['status'] = 'deprecated' + + return releases + +def get_slots(): + if not os.path.isfile(SLOTS_JSON): return {} + with open('slots.json', 'rt') as f: return json.load(f) + +def print_releases(releases, slots, deprecated_only): + def print_filtered(name, status_filter): + filtered = [r for r in releases.values() if r['status'] == status_filter] + if len(filtered) == 0: return + + print(f'{name}:') + for release in filtered: + taken = slots.get(release["id"]) + takenStr = f" | Slot is taken by {taken} (probably not eligible anymore)" if taken else "" + availableStr = f" | Deprecation date: {release['available-until'].strftime('%Y-%m-%d %H:%M')}Z" if 'available-until' in release else "" + print(f' - {release["id"].ljust(24)} | Release date: {release["release-date"].strftime("%Y-%m-%d %H:%M").ljust(12)}Z{takenStr}{availableStr}') + print() + + if deprecated_only: + print_filtered('Deprecated targets', 'deprecated') + else: + print_filtered('Current targets', 'latest') + print_filtered('Future targets', 'future') + +def are_you_sure(prompt): + print(prompt) + res = input("Are you sure you want to continue? (y/n) ") == "y" + print() + return res + +def main(): + releases = get_releases() + slots = get_slots() + + print(f'Server time: {now.strftime("%Y-%m-%dT%H:%M:%S")}Z') + print() + + show_deprecated = False + while True: + print_releases(releases, slots, show_deprecated) + print('Select a target (or type "deprecated" to see deprecated targets):') + release_id = input().strip() + if release_id == "exit" or release_id == "q" or release_id == "quit": + return + + print() + + if release_id == "deprecated": + show_deprecated = True + continue + show_deprecated = False + + release = releases.get(release_id) + if not release: + print('Invalid target. Expected one of the followings: %s' % ', '.join(releases)) + print() + continue + + while True: + print('Actions:') + print(' run) run target') + print(' info) get information about the target') + print(' back) back to the target list') + print() + action = input().strip() + print() + + # long random generated secret, not bruteforcable + root = hashlib.sha1(action.encode('utf-8')).hexdigest() == server_secrets.root_mode_hash + + if action == 'back': + break + elif action == 'exit' or action == "q" or action == "quit": + return + elif action == 'info': + baseUrl = 'https://storage.googleapis.com/kernelctf-build/releases' + print(f'Kernel image (bzImage): {baseUrl}/{release_id}/bzImage') + if release.get('vmlinux', True): + print(f'Kernel image (vmlinux): {baseUrl}/{release_id}/vmlinux.gz') + print(f'Kernel config: {baseUrl}/{release_id}/.config') + print(f' -> derived from COS config: {baseUrl}/{release_id}/lakitu_defconfig') + print(f'Source code info: {baseUrl}/{release_id}/COMMIT_INFO') + print() + elif root or action == 'run': + flagPrefix = 'invalid:' + if release['status'] == 'future': + flagPrefix = 'future:' + if not are_you_sure('[!] Warning: this target is not released yet and not eligible! Use only for pre-testing.'): + continue + elif release['status'] == 'deprecated': + flagPrefix = 'deprecated:' + if not are_you_sure('[!] Warning: this target is already deprecated and not eligible! Use only for reproduction.'): + continue + elif release['status'] == 'latest': + flagPrefix = '' + + if not (root or (isDevel and input('Skip pow? (y/n) ') == 'y')): + import pow + if not pow.ask(7337): + exit(1) + + print('Executing target %s' % release_id) + + with tempfile.TemporaryDirectory() as temp_dir: + flag_fn = f'{temp_dir}/flag' + with open(flag_fn, 'wt') as f: + flag_content = f'{flagPrefix}v1:{release_id}:{int(time.time())}' + signature = hmac.new(server_secrets.flag_key.encode('utf-8'), flag_content.encode('utf-8'), hashlib.sha1).hexdigest() + flag = f'kernelCTF{{{flag_content}:{signature}}}' + f.write(flag + '\n') + + subprocess.check_call(['./qemu.sh', f'{release_dir}/{release_id}', flag_fn, '/bin/bash' if root else '/home/user/run.sh']) + else: + print('Invalid action. Expected one of the followings: run, info, back') + print() + +try: + main() +except EOFError: + pass +except Exception as e: + print('Something went wrong, please contact us on #kernelctf on Discord (https://discord.gg/A3qZcyaZ69).') + traceback.print_exc() + chat_msg('Server exception: ' + traceback.format_exc()) + diff --git a/kernelctf/server/server_cert.pem b/kernelctf/server/server_cert.pem new file mode 100644 index 00000000..846d5fdf --- /dev/null +++ b/kernelctf/server/server_cert.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBazCCAR2gAwIBAgIUSXiRksvnzRI2WYqh7nDZVoZydOIwBQYDK2VwMCsxKTAn +BgNVBAMMIGtlcm5lbGN0Zi52cnAuY3RmY29tcGV0aXRpb24uY29tMB4XDTIzMDYw +ODIyNDA0MFoXDTMzMDYwNTIyNDA0MFowKzEpMCcGA1UEAwwga2VybmVsY3RmLnZy +cC5jdGZjb21wZXRpdGlvbi5jb20wKjAFBgMrZXADIQCTg2ayrs3BsxUocgbd1eWj +WWVzQQmORR5LT3unlZCzFaNTMFEwHQYDVR0OBBYEFCSsjYgVH8funXWPApo32zpS +NhPgMB8GA1UdIwQYMBaAFCSsjYgVH8funXWPApo32zpSNhPgMA8GA1UdEwEB/wQF +MAMBAf8wBQYDK2VwA0EAxJ+NlnvVYZKj/ctSIzcuPm7+4SlziIHDRW43SrLks15v +KQVTtek3sAifw5NuaXWZrGrX7JAqNqci3QPCMHFEDA== +-----END CERTIFICATE----- diff --git a/kernelctf/server/service.sh b/kernelctf/server/service.sh new file mode 100755 index 00000000..cbb7c17e --- /dev/null +++ b/kernelctf/server/service.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo running! + +cd /home/poprdi +socat ssl-l:1337,reuseaddr,fork,cert=server_cert_and_key.pem,verify=0,openssl-min-proto-version=tls1.3 exec:"nsjail/nsjail --chroot / --user 99999 --group 99999 --disable_clone_newnet --rlimit_cpu 1800 -T /tmp/ -- /usr/bin/timeout 1800 /home/poprdi/server.py"