偶然发现 OSU 也有办 CTF,但是没参加上,结束后下载了点直接看附件的 Crypto 和 Reverse 来做做
# crypto
# crypto/rot727

ROT13

# crypto/beyond-wood

压缩包内有一个加密脚本和一个 output.png
# script.py | |
from PIL import Image | |
import random | |
FLAG = Image.open("flag.png") | |
width, height = FLAG.size | |
key = [random.randrange(0, 256) for _ in range(width+height+3)] | |
out = FLAG.copy() | |
for i in range(width): | |
for j in range(height): | |
pixel = FLAG.getpixel((i, j)) | |
pixel = tuple(x ^ k for x, k in zip(pixel, key)) | |
newi, newj = (2134266 + i * 727) % width, (4501511 + j * 727) % height | |
out.putpixel((newi, newj), pixel) | |
out.save("output.png") |
加密过程先生成一个 key 字节数组,与每个像素的 RGB 值异或,然后按照某个算法将像素移位
解密直接将像素放回原位即可,不用管像素的异或,颜色可以区分出来
from PIL import Image | |
import random | |
encrypted_img = Image.open("output.png") | |
width, height = encrypted_img.size | |
decrypted = Image.new("RGB", (width, height)) | |
for i in range(width): | |
for j in range(height): | |
newi, newj = (2134266 + i * 727) % width, (4501511 + j * 727) % height | |
pixel = encrypted_img.getpixel((newi, newj)) | |
decrypted.putpixel((i, j), pixel) | |
decrypted.save("decrypted_flag.png") |
还原图像
# crypto/xnor-xnor-xnor

查看加密脚本
import os | |
flag = open("flag.txt", "rb").read() | |
def xnor_gate(a, b): | |
if a == 0 and b == 0: | |
return 1 | |
elif a == 0 and b == 1: | |
return 0 | |
elif a == 1 and b == 0: | |
return 0 | |
else: | |
return 1 | |
def str_to_bits(s): | |
bits = [] | |
for x in s: | |
bits += [(x >> i) & 1 for i in range(8)][::-1] | |
return bits | |
def bits_to_str(bits): | |
return bytes([sum(x * 2 ** j for j, x in enumerate(bits[i:i+8][::-1])) for i in range(0, len(bits), 8)]) | |
def xnor(pt_bits, key_bits): | |
return [xnor_gate(pt_bit, key_bit) for pt_bit, key_bit in zip(pt_bits, key_bits)] | |
key = os.urandom(4) * (1 + len(flag) // 4) | |
key_bits = str_to_bits(key) | |
flag_bits = str_to_bits(flag) | |
enc_flag = xnor(xnor(xnor(flag_bits, key_bits), key_bits), key_bits) | |
print(bits_to_str(enc_flag).hex()) | |
# 7e5fa0f2731fb9b9671fb1d62254b6e5645fe4ff2273b8f04e4ee6e5215ae6ed6c |
加密过程先生成 4 个字节的 key,然后复制到 len(flag) / 4 + 1 的长度,再逐位同或 3 次
同或 3 次相当于同或 1 次,由于已知 flag 前缀为 osu{ ,可以对 4 字节 key 进行爆破
import binascii | |
enc_flag = binascii.unhexlify(b'7e5fa0f2731fb9b9671fb1d62254b6e5645fe4ff2273b8f04e4ee6e5215ae6ed6c') | |
print(enc_flag) | |
key = b'' | |
for k1 in range(256): | |
if chr(~(k1 ^ enc_flag[0]) & 0xFF) == 'o': | |
key += k1.to_bytes(1, 'big') | |
for k2 in range(256): | |
if chr(~(k2 ^ enc_flag[1]) & 0xFF) == 's': | |
key += k2.to_bytes(1, 'big') | |
for k3 in range(256): | |
if chr(~(k3 ^ enc_flag[2]) & 0xFF) == 'u': | |
key += k3.to_bytes(1, 'big') | |
for k4 in range(256): | |
if chr(~(k4 ^ enc_flag[3]) & 0xFF) == '{': | |
key += k4.to_bytes(1, 'big') | |
key = key * (1 + len(enc_flag) // 4) | |
flag = ''.join([chr(~(x ^ y) & 0xFF) for x, y in zip(key, enc_flag)]) | |
print(flag) |

# crypto/pls-nominate

查看加密脚本
from Crypto.Util.number import * | |
FLAG = open("flag.txt", "rb").read() | |
message = bytes_to_long( | |
b"hello there can you pls nominate my map https://osu.ppy.sh/beatmapsets/2436259 :steamhappy: i can bribe you with a flag if you do: " + FLAG | |
) | |
ns = [getPrime(727) * getPrime(727) for _ in range(5)] | |
e = 5 | |
print(len(FLAG)) | |
print(ns) | |
print([pow(message, e, n) for n in ns]) |
RSA 加密,提供了 flag 的长度、5 个 N 、5 个对应的密文 C
由于使用同一个指数 e=5 加密并使用了多个 N ,存在广播攻击,可用中国剩余定理恢复m^5 mod N, N = n1*n2*...*n5
如果 m < n_i, m^5 < N = n1*n2*...*n5 ,直接开方就能得到 m
但是这里 m > n_i ,需要爆破一个 t 使得 m^5 = x+t*N (x = m^5 mod N)
N 为 7266 位, m^5 为 7274 位,差距约为 2^(7274-7266) = 256
k 的搜索范围大约 256 +- 100
from Crypto.Util.number import * | |
import gmpy2 | |
from functools import reduce | |
message = b"hello there can you pls nominate my map https://osu.ppy.sh/beatmapsets/2436259 :steamhappy: i can bribe you with a flag if you do: " + b'osu{' + b'a' * (51 - 4) | |
message = bytes_to_long(message) | |
ns = [250595396282063209494575040924347535718377683482891600245762578107828862102674761114596255712438613333084596184792828788407383247558654559454089469986479081755983932855108019303831103066796233258382489582930536579371270189763162153515702860564938568045151352777820047894231299381399970139347989102071591624967158663442340607621438098266100701196825065519485094630829410736139969891010792546625692049625954656831699035171996469050264653519, 274193894251894220756361545934981580561716587343502325614560502213231097664469582415896327192024233714369720707686115433619798180404789138894067772483823865845960858748702597162370083356345247341014451005796413454404641015516131648586490172413946109076519615677569833823093216546284937656690546486895353267404868698676199564604463636499575466246860736708986174600028222786416474243294353345680209117811499529853520907091701228114817603341, 250799342406461286072464998395337312450245201655027007410237718631499754452822255347456526289099898965321822671514782488741429427305776533799167428860480805487315744950556177330105418061977431001775317350105402223927656557524000999352457757910475375628290615583082601813818635461940350546377443417651889260364946457646723831198413138381669914264256219982213937975414376723125047495586971424241557485231775101178049132539231505246677535311, 424944038623958440933855665649910939213053530859402286143675513220496021786445337272966523453996205752202713497414819150875478398393046433188515528170856189163826639434032436582059667688688880862276540443913407720932133045261989787061306952765245738545030903458101323357019464537917143233906507656465350475977450213990615572573474700619979502153864626401863294100140176446546365233603532530624614938982882757207592863414287369042546971519, 190902360498923981884467164531021372209737709612751157876047387113070619788864781987406983172298076877277648095765822757629049183301893699559004938030166520411955041284763767367633388446007417241478444104664237424445213634253699407015432110027972438018815169650491766550691229860103594060212173628624522421052861949630723039778975476515078589820738377723637869903050748150998629965678848786824733474968760721078438148119740362699143145661] | |
cs = [123451724931477698812069511077458860459906769716993869507604368750013627155203649026878565706523272034474865506508962044958969096479439140090681403538412644329412752922637248582028201974150208943081418296518937380419465322558643434318990712657710219963788075009287648875763482189708828745866942138140694074154628539909488581608681566648945293332282252866416448399029058170554552880389525160322280616124297247776106117085199571903380300602, 25521272212086093871680233376409740841684880441320606653986457586214228433726846827063346040622596803682405351090250464809787622301562945715858274147064371839443966925136930647541454865892780911461753966896009773200289476756676124794087249400266226027218836477923252144492106017794796924100502787481309583584738175138358520327875032255246435556182629496953900460417813322557030951101538854106453327597012091626682332420269075467900021711, 185794248348100154380171644978622218400518238250460812348815837123659842567285820889770059992960889881347901994420631735102166283167843240900863871790624347481898956733174665116495731603736544724548220276282583347110470600528846477853732162784786525839648496719341390265839130761362980916014963941737879183454424512265005367317359376039242988696224018798645014519276397488302687294083964113652155465518932071360150626192713920598188877877, 253586754830185402755129821208401339617813079433078480306550244166018490731187464131167959996920120199131859308803805312785667542198282217092146066559207637833505962392131719788571007971893923218099583927291388588829681723690416962688366275898952085934742953047229390090702589945168823704791405472122768397486450092804662113840124827388352338015003693741233274505595280978468538153880244070082267537407664878646371738748713917087874183934, 179921278884389482689970386454786826216817164768869732618011304299464646110222440114625532548731013316437178194666578920837571688939194292542951671294588526331670811293769249832666220726222224721812635640949602967313922271367769076173808739323285134139353621782784562334885703861549547716097420971226456620806331145247840335617343331077891979324239020388123429770254863290698575175653976557893390825861821917810477967554648397683909903482] | |
e = 5 | |
# CRT 求解 x ≡ m^5 mod N | |
def crt(residues, moduli): | |
total = 0 | |
N_total = 1 | |
for n in moduli: | |
N_total *= n | |
for r, n in zip(residues, moduli): | |
Ni = N_total // n | |
inv = int(gmpy2.invert(Ni, n)) | |
total += r * Ni * inv | |
return total % N_total | |
N_prod = 1 | |
for n in ns: | |
N_prod *= n | |
x = crt(cs, ns) | |
bit_len_m5 = gmpy2.bit_length(pow(message, e)) | |
log2_N = gmpy2.bit_length(N_prod) | |
t_approx = 1 << (bit_len_m5 - log2_N) | |
print(f"Estimated t: {t_approx} (around {t_approx-100} ~ {t_approx+100})") | |
print(f"N bit length: {log2_N}") | |
print(f"m^5 bit length: {bit_len_m5}") | |
found = False | |
prefix = b"hello there can you pls nominate my map https://osu.ppy.sh/beatmapsets/2436259 :steamhappy: i can bribe you with a flag if you do: " | |
for t in range(max(0, t_approx - 100), t_approx + 100): | |
candidate = x + t * N_prod | |
m, exact = gmpy2.iroot(candidate, e) | |
if not exact: | |
continue | |
m_bytes = long_to_bytes(int(m)) | |
if m_bytes.startswith(prefix): | |
print(f"[+] Found! t = {t}") | |
flag = m_bytes[len(prefix):] | |
print(f"Flag: {flag.decode()}") | |
found = True | |
break | |
if not found: | |
print("[-] Failed to find valid m. Try expanding t range?") |

# crypto/linear-feedback

查看加密脚本
from secrets import randbits | |
from math import floor | |
from hashlib import sha256 | |
class LFSR: | |
def __init__(self, key, taps, format): | |
self.key = key | |
self.taps = taps | |
self.state = list(map(int, list(format.format(key)))) | |
def _clock(self): | |
ob = self.state[0] | |
self.state = self.state[1:] + [sum([self.state[t] for t in self.taps]) % 2] | |
return ob | |
def xnor_gate(a, b): | |
if a == 0 and b == 0: | |
return 1 | |
elif a == 0 and b == 1: | |
return 0 | |
elif a == 1 and b == 0: | |
return 0 | |
else: | |
return 1 | |
key1 = randbits(21) | |
key2 = randbits(29) | |
L1 = LFSR(key1, [2, 4, 5, 1, 7, 9, 8], "{:021b}") | |
L2 = LFSR(key2, [5, 3, 5, 5, 9, 9, 7], "{:029b}") | |
bits = [xnor_gate(L1._clock(), L2._clock()) for _ in range(floor(72.7))] | |
print(bits) | |
FLAG = open("flag.txt", "rb").read() | |
keystream = sha256((str(key1) + str(key2)).encode()).digest() * 2 | |
print(bytes([b1 ^ b2 for b1, b2 in zip(FLAG, keystream)]).hex()) |
定义了一个 LSFR,随机生成了两个 key ,给出了 key1 key2 分别生成 72 位密钥流后同或的结果,并用 SHA256(key1+key2) 与 flag 异或加密
由于 key1 与 key2 分别只有 21 和 29 位,根据 LSFR 的特性,密钥流的前 x 位与密钥本身相同
而题目给出 72 位的同或结果 outstream ,可以爆破其中一个密钥(比如爆破 key1 )后得到密钥流 keystream1 , keystream1 ^ outstream 可以反推另一个密钥 outstream2 ,检查 outstream2 能否由 LFSR(key2, [5, 3, 5, 5, 9, 9, 7], "{:029b}") 生成,即可还原 key1 key2
outstream = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0] | |
for key in range(0, 2**21): | |
L1 = LFSR(key, [2, 4, 5, 1, 7, 9, 8], "{:021b}") | |
keystream1 = [L1._clock() for _ in range(72)] | |
outstream2 = [xnor_gate(keystream1[i], outstream[i]) for i in range(72)] | |
L2 = LFSR(int(''.join(map(str, outstream2[:29])), 2), [5, 3, 5, 5, 9, 9, 7], "{:029b}") | |
keystream2 = [L2._clock() for _ in range(72)] | |
if outstream2 == keystream2: | |
key1 = key | |
key2 = int(''.join(map(str, outstream2[:29])), 2) | |
break | |
print(key1, key2) |

但这组 key 无法解出 flag,推测可能存在多组 key,尝试所有组合
from hashlib import sha256 | |
class LFSR: | |
def __init__(self, key, taps, format): | |
self.key = key | |
self.taps = taps | |
self.state = list(map(int, list(format.format(key)))) | |
def _clock(self): | |
ob = self.state[0] | |
self.state = self.state[1:] + [sum([self.state[t] for t in self.taps]) % 2] | |
return ob | |
def xnor_gate(a, b): | |
if a == 0 and b == 0: | |
return 1 | |
elif a == 0 and b == 1: | |
return 0 | |
elif a == 1 and b == 0: | |
return 0 | |
else: | |
return 1 | |
outstream = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0] | |
flag = bytearray(bytes.fromhex("9f7f799ec2fb64e743d8ed06ca6be98e24724c9ca48e21013c8baefe83b5a304af3f7ad6c4cc64fa4380e854e8")) | |
for key in range(0, 2**21): | |
L1 = LFSR(key, [2, 4, 5, 1, 7, 9, 8], "{:021b}") | |
keystream1 = [L1._clock() for _ in range(72)] | |
outstream2 = [xnor_gate(keystream1[i], outstream[i]) for i in range(72)] | |
L2 = LFSR(int(''.join(map(str, outstream2[:29])), 2), [5, 3, 5, 5, 9, 9, 7], "{:029b}") | |
keystream2 = [L2._clock() for _ in range(72)] | |
if outstream2 == keystream2: | |
key1 = key | |
key2 = int(''.join(map(str, outstream2[:29])), 2) | |
print(f"[+] Match found! key1={key1}, key2={key2}") | |
keystream = sha256((str(key1) + str(key2)).encode()).digest() * 2 | |
print(bytes([b1 ^ b2 for b1, b2 in zip(flag, keystream)])) |

# crypto/ssss
查看加密脚本
#!/usr/local/bin/python3 | |
from Crypto.Util.number import * | |
import random | |
p = 2**255 - 19 | |
k = 15 | |
SECRET = random.randrange(0, p) | |
def lcg(x, a, b, p): | |
return (a * x + b) % p | |
a = random.randrange(0, p) | |
b = random.randrange(0, p) | |
poly = [SECRET] | |
while len(poly) != k: poly.append(lcg(poly[-1], a, b, p)) | |
def evaluate_poly(f, x): | |
return sum(c * pow(x, i, p) for i, c in enumerate(f)) % p | |
print("welcome to ssss", flush=True) | |
for _ in range(k - 1): | |
x = int(input()) | |
assert 0 < x < p, "no cheating!" | |
print(evaluate_poly(poly, x), flush=True) | |
if int(input("secret? ")) == SECRET: | |
FLAG = open("flag.txt").read() | |
print(FLAG, flush=True) |
加密使用了 LCG 线性同余生成器
初始生成三个随机参数 s0 , a , b ,再通过 LCG 生成 s1~s14 (所有计算都在模 p 域中)
题目允许输入 14 次 ,每次给出
可通过输入 1-14,获得 到,得到 14 个方程,但由于 S 共有 15 个,无法直接解出
考虑当 n>0 时,存在递推关系,因此可以把所有 都转化为含有 的表达式,然后尝试解出含有 S0 , a , b 14 个方程的方程组
from pwn import * | |
nc = remote("ssss.challs.sekai.team", 1337) | |
print(nc.recvline().decode().strip()) | |
print(nc.recvline().decode().strip()) | |
points_ = [] | |
for x in range(1, 15): | |
nc.sendline(str(x).encode()) | |
y = int(nc.recvline().decode().strip()) | |
print((x, y)) | |
points_.append((x, y)) | |
from sage.all import * | |
p = 2**255 - 19 | |
k = 15 | |
F = GF(p) | |
# 建立多元多项式环(s,a,b) | |
R = PolynomialRing(F, ['s','a','b']) | |
s, a, b = R.gens() | |
# 根据递推公式计算系数 S_i (s, a, b) | |
def build_coefficients(s, a, b, k): | |
coeffs = [s] | |
for i in range(1, k): | |
next_c = a * coeffs[-1] + b | |
coeffs.append(next_c) | |
return coeffs | |
# 构造多项式 f (x) = sum_{i=0}^{14} S_i * x^i | |
coeffs = build_coefficients(s, a, b, k) | |
def evaluate_poly(x_val): | |
return sum(coeffs[i] * (x_val**i) for i in range(k)) | |
# 收集点后建立方程 | |
equations = [] | |
for x, y in points_: | |
eq = evaluate_poly(x) - y | |
equations.append(eq) | |
# 尝试消元或 Groebner basis 求解 | |
I = ideal(equations) | |
GB = I.groebner_basis() | |
print(I) | |
print(GB) | |
s_val = (-Integer(GB[0].subs({s:0,a:0,b:0}))) % p | |
a_val = (-Integer(GB[1].subs({s:0,a:0,b:0}))) % p | |
b_val = (-Integer(GB[2].subs({s:0,a:0,b:0}))) % p | |
def build_coeffs(s0, a0, b0, k, p): | |
cs = [int(s0)] | |
for i in range(1, k): | |
cs.append((a0 * cs[-1] + b0) % p) | |
return cs | |
cs = build_coeffs(s_val, a_val, b_val, 15, p) | |
for (x,y) in points_: | |
acc = 0 | |
xp = 1 | |
for ci in cs: | |
acc = (acc + ci * xp) % p | |
xp = (xp * int(x)) % p | |
assert acc == int(y), "验证失败" | |
print("所有点验证通过!") | |
print(nc.read()) | |
nc.sendline(str(s_val).encode()) | |
print(nc.recvline().decode().strip()) |

# crypto/please-nominate

查看加密脚本
from Crypto.Util.number import * | |
FLAG = open("flag.txt").read() | |
BNS = ["Plus4j", "Mattay", "Dailycore"] | |
print(len(FLAG)) | |
for BN in BNS: | |
print("message for", BN) | |
message = bytes_to_long( | |
(f"hi there {BN}, " + FLAG).encode() | |
) | |
n = getPrime(727) * getPrime(727) | |
e = 3 | |
print(n) | |
print(pow(message, e, n)) |
RSA 加密,已知明文为 "hi there {BN}, " 开头,给了 3 组 N 和密文 C
已知明文的高位,且使用相同的 e=3 ,属于 stereotyped message 模版消息攻击:
用 表示明文高位, 表示明文低位,明文,因此有
当 时,可通过 Coppersmith 方法快速解出,但这里的 约为 727+727=1454 位,FLAG 的长度为 147 字节,需要求的 约为 8*147=1176 位,不满足该条件,无法直接解得
题目给出了 3 组参数,可以构造出 3 个同余方程,然后通过 CRT 合并为一个方程:
对于每组参数,将方程展开后,得到
合并后的方程中, 的系数可通过 sage 函数 CRT_list() 直接计算得到
CRT_list([ ],[ ])
也就是满足
from Crypto.Util.number import * | |
from sage.all import * | |
BNS = ["Plus4j", "Mattay", "Dailycore"] | |
ns = [398846569478640111212929905737126219425846611917845064245986310899352455531776606361272505433849914145167344554995030812644189047710542954339906669786929747875597103059283954786345252202913509966200329213618547501451752329008531151228646387182403280019664272348231587940227470159846477386295856419407431569159867135365878913551268186765163877676657618137090681937329865631964114691373454627873900294385351135992352798790940857277941472243, 263921537800979838796221921202623647462415714721726394821753160972868778052085367522658133754602607941536627474441882978361116817475949497489399939969612509386335591643019109294105788234910211931439396509289221190345347268312099449152342020093136914687793357372601654532872673983206837150636881928382445566331068237688851345596537893940860402116702078271640048006152159670916299390559068168682951764623558492401318864545619303656361575039, 244031565800210621970295548144726813179733488382314342571474949081381534498271587584918252306707810369627816196681952536809552862200098862267362735277533022204353497254323228274491354364582967316701126783750290886096960998524414107270804949357307485711415647006606381700355291677615735646360495158637105102935658791364633128882874894799509049190571860852436246528522948513282608143921733438326627413507376148669722384969604597622237576689] | |
cs = [158222951303542921410153264594688628146576794503998427093311713650774531277430572380170172031191979123500854263417781975728851277579707820383487572964731381023292312261571110891911863884482902461410218286288183400964045679561296043109527250114073394295486982996930960424139332989226106162113091535475425207610140495532136130360540296097313129598880764160739736823532823136291009499982471028009894097569348660440830784543668141509385395632, 166535918659821916010028099769670832129351247306732465032191446110439632420210106966178779555368253320514619095691319433325610616798380002564235371548166904170934635615481900225094067835806190805967329346507481446735982286060193425107057243896658750175391024795867108700750688771820192759373880324263838550825384190714299697136118712791588291627904942573970568726142296145821435413605295796704576678263679112272532276173497584694652201371, 132630164676661516893599967289982601955380588903536428955472887691456873565355730161547637935630009622741758822400797894281114572338413548852823840995609427904180355965179631480101131212344121270312182355342132383562008365671754190667582150820337165104642347684925014217687937383396003151315200366708307846959894632317624125785370063052712288658901699972320248281442796056361927746314341729204052997736037101265782906021307934740092047950] | |
N = prod(ns) | |
x = PolynomialRing(ZZ, 'x').gen() # CRT 操作需要在整数环上进行 | |
flag_len = 147 | |
polys = [] | |
for BN, n, c in zip(BNS, ns, cs): | |
prefix = f"hi there {BN}, " | |
message = bytes_to_long( | |
(prefix + '\0' * flag_len).encode() | |
) | |
poly = (message + x) ** 3 - c | |
polys.append(poly) | |
Fs = CRT_list(polys, ns).change_ring(Zmod(N)) # small_roots 需要在模 N 环上进行 | |
roots = [] | |
# 爆破参数,这一题里面 beta=0.8 才能解出 x | |
#for beta in [0.01 * i for i in range(1, 100)]: | |
# rts = Fs.small_roots(X=2**(8*(flag_len)), beta=beta) | |
# if rts: | |
# roots.append((beta, rts[0])) | |
root = Fs.small_roots(X=2**(8*(flag_len)), beta=0.8)[0] | |
print(long_to_bytes(Integer(root))) |

# reverse
# rev/bleh

压缩包内给了 3800 + 个文件,每个 15KB,打开第一个看到 ELF 文件头
编写脚本把所有片段合并在一个文件中
import os | |
elf = b'' | |
for block in os.listdir('dist'): | |
elf += open(f'dist/{block}', 'rb').read() | |
with open('elf', 'wb') as f: f.write(elf) |
IDA 分析文件,查看 main 函数
输入为 0x20 字节,逐字节进行 sub_1337() 操作,最后与 s2 进行对比
核心逻辑在 sub_1337 函数
除了 SSE 指令的混淆代码,还调用了两个函数 sub_11C9 和 sub_1280

丢给 AI 分析,可知 sub_11C9(x, k) = x + k , sub_1280(x, k) = x - k
观察后续操作
有效的指令为 xor 和 add
还原函数逻辑,最后只取一个字节
def sub_1337(a1, a2, n): | |
a1 += 6 | |
a2 += 128 | |
n -= 128 | |
return (n + (a1 ^ a2)) & 0xFF |
逐个字节爆破即可
key_str = "PL4YING_CTFS_ISNTBETTER_THAN_OSU" | |
# 小端序 | |
target_bytes = bytes.fromhex( | |
"7393F1587D9FCB34" | |
"A90B7CE14674DD4F" | |
"BD3AB92183F65AC2" | |
"24A31A93FC75E447" | |
) | |
state = 4919 | |
flag = bytearray() | |
for i in range(32): | |
k = ord(key_str[i]) | |
t2 = k + 128 | |
for c in range(256): | |
t1 = c + 6 | |
xor_val = t1 ^ t2 | |
new_state = (state - 128) + xor_val | |
if (new_state & 0xFF) == target_bytes[i]: | |
flag.append(c) | |
state = new_state | |
break | |
print("Flag:", flag.decode('ascii', errors='replace')) |

# rev/tosu-1

tosu.exe chal1.map 运行
一个 note 都不打,最后显示 1080 个 miss
IDA 打开,查看字符串
定位到函数 sub_7FF73FBF58D0() ,大概查看代码应该是游戏主要逻辑
最后结算成绩显示部分,根据 v53 的值输出不同内容
追踪值的生成流程,根据 AI 的分析有两个关键函数 sub_7FF73FBF17A0 和 sub_7FF73FC3A870

sub_7FF73FBF17A0 是一个自定义的 Hash 函数,输出的 Hash 值作为密钥传入 sub_7FF73FC3A870 进行 AES 解密处理
因此需要游戏达到某个条件(应该是全打 300),才会输出正确的 Hash,进而正确解密 flag 数据
观察 Hash 函数 sub_7FF73FBF17A0 ,其中的第三个参数 Block 在代码中多次出现,分析可知这个变量存储了每个 note 的判定结果,所以可以考虑修改程序逻辑,将 miss 等非 300 的判定修改为 300
以下是判断 miss 的逻辑, v3 为某次输入的时间,当 v3 > noteTime + 0.2 判定为 miss
其中 v6 就是 Block , v8 是偏移量,因此 miss 对应的值为 4
往下查看当非 miss 时的分数计算逻辑,可以分析出 300 对应的内存值为 3
直接 patch miss 的判定逻辑,将内存赋值统一修改为 3
同时 nop 掉 0.2 秒的时间判定,可以直接结束游戏,不用等待谱面播放完


运行后虽然左上角显示为 miss,但内存中的数据是判定为 300,最终成功输出 flag