Buffer Overflow Attack
- Stack
- Heap
버퍼 오버플로우 공격은 할당된 메모리의 범위를 넘어선 위치에 자료를 읽거나 쓰려고 할 때 발생한다.
버퍼 오버플로우가 발생하면 프로그램의 오작동이 유발되거나, 악의적인 코드를 실행시킴으로써 공격자 프로그램을 통제할 수 있는 권한을 획득하게 된다.
버퍼 오버플로우 공격에는 스택과 힙 버퍼 오퍼블로우가 있다.
Stack Frame
스택 프레임을 이해하기 위해서 간단한 소스 코드를 예로 들자면
#include <stdio.h>
int add(int a, int b) {
int x = a, y = b;
return (x+y);
}
int main(int argc, char* argv[]) {
int a = 1, b = 2;
printf(%d, add(a,b));
return 0;
}
RET: 돌아가야 할 주소
EBP: 스택의 베이스 포인터
ESP: 스택 포인터
1.
-------------------- ESP (stack pointer)
EBP (main 함수 base pointer)
--------------------
RET (main 함수) <- 높은 주소
처음 상태는 위의 코드를 실행했을 때 스택의 메모리 구조 envp, ergv, ergc는 생략
2.
-------------------- ESP (stack pointer)
2
--------------------
1
--------------------
EBP (main 함수 base pointer)
--------------------
RET (main 함수) <- 높은 주소
ESP는 항상 스택에 끝에 위치한다고 생각하면 된다.
메모리 구조에서 스택은 높은 주소에서 낮은주소로 스택이 쌓이는 형태로 스택보다 높은 주소에 침범하지 않게 하기 위해서 이렇게 한 것으로 알고 있음.
3.
-------------------- EBP (add 함수 base pointer) / ESP (stack pointer)
EBP (main 함수 base pointer)
--------------------
RET (add 함수)
--------------------
1 (== a)
--------------------
2 (== b)
--------------------
2
--------------------
1
-------------------- EBP(main 함수 base pointer)
EBP
--------------------
RET (main 함수) <- 높은 주소
함수의 매개변수는 add(a,b)일 때, b먼저 즉 뒤에서 부터 스택에 삽입함.
main 함수 base pointer를 스택에 저장해놔야 add 함수를 스택에서 pop 시킬 떄, base pointer값을 main 함수의 base pointer로 옮길 수 있다.
3.
-------------------- EBP (add 함수 base pointer) / ESP (stack pointer)
EBP (main 함수 base pointer)
--------------------
RET (add 함수)
--------------------
1 (== a)
--------------------
2 (== b)
--------------------
2
--------------------
1
-------------------- EBP(main 함수 base pointer)
EBP
--------------------
RET (main 함수) <- 높은 주소
4.
-------------------- ESP (stack pointer)
1 = mov EAX, EBP + 8 -> mov EBP - 8, EAX
--------------------
2 = mov ECX, EBP + c -> mov EBP - 4, ECX
-------------------- EBP (add 함수 base pointer)
EBP (main 함수 base pointer)
--------------------
RET (add 함수)
--------------------
1 (== a)
--------------------
2 (== b)
--------------------
2
--------------------
1
-------------------- EBP(main 함수 base pointer)
EBP
--------------------
RET (main 함수) <- 높은 주소
EAX, ECX: 값을 저장하는 레지스터
MOV: 어셈블리어 move로 예를들어 MOV ECX, EBP + C는 ECX 레지스터에 EBP + C에 있는 값을 저장하라는 뜻이고,
MOV EBP - 4, ECX는 EBP - 4에 ECX 값을 저장하란 소리다.
이렇게 1,2값을 add 함수의 스택에 저장되면 ADD EAX, ECX로 ADD또한 어셈블리어 명령어로 EAX값에 ECX 값을 더해서 EAX에 저장하라는 소리임.
5.
-------------------- EBP (add 함수 base pointer) / ESP (stack pointer)
EBP (main 함수 base pointer)
--------------------
RET (add 함수)
--------------------
1 (== a)
--------------------
2 (== b)
--------------------
2
--------------------
1
-------------------- EBP(main 함수 base pointer)
EBP
--------------------
RET (main 함수) <- 높은 주소
EAX 값에는 a + b값인 3이 저장되서 return이 될 것이다. 여기서 살펴보자면 MOV ESP, EBP 명령어가 실행되서 위의 스택 구조를 가지게 되었는데 ESP값이 EBP 값이 되면서 위에 1과 2는 신경쓰지 않게 되었다. (존재는 하지만 지워졌다고 보면됨.) 그리고 POP EBP 를 하게 되면, EBP레지스터에 EBP에 있던 값을 저장하고 ESP가 한칸 줄어들게 된다. 또한
RET 명령어로 ESP가 한칸 줄어들고 원래 돌아가야할 메모리 위치로 돌아가게 된다.(add함수의 스택프레임이 끝남)
6.
-------------------- ESP (stack pointer)
1 (== a)
--------------------
2 (== b)
--------------------
2
--------------------
1
-------------------- EBP(main 함수 base pointer)
EBP
--------------------
RET (main 함수) <- 높은 주소
여기서 또 ESP 바로 밑의 1과 2는 return값인 3이 EAX 레지스터에 저장되어 있음으로 필요 없기 때문에,
ADD ESP, 8 하면 2칸 줄어들기 때문에 1과 2는 신경쓰지 않아도 되게 되고, printf(%d, 3)함수의 스택으로 들어가게 될 것이다.
7.
-------------------- EBP / ESP (stack pointer)
EBP (main 함수 base pointer)
--------------------
RET ( printf함수)
--------------------
%d
--------------------
3
--------------------
2
--------------------
1
-------------------- EBP(main 함수 base pointer)
EBP
--------------------
RET (main 함수) <- 높은 주소
printf의 구조는 알 수 없음으로 생략하겠다. 인제 위에서 3이 출력이 될 것이고, %d와 3등 printf()에 필요한 인수들까지 다 필요없어 졌으니 지워질 것이다.
8.
-------------------- ESP (stack pointer)
2
--------------------
1
-------------------- EBP(main 함수 base pointer)
EBP
--------------------
RET (main 함수) <- 높은 주소
인제 return 0;을 출력하기 위해서 XOR EAX, EAX로 EAX를 0으로 바꾸고, ESP를 MOV ESP, EBP 해서 지우고 그렇게 메인함수 까지 종료가 될 것이다.
이러한 스택프레임의 간단한 예제로 살펴보았는데, 이 스택 프레임이 버퍼 오버 플로우 공격과 무슨 연관이 있냐면,
RET 명령어가 POP과 RET에 있는 메모리 위치로 점프한다는 것을 위에서 살펴 보았는데, RET 위치에 공격자가 원하는 악성코드의 위치를 집어넣으면 악성 코드를 실행 가능하게 된다! 그리고 그 RET에 원하는 악성 코드가 있는 주소값을 넣기 위해서 매개변수의 오버플로우를 이용한다!
예를 들어서 오버플로우에 취약한 c언어의 strcpy 함수를 사용한다면
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[]) {
char buffer[12];
memset(buffer, 0x00, sizeof(buffer));
strcpy(buffer, argv[1]);
}
라는 Vul_code.c 라는 소스코드가 있다고 친다면,
/Vul_code.c aaaaaaaaaaaaaaaa (shell code address) 이런식으로 코드를 실행한다면, EBP값이 4byte값이라고 가정한다면 strcpy함수가 문자열의 길이를 체크하지 않고 버퍼에 저장함으로,
--------------------
aaaaaaaaaaaa = 12bytes
--------------------
aaaa = 4bytes (원래 EBP 위치)
--------------------
RET (shell code address)
이렇게 RET에 원하는 쉘코드(악성코드)의 주소값을 넣을 수 있다.
리눅스에서 suid가 설정되어 있는 실행 파일에 예를 들어 root로 권한 상승및 root 권한의 쉘을 획득하는 쉘 코드의 주소를 RET에 삽입할 수 있다면 shell code를 수행하게 된다.
이런 버퍼 오버플로우를 예방하기 위해서는
- strcpy같은 오버플로우에 취약한 함수 사용하지 않기.
- 스택 가드(Stack Guard): 변수와 RET사이에 특정 값(canary word: 카나리아가 독성에 민감하게 반응해서 색이 변하는 것 처럼 사용하기 위한 특정 값)을 넣어 값이 변한다면 오버플로우가 발생했다고 여겨 프로그램 실행을 중단하는 방법
- 스택 쉴드(Stack Shield): 함수 시작 시 RET를 Global RET라는 특수 스택에 저장해 두었다가 함수 종료 시 저장된 값과 스택의 RET값을 비교해 다를 경우 오버플로우가 발생했다고 여겨 프로그램의 실행을 중단하는 방법
- ASLR(Address Space Layout Randomization): 메모리 공격을 방어하기 위해 주소 공간 배치를 난수화하는 방법으로, 실행 시마다 메모리 주소를 변경시켜 악성코드에 의한 특정주소 호출을 방지한다.
'정보보안기사' 카테고리의 다른 글
정보 보안기사 Day-1(SECTION 01) (1) | 2024.02.09 |
---|