드림핵을 기반으로 작성한 내용이다. 버퍼오버플로우에 대한 익스 테크이므로 아래의 글을 참고하면 좋다.
예제 코드
// 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)
- %s는 문자열 입력을 받을 때, 입력길이를 제한하지 않고, 공백문자, 탭, 개행문자 등이 들어올 때까지 입력을 계속 받는다 ⇒ 버퍼의 크기보다 큰 데이터를 입력하면 오버플로우 발생!
비슷한 문제를 일으키는 코드
- strcpy, strcat, sprintf
- 버퍼의 크기를 같이 입력하는 strncpy, strncat, snprintf, fgets, memcpy 등을 사용하는 것이 바람직
해결방법
scanf("%[n]s",buf)
- 정확히 n개를 입력받는 형태로 사용해 버퍼보다 큰 크기의 입력데이터 방지
- 이는 buf의 크기와 n이 동일할 경우, 버퍼의 뒤에 널바이트가 삽입될 수 있다. 이를 널 바이트 오버플로우(null-byte oveflow)라고 부른다. => scanf("%39s") : 입력에 39바이트만 사용하고 입력 끝에 null바이트를 넣는 코드
취약점 트리거
트리거? 발견한 취약점을 발현시키는 것
긴 입력을 입력했을 때 segmentation fault가 뜨면서 프로그램이 비정상적으로 종료된다. 이는 잘못된 메모리 주소에 접근했다는 의미이며, 프로그램에 버그가 발생했다는 신호이다.
core dumped는 코어파일(core)을 생성했다는 것으로, 프로그램이 비정상 종료됐을 때, 디버깅을 돕기 위해 운영체제가 생성해주는 것이다.
코어 파일 분석
gcc -o rao rao.c -fno-stack-protector -no-pie -g
-g : 디버거에 제공하는 디버깅 정보(변수타입, 전역 심볼명, 주소)를 바이너리에 삽입
코어파일 분석을 위해서는 컴파일 시 -g 옵션을 넣어주는 것이 좋다.
> gdb ./rao ./rao.core
우선 콜스택 backtrace를 확인한다. 어느 함수를 실행하다가 프로세스가 죽었는지 확인할 수 있다. main의 0x401264를 실행하다 죽은 것으로 확인된다. list명령을 통해 해당 부분의 소스코드를 확인한다.
0x401264
부분에 브레이크를 걸고 스택을 확인해보면 다음과 같이 buffer overflow로 인해 ret address
가 0x4141414141414141
로 덮어쓰여 져서 제대로 종료가 되지 않은 것으로 확인된다. 만약 이 부분을 다른 함수의 주소로 바꾼다면 프로세스가 종료되지 않고 다른 함수가 실행될 것으로 예상된다.
익스플로잇
1. 스택 프레임 구조 파악
ret addr을 덮으려면 스택구조가 어떻게 되어있는지 알아야 한다. 그래야 어디까지 더미값을 넣고 ret addr에 원하는 함수 주소를 넣을지 알 수 있다.
main함수의 어셈블리 코드에서 scanf부분을 보자.
위 노란색 부분을 pseudo code로 표현하면 다음과 같다.
scanf("%s", [rbp-0x30]);
즉, 사용자의 입력이 들어갈 스택의 위치(buf)는 rbp-0x30
이고 buf가 0x30만큼 할당되어 있다. 이후 sfp가 저장되고(주소이므로 0x08), rbp+0x08
에 ret address가 저장된다.
2. get_shell() 주소 확인
쉘을 실행해주는 get_shell()함수가 있으므로 이 함수의 주소로 ret addr을 덮어 쉘을 딸 수 있다. gdb를 이용해 get_shell() 주소를 확인한다. 0x4011dd
가 get_shell()의 주소임을 알 수 있다.
3. 페이로드 구성
사용자 입력값은 rbp-0x30부터 쭉 채워진다. 결국 더미값을 buf크기(0x30) + sfp크기(0x08) 만큼 넣어주고 그 후에 get_shell 주소를 넣어주면 ret addr에 get_shell 주소를 넣어 실행할 수 있다.
payload : | buf - 'A' 0X30 | SFP - 'B' 0X08 | RET - get_shell |
4. 엔디언 적용
인텔 x86-64 아키텍처는 리틀엔디언을 사용하므로 get_shell의 주소인 0x4011dd
는 \xdd\x11\x40\x00\x00\x00\x00\x00
로 전달되어야 한다.
리틀엔디언과 빅엔디언
엔디언은 메모리에서 데이터가 정렬되는 방식을 말한다.
리틀엔디언에서는 데이터의 Most Significant Byte(MSB; 가장 왼쪽의 바이트)가 가장 높은 주소에 저장되고, 빅엔디언에서는 데이터의 MSB가 가장 낮은 주소에 저장된다.
5. 익스플로잇
파이썬을 이용해 페이로드를 rao에 전달한다. 쉘 획득 성공!
(python3 -c "import sys; sys.stdout.buffer.write(b'A'*0x30+ b'B'*0x08+b'\xdd\x11\x40\x00\x00\x00\x00\x00')";cat) | ./rao
'wargame - system > DreamHack' 카테고리의 다른 글
[Dreamhack] ssp_001 - Write Up (0) | 2022.08.28 |
---|---|
[Dreamhack] basic_exploitation_001 Write Up (0) | 2022.07.05 |
[Dreamhack] basic_exploitation_000 Write Up (0) | 2022.07.04 |
[Dreamhack] Return Address Overwrite (0) | 2022.05.25 |
[Dreamhack] shell_basic (0) | 2022.04.06 |