[🥈 Silver 4] 소수를 모듈러로 쓴 제곱 PRNG에서 시드 되찾기 — DreamHack the present 풀이

2026-08-17·1분 읽기·

[🥈 Silver 4] 소수를 모듈러로 쓴 제곱 PRNG에서 시드 되찾기 — DreamHack the present 풀이

플래그를 시드로 넣은 제곱 되먹임 PRNG가 두 번째 출력만 공개한다. 모듈러가 두 소수의 곱이 아니라 512비트 소수 하나라서, 제곱근이 거듭제곱 한 번으로 계산된다. 네제곱근 후보를 전부 구하고 이차잉여 판정으로 가지를 쳐내니 남은 건 두 개뿐이었고 그중 하나가 그대로 플래그였다.

문제: DreamHack — the present 분류: crypto 난이도: 🥈 Silver 4 FLAG: DH{M3rry_ChrI5tM4s_And_4_h4pPy_N3w_y34R_3verY0n3}

선물상자를 열었더니 크리스마스 CTF 초대권과 암호화된 쪽지, 그리고 그 쪽지를 만든 코드가 같이 들어 있었다는 설정이다. 문제 페이지에도 "해당 문제는 2022 Christmas CTF 에 출제된 문제입니다"라고 붙어 있다.

받은 코드는 스무 줄이 안 된다. 짧은 crypto 문제는 대개 둘 중 하나인데, 수학적으로 뚫려 있거나 파라미터 하나가 잘못 골라져 있다. 이 문제는 후자다.

드림핵 the present 문제 페이지 — 2022 Christmas CTF 출제작이라는 안내와 문제 설명, 파일 받기 버튼이 보인다
드림핵 the present 문제 페이지 — 2022 Christmas CTF 출제작이라는 안내와 문제 설명, 파일 받기 버튼이 보인다

문제 개요

항목내용
문제명the present
난이도🥈 Silver 4
분류crypto
제공 파일prob.py (암호화 코드), output.txt (M, rand)
서버없음 — 완전 오프라인
핵심 취약점제곱 되먹임 PRNG의 모듈러가 합성수가 아니라 소수 하나
사용 도구Python(pycryptodome), OpenSSL, SageMath

흐름을 한 줄로 줄이면 이렇다. 플래그를 정수로 바꿔 시드로 넣고 제곱을 두 번 돌린 값이 rand로 공개되니, 제곱근을 두 번 거슬러 올라가면 시드가 나온다. 그 제곱근이 계산 가능한 이유가 이 문제의 전부다.



🔬 정찰 — 받은 건 코드 한 장과 숫자 두 개

압축을 풀면 파일이 둘뿐이다. 실행 파일도, 서버 주소도 없다.

ls -l extracted && file extracted/*
문제 배포본 파일 목록 — output.txt와 prob.py 두 개뿐이고 file 명령은 각각 ASCII text와 Python script로 인식한다
문제 배포본 파일 목록 — output.txt와 prob.py 두 개뿐이고 file 명령은 각각 ASCII text와 Python script로 인식한다

암호화 코드부터 본다.

cat extracted/prob.py
prob.py 전체 소스 화면 — PRNG 클래스의 next가 seed를 제곱해 M으로 나눈 나머지를 돌려주고 main에서 next를 두 번 호출한다
prob.py 전체 소스 화면 — PRNG 클래스의 next가 seed를 제곱해 M으로 나눈 나머지를 돌려주고 main에서 next를 두 번 호출한다

내용은 이게 전부다.

from Crypto.Util.number import *
 
flag = bytes_to_long(b'DH{?????????????????????????????????????????????}')
 
class PRNG():
    def __init__(self, seed):
        self.seed = seed
        self.M = getStrongPrime(512)
 
    def next(self):
        self.seed = self.seed ** 2 % self.M
        return self.seed
 
 
 
prng = PRNG(flag)
 
prng.next()
print(f'M = {prng.M}')
print(f'rand = {prng.next()}')

읽을 곳은 세 군데다.

  • PRNG(flag) — 시드가 난수가 아니라 플래그 자체다. 복원 대상이 곧 내부 상태다.

  • next() — 상태 전이가 seed = seed² mod M 하나뿐이다. 출력 가공도, 비트 자르기도 없다. 상태를 그대로 뱉는다.

  • next() 호출이 두 번 — 한 번은 반환값을 버리고, 두 번째 것만 출력한다. 그래서 공개된 값은 flag²가 아니라 flag⁴ mod M이다. 이 한 줄을 흘려 읽으면 뒤에서 조용히 틀린다.

숫자 쪽도 확인한다.

cat extracted/output.txt
cat 으로 연 output.txt — 155자리 십진수 M 한 줄과 그보다 조금 작은 rand 한 줄이 전부이고, 힌트가 될 다른 값은 들어 있지 않다
cat 으로 연 output.txt — 155자리 십진수 M 한 줄과 그보다 조금 작은 rand 한 줄이 전부이고, 힌트가 될 다른 값은 들어 있지 않다

정리하면 우리가 아는 것은 M, rand = flag⁴ mod M 두 개고, 구할 것은 flag다.


🧩 배경 — 제곱 되먹임 PRNG는 왜 안전하다고 하나

x ← x² mod N 형태의 난수 생성기는 Blum Blum Shub(BBS)라는 이름으로 잘 알려져 있다. 상태를 제곱해서 다음 상태로 쓰고, 보통은 최하위 비트 몇 개만 출력으로 흘린다.

이 구조의 안전성은 전적으로 모듈러 N이 두 소수의 곱이라는 데서 나온다. 합성수 위에서 제곱근을 구하는 문제는 N을 인수분해하는 것과 같은 난이도라, 인수를 모르면 한 스텝도 되돌릴 수 없다. 라빈(Rabin) 암호가 "인수분해만큼 어렵다"고 말할 때의 그 성질이다.

반대로 모듈러가 소수 하나면 이야기가 끝난다. 소수 p에서는 어떤 수가 제곱수인지 오일러 판정법으로 즉시 알 수 있고, 제곱근도 Tonelli–Shanks 알고리즘으로 다항시간에 나온다. 되먹임을 몇 번 돌렸든 그 횟수만큼 거슬러 올라가면 된다.

이 문제의 M은 getStrongPrime(512)로 만들어졌다. 이름 그대로 "강한 소수"지, 두 소수의 곱이 아니다.

문제의 암호화 흐름과 복원 흐름 다이어그램 — 위쪽은 플래그를 두 번 제곱해 rand를 만드는 과정, 아래쪽은 제곱근을 두 번 취해 플래그로 돌아오는 경로
문제의 암호화 흐름과 복원 흐름 다이어그램 — 위쪽은 플래그를 두 번 제곱해 rand를 만드는 과정, 아래쪽은 제곱근을 두 번 취해 플래그로 돌아오는 경로


💣 핵심 — 소수면 제곱근이 거듭제곱 한 번

먼저 M이 정말 소수인지부터 확인했다. 코드가 getStrongPrime을 부른다고 해서 배포된 숫자가 그렇다는 보장은 없으니, 받은 값을 직접 판정한다. OpenSSL에 소수 판정기가 붙어 있어 그대로 쓸 수 있다.

openssl prime $(grep -oP "(?<=M = )\d+" extracted/output.txt)
openssl prime 실행 결과 — output.txt에서 뽑아낸 155자리 M을 16진수로 다시 보여주며 is prime 이라고 판정한다
openssl prime 실행 결과 — output.txt에서 뽑아낸 155자리 M을 16진수로 다시 보여주며 is prime 이라고 판정한다

소수인 걸 확인했으면 다음은 M mod 4다. 이 값이 3이면 Tonelli–Shanks를 돌릴 필요도 없다. 이차잉여 a의 제곱근이 거듭제곱 한 번으로 나오기 때문이다.

r  = a^((M+1)/4) mod M
r² = a^((M+1)/2) = a · a^((M-1)/2) = a · 1 = a      (a 가 이차잉여일 때)

마지막 등식이 성립하는 건 a가 이차잉여라 오일러 판정법에서 a^((M-1)/2) ≡ 1이기 때문이다. 실제로 이 문제의 M은 M mod 4 = 3이고, rand도 이차잉여다.

여기서 한 번 더 생각할 게 있다. 제곱근은 항상 두 개(r과 M-r)라, 두 단계를 거치면 후보가 네 개로 불어날 것처럼 보인다. 그런데 실제로는 두 개만 나온다.

M mod 4 = 3이면 -1이 비잉여다. 그래서 부호가 다른 두 근 s와 M-s 중 정확히 한쪽만 다시 이차잉여가 되고, 나머지 한쪽은 제곱근 자체가 존재하지 않는다. 가지 하나가 그 자리에서 끊긴다.

네제곱근 후보 분기 다이어그램 — rand의 두 제곱근 중 이차잉여인 쪽만 다음 단계로 이어지고 비잉여 가지는 제곱근이 없어 끊긴다
네제곱근 후보 분기 다이어그램 — rand의 두 제곱근 중 이차잉여인 쪽만 다음 단계로 이어지고 비잉여 가지는 제곱근이 없어 끊긴다

살아남은 두 후보를 가르는 것도 간단하다. 플래그는 49바이트, 즉 392비트라 512비트 모듈러보다 한참 작다. 나머지 연산으로 잘려나간 정보가 없다는 뜻이고, 동시에 후보 중 하나만 391비트라는 뜻이기도 하다. 나머지 하나는 512비트로 길이부터 어긋난다.


🐛 삽질 — next()를 몇 번 셌나

▶🐛 삽질 — 제곱근을 한 번만 취하면 뭐가 나오나

prng.next()가 출력 없이 호출되는 줄은 화면상 존재감이 거의 없다. rand = flag² mod M이라고 읽어 버리면 제곱근을 한 번만 취하게 되는데, 그때 손에 뭐가 남는지 실제로 찍어 봤다. 덤으로 "가지 하나가 끊긴다"는 앞의 주장도 같이 확인한다.

#!/usr/bin/env python3
"""제곱근을 한 번만 취하면 무엇이 나오나 — next() 호출 횟수를 세는 이유
 
prob.py 는 print 하기 전에 prng.next() 를 한 번 버린다. 그 한 줄을 못 보고
rand = flag^2 라고 읽으면 제곱근을 한 번만 취하게 되는데, 그때 손에 남는 값이
무엇인지 실제로 찍어 본다. 덤으로 {s, M-s} 중 하나만 다시 제곱근을 가진다는
것(= 후보가 두 배로 늘지 않는 이유)도 같이 확인한다.
"""
import re
import pathlib
from Crypto.Util.number import long_to_bytes, bytes_to_long
 
HERE = pathlib.Path(__file__).resolve().parent
FLAG = b"DH{M3rry_ChrI5tM4s_And_4_h4pPy_N3w_y34R_3verY0n3}"
 
text = (HERE / "extracted" / "output.txt").read_text()
M = int(re.search(r"M\s*=\s*(\d+)", text).group(1))
rand = int(re.search(r"rand\s*=\s*(\d+)", text).group(1))
 
s = pow(rand, (M + 1) // 4, M)  # M % 4 == 3 이라 제곱근이 거듭제곱 한 번
for label, v in (("s      ", s), ("M - s  ", M - s)):
    raw = long_to_bytes(v)
    qr = pow(v, (M - 1) // 2, M) == 1
    print(f"[{label}] {v.bit_length()}비트  이차잉여={qr}  "
          f"출력가능={all(32 <= c < 127 for c in raw)}  앞16B={raw[:16].hex()}")
 
print()
print(f"[확인] s == flag^2 mod M ?  {s == pow(bytes_to_long(FLAG), 2, M)}")
print("       한 번만 취한 제곱근은 flag 가 아니라 버려진 첫 next() 의 결과다")
python3 wrong_once.py
wrong_once.py 실행 화면 — 제곱근을 한 번만 취한 값이 512비트 이차잉여이고 출력 가능한 문자가 아니며 버려진 첫 next 결과와 일치한다
wrong_once.py 실행 화면 — 제곱근을 한 번만 취한 값이 512비트 이차잉여이고 출력 가능한 문자가 아니며 버려진 첫 next 결과와 일치한다

한 번만 취한 제곱근은 512비트짜리 아무 의미 없는 바이트열이고, 마지막 줄이 확인해 주듯 그 값은 정확히 flag² mod M — 코드가 버린 첫 next()의 결과다. 플래그 형식이 아니라고 "복호가 틀렸나" 의심하며 알고리즘을 다시 뜯을 게 아니라, 그냥 한 번 더 제곱근을 취하면 되는 상황이었다.

같은 출력에서 M - s가 비잉여(이차잉여=False)로 찍히는 것도 보인다. 이 가지에서는 다음 제곱근을 구할 수 없으니 후보가 네 개로 늘지 않는다.


🎯 익스플로잇 — 네제곱근 두 개 중 읽히는 쪽

여기까지 오면 코드는 짧다. output.txt를 파싱하고, 제곱근을 두 번 취하고, 나온 후보를 바이트로 되돌려 DH{로 시작하는 것을 고르면 끝이다.

후보 검증은 눈으로 하지 않고 스크립트가 직접 한다. 각 후보를 문제의 암호화 그대로 네제곱해서 rand와 같은지 대조하는 줄(pow(t, 4, M) == rand)이 그 역할이다. 플래그 형식만 보고 고르면 형식이 우연히 맞는 값에 속을 여지가 남는다.

#!/usr/bin/env python3
"""the present (DreamHack, Silver 4, crypto) — flag 복구
 
prob.py 는 flag 를 정수로 만들어 PRNG 의 seed 로 넣고 next() 를 두 번 부른다.
next() 가 seed <- seed^2 mod M 이므로 출력된 rand 는 flag^4 mod M 이다.
M 이 getStrongPrime(512) 로 만든 '소수'라서 4제곱근은 곧바로 계산된다.
 
M % 4 == 3 이면 QR x 의 제곱근이 x^((M+1)/4) mod M 로 한 번에 나오고,
-1 이 비잉여라 {s, M-s} 중 정확히 하나만 다시 QR 이 된다. 그 하나만 따라가면
4제곱근 후보는 두 개(t, M-t)뿐이라 브루트포스가 필요 없다.
"""
import re
import pathlib
from Crypto.Util.number import long_to_bytes
 
HERE = pathlib.Path(__file__).resolve().parent
OUT = HERE / "extracted" / "output.txt"
 
 
def load_output(path):
    """output.txt 에서 M 과 rand 를 읽는다."""
    text = path.read_text()
    M = int(re.search(r"M\s*=\s*(\d+)", text).group(1))
    rand = int(re.search(r"rand\s*=\s*(\d+)", text).group(1))
    return M, rand
 
 
def is_qr(x, p):
    """오일러 판정법: x 가 p 의 이차잉여인가."""
    return pow(x, (p - 1) // 2, p) == 1
 
 
def sqrt_mod_p3mod4(x, p):
    """p % 4 == 3 인 소수에서 x 의 제곱근 두 개. QR 이 아니면 빈 리스트."""
    if x % p == 0:
        return [0]
    if not is_qr(x, p):
        return []
    r = pow(x, (p + 1) // 4, p)
    return [r, p - r]
 
 
def fourth_roots(x, p):
    """x 의 4제곱근 전부 (p % 4 == 3 가정)."""
    roots = []
    for s in sqrt_mod_p3mod4(x, p):
        roots += sqrt_mod_p3mod4(s, p)
    return sorted(set(roots))
 
 
def main():
    M, rand = load_output(OUT)
    assert M % 4 == 3, "이 스크립트는 M mod 4 == 3 전용 지름길을 쓴다"
    print(f"[*] M    ({M.bit_length()}비트, M mod 4 = {M % 4}) = {M}")
    print(f"[*] rand ({rand.bit_length()}비트) = {rand}")
    print(f"[*] rand 는 이차잉여인가? {is_qr(rand, M)}")
 
    roots = fourth_roots(rand, M)
    print(f"[*] 4제곱근 후보 {len(roots)}개")
    flag = None
    for i, t in enumerate(roots):
        raw = long_to_bytes(t)
        ok = pow(t, 4, M) == rand  # 문제의 암호화를 그대로 되돌려 자체 검증
        printable = all(32 <= c < 127 for c in raw)
        head = raw[:16] if printable else raw[:16].hex()
        print(f"    [{i}] {t.bit_length():3d}비트  검산={ok}  출력가능={printable}  앞부분={head}")
        if ok and printable and raw.startswith(b"DH{") and raw.endswith(b"}"):
            flag = raw.decode()
 
    if flag:
        print(f"\n[+] FLAG = {flag}")
    else:
        print("\n[-] 조건을 만족하는 후보가 없다")
 
 
if __name__ == "__main__":
    main()
python3 solve.py
solve.py 실행 화면 — 네제곱근 후보 두 개가 모두 검산을 통과하고 391비트 후보만 출력 가능한 바이트라서 플래그로 확정된다
solve.py 실행 화면 — 네제곱근 후보 두 개가 모두 검산을 통과하고 391비트 후보만 출력 가능한 바이트라서 플래그로 확정된다

후보는 예상대로 두 개다. 둘 다 네제곱하면 rand가 되지만, 391비트짜리만 전부 출력 가능한 문자로 떨어진다. 512비트 쪽은 e5fba265…로 시작하는 바이트열이라 볼 것도 없다.

플래그는 DH{M3rry_ChrI5tM4s_And_4_h4pPy_N3w_y34R_3verY0n3}. Christmas CTF 출제작이라는 문제 페이지 안내와 내용이 맞아떨어진다.



🧪 교차 검증 — SageMath로 한 번 더

직접 짠 제곱근 루틴은 지름길(M mod 4 = 3)에 기대고 있어서, 그 가정이 틀렸다면 조용히 엉뚱한 값을 뱉는다. 그래서 계산 대수 시스템으로 같은 값을 독립적으로 구해 대조했다. Sage의 nth_root(4, all=True)는 지름길을 쓰지 않고 일반적인 방법으로 네제곱근을 전부 찾아 준다.

# verify.sage — SageMath 로 독립 검증
#  1) M 이 정말 소수인가 (소수면 제곱근이 다항시간에 계산된다)
#  2) rand 의 4제곱근을 Sage 의 nth_root 로 전부 구해 solve.py 결과와 대조
 
M = 12045184283108733282877246918018456795629932264538806786957088609817236359725183272225617590814403324378457302095025664842376149624271113548645334599894051
rand = 321609188014782817530081099017386870899897457530475334351599692725892773281779472409503390776505529932157130216692885548245652532540341302761980076422023
 
print("is_prime(M)      =", is_prime(M))
print("M.nbits()        =", Integer(M).nbits())
print("M mod 4          =", M % 4)
print("rand is QR       =", kronecker(rand, M) == 1)
 
roots = Integers(M)(rand).nth_root(4, all=True)
print("네제곱근 개수     =", len(roots))
 
for r in sorted(Integer(x) for x in roots):
    raw = int(r).to_bytes((r.nbits() + 7) // 8, "big")
    ok = power_mod(int(r), 4, M) == rand
    tag = raw.decode() if all(32 <= c < 127 for c in raw) else raw.hex()
    print(f"  {r.nbits():3d}비트  네제곱_검산:{ok}  ->  {tag}")
sage verify.sage
SageMath로 검증한 화면 — is_prime이 True, M mod 4가 3, rand가 이차잉여이며 네제곱근 개수가 2개로 solve.py 결과와 일치한다
SageMath로 검증한 화면 — is_prime이 True, M mod 4가 3, rand가 이차잉여이며 네제곱근 개수가 2개로 solve.py 결과와 일치한다

is_prime(M) = True, M mod 4 = 3, 네제곱근 개수 2개, 그리고 그중 하나가 같은 플래그. 손으로 짠 지름길과 Sage의 일반 루틴이 같은 답에 도달했다.

참고로 kronecker(rand, M) == 1은 오일러 판정법과 같은 얘기다. Sage에서는 르장드르 기호를 바로 부를 수 있어 거듭제곱을 직접 돌릴 필요가 없다.


🔁 재현 — 파일 두 개면 끝

이 문제는 서버도 VM도 필요 없어서, 배포본과 스크립트만 있으면 언제든 다시 돌릴 수 있다. 재현 스크립트는 플래그를 눈으로 대조하게 두지 않고 문제.json에 기록된 정답과 직접 비교한다.

#!/usr/bin/env bash
# the present (DreamHack Silver 4, crypto) — 한 방 재현.
#   extracted/output.txt 의 (M, rand) 만으로 flag 를 복원하고 정답과 대조한다.
#   외부 서버·VM 이 필요 없는 완전 오프라인 문제라 이 스크립트만으로 끝난다.
set -eu
cd "$(dirname "$(readlink -f "$0")")"
 
EXPECT=$(python3 -c "import json;print(json.load(open('문제.json'))['flag'])" 2>/dev/null || echo '')
 
python3 -c "import Crypto" 2>/dev/null || { echo "pycryptodome 이 필요하다: pip install pycryptodome"; exit 1; }
 
OUT=$(timeout 120 python3 solve.py 2>&1) || true
 
echo "$OUT" | tail -8
FLAG=$(printf '%s' "$OUT" | grep -aoE 'DH\{[^}]+\}' | head -1)
if [ -n "$FLAG" ] && { [ -z "$EXPECT" ] || [ "$FLAG" = "$EXPECT" ]; }; then
  echo; echo "✅ PASS  $FLAG"; exit 0
fi
echo; echo "❌ FAIL (얻은 값: '${FLAG:-없음}' / 기대: '$EXPECT')"; exit 1
./reproduce.sh
reproduce.sh 실행 화면 — solve.py 출력을 그대로 보여준 뒤 복원한 플래그가 기록된 정답과 일치해 PASS로 끝난다
reproduce.sh 실행 화면 — solve.py 출력을 그대로 보여준 뒤 복원한 플래그가 기록된 정답과 일치해 PASS로 끝난다

로컬 환경: Ubuntu 25.10, Python 3.13.7(pycryptodome 3.23.0), OpenSSL 3.5.3, SageMath 10.9. Sage는 nth_root 교차검증에만 쓰이니 없어도 solve.py는 그대로 돌아간다.



📝 결론

모듈러 하나를 잘못 고르면 구조 전체가 무너진다

x ← x² mod N 되먹임은 그 자체로는 멀쩡한 설계다. 무너진 건 N을 두 소수의 곱이 아니라 소수 하나로 잡은 지점이고, 나머지 코드는 아무 잘못이 없다. 암호 구현을 볼 때 알고리즘 이름보다 파라미터가 어떻게 만들어지는지를 먼저 보는 이유다.

되돌릴 횟수는 출력이 아니라 호출 횟수가 정한다

공개된 값은 하나였지만 next()는 두 번 불렸다. 출력 개수만 세고 제곱근을 한 번만 취하면 버려진 중간 상태를 손에 쥐고 "복호가 안 된다"며 헤매게 된다. 상태 기계를 되돌릴 때는 화면에 찍힌 값이 몇 번째 상태인지부터 세는 게 맞다.

후보가 여럿이면 검산으로 좁힌다

네제곱근은 원리상 여러 개일 수 있다. 플래그 형식으로 고르는 건 편하지만, 그 전에 후보를 문제의 암호화 그대로 다시 돌려 원본과 맞는지 확인하는 게 먼저다. 이 문제에서는 pow(t, 4, M) == rand 한 줄이 그 역할을 했고, 길이(391비트 대 512비트)가 보조 지표가 됐다.

방어 쪽에서 정리하면

BBS 계열 생성기를 쓸 거라면 모듈러는 반드시 두 개의 큰 소수 곱(각 소수는 4로 나눈 나머지가 3인 Blum 소수)이어야 하고, 인수는 폐기해야 한다. 상태를 그대로 출력하는 것도 문제다. 원래 BBS는 상태 전체가 아니라 최하위 비트 소수만 흘리는데, 이 코드는 seed를 통째로 뱉어서 되돌릴 지점을 그대로 알려 준다.

시드에 비밀값을 넣는 것 역시 위험하다. 여기서는 플래그가 시드였기 때문에 상태 복원이 곧 비밀 복원이 됐다. 비밀은 시드가 아니라 키로 다루고, 생성기 상태는 비밀과 분리해야 한다.

이 글이 도움이 됐나요?

Comments

댓글

0개

댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)

댓글 불러오는 중…

Related

관련 글

3개
[🥈 Silver 3] 난수 생성기가 아니라 1/FLAG 를 나눈 몫이었다 — DreamHack AFSR 풀이
blog

[🥈 Silver 3] 난수 생성기가 아니라 1/FLAG 를 나눈 몫이었다 — DreamHack AFSR 풀이

LFSR 을 개량했다는 난수 생성기가 사실은 1/base 를 이진수로 길게 나누는 회로였다. 출력 200바이트를 그대로 정수로 읽으면 그게 곧 2의 1600승을 FLAG 로 나눈 몫이라, 같은 나눗셈을 거꾸로 한 번 하면 플래그가 통째로 돌아온다.
#dreamhack#ctf#crypto+6
2026-10-09#dreamhack +4
[🥇 Gold 2] 두 배점 x 좌표 하나로 키 두 개 되찾기 — DreamHack Elliptic Curve Basic 풀이
blog

[🥇 Gold 2] 두 배점 x 좌표 하나로 키 두 개 되찾기 — DreamHack Elliptic Curve Basic 풀이

NIST P-256 위의 랜덤 점 P와 그 두 배점 2P를 10쌍 뽑아, 두 점의 x 좌표만 key1·key2에 대한 1차식으로 흘린다. 점도 y 좌표도 공개되지 않지만 y가 제곱으로만 등장하는 덕분에 두 x 좌표는 하나의 유리함수로 묶인다. key2를 소거해 7차 다항식을 세우고 GF(p) 근을 전부 구한 뒤, 남은 여덟 쌍으로 가짜 후보 둘을 걸러냈다.
#dreamhack#ctf#crypto+5
2026-08-19#dreamhack +4
[🥈 Silver 4] SubBytes 하나 빼면 AES가 그냥 1차식이 된다 — DreamHack No sub please! 풀이
blog

[🥈 Silver 4] SubBytes 하나 빼면 AES가 그냥 1차식이 된다 — DreamHack No sub please! 풀이

AES의 네 연산 중 비선형인 것은 SubBytes 하나뿐이다. 그걸 빼면 남은 ShiftRows·MixColumns·AddRoundKey가 모두 GF(2) 위의 선형·아핀 연산이라 암호 전체가 M·v 더하기 상수 d 꼴로 주저앉는다. 서버는 enc(secret) 하나만 복호를 거부하지만, XOR이 enc(secret)이 되는 다른 세 블록을 대신 복호시키면 상수항이 홀수 번 남아 secret이 그대로 나온다. 복호 질의 세 번으로 끝났다.
#dreamhack#ctf#crypto+7
2026-08-14#dreamhack +4