> For the complete documentation index, see [llms.txt](https://docs.cooku222.kr/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.cooku222.kr/project-remind/contest/undefined/buckeye-2025-ctf-write-up.md).

# Buckeye 2025 CTF Write-up

Write up은 맞는데 이제... 삽질 기록에 가까운. 솔브를 못 냈다.

Crypto/cube cipher2

풀진 못하고 삽질 코드.. ㅜㅜ

```
# cube_bruteforce_fullmoves.py
# Usage: put your cipher hex into CIPHER_HEX and run. Adjust MAX_DEPTH for search depth (>=1).
# Be aware deeper search grows as 12^depth (12 basic moves including inverses).

from itertools import product
import binascii
from collections import deque

# -------------------- User inputs --------------------
CIPHER_HEX = "754477f367633676ef02347641d63d65529663b6007360f40f0ebe"
# Maximum search depth (number of moves). 5 is ~248k sequences (12^5) — may take ~seconds-minutes depending on machine.
MAX_DEPTH = 5
# Flag/crib patterns to stop early (bytes)
FLAG_PATTERNS = [b"bctf{"]
# Minimum printable fraction to consider "mostly printable"
PRINTABLE_THRESHOLD = 0.9
# -----------------------------------------------------

# --- cube net / positions ---
# Faces: indices are 0..53 as:
# F: 0..8, R:9..17, U:18..26, L:27..35, D:36..44, B:45..53
FACE_BASE = {'F':0, 'R':9, 'U':18, 'L':27, 'D':36, 'B':45}
def face_indices(face):
    b = FACE_BASE[face]
    return [b + i for i in range(9)]

# pos_order used by Cube Cipher (given in problem)
POS_ORDER = list(range(18,27)) + list(range(27,36)) + list(range(0,9)) + list(range(9,18)) + list(range(36,45)) + list(range(45,54))
assert len(POS_ORDER) == 54

# nibble helpers
def bytes_to_nibbles(block: bytes):
    n = []
    for b in block:
        n.append((b >> 4) & 0xF)
        n.append(b & 0xF)
    return n

def nibbles_to_bytes(nibs):
    out = []
    for i in range(0, len(nibs), 2):
        out.append((nibs[i] << 4) | nibs[i+1])
    return bytes(out)

# apply perm helper (perm[i] = j means sticker at i moves to j)
def apply_perm(cube, perm):
    new = [0]*54
    for i in range(54):
        new[perm[i]] = cube[i]
    return new

def invert_perm(perm):
    inv = [0]*54
    for i,p in enumerate(perm):
        inv[p] = i
    return inv

# face rotation mapping (clockwise) within a face (3x3)
def face_rotation_map(face_idxs):
    # mapping old_index -> new_index after clockwise rotation of face
    # positions 0..8 in row-major
    fm = {6:0,3:1,0:2,7:3,4:4,1:5,8:6,5:7,2:8}
    mapping = {}
    for old_pos, new_pos in fm.items():
        mapping[face_idxs[old_pos]] = face_idxs[new_pos]
    return mapping

# Build full move permutation for each face (clockwise). We'll explicitly map side strips per the net.
# The side cycles were derived for this net arrangement:
# F: neighbors U (row2), R (col0), D (row0 rev), L (col2 rev)
# R: neighbors U (col2), B (col0 rev), D (col2), F (col2)
# B: neighbors U (row0 rev), L (col0 rev), D (row2 rev), R (col2)
# L: neighbors U (col0), F (col0), D (col0), B (col2 rev)
# U: neighbors B (row0), R (row0), F (row0), L (row0)  -- careful orientation
# D: neighbors F (row2), R (row2), B (row2), L (row2)  -- careful orientation
#
# The specific index lists below were carefully computed to match the net in the prompt.
# If something seems off, adjust these lists with visual aid of the net.

def make_move_perm_clockwise(face):
    perm = list(range(54))
    # rotate face itself
    fidxs = face_indices(face)
    fmap = face_rotation_map(fidxs)
    for k,v in fmap.items():
        perm[k] = v

    # side strips for each face (each is list-of-3 indices)
    if face == 'F':
        s1 = [18+6, 18+7, 18+8]     # U row2  => 24,25,26
        s2 = [9+0, 9+3, 9+6]        # R col0  => 9,12,15
        s3 = [36+2,36+1,36+0]       # D row0 reversed => 38,37,36
        s4 = [27+8,27+5,27+2]       # L col2 reversed => 35,32,29
        strips = [s1, s2, s3, s4]
    elif face == 'R':
        s1 = [18+8,18+5,18+2]       # U col2 reversed => 26,23,20
        s2 = [45+0,45+3,45+6]       # B col0 => 45,48,51   (orientation)
        s3 = [36+8,36+5,36+2]       # D col2 => 44,41,38
        s4 = [0+8,0+5,0+2]          # F col2 => 8,5,2
        strips = [s1, s2, s3, s4]
    elif face == 'B':
        s1 = [18+0,18+1,18+2]       # U row0 => 18,19,20
        s2 = [27+0,27+3,27+6]       # L col0 => 27,30,33
        s3 = [36+6,36+7,36+8]       # D row2 => 42,43,44
        s4 = [9+8,9+5,9+2]          # R col2 reversed => 17,14,11
        strips = [s1, s2, s3, s4]
    elif face == 'L':
        s1 = [18+0,18+3,18+6]       # U col0 => 18,21,24
        s2 = [0+0,0+3,0+6]          # F col0 => 0,3,6
        s3 = [36+0,36+3,36+6]       # D col0 => 36,39,42
        s4 = [45+8,45+5,45+2]       # B col2 reversed => 53,50,47
        strips = [s1, s2, s3, s4]
    elif face == 'U':
        s1 = [45+0,45+1,45+2]       # B row0 => 45,46,47
        s2 = [9+0,9+1,9+2]          # R row0 => 9,10,11
        s3 = [0+0,0+1,0+2]          # F row0 => 0,1,2
        s4 = [27+0,27+1,27+2]       # L row0 => 27,28,29
        strips = [s1, s2, s3, s4]
    elif face == 'D':
        s1 = [0+6,0+7,0+8]          # F row2 => 6,7,8
        s2 = [9+6,9+7,9+8]          # R row2 => 15,16,17
        s3 = [45+6,45+7,45+8]       # B row2 => 51,52,53
        s4 = [27+6,27+7,27+8]       # L row2 => 33,34,35
        strips = [s1, s2, s3, s4]
    else:
        raise ValueError("unknown face")

    # For clockwise face turn, values move from strips[-1] -> strips[0] -> strips[1] -> ...
    # So new strips[i] = old strips[i-1]
    for i in range(4):
        src = strips[(i-1) % 4]
        dst = strips[i]
        for j in range(3):
            perm[dst[j]] = src[j]
    return perm

# Build all moves (clockwise and inverse)
basic_faces = ['U','R','F','D','L','B']
MOVE_PERMS = {}
for f in basic_faces:
    cw = make_move_perm_clockwise(f)
    MOVE_PERMS[f] = cw
    # inverse is just applying clockwise thrice, but compute via invert
    MOVE_PERMS[f + "i"] = invert_perm(cw)

# Compose move sequence into a single perm (apply in order left-to-right)
def compose_perms(seq):
    perm = list(range(54))
    for mv in seq:
        mperm = MOVE_PERMS[mv]
        # composition: new_perm[i] = mperm[perm[i]]
        newp = [0]*54
        for i in range(54):
            newp[i] = mperm[perm[i]]
        perm = newp
    return perm

# Encrypt/decrypt single 27-byte block with a given perm
def encrypt_block(block27, perm):
    n = bytes_to_nibbles(block27)
    cube = [0]*54
    for i,pos in enumerate(POS_ORDER):
        cube[pos] = n[i]
    cube2 = apply_perm(cube, perm)
    out_n = [cube2[pos] for pos in POS_ORDER]
    return nibbles_to_bytes(out_n)

def decrypt_all(data_bytes, perm):
    inv = invert_perm(perm)
    out = b''
    for i in range(0, len(data_bytes), 27):
        blk = data_bytes[i:i+27]
        if len(blk) < 27:
            blk = blk + b'\x00'*(27-len(blk))
        n = bytes_to_nibbles(blk)
        cube = [0]*54
        for idx,pos in enumerate(POS_ORDER):
            cube[pos] = n[idx]
        cube2 = apply_perm(cube, inv)
        out_n = [cube2[pos] for pos in POS_ORDER]
        out += nibbles_to_bytes(out_n)
    return out

# Utility checks
def is_mostly_printable(b):
    if not b: return False
    good = sum(1 for x in b if 32 <= x <= 126 or x in (9,10,13))
    return (good / len(b)) >= PRINTABLE_THRESHOLD

# Main brute-force search (BFS by depth)
cipher_bytes = binascii.unhexlify(CIPHER_HEX)
if len(cipher_bytes) % 27 != 0:
    # pad to 27 boundary (spec says plaintext padded; so cipher could be exactly 27)
    # we'll work with actual length but decrypt_all pads last block during decryption
    pass

# Prepare move names (12 total: 6 cw + 6 inverse)
move_names = []
for f in basic_faces:
    move_names.append(f)
    move_names.append(f + "i")

print("Attempting full Rubik-style brute force up to depth", MAX_DEPTH)
print("Total basic moves:", len(move_names))

# We'll iterate in increasing depth and stop on first plausible plaintext
seq_count = 0
for depth in range(0, MAX_DEPTH+1):
    if depth == 0:
        seqs = [[]]
    else:
        seqs = product(move_names, repeat=depth)
    for seq in seqs:
        seq_count += 1
        perm = compose_perms(seq)
        plaintext = decrypt_all(cipher_bytes, perm)
        # quick checks
        if any(pat in plaintext for pat in FLAG_PATTERNS) or is_mostly_printable(plaintext):
            print("=== Candidate found! ===")
            print("Sequence:", seq)
            print("Plaintext (len={}):".format(len(plaintext)))
            print(plaintext)
            raise SystemExit("Stopped after finding candidate")
    # progress report per depth
    print("Searched depth", depth, "sequences total so far:", seq_count)

print("Search complete up to depth", MAX_DEPTH, "no candidates matched flag patterns or printable threshold.")
print("Tried sequences:", seq_count)
```

&#x20;

&#x20;

misc / monkeys

풀진 못했음.&#x20;

우리가 Lua 함수를 작성하고 서버가 32000 바이트 랜덤 데이터를 생성 후 우리 함수로 데이터를 변환해 확률성 비트를 플립 후 우리가 Python으로 디코딩해 원본을 복원한 값이 100% 일치하면 플래그를 준다...고 한다..(@p2gone님 로그..) 물론 실패임

```
function(input)
    -- Cube-Cipher noisy channel companion: LT-style fountain encoder (systematic + redundancy)
    -- Params: 32000-byte input -> <=51200-byte output, resilient to ~30% corrupted packets
    -- Packet format: [4-byte seed][128-byte payload][2-byte CRC16-CCITT] = 134 bytes each
    -- We emit 382 packets: 250 systematic (degree=1) + 132 redundant.
    -- Total bytes = 382 * 134 = 51188 <= 51200.

    local K = 250            -- number of source symbols
    local SYM = 128          -- bytes per symbol (K*SYM = 32000)
    local REDUND = 132       -- number of extra droplets (382 total)

    -- Ensure input length is exactly 32000 (pad with zeros if shorter; truncate if longer)
    local n = #input
    if n < K*SYM then
        input = input .. string.rep("\0", K*SYM - n)
    elseif n > K*SYM then
        input = string.sub(input, 1, K*SYM)
    end

    -- Slice input into symbols
    local symbols = {}
    for i = 0, K-1 do
        symbols[i+1] = string.sub(input, i*SYM+1, (i+1)*SYM)
    end

    -- --- Utility: 32-bit xorshift PRNG (deterministic from seed) ---
    local function xorshift32(s)
        s = s ~ (s << 13) & 0xFFFFFFFF
        s = s ~ (s >> 17)
        s = s ~ (s << 5) & 0xFFFFFFFF
        return s & 0xFFFFFFFF
    end

    -- --- Utility: CRC16-CCITT (poly 0x1021, init 0xFFFF, no reflect, no xorout) ---
    local function crc16_ccitt(bytes)
        local crc = 0xFFFF
        for i = 1, #bytes do
            local b = string.byte(bytes, i)
            crc = ((crc ~ (b << 8)) & 0xFFFF)
            for _ = 1, 8 do
                if (crc & 0x8000) ~= 0 then
                    crc = ((crc << 1) ~ 0x1021) & 0xFFFF
                else
                    crc = (crc << 1) & 0xFFFF
                end
            end
        end
        return crc & 0xFFFF
    end

    -- --- Bytewise XOR of two equal-length strings ---
    local function bxor_str(a, b)
        local t = {}
        for i = 1, #a do
            t[i] = string.char((string.byte(a, i) ~ string.byte(b, i)) & 0xFF)
        end
        return table.concat(t)
    end

    -- --- Degree sampler (lightweight robust-ish distribution) ---
    -- Keeps degrees small but with a tail; reproducible from PRNG state.
    local function sample_degree(state)
        -- Map a 32-bit state to a small degree in {1..8} with a skew favoring small numbers.
        -- CDF roughly: d=1:0.35, 2:0.25, 3:0.15, 4:0.10, 5:0.07, 6:0.04, 7:0.025, 8:0.015
        local r = (state >> 1) & 0x7FFFFFFF
        local m = r % 1000  -- 0..999
        if m < 350 then return 1
        elseif m < 600 then return 2
        elseif m < 750 then return 3
        elseif m < 850 then return 4
        elseif m < 920 then return 5
        elseif m < 960 then return 6
        elseif m < 985 then return 7
        else return 8 end
    end

    -- Build one packet: seed (u32), payload (128B), crc16 over seed||payload
    local function pack(seed, payload)
        local s0 = string.char((seed >> 24) & 0xFF, (seed >> 16) & 0xFF, (seed >> 8) & 0xFF, seed & 0xFF)
        local body = s0 .. payload
        local crc = crc16_ccitt(body)
        local c0 = string.char((crc >> 8) & 0xFF, crc & 0xFF)
        return body .. c0
    end

    local out = {}

    -- 1) Systematic packets: one per source symbol (degree=1, index=i)
    -- Seed format: 0x53000000 | i   (0x53 = 'S' for systematic)
    for i = 0, K-1 do
        local seed = (0x53 << 24) | (i & 0xFFFFFF)
        local payload = symbols[i+1]
        out[#out+1] = pack(seed, payload)
    end

    -- 2) Redundant packets: degree sampled from PRNG seeded by (0x52<<24 | j)
    -- For each, select 'deg' distinct indices via PRNG and XOR their symbols.
    for j = 1, REDUND do
        local seed = (0x52 << 24) | (j & 0xFFFFFF)   -- 0x52 = 'R'
        local st = seed & 0xFFFFFFFF
        local deg = sample_degree(st)
        if deg > K then deg = K end

        -- Choose deg distinct indices using reservoir-like sampling with PRNG
        local chosen = {}
        local count = 0
        while count < deg do
            st = xorshift32(st)
            local idx = (st % K) + 1
            if not chosen[idx] then
                chosen[idx] = true
                count = count + 1
            end
        end

        -- XOR the chosen symbols
        local acc = string.rep("\0", SYM)
        for idx,_ in pairs(chosen) do
            acc = bxor_str(acc, symbols[idx])
        end

        out[#out+1] = pack(seed, acc)
    end

    return table.concat(out)
end
EOF
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.cooku222.kr/project-remind/contest/undefined/buckeye-2025-ctf-write-up.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
