Skip to content

Commit eb00daa

Browse files
committed
+
1 parent 947ea42 commit eb00daa

File tree

6 files changed

+266
-1
lines changed

6 files changed

+266
-1
lines changed

_posts/2025-10-28-osugamingCTF2025.md

Lines changed: 266 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,272 @@ dddfasdsasdasdd
8888

8989
### 분석 과정
9090

91-
.
91+
![_](/assets/img/posts/2025-10-28-14.png){: style="max-width: 100%; height: auto;"}
92+
93+
`tosu-1`문제와 로직 나머지는 비슷하지만, 올 콤보를 해야하는것이 아닌 특정 값을 맞춰야 하는 문제입니다.
94+
95+
`GIVEN`값은 변하지 않으며, `CALCED`값은 지난 성공 처리하던 곳에 인자로 들어갑니다.
96+
97+
![_](/assets/img/posts/2025-10-28-15.png){: style="max-width: 100%; height: auto;"}
98+
99+
![_](/assets/img/posts/2025-10-28-16.png){: style="max-width: 100%; height: auto;"}
100+
101+
점수 타입 및 노트 인덱스를 이용해 연산하는 로직이 보입니다. `4c00`함수와 `1670`함수, `53c0`함수 및 초기값, 기대값을 ai에 넣어주면 바로 풀어주긴 합니다만 손으로 직접 다시 풀어보겠습니다.
102+
103+
ai가 짜준 코드는 아래와 같습니다.
104+
105+
```python
106+
# -*- coding: utf-8 -*-
107+
# HIT/MISS 스케줄을 계산하는 레퍼런스 구현
108+
# - 상태는 128바이트
109+
# - M: out[p] = in[p-1] XOR in[p+1] (경계 밖은 0)
110+
# - HIT: 위치 (t & 0x7F)에 c_t = (t*0x1B + 0x37) & 0xFF 를 XOR한 뒤 M
111+
# - MISS: M 만 수행
112+
# - 최종 상태를 목표 바이트열(Target)과 같게 만드는 h_t ∈ {0,1} 시퀀스를 Gaussian 소거로 풂 (GF(2))
113+
114+
from typing import List, Tuple
115+
116+
L = 128 # bytes
117+
MASK_128 = (1 << L) - 1
118+
119+
# ---------- 비트-평면 표현 도우미 ----------
120+
121+
def bytes_to_planes(bs: bytes) -> List[int]:
122+
"""128바이트 -> 8개 비트평면(각 128비트 int). plane[b]의 bit p는 bs[p]의 b번째 비트."""
123+
assert len(bs) == L
124+
planes = [0]*8
125+
for p, val in enumerate(bs):
126+
for b in range(8):
127+
if (val >> b) & 1:
128+
planes[b] |= (1 << p)
129+
return planes
130+
131+
def planes_to_bytes(planes: List[int]) -> bytes:
132+
"""8개 비트평면 -> 128바이트"""
133+
out = bytearray(L)
134+
for p in range(L):
135+
v = 0
136+
for b in range(8):
137+
if (planes[b] >> p) & 1:
138+
v |= (1 << b)
139+
out[p] = v & 0xFF
140+
return bytes(out)
141+
142+
def pack_planes_to_bigint(planes: List[int]) -> int:
143+
"""8개 평면을 1024비트 정수로 패킹(plane 0이 낮은 비트 구간). 열(column) 벡터로 취급한다."""
144+
big = 0
145+
for b in range(8):
146+
big ^= (planes[b] << (b * L))
147+
return big
148+
149+
def bigint_to_planes(big: int) -> List[int]:
150+
"""1024비트 정수를 8개 평면으로 역변환"""
151+
planes = []
152+
for b in range(8):
153+
planes.append( (big >> (b * L)) & MASK_128 )
154+
return planes
155+
156+
# ---------- 변환 M (FUN_1400053c0) ----------
157+
158+
def mix_planes(planes: List[int]) -> List[int]:
159+
"""M: out[p] = in[p-1] XOR in[p+1] on each bit-plane with zero boundary."""
160+
out = []
161+
for b in range(8):
162+
x = planes[b]
163+
out.append( ((x << 1) ^ (x >> 1)) & MASK_128 )
164+
return out
165+
166+
def advance_planes(planes: List[int], steps: int) -> List[int]:
167+
"""M^steps (naive). T가 매우 크면 시간이 늘어남."""
168+
cur = planes[:]
169+
for _ in range(steps):
170+
cur = mix_planes(cur)
171+
return cur
172+
173+
# ---------- 한 스텝 HIT 효과 U_t 계산 ----------
174+
175+
def hit_injection_vector(t: int) -> bytes:
176+
"""U_t: 위치 (t & 0x7F)에 c_t = (t*0x1B + 0x37) & 0xFF 만 비트가 있는 128바이트"""
177+
j = t & 0x7F
178+
c = (t * 0x1B + 0x37) & 0xFF
179+
bs = bytearray(L)
180+
bs[j] = c
181+
return bytes(bs)
182+
183+
# ---------- 가우스 소거 (GF(2))로 A*h = delta 풀기 ----------
184+
# A = [v_0 ... v_{T-1}] (1024비트 column 들), delta = 1024비트
185+
186+
def solve_gf2_columns(columns: List[int], delta: int) -> Tuple[bool, List[int]]:
187+
"""
188+
columns: 길이 T, 각 원소는 1024비트(int) column
189+
delta : 1024비트(int)
190+
반환: (성공여부, h(0/1) 리스트)
191+
"""
192+
T = len(columns)
193+
# basis: pivot_bit -> (col_vector, combination_bitmask)
194+
basis_vec = {} # pivot_bit -> int(1024b)
195+
basis_comb = {} # pivot_bit -> int(Tb), 원본 column들의 조합
196+
# 각 column에 대한 combination bitmask (초기엔 자기 자신만 1)
197+
combs = [1 << i for i in range(T)]
198+
# 빌드 (column basis 생성)
199+
for i, col in enumerate(columns):
200+
v = col
201+
c = combs[i]
202+
# pivot 낮은 비트부터/높은 비트부터 아무거나 가능. 여기선 높은 비트 우선.
203+
while v:
204+
p = v.bit_length() - 1 # pivot 위치
205+
if p in basis_vec:
206+
v ^= basis_vec[p]
207+
c ^= basis_comb[p]
208+
else:
209+
basis_vec[p] = v
210+
basis_comb[p] = c
211+
break
212+
# v==0 이면 기존 basis로 표현 가능(선형 종속)
213+
# delta를 basis로 소거
214+
w = delta
215+
sol_comb = 0
216+
while w:
217+
p = w.bit_length() - 1
218+
if p not in basis_vec:
219+
# 해 없음
220+
return False, []
221+
w ^= basis_vec[p]
222+
sol_comb ^= basis_comb[p]
223+
# sol_comb 의 각 비트가 해당 column(=스텝)을 사용할지(HIT) 결정
224+
hits = [ (sol_comb >> i) & 1 for i in range(T) ]
225+
return True, hits
226+
227+
# ---------- 메인: 스케줄 계산 ----------
228+
229+
def compute_schedule(
230+
T: int,
231+
init_bytes: bytes,
232+
target_bytes: bytes
233+
) -> Tuple[bool, List[int]]:
234+
"""
235+
주어진 T(스텝 수), 초기 상태(init_bytes), 타겟 상태(target_bytes)에 대해
236+
HIT/MISS 스케줄(h[t]∈{0,1})을 계산.
237+
"""
238+
assert len(init_bytes) == L and len(target_bytes) == L
239+
240+
# 1) base = M^T(S0)
241+
base_planes = advance_planes(bytes_to_planes(init_bytes), T)
242+
base_big = pack_planes_to_bigint(base_planes)
243+
244+
# 2) columns v_t = M^{T-t}(U_t)
245+
columns_big = []
246+
for t in range(T):
247+
U_bytes = hit_injection_vector(t)
248+
U_planes = bytes_to_planes(U_bytes)
249+
V_planes = advance_planes(U_planes, T - t) # M^{T-t}
250+
V_big = pack_planes_to_bigint(V_planes)
251+
columns_big.append(V_big)
252+
253+
# 3) delta = TARGET ^ base
254+
target_big = pack_planes_to_bigint(bytes_to_planes(target_bytes))
255+
delta = target_big ^ base_big
256+
257+
# 4) Solve A*h = delta
258+
ok, hits = solve_gf2_columns(columns_big, delta)
259+
return ok, hits
260+
261+
# ---------- 시뮬레이터(검증용) ----------
262+
263+
def simulate_with_schedule(T: int, init_bytes: bytes, hits: List[int]) -> bytes:
264+
"""계산된 스케줄로 실제 최종 상태를 시뮬레이션해서 검증한다."""
265+
planes = bytes_to_planes(init_bytes)
266+
for t in range(T):
267+
if hits[t]:
268+
# inject U_t
269+
Ub = hit_injection_vector(t)
270+
Ubp = bytes_to_planes(Ub)
271+
# XOR
272+
for b in range(8):
273+
planes[b] ^= Ubp[b]
274+
# mix
275+
planes = mix_planes(planes)
276+
return planes_to_bytes(planes)
277+
278+
# ---------- 입력(초기값/타깃) 세팅 ----------
279+
280+
# 1) 초기 상태 (네가 준 16개 QWORD, little-endian 메모리 순서로 128바이트 구성)
281+
QWORDS_INIT = [
282+
0x2cfe542af6bcdfc9, 0xf827a156b5343296,
283+
0xc2cf199748081bd1, 0x6e26919c7c7bf2f9,
284+
0x8b5b00d8abffae1b, 0x96e0cde33808f13d,
285+
0xc59babdd835b5d0b, 0x6b9e07b75d498495,
286+
0xf18c48ce17b46361, 0xf9b94c2601dfe836,
287+
0xdc46ed6a800e449c, 0xb67a2df488263a5b,
288+
0x6621c22f7d4d5b15, 0xe4b8d8381dc49605,
289+
0x82df48ce211e68a5, 0x1a6836a93b27b7e4,
290+
]
291+
INIT_BYTES = b''.join(q.to_bytes(8, 'little') for q in QWORDS_INIT)
292+
293+
# 2) 타깃 상태 (예: DAT_140077660) — 네가 올린 128바이트를 그대로 넣으면 됨
294+
TARGET_HEX = """
295+
14 64 42 4a 00 cc 14 34 21 43 26 82 98 11 2f 6f
296+
3e 89 3a b8 d0 1c af 57 ef 3a 6b 23 dd 83 6c d8
297+
3b ca c1 01 d4 30 52 a4 aa 8f e6 55 b6 c3 6d 1c
298+
86 c9 2c 94 6b 7a 91 ae 31 e3 7e cd 8f e9 dd 1a
299+
d1 c5 d0 c0 81 6f ee 9b 78 b0 aa fd 75 80 db 90
300+
55 ff fd 90 f9 04 e9 54 2a 94 ea be 0e ec 44 35
301+
28 18 bd 86 51 88 44 b1 d6 2d 27 d5 1b 4d 8d d2
302+
df a4 f3 cf 94 aa 15 2d 8d 75 99 15 47 10 f7 08
303+
"""
304+
TARGET_BYTES = bytes(int(x,16) for x in TARGET_HEX.split())
305+
306+
# 3) 노트 수(T) — 정확한 값으로 채워야 함!
307+
# (게임 내부: T = (DAT_140099008 - DAT_140099000) >> 4)
308+
T = 1080 # TODO: 여기에 실제 노트 개수를 넣으세요 (정확히!)
309+
310+
if __name__ == "__main__":
311+
if T <= 0:
312+
print("T(노트 수)를 먼저 정확히 설정하세요.")
313+
else:
314+
ok, hits = compute_schedule(T, INIT_BYTES, TARGET_BYTES)
315+
if not ok:
316+
print("[!] 주어진 T로는 정확히 맞출 수 없습니다 (선형계 해 없음). T 또는 타깃/초기값을 확인하세요.")
317+
else:
318+
print("[+] HIT/MISS 시퀀스 산출 완료.")
319+
print(" 예: 첫 64스텝:", ''.join('1' if h else '0' for h in hits[:64]))
320+
print("".join("1" if h else "0" for h in hits))
321+
# 검증
322+
final_bytes = simulate_with_schedule(T, INIT_BYTES, hits)
323+
if final_bytes == TARGET_BYTES:
324+
print("[OK] 시뮬레이션 검증 통과: 최종 128바이트가 Target과 정확히 일치합니다.")
325+
else:
326+
print("[!] 시뮬레이션 검증 실패: 계산/입력 값을 점검하세요.")
327+
```
328+
329+
---
330+
331+
![_](/assets/img/posts/2025-10-28-17.png){: style="max-width: 100%; height: auto;"}
332+
333+
`1670`함수는 `CALCED + 0x80`위치의 데이터만 건드리고 있는 것으로 보입니다. 계산용 버퍼인듯합니다. 직접적으로 건드리지 않아서 패스합니다. (인자값으로 '0' 혹은 '1'이 들어간것을 보니 노트 히트에 따른 기록을 하는 듯 합니다)
334+
335+
![_](/assets/img/posts/2025-10-28-18.png){: style="max-width: 100%; height: auto;"}
336+
337+
| 변수가 많아 `calced + 1``calcedP1`로 바꿔두었습니다.
338+
339+
로직이 좀 복잡해보일수 있지만, 선택된 블럭이 16개정도 반복된 후 do while루프를 돌고 있습니다.
340+
341+
`if (calcedP1 + ...calced < 0x7f)`은 바운더리 관련 연산이여서 마지막 부분 제외하고는 true로 보고 계산하면 됩니다
342+
343+
치환하면 다음과 같은 코드가 됩니다.
344+
345+
```python
346+
dat = [None] * 128
347+
for i in range(128):
348+
# 바운더리 오류 날 경우 a나 b를 0으로 처리하면 됩니다.
349+
a = dat[i + 1]
350+
b = dat[i - 1]
351+
dat[i] = a ^ b
352+
```
353+
354+
이게 로직 전부인데, ai가 출력한 코드는 좀 깁니다. 솔버를 만드느라 그런 듯 합니다.
355+
356+
위 함수 이용해서 `z3`솔버 이용했으면 됐었을것같습니다.
92357

93358
## modulation-master
94359

assets/img/posts/2025-10-28-14.png

17.5 KB
Loading

assets/img/posts/2025-10-28-15.png

23 KB
Loading

assets/img/posts/2025-10-28-16.png

19.2 KB
Loading

assets/img/posts/2025-10-28-17.png

38.3 KB
Loading

assets/img/posts/2025-10-28-18.png

27.6 KB
Loading

0 commit comments

Comments
 (0)