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:
- 0x00 arg1: equivalent to
head -= 1; memory[head] = arg1;
. Pushes an element to the head.
- 0x01: equivalent to
head += 1;
. Move right by 1.
- 0x02: equivalent to
head -= 1;
. Move left by 1.
- 0x10: equivalent to
head += 2; if (memory[head - 1] == 0) goto +memory[head - 2];
. Jump relative if zero.
- 0x12: equivalent to
head += 2; if (memory[head - 1] != 0) goto +memory[head - 2];
. Jump relative if non-zero.
- 0x20: equivalent to
head += 1; memory[head] = memory[head - 1] + memory[head];
. Add.
- 0x21: equivalent to
head += 1; memory[head] = memory[head - 1] - memory[head];
. Sub.
- 0x22: equivalent to
head += 1; memory[head] = memory[head - 1] * memory[head];
. Mul.
- 0x23: equivalent to
head += 1; memory[head] = memory[head - 1] / memory[head];
. Div.
- 0x24: equivalent to
head += 1; memory[head] = ~(memory[head - 1] & memory[head]);
. Nand.
- 0x40: equivalent to
head += 1; print(memory[head - 1]);
. Print the character directly.
- 0x41: equivalent to
head += 1; print_hex(memory[head - 1]);
. Print the hex.
- 0x42: equivalent to
head += 1; read(memory[head - 1]);
. Read a character from stdin.
- 0x43: equivalent to
head += 1; read_hex(memory[head - 1]);
. Read a hex from stdin.
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}'