Sep 30, 2023

BuckeyeCTF 2023

Currency Converter (rev)

File

Need to convert USD to another currency? Well I hope its either Euros, Canadian, or Yen!

Use any Java decompiler.

coding (crypto)

File

i hear data modeling is compression, what's your opinion on ascii being uniform? also, apparently arithmetic coding is lossless?

Apparently copying some random code online solves it...

content = b"" with open("flag.compressed", "rb") as f: content = f.read() val = int.from_bytes(content) print(val) ascii = 128 prob = [1 / ascii] * ascii print(prob) prefix = [0 for i in range(ascii + 1)] prefix[0] = 0.0 for i in range(1, ascii + 1): prefix[i] = prefix[i - 1] + prob[i - 1] l = 80 ans = "" delta = 2 ** (len(content) * 8) for i in range(l): tmp = [round(x * delta) for x in prefix] it = 0 while it < ascii and tmp[it] < val: it += 1 it -= 1 ans += chr(it) val -= tmp[it] delta = tmp[it + 1] - tmp[it] print(ans)

Skribl (rev)

File

The modern paste service for the New World.

https://skribl.chall.pwnoh.io

Open the file and see that it imports some backend but we do not have the python file for it. However, we can find that backend.cpython-313.pyc is somehow in __pycache__/. Based on the naming we can see that the file is generated by Python 3.13 (which is in alpha now). So we need to download it and build it ourselves.

Once we build Python 3.13 we can straight up decompile the code:

$ ./cpython/python -i backend.cpython-313.pyc >>> import dis >>> dis.dis(init_backend) 18 0 RESUME 0 19 2 LOAD_GLOBAL 0 (random) 12 LOAD_ATTR 2 (seed) 32 PUSH_NULL 34 LOAD_GLOBAL 4 (math) 44 LOAD_ATTR 6 (floor) 64 PUSH_NULL 66 LOAD_GLOBAL 8 (time) 76 LOAD_ATTR 8 (time) 96 PUSH_NULL 98 CALL 0 106 CALL 1 114 CALL 1 122 POP_TOP 21 124 LOAD_GLOBAL 11 (create_skribl + NULL) 134 LOAD_FAST 0 (skribls) 136 LOAD_GLOBAL 12 (os) 146 LOAD_ATTR 14 (environ) 166 LOAD_CONST 1 ('FLAG') 168 BINARY_SUBSCR 172 LOAD_CONST 2 ('rene') 174 CALL 3 182 POP_TOP 184 RETURN_CONST 0 (None) >>> dis.dis(create_skribl) 8 0 RESUME 0 9 2 LOAD_GLOBAL 1 (print + NULL) 12 LOAD_CONST 1 ('Creating skribl ') 14 LOAD_FAST 1 (message) 16 FORMAT_SIMPLE 18 BUILD_STRING 2 20 CALL 1 28 POP_TOP 11 30 LOAD_GLOBAL 2 (string) 40 LOAD_ATTR 4 (ascii_lowercase) 60 LOAD_GLOBAL 2 (string) 70 LOAD_ATTR 6 (ascii_uppercase) 90 BINARY_OP 0 (+) 94 LOAD_GLOBAL 2 (string) 104 LOAD_ATTR 8 (digits) 124 BINARY_OP 0 (+) 128 STORE_FAST 3 (alphabet) 12 130 LOAD_GLOBAL 11 (range + NULL) 140 LOAD_CONST 2 (40) 142 CALL 1 150 GET_ITER 152 LOAD_FAST_AND_CLEAR 4 (i) 154 SWAP 2 156 BUILD_LIST 0 158 SWAP 2 >> 160 FOR_ITER 25 (to 214) 164 STORE_FAST 4 (i) 166 LOAD_GLOBAL 12 (random) 176 LOAD_ATTR 14 (choice) 196 PUSH_NULL 198 LOAD_FAST 3 (alphabet) 200 CALL 1 208 LIST_APPEND 2 210 JUMP_BACKWARD 27 (to 160) >> 214 END_FOR 216 STORE_FAST 5 (key_list) 218 STORE_FAST 4 (i) 14 220 LOAD_CONST 3 ('') 222 LOAD_ATTR 17 (join + NULL|self) 242 LOAD_FAST 5 (key_list) 244 CALL 1 252 STORE_FAST 6 (key) 15 254 LOAD_FAST_LOAD_FAST 18 (message, author) 256 BUILD_TUPLE 2 258 LOAD_FAST_LOAD_FAST 6 (skribls, key) 260 STORE_SUBSCR 16 264 LOAD_FAST 6 (key) 266 RETURN_VALUE None >> 268 SWAP 2 270 POP_TOP 12 272 SWAP 2 274 STORE_FAST 4 (i) 276 RERAISE 0 ExceptionTable: 156 to 214 -> 268 [2]

Observing the code we see that init_backend uses time.time() as the seed for randomization and calls create_skribl to create a post with the flag. Fortunately the website shows the time elapsed since it was online in the source code, so we effectively know a range of the possible random seeds. We can use these seeds to create posts locally and get the post key, which we can then query online with Burp:

import math import time import random from flask import Flask, render_template, redirect, url_for, request from flask_bootstrap import Bootstrap5 from flask_wtf import FlaskForm, CSRFProtect from wtforms import StringField, SubmitField from wtforms.validators import DataRequired, Length # Don't try this at home, kids try: from backend import create_skribl, init_backend except: from .backend import create_skribl, init_backend app = Flask(__name__) app.secret_key = 'tO$&!|0wkamvVia0?n$NqIRVWOG' bootstrap = Bootstrap5(app) csrf = CSRFProtect(app) skribls = {} stime = 1696023750 # grab from the source code of the website init_backend(skribls) for i in range(stime, stime + 100): random.seed(i) create_skribl(skribls, '111', 'rene') for k in skribls.keys(): print(k)

Tape (rev)

File

Modern day architectures are too complicated.

Throw the executable into Ghidra and see that it is made with Rust. With some observation we can see that apparently it is a intepreter that executes a program file (the first argument). With the hint of the problem title and analyzing belt::main we can quickly see that the program works on a 64-byte cycling tape memory (represented by a VecDeque in Rust). We can further find that every address is initialized to 0xbb. Relevant decompiled code at 0x00109f69 is shown below:

vector[0xc] = 0xbbbbbbbb; vector[0xd] = 0xbbbbbbbb; vector[0xe] = 0xbbbbbbbb; vector[0xf] = 0xbbbbbbbb; vector[8] = 0xbbbbbbbb; vector[9] = 0xbbbbbbbb; vector[10] = 0xbbbbbbbb; vector[0xb] = 0xbbbbbbbb; vector[4] = 0xbbbbbbbb; vector[5] = 0xbbbbbbbb; vector[6] = 0xbbbbbbbb; vector[7] = 0xbbbbbbbb; *vector = 0xbbbbbbbb; vector[1] = 0xbbbbbbbb; vector[2] = 0xbbbbbbbb; vector[3] = 0xbbbbbbbb; cap = 0x40; head = 0; len = 0x40; local_160 = 0;

We observe that the intepreter will then act based on the program and execute the commands in it. Very annoyingly we can try to work through the decompiled code and map all the functionalities:

We can then write a decompiler to figure out what the given program does:

import os with open('flag_checker', 'rb') as f: content = f.read() cur = 0 while cur < len(content): print(f"{cur} ",end="") if content[cur] == 0x1 or content[cur] == 0x2: l = 0 while content[cur] == content[cur + l]: l = l + 1 if content[cur] == 1: print(f"right {l}") else: print(f"left {l}") cur = cur + l continue elif content[cur] == 0x0: print(f"set {content[cur + 1]} {content[cur + 1].to_bytes()}") cur = cur + 2 continue elif content[cur] == 0x10: print(f"conditional jump if zero") elif content[cur] == 0x12: print(f"conditional jump if non-zero") elif content[cur] == 0x20: print(f"add") elif content[cur] == 0x21: print(f"sub") elif content[cur] == 0x22: print(f"mul") elif content[cur] == 0x23: print(f"div") elif content[cur] == 0x24: print(f"nand") elif content[cur] == 0x40: print(f"print") elif content[cur] == 0x41: print(f"print_hex") elif content[cur] == 0x42: print(f"input") elif content[cur] == 0x43: print(f"input_hex") else: print(f"unknown {content[cur]}") cur = cur + 1

The output is very long. However, the end part is interesting:

10129 conditional jump if non-zero 10130 right 66 10196 set 187 b'\xbb' 10198 right 63 10261 sub 10262 set 40 b'(' 10264 conditional jump if zero 10265 set 73 b'I' 10267 print 10268 set 110 b'n' 10270 print 10271 set 118 b'v' 10273 print 10274 set 97 b'a' 10276 print 10277 set 108 b'l' 10279 print 10280 set 105 b'i' 10282 print 10283 set 100 b'd' 10285 print 10286 set 32 b' ' 10288 print 10289 set 70 b'F' 10291 print 10292 set 108 b'l' 10294 print 10295 set 97 b'a' 10297 print 10298 set 103 b'g' 10300 print 10301 set 33 b'!' 10303 print 10304 unknown 80 10305 set 67 b'C' 10307 print 10308 set 111 b'o' 10310 print 10311 set 114 b'r' 10313 print 10314 set 114 b'r' 10316 print 10317 set 101 b'e' 10319 print 10320 set 99 b'c' 10322 print 10323 set 116 b't' 10325 print 10326 set 33 b'!' 10328 print

Observe that to achieve 'Correct!', the program must not jump at address 10129 and must jump at address 10264. We can check the program and see that there are 29 pairs of jump instructions for 29 bytes of input. We can conjecture that one pair of jump instructions checks one byte. Therefore, we need to ensure that the second byte from the head is zero prior to every jump instruction. We can then write a script to enumerate the flag one byte by one byte:

import subprocess FLAG_LEN = 29 s = b"" flag = [b"a"] * FLAG_LEN with open("flag_checker", "rb") as f: content = f.read() cur = 0 jump_list_10 = [] jump_list_12 = [] while cur < len(content): if content[cur] == 0x1 or content[cur] == 0x2: l = 0 while content[cur] == content[cur + l]: l = l + 1 cur = cur + l continue elif content[cur] == 0x0: cur = cur + 2 continue elif content[cur] == 0x10: jump_list_10.append(cur) elif content[cur] == 0x12: jump_list_12.append(cur) cur = cur + 1 for id, (jump_10, jump_12) in enumerate(zip(jump_list_10, jump_list_12)): with open("program_10", "wb") as f: s = content[:jump_10] s += b"\x01\x41" f.write(s) with open("program_12", "wb") as f: s = content[:jump_12] s += b"\x01\x41" f.write(s) is_find = False for ch in range(0x20, 0x7f): flag[FLAG_LEN - 1 - id] = ch.to_bytes() input = b"\n".join(flag) ss_10 = subprocess.run( ["./belt", "program_10"], stdout=subprocess.PIPE, input=input ).stdout ss_12 = subprocess.run( ["./belt", "program_12"], stdout=subprocess.PIPE, input=input ).stdout if ss_10.startswith(b"0x0\n") and ss_12.startswith(b"0x0\n"): print(f"Current flag: {b''.join(flag)}") is_find = True break if is_find == False: print("Not finding the flag... :(") break

Running it gives us the flag:

Current flag: b'aaaaaaaaaaaaaaaaaaaaaaaaaaaa}' Current flag: b'aaaaaaaaaaaaaaaaaaaaaaaaaaad}' Current flag: b'aaaaaaaaaaaaaaaaaaaaaaaaaaNd}' Current flag: b'aaaaaaaaaaaaaaaaaaaaaaaaauNd}' Current flag: b'aaaaaaaaaaaaaaaaaaaaaaaa0uNd}' Current flag: b'aaaaaaaaaaaaaaaaaaaaaaaR0uNd}' Current flag: b'aaaaaaaaaaaaaaaaaaaaaa_R0uNd}' Current flag: b'aaaaaaaaaaaaaaaaaaaaat_R0uNd}' Current flag: b'aaaaaaaaaaaaaaaaaaaaHt_R0uNd}' Current flag: b'aaaaaaaaaaaaaaaaaaagHt_R0uNd}' Current flag: b'aaaaaaaaaaaaaaaaaa1gHt_R0uNd}' Current flag: b'aaaaaaaaaaaaaaaaaR1gHt_R0uNd}' Current flag: b'aaaaaaaaaaaaaaaa_R1gHt_R0uNd}' Current flag: b'aaaaaaaaaaaaaaa3_R1gHt_R0uNd}' Current flag: b'aaaaaaaaaaaaaaM3_R1gHt_R0uNd}' Current flag: b'aaaaaaaaaaaaa_M3_R1gHt_R0uNd}' Current flag: b'aaaaaaaaaaaan_M3_R1gHt_R0uNd}' Current flag: b'aaaaaaaaaaa1n_M3_R1gHt_R0uNd}' Current flag: b'aaaaaaaaaap1n_M3_R1gHt_R0uNd}' Current flag: b'aaaaaaaaaSp1n_M3_R1gHt_R0uNd}' Current flag: b'aaaaaaaa_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'aaaaaaau_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'aaaaaa0u_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'aaaaaY0u_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'aaaa{Y0u_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'aaaf{Y0u_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'aatf{Y0u_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'actf{Y0u_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'bctf{Y0u_Sp1n_M3_R1gHt_R0uNd}' Current flag: b'bctf{Y0u_Sp1n_M3_R1gHt_R0uNd}'