Back to Writeups
wargamepwneasy-medium

exploit.me: Vulnerable Service Exploitation

exploit.me 취약 서비스 풀이: x86 Linux 환경에서의 스택 버퍼 오버플로우, 리턴 주소 덮어쓰기, 셸코드 인젝션, ROP 기법

pwnbuffer-overflowshellcodeROPexploit-devx86

개요

exploit.me는 x86, x64, ARM, AArch64 타깃에 걸쳐 13가지 전형적인 취약점 클래스를 다루는 의도적으로 취약하게 만들어진 바이너리다. 비x86 아키텍처에서의 ROP로 넘어가기 전 구조적인 방식으로 바이너리 익스플로잇을 연습하기 위해 진행했다.

취약점 인덱스:

Level 1:  Integer overflow
Level 2:  Stack overflow
Level 3:  Array overflow
Level 4:  Off by one
Level 5:  Stack cookie
Level 6:  Format string
Level 7:  Heap overflow
Level 8:  Structure redirection / Type confusion
Level 9:  Zero pointers
Level 10: Command injection
Level 11: Path Traversal
Level 12: Basic ROP
Level 13: Use-after-free

각 레벨은 이전 레벨의 패스워드로 잠금 해제된다. 바이너리는 다음과 같이 실행된다:

./exploit64 <level_password> [args...]

Level 1: Integer Overflow

첫 번째 패스워드는 사용 방법 메시지에 하드코딩되어 있다: hello.

int int_overflow(char* value)
{
    unsigned short number;
    int i = atoi(value);
    unsigned int h = i;
 
    if (h <= 0) {
        printf("Value less or equal 0 is not allowed.\n");
        exit(0);
    }
    number = i;
 
    if (number != 0) {
        printf("Value %d defined.\n", number);
        exit(0);
    }
 
    if (i < 0 || number == 0)  // Two ways of overflow possible ... int AND short :)
    {
        printf("Level 2 Password: \"%s\"\n", passwds[1]);
    }
    return 0;
}

이 함수는 문자열을 받아 atoi로 변환하고, 결과를 unsigned int hunsigned short number 모두에 할당한다. 제약 조건은 다음과 같다:

  • h > 0 — 음수와 0을 배제
  • number != 0 — 조기 종료를 피하려면 이 검사를 통과하지 못해야 함
  • i < 0 || number == 0 — 패스워드를 출력하려면 이를 만족해야 함

ih는 32비트이고, number는 16비트다. 32비트 값을 16비트 변수에 할당하면 상위 2바이트가 잘린다. 하위 16비트가 모두 0이면 number는 0이 되지만 h는 양수로 유지된다.

목표 값은 10진수로 0x7FFF0000: 2147418112.

./exploit64 hello 2147418112
Level 2 Password: "help"

상위 16비트 0x7FFFh > 0을 만족하고, 하위 16비트 0x0000number == 0을 만들어 패스워드가 출력된다.

Level 2: Stack Overflow

패스워드: help. 이제 바이너리는 사용자명과 패스워드 인자를 요구한다.

./exploit64 help
usage: ./exploit64 help <username> <password>

소스:

void level3password()
{
    printf("Level 3 Password: \"%s\"\n", passwds[2]);
    return;
}
 
int stack_overflow(char* username, char* password)
{
    char buffer[8] = {"\0"};
    char admin_user[] = "admin";
    char admin_pass[] = "funny";
 
    int i;
    strcpy(buffer, username);
    buffer[strlen(admin_user)] = '\0';
    if (strcmp(buffer, admin_user)) {
        printf("Login failed\n");
        return 0;
    }
 
    strcpy(buffer, password);
    buffer[strlen(admin_pass)] = '\0';
    if (strcmp(buffer, admin_pass)) {
        printf("Login failed\n");
        return 0;
    }
    printf("Login succeeded, but still you failed :P\n");
    return 1;
}

스택 레이아웃은 buffer[8]이 스택에서 admin_useradmin_pass와 인접하게 배치된다. strcpy는 경계 검사 없이 사용자명을 8바이트 버퍼로 복사하므로, 긴 사용자명이 admin_useradmin_pass까지 오버플로우할 수 있다.

공격 전략:

  1. 인접한 admin_user 저장 공간에 "admin"을 쓰도록 정밀하게 설계된 사용자명으로 buffer를 오버플로우하여 첫 번째 strcmp를 통과한다.
  2. 패스워드 인자로 다시 오버플로우하여 admin_pass"funny"를 쓴다.

버퍼가 스택의 자격증명 변수 바로 앞에 위치하므로, 익스플로잇에는 정확한 바이트 오프셋만 있으면 된다. 최종 목표는 리턴 주소를 덮어써 level3password()로 실행 흐름을 바꾸는 것이다.

스택 시각화:

[buffer  8B][admin_user 6B][admin_pass 6B][padding][SFP][RET]

길이 > 8인 사용자명으로 buffer를 오버플로우하면 strcmpadmin_user로 읽는 바이트를 덮어쓴다. 올바른 오프셋에 정확히 "admin\0"을 쓰면 첫 번째 검사를 통과한다. 패스워드도 같은 방식으로 적용된다.

핵심 교훈

strcpy는 항상 안전하지 않다. 목적지 버퍼 크기를 무시하고 null 바이트를 찾을 때까지 복사한다. 대신 strncpystrlcpy를 사용해야 한다.

인접한 스택 변수는 격리되어 있지 않다. C 표준은 스택에서 지역 변수의 순서를 정의하지 않는다. 실제로는 컴파일러가 선언 순서대로 배치하므로(최적화 제외), 하나의 변수를 오버플로우하면 다음 변수를 조용히 손상시킬 수 있다.

정수 잘림은 악용 가능하다. 넓은 정수를 좁은 타입에 할당하는 것은 C에서 정의된 동작(모듈러 감소)이지만, 공격자는 잘린 결과가 원래 값은 만족하지 않는 조건을 만족하도록 입력 값을 설계할 수 있다.

부호 있는 타입과 부호 없는 타입 사이의 타입 혼동은 미묘하다. 여기서 int i / unsigned short number의 분리는 전형적인 예다: i에 대한 부호 검사와 number에 대한 범위 검사가 비직관적인 방식으로 상호작용하여 익스플로잇 가능한 창을 만든다.