본문 바로가기

Cryptography/Cryptography CTF

[Dreamhack] TOP secret!

문제 설명

아모와 보코는 난도 몰래 비밀 메시지을 주고받고 있어요.

내용이 플래그라는 것은 난도에겐 절대, 절대 비밀이에요!

 

키워드

비제네르 암호, RSA, Cube Attack

💡 비제네르 암호
- 비제네르 표(Vigenère Square)를 보고 암호화를 진행한다.
- 암호화 원리는 다음과 같다.
      1. 평문과 키워드(비밀번호)를 준비한다.
      2. 평문 길이에 맞춰 키워드를 반복해서 적는다.
      3. 표에서 두 글자가 만나는 곳을 찾는다.

- 비즈네르 암호법 수식 : $C_i = E_K(M_i) = (M_i+K_i) \pmod{26}$
- 카시스키 테스트를 사용해서 해독할 수 있다.

 

💡 Cube Attack
- Stream Cipher를 깨기 위해 고안된 강력한 해킹 방법
- 복잡한 수학 공식을 아주 단순한 1차 방정식으로 바꿔서 비밀키는 알아내는 것이 핵심이다.

- Cube의 의미: 공격자가 마음대로 바꿀 수 있는 공개 변수(예: 초기화 벡터, IV) 중 일부를 선택해서, 그 값들이 가질 수 있는 모든 경우의 수(0 또는 1)를 다 넣어보는 것을 말한다. 이 모양이 마치 다차원 큐브(입방체) 같다고 해서 붙은 이름이다.

 

소스코드

#prob.py

import random
from Crypto.Util.number import getPrime, bytes_to_long, inverse

class RSA_Cipher:
    def __init__(self):
        self.p = getPrime(1024)
        self.q = getPrime(1024)
        self.N = self.p * self.q
        self.e = 5
        self.d = inverse(self.e, self.N - self.p - self.q + 1)

    def encrypt(self, pt):
        return pow(pt, self.e, self.N)

class Vigenere_Cipher:
    def __init__(self, key):
        self._key = key

    def encrypt(self, msg):
        enc = b""
        for i in range(len(msg)):
            enc += bytes([(msg[i] + self._key[i % len(self._key)]) % 256])
        return enc

def main():
    FLAG = open("flag", "rb").read()
    key = random.randbytes(4)

    while(1):
        try:
            vgn = Vigenere_Cipher(key)
            rsa = RSA_Cipher()
            break
        except Exception as e:
            continue

    print("Hi, this is Amo, I'll send you my public key.")
    print("e:", rsa.e)
    print("N:", rsa.N)
    print()

    enc1 = bytes_to_long(vgn.encrypt(FLAG))
    enc2 = rsa.encrypt(bytes_to_long(key))

    print("Hello, this is Boko! I'll send you ciphertexts!")
    print("enc1:", enc1)
    print("enc2:", enc2)


if __name__ == "__main__":
    main()

 

소스코드 분석

1. RSA_Cipher 클래스

class RSA_Cipher:
    def __init__(self):
        self.p = getPrime(1024)
        self.q = getPrime(1024)
        self.N = self.p * self.q
        self.e = 5
        self.d = inverse(self.e, self.N - self.p - self.q + 1)

    def encrypt(self, pt):
        return pow(pt, self.e, self.N)
  • 공개지수가 5로 매우 작다.
  • 개인키 d 계산: $d = e^{-1} \pmod{(p-1)(q-1)}$

 

2. Vigenere_Cipher 클래스

class Vigenere_Cipher:
    def __init__(self, key):
        self._key = key

    def encrypt(self, msg):
        enc = b""
        for i in range(len(msg)):
            enc += bytes([(msg[i] + self._key[i % len(self._key)]) % 256])
        return enc
  • Byte 단위의 덧셈 암호
  • 암호화 방법 : (평문 + 키) % 256
  • 복호화 방법 : (암호문 - 키) % 256

 

3. main 함수

def main():
    FLAG = open("flag", "rb").read()
    key = random.randbytes(4)

    while(1):
        try:
            vgn = Vigenere_Cipher(key)
            rsa = RSA_Cipher()
            break
        except Exception as e:
            continue
  • 4바이트 크기의 랜덤한 키를 생성한다.
  • vgn, rsa: 각각 비제네르 암호기와 RSA 암호기 객체를 생성한다.
  • vgn의 경우 랜덤한 키 값을 가지고 객체를 생성한다.
  • RSA_Cipher() 안에서는 inverse(e, phi) 함수를 써서 개인키($d$)를 계산한다. 수학적으로 $e(=5)$와 $\phi(N)$이 서로소가 아니면, 역수($d$)를 구할 수 없어 에러가 발생할 수 있다.
  • 또한 랜덤으로 소수($p, q$)를 뽑다 보면 가끔 에러가 날 수 있으므로, 성공적으로 키가 만들어질 때까지 계속 시도하기 위해서 무한 루프를 사용한다.
print("Hi, this is Amo, I'll send you my public key.")
print("e:", rsa.e)
print("N:", rsa.N)

#output.txt
Hi, this is Amo, I'll send you my public key.
e: 5
N: 24778450034785355796150191255487074823099958164427517612668815658468206009158475774203229828058652831641389747402272728790787685762568229069520469756247804941312947307153713830371750706901868389560472732254665749033734649996443767231968425511092244591774647092925931126950380935008196052393893271837275626174525444417778170526468251066473481105512939105882134615031671691748551289394109269703632798650982887859648332846094423809290782207835604174269463315884480062803289020119565250762542625596177768616201281918850432872639983965071018579891448754659608103400036049016809640134053891855019010729470727777892901808607
  • 사용자에게 RSA의 공개키를 준다.
enc1 = bytes_to_long(vgn.encrypt(FLAG))
enc2 = rsa.encrypt(bytes_to_long(key))

print("Hello, this is Boko! I'll send you ciphertexts!")
print("enc1:", enc1)
print("enc2:", enc2)

#output.txt
Hello, this is Boko! I'll send you ciphertexts!
enc1: 25889043021335548821260878832004378483521260681242675042883194031946048423533693101234288009087668042920762024679407711250775447692855635834947612028253548739678779
enc2: 332075826660041992234163956636404156206918624
  • 비제네르 암호화를 사용해서 FLAG를 암호화 한다.
  • key를 RSA로 암호화 한다.
  • 최종적으로 암호화된 값을 사용자에게 출력한다.

 

취약점 분석

  • enc2를 해독해서 key 값을 얻어낸 뒤, 그 key를 사용해 enc1을 푸는 문제이다.

핵심 취약점

  1. key가 너무 작음 : 4바이트는 숫자로 치면 0~42억정도의 숫자로 컴퓨터 연산으로는 빠르게 계산할 수 있다.
  2. 공개 지수 $e$가 너무 작음 : 공개지수 $e=5$로 너무 작아서 공격에 취약하다.
  3. 모듈러($N$)은 너무 큼

RSA 암호화 식은 $C = M^e \pmod{N}$이다. 여기서 $M$이 4바이트 정수이므로 $M < 2^{32}$이다. 이를 5제곱 해도 $M^5 < (2^{32})^5 = 2^{160}$dlek.

반면 $N$은 $2^{2048}$정도의 엄청나게 큰 수이다. 즉, $M^5$가 $N$보다 훨씬 작아서 모듈러 연산 $\pmod{N}$이 사실상 일어나지 않는다.

따라서 enc2 값은 그냥 모듈러 연산이 일어나지 않은 $M^5$ 그 자체이다. enc2의 정수 5제곱근을 구하면 바로 key 찾을 수 있다는 것이다.

 

공격

import gmpy2
from Crypto.Util.number import long_to_bytes

# --- 문제에서 주어진 값 ---
e = 5
N = 24778450034785355796150191255487074823099958164427517612668815658468206009158475774203229828058652831641389747402272728790787685762568229069520469756247804941312947307153713830371750706901868389560472732254665749033734649996443767231968425511092244591774647092925931126950380935008196052393893271837275626174525444417778170526468251066473481105512939105882134615031671691748551289394109269703632798650982887859648332846094423809290782207835604174269463315884480062803289020119565250762542625596177768616201281918850432872639983965071018579891448754659608103400036049016809640134053891855019010729470727777892901808607
enc1_int = 25889043021335548821260878832004378483521260681242675042883194031946048423533693101234288009087668042920762024679407711250775447692855635834947612028253548739678779
enc2_int = 332075826660041992234163956636404156206918624

def solve():
    # [Step 1] Key 복구 
    # 모듈러 연산 없이 단순히 5제곱근을 구합니다.
    key_int, is_exact = gmpy2.iroot(enc2_int, e)

    if not is_exact:
        print("[-] Error: Key recovery failed (Not an exact root).")
        return

    key = long_to_bytes(key_int)
    print(f"Recovered Key: {key}")

    # [Step 2] Flag 복구 (Vigenère Decryption)
    # (평문 + 키) % 256 = 암호문  ==>  (암호문 - 키) % 256 = 평문
    enc1 = long_to_bytes(enc1_int)
    flag = b""
    
    for i, char_byte in enumerate(enc1):
        # 복호화: (C - K) % 256
        k = key[i % len(key)]
        decoded_byte = (char_byte - k) % 256
        flag += bytes([decoded_byte])

    print(f"FLAG: {flag.decode()}")

if __name__ == "__main__":
    solve()

 

코드 설명

 

  • gmpy2.iroot(enc2_int, 5):
    • 이 함수는 매우 큰 숫자의 n제곱근을 빠르게 계산해준다.
    • is_exact 값을 통해 딱 떨어지는 정수인지 확인할 수 있어, 성공했는지 바로 검증 가능하다.
  • long_to_bytes:
    • 숫자로 되어 있는 암호문과 키를 바이트 문자열(b'...')로 변환해준다.
  • 복호화 로직:
    • 문제의 암호화 방식 (m + k) % 256의 역연산인 (c - k) % 256을 수행하여 플래그를 복원한다.

 

'Cryptography > Cryptography CTF' 카테고리의 다른 글

[Dreamhack] Common things between us  (0) 2026.02.12
[Dreamhack] uncommon e  (0) 2026.01.27
[Dreamhack] Textbook-RSA  (0) 2026.01.22
[Dreamhack] safeprime  (0) 2026.01.18
[Dreamhack] Padding Miracle Attack  (0) 2025.11.17