본문 바로가기

Cryptography/Cryptography CTF

[Dreamhack] Textbook-RSA

문제 설명

드림이가 비밀 플래그를 가지고 있는 RSA 서버를 운영하고 있습니다. 서버를 공격해 플래그를 탈취해주세요!

플래그 형식은 DH{...} 입니다.

 

소스코드

#challenge.py

#!/usr/bin/python3
from Crypto.Util.number import getStrongPrime, bytes_to_long, inverse


class RSA(object):
    def __init__(self):
        self.p = getStrongPrime(512)
        self.q = getStrongPrime(512)
        self.N = self.p * self.q
        self.e = 0x10001
        self.d = inverse(self.e, self.N - self.p - self.q + 1)

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

    def decrypt(self, ct):
        return pow(ct, self.d, self.N)


rsa = RSA()
FLAG = bytes_to_long(open("flag", "rb").read())
FLAG_enc = rsa.encrypt(FLAG)

print("Welcome to dream's RSA server")

while True:
    print("[1] Encrypt")
    print("[2] Decrypt")
    print("[3] Get info")

    choice = input()

    if choice == "1":
        print("Input plaintext (hex): ", end="")
        pt = bytes_to_long(bytes.fromhex(input()))
        print(rsa.encrypt(pt))

    elif choice == "2":
        print("Input ciphertext (hex): ", end="")
        ct = bytes_to_long(bytes.fromhex(input()))
        if ct == FLAG_enc or ct > rsa.N:
            print("Do not cheat !")
        else:
            print(rsa.decrypt(ct))

    elif choice == "3":
        print(f"N: {rsa.N}")
        print(f"e: {rsa.e}")
        print(f"FLAG: {FLAG_enc}")

    else:
        print("Nope")

 

소스코드 분석

1. 라이브러리 임포트 및 RSA 클래스 정의

from Crypto.Util.number import getStrongPrime, bytes_to_long, inverse

class RSA(object):
    def __init__(self):
        self.p = getStrongPrime(512)   # 512비트의 강한 소수 p 생성
        self.q = getStrongPrime(512)   # 512비트의 강한 소수 q 생성
        self.N = self.p * self.q       # 모듈러스 N 계산 (1024비트)
        self.e = 0x10001               # 공개 지수 e = 65537
        self.d = inverse(self.e, self.N - self.p - self.q + 1)

    def encrypt(self, pt):
        return pow(pt, self.e, self.N) # 암호화: pt^e mod N

    def decrypt(self, ct):
        return pow(ct, self.d, self.N) # 복호화: ct^d mod N
  • 키 생성
    • 두 개의 큰 소수 $p,q$를 생성해서 RSA 키를 만든다.
    • self.d를 구하는 공식에서 self.N - self.p - self.q + 1을 사용했는데, 이는 수학적으로 $(p-1)(q-1)$인 $\phi(N)$과 동일하다.
  • 암호화/복호화
    • 파이썬의 pow(base, exp, mod) 함수를 사용하여 RSA의 기본 공식인 $C \equiv M^e \pmod N$$M \equiv C^d \pmod N$을 수행한다.

 

2. 초기 설정 및 플래그 암호화

rsa = RSA()
FLAG = bytes_to_long(open("flag", "rb").read())
FLAG_enc = rsa.encrypt(FLAG)
  • 서버가 시작되자마자 flag라는 파일의 내용을 읽어 숫자로 변환(FLAG)한 뒤, 위에서 만든 RSA 키로 암호화(FLAG_enc) 한다.

 

3. 메인 루프 (사용자 인터페이스)

  • 서버는 무한 루프(while True)를 돌며 사용자에게 3가지 메뉴를 제공한다.

[1] Encrypt (암호화)

if choice == "1":
    print("Input plaintext (hex): ", end="")
    pt = bytes_to_long(bytes.fromhex(input()))
    print(rsa.encrypt(pt))
  • 사용자가 평문(Hex)을 입력하면, 서버의 공개키($N, e$)를 이용해 암호화한다.
  • 공격자가 원하는 임의의 데이터에 대한 암호문을 얻을 수 있다.

[2] Decrypt (복호화)

elif choice == "2":
    print("Input ciphertext (hex): ", end="")
    ct = bytes_to_long(bytes.fromhex(input()))
    
    # [중요] 치팅 방지 로직
    if ct == FLAG_enc or ct > rsa.N:
        print("Do not cheat !")
    else:
        print(rsa.decrypt(ct))
  • 사용자가 암호문(Hex)을 입력하면, 서버의 비밀키($d$)를 이용해 복호화한다.
  • Challenge
    • 사용자가 입력한 암호문 ct가 서버가 가지고 있는 플래그 암호문(FLAG_enc)과 똑같으면 복호화를 거부한다.

[3] 정보 확인

elif choice == "3":
    print(f"N: {rsa.N}")       # 공개키 모듈러스
    print(f"e: {rsa.e}")       # 공개 지수
    print(f"FLAG: {FLAG_enc}") # 암호화된 플래그
  • 공개키 정보($N, e$)와 암호화된 플래그($FLAG_{enc}$)를 알려준다.

 

취약점 분석

서버의 방어 로직을 다시 보면 다음과 같다.

if ct == FLAG_enc or ct > rsa.N:
    print("Do not cheat !")
else:
    print(rsa.decrypt(ct))

 

  • 공격자는 FLAG_enc를 알고 있다.
  • 하지만 FLAG_enc를 그대로 2번 메뉴(복호화)에 넣으면 서버가 "Do not cheat!"라며 거절한다.
  • 따라서, FLAG_enc와 다르게 생겼지만, 수학적으로 플래그와 연결된 새로운 암호문을 만들어야 한다.

 

핵심 취약점 : RSA Homomorphic

RSA 암호화에는 두 암호문을 곱하면, 그 안의 평문끼리도 곱해지는 성질이 있다.

이를 수학적으로 표현하면 다음과 같다. ($E(m)$은 메시지 $m$을 암호화한 것을 의미)

 

$E(A) \times E(B) = E(A \times B)$

 

즉, 암호문 상태에서 곱하기를 하면, 나중에 복호화했을 때 평문도 곱해진 상태로 나온다.

 

 

 

공격 시나리오

RSA의 Homomorphic 성질 이용해서 플래그 암호문을 변장시켜 플래그를 얻을 것이다. 가장 간단한 숫자 2를 사용해서 변장시켜보자.

 

STEP 1 : 숫자 2 암호화

직접 숫자 2를 RSA로 암호화한다.

$C_{two} = 2^e \pmod N$

 

STEP 2 : 변장된 암호문 만들기

원본 플래그 암호문($C_{flag}$)에 방금 만든 $C_{two}$를 곱한다.

 

$C_{fake} = (C_{flag} \times C_{two}) \pmod N$

 

$C_{fake} = E(\text{FLAG}) \times E(2) = E(\text{FLAG} \times 2)$

 

$C_{fake}$는 플래그에 2를 곱한 값의 암호문이 된다.

 

STEP 3 : 서버에게 전송 후 플래그 복구

$C_{fake}$를 서버의 2번 메뉴에 보낸다.

서버가 복호화해서 준 값($P_{fake}$)은 다음과 같다.

 

$P_{fake} = \text{FLAG} \times 2$

 

우리는 평문 상태의 결과를 받았으므로, 여기서 그냥 2로 나누기만 하면 원본 FLAG를 얻을 수 있다.

 

 

공격 코드

from pwn import *
from Crypto.Util.number import long_to_bytes

# 통신 과정을 자세히 보기 위해 디버그 모드 활성화
context.log_level = 'debug'

r = remote('host8.dreamhack.games', 13064) 

def solve():
    print("[*] 서버 접속 및 정보 수집 중...")
    
    # 1. 메뉴 [3]번 선택하여 정보(N, e, FLAG_enc) 가져오기
    # 확실하게 메뉴가 다 출력될 때까지 기다린 후 입력
    r.sendlineafter(b"Get info", b"3")
    
    r.recvuntil(b"N: ")
    N = int(r.recvline().strip())
    
    r.recvuntil(b"e: ")
    e = int(r.recvline().strip())
    
    r.recvuntil(b"FLAG: ")
    FLAG_enc = int(r.recvline().strip())
    
    print(f"[+] N: {N}")
    print(f"[+] e: {e}")
    print(f"[+] FLAG_enc: {FLAG_enc}")

    # 2. Chosen Ciphertext Attack (CCA) 준비
    factor = 2
    C_factor = pow(factor, e, N)
    
    # 변조된 암호문 생성: (FLAG_enc * 2^e) % N
    C_forged = (FLAG_enc * C_factor) % N
    
    print(f"[*] 변조된 암호문 생성 완료")

    # 3. 메뉴 [2]번 선택하여 변조된 암호문 복호화 요청
    r.sendlineafter(b"Get info", b"2")
    
    # Hex 문자열 변환 시 짝수 길이 맞추기
    # bytes.fromhex()는 홀수 길이 문자열을 받으면 서버가 튕긴다.
    payload = hex(C_forged)[2:]
    if len(payload) % 2 != 0:
        payload = '0' + payload
        
    print(f"[*] Sending Payload Length: {len(payload)}")
    
    # 프롬프트 "Input ciphertext (hex): "를 기다림
    r.sendlineafter(b"(hex): ", payload.encode())
    
    # 4. 복호화된 결과 받기
    try:
        # 서버가 숫자만 보내고 줄바꿈 하므로 recvline() 사용
        # 간혹 통신 지연으로 빈 줄이 올 수 있어 예외 처리
        received = r.recvline().strip()
        if not received:
             received = r.recvline().strip() # 한 번 더 시도
             
        P_forged = int(received)
        print(f"[+] 서버로부터 받은 복호화 값 (FLAG * 2): {P_forged}")

        # 5. 원래 플래그 복구
        FLAG_int = P_forged // factor
        FLAG = long_to_bytes(FLAG_int)
        
        print("\n" + "="*40)
        print(f"FLAG: {FLAG.decode(errors='ignore')}") # decode 에러 방지
        print("="*40)
        
    except Exception as e:
        print(f"[-] Error parsing result: {e}")

    r.close()

if __name__ == "__main__":
    solve()

 

주의사항

파이썬의 bytes.fromhex() 함수는 반드시 짝수 길이의 문자열만 입력받는다.

만든 암호문 숫자(C_forged)를 hex()로 변환할 때, 앞의 0이 잘리면서 홀수 길이가 되는 경우가 종종 발생한다.

따라서 payload의 길이가 홀수면 앞에 0을 넣어 짝수로 만드는 코드를 넣어주었다.

 

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

[Dreamhack] uncommon e  (0) 2026.01.27
[Dreamhack] TOP secret!  (0) 2026.01.22
[Dreamhack] safeprime  (0) 2026.01.18
[Dreamhack] Padding Miracle Attack  (0) 2025.11.17
[Dreamhack] Textbook-CBC  (0) 2025.11.17