Dreamhack/Dreamhack Wargame (Challenge)

[105] IT 비전공자 [dreamhack]Robot Only문제 풀기

imaginefuture-1 2024. 12. 23. 09:05

로봇온니~난 휴먼인걸 ㅠ

 

 

 

#!/usr/bin/env python3
import random
import signal
import sys

MENU_GAMBLE     = 1
MENU_VERIFY     = 2
MENU_FLAG       = 3
MENU_LEAVE      = 4

money = 500
verified = False

def show_menu():
    print('=======================================')
    print('1. go to gamble')
    print('2. verify you\'re a robot')
    print('3. buy flag')
    print('4. leave')

def get_randn():
    return random.randint(0, 0xfffffffe)

def gamble():
    global money
    global verified

    if verified is False:
        print('you\'re are not verified as a robot ;[')
        return

    print('greetings, robot :]')

    bet = int(input('how much money do you want to bet (your money: ${0})? '.format(money)))
    if money < bet:
        print('you don\'t have enough money (your money: ${0}).'.format(money))
        return

    randn = get_randn()
    answer = randn % 5 + 1

    print('[1] [2] [3] [4] [5]')
    user_answer = int(input('pick one of the box > '))

    print('answer is [{0}]!'.format(answer))

    if user_answer == answer:
        print('you earned ${0}.'.format(bet))
        money += bet
    else:
        print('you lost ${0}.'.format(bet))
        money -= bet

    if money <= 0:
        print('you busted ;]')
        sys.exit()

class MyTimeoutError(Exception):
    def __init__(self):
        pass

def timeout_handler(signum, frame):
    raise MyTimeoutError()

def verify():
    global verified

    if verified is True:
        print('you have already been verified as a robot :]')
        return

    randn224 = (get_randn() | get_randn() << 32 | get_randn() << 64 |
                get_randn() << 96 | get_randn() << 128 | get_randn() << 160)

    challenge = randn224 ^ 0xdeaddeadbeefbeefcafecafe13371337DEFACED0DEFACED0

    signal.alarm(3)
    signal.signal(signal.SIGALRM, timeout_handler)

    try:
        print('please type this same: "{0}"'.format(challenge))
        user_challenge = input('> ')

        if user_challenge == str(challenge):
            verified = True
            print('you\'re are now verified as a robot :]')
        else:
            print('you\'re not a robot ;[')
        signal.alarm(0)

    except MyTimeoutError:
        print('\nyou failed to verify! robots aren\'t that slow ;[')

def flag():
    global money

    print('price of the flag is $10,000,000,000.')

    if money < 10000000000:
        print('you don\'t have enough money (your money: ${0}).'.format(money))
        return

    with open('./flag', 'rb') as f:
        print(b'flag is ' + f.read())
    sys.exit()

def main():
    while True:
        show_menu()
        menu = int(input('> '))

        if menu == MENU_GAMBLE:
            gamble()

        elif menu == MENU_VERIFY:
            verify()

        elif menu == MENU_FLAG:
            flag()

        elif menu == MENU_LEAVE:
            sys.exit()

        else:
            print('wrong menu :[')

if __name__ == '__main__':
    main()

 

 

 

 

 

이 Python 스크립트는 간단한 게임 및 인증 시스템으로 설계되었습니다. 주요 기능은 게임, 로봇 인증, 플래그 구매, 그리고 프로그램 종료입니다. 아래에서 스크립트의 각 부분을 분석하고, 잠재적인 취약점 및 익스플로잇 가능성을 검토합니다.


주요 기능 분석

1. show_menu()

  • 사용자에게 선택 가능한 메뉴를 출력합니다:
    1. go to gamble
    2. verify you're a robot
    3. buy flag
    4. leave
    

2. gamble()

  • 목적: 사용자가 로봇으로 인증되었는지 확인한 뒤, 돈을 걸고 숫자를 맞추는 게임.
  • 동작:
    1. 로봇 인증 확인:
      • verified 변수가 False면 게임 불가.
    2. 베팅 금액 입력:
      • 현재 금액보다 큰 금액을 걸 수 없음.
    3. 숫자 맞추기 게임:
      • 프로그램이 생성한 숫자(1~5) 중 하나를 사용자와 비교.
      • 맞추면 베팅 금액만큼 이익, 틀리면 손실.
    4. 파산 처리:
      • money가 0 이하가 되면 프로그램 종료.

3. verify()

  • 목적: 사용자에게 **로봇 인증(챌린지)**을 요구.
  • 동작:
    1. 224비트 랜덤 숫자(randn224) 생성:
      • 6개의 랜덤 숫자를 조합하여 큰 숫자를 만듦.
    2. XOR 연산으로 챌린지 값 생성:
      • 랜덤 숫자와 상수를 XOR 연산.
    3. 사용자 입력 요구:
      • 3초 내에 올바른 값을 입력하면 인증 성공.
      • 3초가 지나면 MyTimeoutError 예외 발생.

4. flag()

  • 목적: 사용자가 충분한 돈($10,000,000,000)을 가지고 있으면 플래그를 출력.
  • 동작:
    1. 돈을 확인:
      • 충분하지 않으면 플래그를 출력하지 않음.
    2. 플래그 파일(./flag) 읽기:
      • flag 파일의 내용을 출력하고 프로그램 종료.

5. main()

  • 목적: 프로그램의 메인 루프.
  • 동작:
    • 사용자 입력에 따라 각 기능 호출.

취약점 및 익스플로잇 가능성

1. 돈(money) 조작

  • 문제:
    • money는 글로벌 변수로 정의되어 있으며, 프로그램 내에서 조작 가능.
    • 직접적으로 입력값을 통해 돈을 변경할 수는 없지만, 메모리 조작을 통해 값을 바꿀 수 있음.
  • 익스플로잇 방법:
    • money 값을 강제로 조작하여 $10,000,000,000 이상으로 설정.
    • 예를 들어, 메모리 덤프 도구나 디버거를 사용하여 값을 변경.

2. verify()의 XOR 취약점

  • 문제:
    • verify() 함수에서 생성된 챌린지는 XOR 연산으로 생성됩니다:
      challenge = randn224 ^ 0xdeaddeadbeefbeefcafecafe13371337DEFACED0DEFACED0
      
    • XOR 연산은 다음과 같은 특성을 가짐:
      A ^ B = C  =>  A = B ^ C
      
    • randn224를 알고 있다면, 상수를 다시 XOR하여 챌린지 값을 쉽게 계산 가능.
  • 익스플로잇 방법:
    • randn224 생성 코드를 분석하여 직접 계산.
    • 또는 프로그램의 디버깅을 통해 randn224 값을 추출.

3. 인증 없이 플래그 접근

  • 문제:
    • 프로그램의 플래그 파일 읽기 제한은 단순히 돈(money) 변수에 의존.
  • 익스플로잇 방법:
    • money 값을 $10,000,000,000 이상으로 설정하여 플래그 출력 조건을 우회.

4. 플래그 파일 경로 하드코딩

  • 문제:
    • 플래그 파일 경로가 하드코딩되어 있음:
      with open('./flag', 'rb') as f:
          print(b'flag is ' + f.read())
      
    • 파일 경로를 변경하거나 심볼릭 링크를 생성하여 플래그를 우회적으로 접근 가능.

5. 시간 제한 우회

  • 문제:
    • 인증 시 signal.alarm(3)으로 3초 제한이 설정됨.
  • 익스플로잇 방법:
    • 로컬 실행 시 디버거를 사용하여 시간 제한을 우회.

익스플로잇 예시

1. XOR 연산 우회

import random

# XOR 상수
xor_constant = 0xdeaddeadbeefbeefcafecafe13371337DEFACED0DEFACED0

# 랜덤 값 생성 (randn224를 추정)
randn224 = random.getrandbits(224)  # 임의로 추정
challenge = randn224 ^ xor_constant

# 챌린지 값 복원
recovered_randn224 = xor_constant ^ challenge

print(f"Recovered randn224: {recovered_randn224}")

2. 플래그 직접 출력

  • 디버깅 도구로 money 조작:
    gdb ./program
    (gdb) set money = 10000000000
    (gdb) continue
    

보안 개선 방안

  1. 돈(money) 변수 보호:
    • money 값을 수정할 수 없도록 암호화된 상태로 유지.
  2. 챌린지 생성 방식 강화:
    • XOR 대신, 해시 함수(예: SHA256)를 사용하여 인증 값을 생성.
  3. 시간 제한 강화:
    • 시간 제한 우회를 방지하기 위해 입력 읽기를 별도의 스레드에서 처리.
  4. 플래그 접근 제한:
    • 플래그 출력 조건을 단순한 변수 확인에서 추가 검증으로 강화.

결론

이 프로그램은 XOR 연산 및 글로벌 변수 사용의 취약점을 통해 인증 우회 및 플래그 접근이 가능합니다. 보안 개선 방안을 적용하여 이러한 문제를 방지할 수 있습니다. 😊

 

 

 

 

 

히잉 너무 빨라요 선생님

 

 

 

from pwn import *

p = remote(
'host3.dreamhack.games', 14378)

p.recvuntil(
b'> ')

p.send_raw(
b'2\n')

p.recvuntil(
b': ')

num = p.recvline()[
1:-2]

p.recvuntil(
b'> ')

p.send_raw(num +
b'\n')

p.interactive()

출처 ㅣ https://jy05un.tistory.com/239

 

 

 

 

 

 

이 코드 스니펫은 Dreamhack CTF 문제를 익스플로잇하기 위한 Python 스크립트입니다. 이를 분석하고 주요 동작을 설명하겠습니다.


코드 분석

1. p = remote('host3.dreamhack.games', 9265)

  • 설명:
    • pwntools 라이브러리의 remote 함수를 사용하여 원격 서버에 연결.
    • 서버 주소: host3.dreamhack.games.
    • 포트 번호: 9265.
  • 결과:
    • 원격 서버와의 TCP 연결이 설정되고, p 객체를 통해 통신 가능.

2. p.recvuntil(b'> ')

  • 설명:
    • 서버로부터 데이터를 수신하여 b'> '(프롬프트)까지 읽음.
    • 이는 메뉴가 표시된 후 사용자의 입력을 기다리는 상태.

3. p.send_raw(b'2\n')

  • 설명:
    • 메뉴 옵션 2를 선택하기 위해 b'2\n'(옵션 번호와 개행 문자)를 전송.
    • 메뉴에서 "verify you're a robot" 옵션에 해당.
  • 결과:
    • verify() 함수가 호출되어 서버가 챌린지를 생성.

4. p.recvuntil(b': ')

  • 설명:
    • 서버가 "please type this same: {challenge}" 메시지를 출력한 뒤, b': '까지 데이터를 읽음.
    • p.recvuntil 이후에는 챌린지 값이 제공됨.

5. num = p.recvline()[1:-2]

  • 설명:
    • 서버가 출력한 챌린지 값을 읽음.
    • 챌린지 값 정리:
      • p.recvline()은 한 줄을 읽음.
      • [1:-2] 슬라이싱:
        • [1:]: 문자열의 첫 번째 문자(") 제거.
        • [:-2]: 마지막 문자("\n 및 끝나는 따옴표 " 제거).
  • 결과:
    • 챌린지 값이 정리된 형태로 변수 num에 저장.

6. p.recvuntil(b'> ')

  • 설명:
    • 다시 메뉴 프롬프트를 기다림.
    • 서버가 사용자의 응답을 입력받을 준비가 완료된 상태.

7. p.send_raw(num + b'\n')

  • 설명:
    • 서버에 챌린지 값을 입력하여 응답.
    • 변수 num에 저장된 값과 개행 문자(\n)를 전송.
  • 결과:
    • 서버가 챌린지 값과 입력값을 비교하여 인증 성공 여부를 판단.

익스플로잇 동작 원리

verify() 함수의 동작

  • 문제 요약:
    • verify() 함수는 서버가 생성한 랜덤 숫자와 상수(XOR)를 사용해 챌린지 값을 생성:
      challenge = randn224 ^ 0xdeaddeadbeefbeefcafecafe13371337DEFACED0DEFACED0
      
    • 서버는 사용자에게 올바른 챌린지 값을 입력하도록 요구.

스니펫의 익스플로잇

  1. 서버로부터 챌린지 값 수신:
    • recvline()으로 서버가 제공한 challenge 값을 직접 읽음.
  2. 동일한 값을 전송:
    • 서버는 단순히 입력값이 자신이 출력한 값과 일치하는지만 확인.
    • 따라서, 생성 과정을 분석하거나 수학적으로 계산할 필요 없이 서버가 제공한 값을 다시 전송하면 인증이 성공.

전체 익스플로잇 스크립트

from pwn import *

# 원격 서버 연결
p = remote('host3.dreamhack.games', 9265)

# 메뉴 프롬프트 대기
p.recvuntil(b'> ')

# 메뉴에서 "verify you're a robot" 선택
p.send_raw(b'2\n')

# 챌린지 값 수신
p.recvuntil(b': ')
num = p.recvline()[1:-2]  # 챌린지 값 정리

# 메뉴 프롬프트 대기
p.recvuntil(b'> ')

# 챌린지 값 입력
p.send_raw(num + b'\n')

# 인터랙티브 모드로 서버와 상호작용
p.interactive()

결과

  • 이 익스플로잇 스크립트는 서버의 챌린지 값을 그대로 반환하여 로봇 인증을 우회합니다.
  • 인증 성공 후, 서버가 제공하는 추가 메뉴나 기능(예: gamble 또는 flag)을 이용할 수 있습니다.

보안 개선 방안

  1. 챌린지 값 숨김:
    • 서버가 챌린지 값을 출력하지 않고, 사용자가 올바른 답을 계산하도록 설계.
    • 예: 해시 함수 또는 비대칭 암호화로 값을 확인.
  2. 입력 검증 강화:
    • 사용자 입력이 서버의 내부 계산 결과와 비교되도록 수정.
    • 단순 문자열 비교를 사용하는 대신, 서버 내부적으로 값 검증.
  3. 타임아웃 및 입력 제한:
    • 서버가 인증 요청을 받은 후 일정 시간 내에 응답하지 않으면 인증 실패 처리.
def gamble():
    global money
    global verified
    if verified is False:
        print('you\'re are not verified as a robot ;[')
        return
    print('greetings, robot :]')
    bet = int(input('how much money do you want to bet (your money: ${0})? '.format(money)))
    if money < bet:
        print('you don\'t have enough money (your money: ${0}).'.format(money))
        return
    randn = get_randn()
    answer = randn % 5 + 1
    print('[1] [2] [3] [4] [5]')
    user_answer = int(input('pick one of the box > '))
    print('answer is [{0}]!'.format(answer))
    if user_answer == answer:
        print('you earned ${0}.'.format(bet))
        money += bet
    else:
        print('you lost ${0}.'.format(bet))
        money -= bet
    if money <= 0:
        print('you busted ;]')
        sys.exit()

 

 

 

 

 

 

 


여기서 버그 하나 존재하는데, 바로 음수에 대한 검증이 이루어지지 않다는 것이다. 즉 우리가 실패 시 배팅할 금액을 "-"하게 되는데, 음수를 배팅한 경우 "--" = "+"가 된다.

출처 ㅣ https://jy05un.tistory.com/239