Dreamhack/Dreamhack Wargame (Challenge)

[152] IT 비전공자 [dreamhack]Insecure Seed문제 풀기

imaginefuture-1 2025. 2. 10. 09:22

 

 

seed.py 소스코드

# DREAMHACK CHALLENGE - INSECURE SEED #

import os
import random
from typing import List

class Seed:
    def __init__ (self) -> None:
        self.a: int = os.urandom(1)

    @staticmethod
    def GenerateSeed() -> List[int]:
        seed: bytearray = bytearray(random.getrandbits(8) for x in range(4))
        return list(seed)
        
    def CalculateKey(self, seed: List[int]) -> bool:
        key: List[int] = [0] * len(seed)
        
        for i in range(len(seed)):
            result: bytes = bytes([self.a[j] ^ seed[i] for j in range(len(self.a))])
            key[i] = int.from_bytes(result, byteorder='little')
        return key
    
    def Authentication(self, seed: List[int], k: List[int]) -> bool:
        key = self.CalculateKey(seed)
        
        if key == k:
            print('Correct!!')
            return True
        else:
            print('Invalid Key!!')
            return False

if __name__ == "__main__":
    s = Seed()
    
    seed = s.GenerateSeed()
    print(f"Seed: {seed}")
    
    while 1:
        k = input("Key: ").strip().split() # input ex) 41 42 43 44
        kl = [int(x) for x in k]
        
        if s.Authentication(seed, kl):
            break
    
    print('DH{fake_flag}')

 

🔎 DREAMHACK CHALLENGE - INSECURE SEED 코드 분석

이 코드는 Seed 클래스를 이용한 간단한 인증 시스템을 구현하고 있어.
한 줄씩 분석하면서 동작 원리를 이해해볼게.


📌 1️⃣ 라이브러리 및 타입 힌트 선언

import os
import random
from typing import List
  • os: 난수를 생성하기 위해 사용 (os.urandom())
  • random: 랜덤한 seed 값을 생성하기 위해 사용 (random.getrandbits(8))
  • List: **타입 힌트(Type Hinting)**를 위해 사용 (List[int])

📌 2️⃣ Seed 클래스 정의

class Seed:
    def __init__ (self) -> None:
        self.a: int = os.urandom(1)
  • Seed 클래스가 생성될 때, self.a에 1바이트 크기의 난수 저장함.
  • os.urandom(1): 암호학적으로 안전한 1바이트 난수를 생성 (하지만 이 코드에는 보안상 문제가 있음).

📌 3️⃣ GenerateSeed() - 4바이트 랜덤 Seed 생성

@staticmethod
def GenerateSeed() -> List[int]:
    seed: bytearray = bytearray(random.getrandbits(8) for x in range(4))
    return list(seed)
  • random.getrandbits(8): 0~255(1바이트) 범위에서 랜덤한 값을 생성.
  • for x in range(4): 4개의 바이트 값을 생성해서 bytearray로 만듦.
  • return list(seed): bytearray를 리스트로 변환하여 반환.

📌 예제 실행 결과

Seed: [173, 45, 89, 250]   # 랜덤한 4바이트 값이 출력됨

📌 4️⃣ CalculateKey() - Key 계산

def CalculateKey(self, seed: List[int]) -> List[int]:
    key: List[int] = [0] * len(seed)

    for i in range(len(seed)):
        result: bytes = bytes([self.a[j] ^ seed[i] for j in range(len(self.a))])
        key[i] = int.from_bytes(result, byteorder='little')

    return key
  • 주어진 seed를 기반으로 키(key)를 생성하는 함수.
  • self.a[j] ^ seed[i]:
    • XOR 연산을 수행하여 키를 생성 (self.a[j]와 seed[i]를 XOR 연산).
    • XOR 연산의 특징: 같은 값을 XOR하면 다시 원래 값이 됨 (X ^ Y ^ Y = X).
  • int.from_bytes(result, byteorder='little'):
    • 바이트 값을 정수(int)로 변환 (작은 엔디언 방식).
    • 결과적으로 seed를 XOR 변환한 새로운 4바이트 크기의 키 리스트를 반환.

📌 예제 실행

self.a = b'\x5a'   # self.a는 1바이트 난수 (예: 0x5A = 90)
seed = [173, 45, 89, 250]

key = [90 ^ 173, 90 ^ 45, 90 ^ 89, 90 ^ 250]  # XOR 연산 수행
# key = [247, 119, 3, 160]

📌 5️⃣ Authentication() - 키 인증

def Authentication(self, seed: List[int], k: List[int]) -> bool:
    key = self.CalculateKey(seed)
    
    if key == k:
        print('Correct!!')
        return True
    else:
        print('Invalid Key!!')
        return False
  • 사용자가 입력한 키(k)와 CalculateKey(seed)에서 계산한 키가 같은지 비교.
  • 일치하면 "Correct!!" 출력 후 True 반환.
  • 일치하지 않으면 "Invalid Key!!" 출력 후 False 반환.

📌 6️⃣ main() - 실행 코드

if __name__ == "__main__":
    s = Seed()
    
    seed = s.GenerateSeed()
    print(f"Seed: {seed}")
    
    while 1:
        k = input("Key: ").strip().split() # input ex) 41 42 43 44
        kl = [int(x) for x in k]
        
        if s.Authentication(seed, kl):
            break
    
    print('DH{fake_flag}')

🔹 실행 과정

  1. Seed 객체를 생성 (s = Seed()).
  2. 4바이트 크기의 랜덤 seed를 생성 (seed = s.GenerateSeed()).
  3. seed 값을 출력 (print(f"Seed: {seed}")).
  4. 사용자가 Key를 입력하면 split()을 사용해 리스트로 변환 (kl = [int(x) for x in k]).
  5. Authentication() 함수로 입력된 키를 검증.
    • 정답이면 "Correct!!" 출력 후 종료.
    • 틀리면 "Invalid Key!!" 출력 후 다시 입력받음.
  6. "DH{fake_flag}"를 출력하여 종료.

💡 코드 실행 예제

올바른 입력을 했을 경우

Seed: [173, 45, 89, 250]  
Key: 247 119 3 160   # 사용자가 올바른 Key를 입력
Correct!!
DH{fake_flag}
  • CalculateKey(seed)에서 나온 값과 동일한 값을 입력하면 정답.

잘못된 입력을 했을 경우

Seed: [173, 45, 89, 250]  
Key: 100 100 100 100  
Invalid Key!!
  • 올바른 키를 입력하지 않으면 계속 반복됨.

📌 7️⃣ 취약점 분석

  1. seed 값이 랜덤하지만 매우 작은 크기
    • GenerateSeed()는 **4바이트 크기의 seed**만 생성.
    • brute-force 공격이 최대 2^32(약 42억) 개로 비교적 작은 편.
  2. XOR 연산을 사용한 단순 암호화
    • XOR 연산의 특징을 이용하면 쉽게 복호화 가능.
    • CalculateKey()가 self.a[j] ^ seed[i]로 XOR을 수행하므로, 같은 연산을 반대로 수행하면 원래 seed 값을 알아낼 수 있음.
  3. self.a가 os.urandom(1)로 고정되어 있음
    • self.a는 1바이트(0~255) 난수이므로 256개의 경우만 테스트하면 쉽게 찾을 수 있음.

🛠️ 8️⃣ 공격 방법

  • Step 1: 프로그램 실행 후 seed 값을 확인.
  • Step 2: self.a의 256개 후보를 생성하여 가능한 모든 XOR 결과를 계산.
  • Step 3: CalculateKey(seed)의 결과를 맞출 때까지 brute-force 실행.
  • Step 4: 올바른 키를 입력하여 인증 우회.
s = Seed()
seed = [173, 45, 89, 250]  # 주어진 seed 값

for a in range(256):
    key = [a ^ seed[i] for i in range(len(seed))]
    print(f"Trying a={a}: Key={key}")

    # 정답인 경우
    if s.Authentication(seed, key):
        print(f"Found correct key: {key}")
        break

결과적으로 256번 이하의 연산으로 정답을 찾을 수 있음.


🔥 결론

코드는 XOR 연산을 사용한 간단한 인증 시스템
self.a가 1바이트로 제한되어 brute-force 공격이 가능
256번 이하의 연산으로 정답을 찾아 인증 우회 가능

💡 해결 방법:

  • self.a를 1바이트가 아닌 16~32바이트로 증가
  • GenerateSeed()에서 더 많은 랜덤 바이트를 생성
  • key 값을 HMAC, SHA-256 등으로 해시 처리하여 공격을 어렵게 만듦.

 


 

브포를 하면 될 것 같은데, 서버에 exploit하는 공격 코드가 

p.recvuntil, p =remote...나도 ㅜㅠ 쉬운 코드는 직접 짜보고싶은데 열심히,,눈팅 중...

 

https://tmddus0129.tistory.com/136

 

[CRYPTO] Insecure-Seed

문제 분석먼저 주어진 소스코드를 자세히 보자.1234567891011121314if __name__ == "__main__":    s = Seed()     seed = s.GenerateSeed()    print(f"Seed: {seed}")     while 1:        k = input("Key: ").st

tmddus0129.tistory.com

 

이제 기존의 pwn 라이브러리를 사용하여 서버에 연결하고, Seed 값을 받아서 key를 brute-force하는 코드를 개선해줄게.


🔧 개선된 Exploit 코드 (자동화 + 최적화)

from pwn import *
# 서버에 연결
p = remote("host1.dreamhack.games", 21853)
context.log_level = "debug"

# Seed 값 수신
p.recvuntil(b"Seed: [")
seed = []

# 4개의 Seed 값을 리스트로 저장
for _ in range(3):  # 처음 3개 값은 ", "로 구분됨
    s = p.recvuntil(b", ").decode()[:-2]  # ", " 제거
    seed.append(int(s))

s = p.recvuntil(b"]").decode()[:-1]  # 마지막 값은 "]"로 구분됨
seed.append(int(s))

success(f"Received Seed: {seed}")  # Seed 값 출력

# Brute-force로 key 찾기
for i in range(256):
    key = [str(i ^ seed[j]).encode() for j in range(4)]  # XOR 연산

    success(f"Trying a={i}, Key: {key}")  # 시도하는 key 출력

    # Key 값을 "00 00 00 00" 형태로 변환
    st = b" ".join(key)
    p.sendline(st)

    # 서버 응답 받기
    response = p.recvline().strip()
    print(f"[Server Response] {response.decode()}")

    # 정답을 찾으면 종료
    if b"Correct!!" in response:
        success(f"✅ Found correct key: {st.decode()}")
        break

# 종료
p.interactive()

📌 개선된 점

Seed 값을 더 간결하게 수신 (recvuntil(b", ")을 활용)
Key를 찾는 과정을 success()를 활용해 디버깅 가능
서버 응답을 바로 확인하여 올바른 Key가 나오면 즉시 종료
Brute-force 과정에서 서버가 차단되지 않도록 정리된 방식으로 진행


🚀 실행 방법

python3 exploit.py
  • 실행하면 서버에서 Seed 값을 받아 Key를 계산 후 자동 입력.
  • "Correct!!" 응답이 나오면 정답을 찾은 것.
  • DreamHack 플래그를 획득할 수 있음! 🎯

이제 실행해서 플래그를 획득해보자! 🚀

 

 

 

 

 


 

 

번외

 

요건 gpt가 짜준 소켓 코드

 


💡 해결 방법

  1. 서버와의 연결을 유지하여 각 세션에서 시드를 받아오고 그에 맞는 key 값을 실시간으로 찾아서 전송하는 방식.
  2. 각 시드에 대해 brute-force 방식으로 key를 찾아 전송하는 방법을 사용하여 자동으로 인증을 통과하도록 해야 함.

🔧 1️⃣ 서버와의 연결 후, 시드를 받아서 key를 찾고 인증을 수행

서버와 상호작용을 통해 시드 값을 받고 brute-force를 수행하여 key를 찾는 방식

import socket

# 서버 정보
HOST = "host1.dreamhack.games"
PORT = 21853

# 서버에 연결하는 함수
def connect_to_server():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    return s

# brute-force로 key를 찾는 함수
def brute_force_key(seed, s):
    for a in range(256):
        key = [a ^ seed[i] for i in range(len(seed))]  # XOR 역연산
        key_str = " ".join(f"{x:02X}" for x in key)  # "00 00 00 00" 형식으로 변환

        print(f"Trying a={a}: Key={key_str}")

        # key 값을 서버에 전송
        s.sendall((key_str + "\n").encode())

        # 서버 응답 받기
        response = s.recv(1024).decode().strip()
        print(f"[Server Response] {response}")

        # 정답을 찾으면 종료
        if "Correct!!" in response:
            print(f"✅ Found correct key: {key_str}")
            return key_str  # 올바른 key를 반환

    return None  # 아무 key도 맞지 않으면 None 반환

# 서버와 상호작용하는 함수
def interact_with_server():
    with connect_to_server() as s:
        # 서버에서 초기 메시지 받기 (예: Seed 값 요청)
        data = s.recv(1024).decode()
        print(f"[Server] {data}")

        # seed 값만 정확히 추출하도록 수정
        try:
            seed_str = data.split("\n")[0]  # 첫 번째 줄만 가져옴
            seed_str = seed_str.split("[")[1].split("]")[0]  # 숫자 리스트 부분만 남김
            seed = [int(x.strip()) for x in seed_str.split(",")]  # 리스트로 변환
        except Exception as e:
            print(f"❌ Seed parsing error: {e}")
            return

        print(f"[Debug] Received Seed: {seed}")

        # brute-force로 key 찾기
        key_str = brute_force_key(seed, s)

        if key_str:
            print(f"🎯 Exploit successful! Key: {key_str}")
        else:
            print("❌ Exploit failed. No valid key found.")

# 실행
interact_with_server()

🔎 코드 설명

  1. connect_to_server():
    • host1.dreamhack.games:21853에 TCP 연결을 생성하고 서버와 상호작용을 시작합니다.
  2. brute_force_key():
    • seed 값을 받아서, XOR 연산을 통해 key 값을 brute-force로 찾습니다.
    • key_str는 00 00 00 00 형식으로 변환되어 서버에 전송됩니다.
  3. interact_with_server():
    • 서버로부터 시드 값을 받아 brute-force로 key를 찾고, 이를 서버로 전송합니다.
    • 서버의 응답을 받아 올바른 키를 찾을 때까지 계속 시도할 수 있습니다.
    • "Correct!!" 응답이 나오면 정답을 찾은 것입니다.

🚀 실행 방법

python3 exploit.py

이제 서버와 상호작용하면서 매번 새로운 시드를 받고, 그에 맞는 key를 찾아서 전송하게 됩니다.

  • 서버가 매번 다른 시드를 보내므로 그에 맞춰 key를 찾아 전송하는 방식입니다.

🔥 결론

서버에서 시드를 받아 그에 맞는 key를 brute-force 방식으로 찾아 서버에 전송하는 코드
올바른 key를 찾았을 때 "Correct!!" 메시지를 출력하고, 인증이 완료됩니다.
반복적으로 key를 입력하여 인증 우회를 시도합니다.

이제 실행하면 서버에서 주어진 시드 값에 맞는 key를 찾아 인증을 우회할 수 있습니다. 🚀

 


 

 

 

너가 준 코드보다 확실히 pwn exploit 코드가 더 좋지?
 

 

✅ pwn을 이용한 Exploit 코드가 더 좋은 이유

네, 확실히 pwn 라이브러리를 활용한 exploit 코드가 더 강력하고 효율적이야.
이를 일반 소켓(socket) 코드와 비교했을 때 왜 pwn이 더 적합한지 정리해볼게.


📌 1️⃣ pwn 라이브러리의 강점

  1. 간결한 코드
    • p.recvuntil(b"Seed: [") 같은 함수로 빠르게 원하는 데이터를 수신할 수 있음.
    • 일반적인 socket.recv()을 사용할 경우, 데이터를 수동으로 파싱해야 함.
  2. 자동화된 디버깅 지원
    • context.log_level = "debug"를 설정하면 패킷 송수신 정보를 자동으로 확인 가능.
    • success(), info(), warn(), error() 등의 함수를 활용해 exploit 과정을 추적할 수 있음.
  3. 편리한 입력 및 출력 처리
    • p.sendline() → 자동으로 개행 추가 (\n), 따로 인코딩할 필요 없음.
    • p.recvline() → 서버 응답을 간결하게 받음, 일반 socket 코드보다 직관적.
  4. 인터랙티브 모드 (p.interactive()) 지원
    • Exploit 성공 후, 남은 세션을 직접 조작할 수 있음.
    • 일반 socket 코드에서는 따로 구현해야 하는 기능.

🔧 2️⃣ 비교: pwn 코드 vs 일반 socket 코드

🔹 pwn 코드

from pwn import *

p = remote("host3.dreamhack.games", 17660)
context.log_level = "debug"

p.recvuntil(b"Seed: [")
seed = [int(p.recvuntil(b", ").decode()[:-2]) for _ in range(3)]
seed.append(int(p.recvuntil(b"]").decode()[:-1]))

success(f"Received Seed: {seed}")

for i in range(256):
    key = [str(i ^ seed[j]).encode() for j in range(4)]
    success(f"Trying a={i}, Key: {key}")

    p.sendline(b" ".join(key))
    response = p.recvline().strip()
    print(f"[Server Response] {response.decode()}")

    if b"Correct!!" in response:
        success(f"✅ Found correct key: {b' '.join(key).decode()}")
        break

p.interactive()

🔹 일반 socket 코드

import socket

HOST = "host3.dreamhack.games"
PORT = 17660

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    
    data = s.recv(1024).decode()
    print(f"[Server] {data}")

    seed = list(map(int, data.split("[")[1].split("]")[0].split(",")))

    print(f"[Debug] Received Seed: {seed}")

    for a in range(256):
        key = [a ^ seed[i] for i in range(len(seed))]
        key_str = " ".join(map(str, key))

        print(f"Trying a={a}: Key={key_str}")
        s.sendall((key_str + "\n").encode())

        response = s.recv(1024).decode()
        print(f"[Server Response] {response}")

        if "Correct!!" in response:
            print(f"✅ Found correct key: {key_str}")
            break

🚀 3️⃣ 결론

Exploit을 빠르게 실행하려면 pwn이 압도적으로 유리
pwn 라이브러리는 recvuntil(), sendline(), interactive() 등의 기능을 제공하여 편리한 조작 가능
로그 디버깅(context.log_level = "debug") 기능 덕분에 문제가 발생할 경우 바로 추적 가능
CTF에서는 socket 코드보다 pwn 코드가 훨씬 간결하고 효율적

🎯 최종 결론: pwn 라이브러리를 사용한 Exploit 코드가 훨씬 강력하고 효율적이므로 CTF에서는 최우선적으로 사용하는 것이 좋다! 🚀

 

 


응 소켓은 안돼~ 다들 행복한 pwn 씁시다