일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 리버싱
- 리버싱 핵심원리
- ctf
- cmd
- 보안
- 설치
- sql
- 악성코드
- 코딩
- 다운로드
- 정보보안기사
- 오늘의 보안동향
- codeup
- 오늘의 영어
- 랜섬웨어
- defcon.mem
- C
- Defcon DFIR CTF 2019
- 코드업
- 보안동향
- c언어
- DEFCON
- 멀웨어
- Volatility
- 오늘의 보안
- C language
- Code Up
- SQLD
- C 프로그래밍
- Memory Forensics
- Today
- Total
오브의 빛나는 별
Buffer Overflow와 Return Oriented Programming 본문
안녕하세요, 오브입니다.
오늘은 프로세스 메모리 구조와 버퍼오버플로우, ASLR을 알기 위한 ROP 개념, 기본 어셈블리 개념에 대해 알아보겠습니다.
프로세스 메모리 구조
스택
- 한쪽 끝에서만 자료를 넣고 뺄 수 있는 LIFO 형식의 자료구조. 가장 최근에 스택에 추가한 항목이 가장 먼저 제거될 항목
- pop(): 스택에서 가장 위에 있는 항목 제거
- push(): 스택의 가장 윗 부분에 추가
- 연결리스트로 구현 가능
- 재귀 알고리즘, 웹 브라우저 방문 기록, 실행 취소, 역순 문자열 만들기, 수식의 괄호 검사, 후위 표기법 계산 등에 사용
스택의 구조
■ 스택 프레임 저장 내용
- return Address: 함수가 종료되었을 떄 호출했던 함수의 복귀 주소. 되돌아가야 하는 위치. 함수 관련 데이터만 들어감. 호출한 프로그램 내에서 호출된 함수 바로 다음 번지의 명령어를 가리킴
- 이전 프레임 포인터: 최상단 스택 프레임에서 바로 이전 단계의 스택 프레임의 시작점으로 이동시 필요한 주소 값 저장
- 함수 입력 및 파라미터. 지역변수. 문맥 교환을 위한 레지스터를 저장한 값
- 지역변수: 블록 내에 선언된 변수. 스택 영역에 저장. 초기화하지 않으면 의미없는 값으로 초기화됨
- 전역변수: 함수의 외부에서 선언된 변수. 데이터 영역에 저장. 0으로 자동 초기화
main() -> foo() -> bar() 순으로 함수 호출 시 프로세스 구조
■ foo()가 호출되었을 때 순서
main 스택 프레임 (1) main 내에서의 복귀 주소 (2) 이전 프레임 포인터 foo의 스택 프레임 Local 1 Local 2 buffer
- 스택은 낮은 주소쪽으로 증가. ex) 일반 버퍼는 1, 2, 3...으로 증가하는데 스택은 100, 99, 98로 증가.
1. foo()를 호출함으로써 main() 내에서의 복귀 주소를 넣는다. 즉, main 스택 프레임 시작이 100이고 10이라면 처음에 esp는 90을 가리킴. 복귀 주소(4)를 넣으면 86을 가리킴
2. main 스택 프레임의 시작점을 넣음. 스택 포인터는 foo 스택 프레임의 이전 프레임 포인터를 가리킴. 즉, 이전 프레임 포인터에 100을 넣음.
3. 프레임 포인터를 스택 포인터 값으로 설정. 프레임 포인터는 foo 스택 프레임의 시작점을 가리킴. 즉, ebp가 처음에 100이었는데 86을 가리킴.
4. 지역 변수 저장을 위해 스택 포인터를 아래로 이동. 지역 변수 Local1, Local2 할당. 스택 포인터는 Local2의 상단을 가리킴.
- 할당을 많이 한다 = 지역변수의 거리(크기)가 크다. 메모리 감소가 크다
■ bar()가 호출되었을 때 순서
main 내에서의 복귀 주소 이전 프레임 포인터 foo의 스택 프레임 Local 1 Local 2 foo 내에서의 복귀 주소 (1) 이전 프레임 포인터 (2) bar의 스택 프레임 buffer[10]
1. bar()를 호출함으로써 foo() 내에서의 복귀 주소를 넣음. 즉, 복귀 주소가 10이라면 76을 가리킴
2. foo 스택 프레임의 시작점을 넣음. 스택 포인터는 bar 스택 프레임의 이전 프레임 포인터를 가리킴. 즉, 86을 넣음.
3. 프레임 포인터를 스택 포인터 값으로 설정. 프레임 포인터는 bar 스택 프레임의 시작을 가리킴
4. 호출되는 파라미터 및 지역변수 buffer [10]를 넣음. 스택 포인터는 buffer [10]의 상단을 가리킴.
b
- 버퍼가 10인데 강제로 10 이상의 데이터를 넣으면 이전 프레임 포인터를 사용. 따라서 리턴 주소가 변경됨 ==> 버퍼오버플로우
■ bar()가 반환될 때 순서
1. bar()의 수행이 완료되거나 retuen 명령을 만나면 스택 포인터를 프레임 포인터 값으로 복원. 파라미터 및 지역변수에 의해 사용된 공간을 운영체제에 반환.
2. bar()의 이전 프레임 포인터에 들어있는 주소 값을 꺼내서 프레임 포이터에 설정. 프레임 포인터는 foo 스택 프레임의 시작점을 가리킴.
3. 2에서 주소값을 꺼냄으로 인해 스택 포인터는 foo 내의 복귀 주소의 상단을 가리킴.
■ foo()가 반환될 때 순서
- foo() 내부의 복귀 주소에 있는 명령어 수행
1. foo()의 수행이 완료되거나 retuen 명령을 만나면 스택 포인터를 프레임 포인터 값으로 복원. 지역변수에 의해 사용된 공간을 운영체제에 반환. (확보해놓은 것을 해제)
2. foo()의 이전 프레임 포인터에 들어있는 주소 값을 꺼내서 프레임 포이터에 설정. 프레임 포인터는 main 스택 프레임의 시작점을 가리킴.
3. 2에서 주소값을 꺼냄으로 인해 스택 포인터는 main 내의 복귀 주소의 상단을 가리킴.
버퍼 오버플로우 공격 원리
main 스택 프레임 main() 할당 영역 main 스택 프레임 main() 할당 영역 foo 스택 프레임 foo() 할당 영역 foo 스택 프레임 foo() 할당 영역 복귀 주소 영역 침범 후 복귀 주소 변조
Buffer [10]bar 스택 프레임 이전 프레임 포인터 bar 스택 프레임 Buffer [10] 코드 영역 코드 영역
■ 버퍼오버플로우 공격 원리
- segmentation fault가 발생하여 비정상적으로 종료
정상적인 길이의 문자열 입력 비정상적인 길이의 문자열 입력 복귀 주소 HHHH 프레임 포인터 GGGG FFFF FFFF EEEE EEEE DDDD DDDD CCCC CCCC BBBB BBBB AAAA AAAA - buf [24]: AAAA~FFFF 각각 4byte씩 해서 24byte.
- 만약 0x08C05610가 쉘 코드 리턴 값일 수 없음. 0~256 중 아스키는 일부이기 때문. 따라서 한 글자만 바꾸는 것은 가능. ex) 0xF1C05637 중 0x37을 0x47로 변경
- 복귀 주소 값을 쉘 코드의 시작 주소 변경하여 쉘코드 실행 공격
- 현재는 NX, DEP를 사용하여 이렇게 공격 못함
버퍼 오버플로우 대응 방안
■ 컴파일 시간 방어
- 마이크로소프트의 GS 옵션 = 리눅스의 Canary
- canary: 버퍼와 제어 데이터 사이에 설정된 값. 버퍼 오버플로우가 발생하면 Canary 값이 손상되며, Canary 데이터의 검증에 실패하여, 오버플로우에 대한 경고가 출력되고, 손상된 데이터를 무효화 처리. Canary 값은 랜덤으로 생성. 프로그램은 기억하는데 사용자는 모름. esp가 ret값 가져가기 전에 Canary 값 확인. Canary 값이 변하지 않아야만 리턴.
ret canary buf
- Stack smashing Detected: Canary 때문에 뜨는 오류 메시지
- ASLR: 주소 공간의 랜덤화: ROP에 대한 대응 방안
- 문자열의 길이를 체크하는 함수를 쓰는 방안 ex) strcpy => strncpy. strcpy: null가 올 때까지 버퍼에 넣는 것
- 컴파일러가 범위 검사를 강제로 수행하는 코드를 자동으로 삽입하는 프로그래밍 언어를 사용하는 방안. java와 python는 버퍼 오버플로우가 안 생김. 그러나 모든 s/w를 이 언어로 만들 수 없기 때문에 이 언어로 s/w를 100% 만들 수 없음.
- Libsafe와 같은 안전한 라이브러리를 사용하는 방안
■ 실행 시간 방어
- NX(No Execute): 스택과 힙 영역에서 아예 코드가 실행되는 것을 막는 방안
- ASLR: 주소 공간의 랜덤화. ROP 막는 기법
■ 마이크로소프트의 /GS 옵션
- 스택 영역과 데이터 영역에 쿠키(임의의 값)를 삽입하여, 버퍼 오버플로우가 발생하게 되면 복귀 주소의 값은 물론 쿠키값도 변조될 가능성이 매우 높음
- 함수가 종료될 때 스택 영역의 쿠키와 데이터 영역의 쿠키값이 다르면 변조가 이루어졌다고 판단하여 오류 메시지 발생시킴
스택 영역 복귀 주소 (EIP) 스택 영역 복귀주소 (EIP) 이전 프레임 포인터 (EBP) 이전 프레임 포인터 (EBP) 버퍼 쿠키 버퍼 데이터 영역 데이터 영역 쿠키(Canary) 일반적인 함수의 스택 GS를 적용한 함수의 스택
ASLR이 생기는 이유
1. 버퍼오버플로우
- 메모리에 악의적 데이터를 삽입
2. NX/DEP
- 데이터 실행 방지 -> 버퍼오버플로우 방어
3. Return Oriented Programming
- 삽입한 악성 데이터를 곧바로 실행하는 대신 메모리상에 흩어진 코드 조각을 모아서 실행. 여기 조금 저기 조금 실행시키고 막상 모아서 보면 악성 행위 가능
4. ASLR
- 흩어진 코드(공유 라이브러리)의 위치를 랜덤 화하여 알 수 없도록 함
- ROP를 방어
Return Oriented Programming
- Stack과 함수 호출 형태 악용. 함수는 실행 흐름의 분기를 야기. 복귀 주소를 기억해야 함.
- Stack: 함수의 return 주소를 저장. 그래서 Return Oriented Programming
- ROP(Return Oriented Programming): 반환 지향형 프로그래밍. NX bit와 ASLR 같은 메모리 보호 기법을 우회하기 위한 공격 기법. RTL, RTL Chaining, GOT Overwrite 기법을 활용하여 취약한 프로그램 내부의 기계어 코드들을 이용해 콜 스택을 제어하는 공격 기법. 버퍼 오버플로우 취약점이 발생하는 바이너리를 exploit 할 때 가장 많이 사용되는 기법. 바이너리 내부에 존재하는 gadget을 사용하여 호출 함수 및 인자를 조작하는 방법. 해커가 어떻게 이 기법을 활용하여 콜 스택을 조작하느냐에 따라 불가능해 보이는 공격마저 가능
어셈블리
어셈블리: 상징적인 기호 언어를 사용하여 작성한 프로그램을 기계어로 된 프로그램으로 번역하는 것
- 어셈블리와 바이너리는 거의 1:1 매칭
어셈블리 언어의 종류
■ CPU 아키텍처마다 존재
- x86, x64(대중적), ARM, MIPS, PPC
■ Intel/AMD CPU
- x86/64
- 일반적인 Windows 운영체제가 설치된 데스크톱
■ ARM (안드로이드, 아이폰)
- ARM/ARM64
- 핸드폰, 태블릿 등 Android, iOS가 설치된 컴퓨터 시스템에서 사용
어셈블리 문법 표기
Intel vs AT&T
Intel AT&T mov eax, 1 mov1 $1, %eax mov eax, [ebx+3] mov1 3(%ebx), %eax - 레지스터를 나타낼 때 intel은 eax라고 하고 AT&T는 %eax라고 함.
- operand 순서는 intel은 destination, source이고 AT&T는 source, destination임.
- 상수는 intel은 5로 표기하고 AT&T는 $5로 표기
- 메모리 주소 참조는 intel은 [eax]라고 표기하고 AT&T는 (%eax)라고 표기
- 레지스터 + offset 위치의 메모리 주소를 참조할 때 intel은 [eax + 4]라고 표기하고 AT&T
레지스터
- 범용 레지스터: 작은 데이터의 임시 저장 공간. 연산처리 및 번지 지정을 도와줌. 컴퓨터 장치 제어
- 8가지 종류가 있음
1. EAX: 산술, 논리 연산을 수행할 때 사용. 함수의 반환 값이 이 레지스터에 저장됨. EAX를 반으로 쪼갤 경우 AX(16bit)가 됨. AX를 반으로 쪼개면 AH, AL(8bit)가 됨
2. EBX: 메모리 주소를 저장하기 위한 용도로 사용. EBX를 반으로 쪼갤 경우 BX(16bit)가 됨. BX를 반으로 쪼개면 BH, BL(8bit)가 됨. int, float 값 등 저장 가능
3. ECX: 반복 명령어(while, for 등) 사용 시 반복 카운터로 사용되는 레지스터. 반복할 횟수를 지정하고, 반복 작업 수행 시 사용. int 값만 저장 가능
4. EDX: EAX 레지스터와 같이 쓰이며 부호 확장 명령 등에 쓰이고 큰 수의 곱셈 또는 나눗셈 등의 연산이 이루어질 때 EDX 사용. 32bit로 구성. EDX를 반으로 쪼갤 경우 DX(16bit)가 됨. DX를 반으로 쪼개면 DH, DL(8bit)가 됨.
5. ESI / EDI: ESI는 데이터 조작 또는 복사 시 데이터 주소 저장. EDI는 복사할 때 목적지의 주소 저장
6. ESP / EBP: ESP는 Stack의 끝 지점 주소를 가리키고, EBP는 Stack의 첫 시작 주소를 가리킴