본문 바로가기
Security/보안기초

Memory Theory(01)

by 계영수 2023. 11. 14.
728x90

자체 익스플로잇을 작성하기 전에 메모리 작동 방식에 대한 기본 사항을 이해해야 합니다. 우리의 목표는 메모리를 조작하고 CPU가 우리를 대신하여 명령을 실행하도록 속이는 것입니다. 우리는 프로그램의 메모리 스택에 있는 변수를 과도하게 채우고 인접한 메모리 위치를 덮어쓰는 스택 기반 버퍼 오버플로라 는 기술을 사용할 것입니다 . 하지만 먼저 아래 그림과 같이 프로그램의 메모리가 어떻게 배치되어 있는지 조금 알아야 합니다 .

 

텍스트Text 세그먼트에는 실행될 프로그램 코드가 포함되고, 데이터Data 세그먼트 에는 프로그램에 대한 전역 정보가 포함됩니다. 더 높은 주소에는 런타임에 할당되는 스택과 힙이 공유하는 부분이 있습니다. 스택  크기가 고정되어 있으며 함수 인수, 지역 변수 등을 저장하는 데 사용됩니다. 힙Heap 동적 변수를 보유합니다. 더 많은 함수나 서브루틴이 호출될수록 스택의 소비는 증가하고, 더 많은 데이터가 스택에 저장될수록 스택의 상단은 더 낮은 메모리 주소를 가리킵니다.

 

Intel 기반 CPU에는 나중에 사용할 수 있도록 데이터를 저장할 수 있는 범용 레지스터가 있습니다. 여기에는 다음이 포함됩니다.

EIP 명령 포인터
ESP 스택 포인터
EBP 기본 포인터
ESI 소스 인덱스
EDI 목적지 색인
EAX 누산기
EBX 베이스
ECX 카운터
EDX 데이터

 

ESP, EBP, EIP는 특히 흥미롭습니다. ESP와 EBP는 함께 현재 실행 중인 함수의 스택 프레임을 추적합니다.

 

그림 16-2 에서 볼 수 있듯이 ESP는 스택 프레임의 가장 낮은 메모리 주소에서 상단을 가리키고, 마찬가지로 EBP는 스택 프레임의 하단에서 가장 높은 메모리 주소를 가리킵니다. EIP는 다음에 실행될 명령어의 메모리 주소를 보유합니다. 우리의 목표는 실행을 하이재킹하고 대상 시스템이 우리가 원하는 것을 실행하도록 하는 것이기 때문에 EIP는 타협의 주요 대상처럼 보입니다. 하지만 EIP에 대한 지침을 어떻게 얻을 수 있습니까? EIP는 읽기 전용이므로 이 레지스터에 실행할 메모리 주소를 넣을 수는 없습니다. 우리는 좀 더 영리해질 필요가 있습니다.

그림 16-2.  스택 프레임

 

스택은 후입선출(Last In First Out) 방식의 데이터 구조입니다. 구내식당의 도시락 쟁반 더미처럼 생각하시면 됩니다. 스택에 추가된 마지막 트레이는 필요할 때 제거되는 첫 번째 트레이입니다. 스택에 데이터를 추가하려면 PUSH명령어가 사용됩니다. 마찬가지로 스택에서 데이터를 제거하려면 POP 명령어를 사용합니다 . (더 낮은 메모리 주소로 스택 소비가 증가하므로 데이터가 현재 스택 프레임으로 푸시되면 ESP는 메모리의 더 낮은 주소로 이동합니다.)

 

프로그램 기능이 실행되면 해당 정보(예: 지역 변수)에 대한 스택 프레임이 스택에 푸시됩니다. 함수 실행이 끝나면 전체 스택 프레임이 해제되고 ESP와 EBP는 호출자 함수의 스택 프레임을 다시 가리키며 중단된 호출자 함수에서 실행이 계속됩니다. 그러나 CPU는 계속할 메모리의 위치를 ​​알아야 하며 함수가 호출될 때 스택에 푸시되는 반환 주소 에서 해당 정보를 얻습니다.

 

예를 들어 우리가 C 프로그램을 실행하고 있다고 하자. 당연히 프로그램이 시작될 때 main function이 호출되고 그것을 위해 스택 프레임이 할당된다. 그리고 나서 메인은 또 다른 함수 function1을 호출한다. function1의 스택 프레임을 스택에 푸시하고 실행을 넘겨주기 전에, 이 값(Return Address)을 스택에 푸시함으로써 function1이 반환될 때(일반적으로 function1로 호출한 직후의 코드 라인)  다시 어디서부터 실행되어야 하는지 리턴 주소를 적어놓게 된다.. 그림 16-3은 메인이 function1로 호출한 후의 스택을 보여줍니다

 

 

 

호출된 함수 function1이 종료되면, 스택을 반환하게 되고, 이 때 앞서 저장된 리턴 어드레스Return Address가 EIP 레지스터에 로드되게 됩니다. 그리고 프로그램 실행의 주도권이 다시 Main 함수로 넘어가게 된다. 이때 공격자 관점의 우리는 만약 리턴 주소를 조작할 수 있다면, Function1의 호출이 완료되고, 스택이 반환되는 시점에서, EIP로 반환되는 리턴주소를 공격자의 의도대로 변경할 수 있습니다. 다시 말하지만 EIP 레지스터의 값을 변경할 수는 없지만, 스택의 값을 조작하여 EIP로 반환되는 리턴 주소값을 조작할 수는 있겠습니다. 다음 절에서는 이 점을 설명하기 위해 간단한 스택 기반 버퍼 오버플로 예를 살펴보도록 하겠습니다.

 

계속하기 전에 몇 가지 사항을 더 염두에 두세요. 이 포스팅의 예에서 우리는 Linux의 최신 버전에서 볼 수 있는 몇 가지 고급 악용 방지 기술을 회피하기 위해 이전 운영 체제를 사용하고 있습니다. 특히 DEP(데이터 실행 방지)  ASLR(주소 공간 레이아웃 랜덤화) 기능이 빠져있는 점을 활용하겠습니다 . 두 가지 모두 공격의 기본을 배우기 어렵게 만들기 때문입니다. DEP는 특정 메모리 섹션을 실행 불가능으로 설정합니다. 이는 스택을 쉘코드로 채우고 실행을 위해 EIP를 가리키는 것을 방지합니다. ASLR은 라이브러리가 메모리에 로드되는 위치를 무작위로 지정합니다. 예제에서는 반환 주소를 메모리에 넣고 싶은 위치로 하드코딩하지만 ASLR 공격 이후의 세계에서는 실행을 보낼 올바른 위치를 찾는 것이 조금 더 까다로울 수 있습니다. 

 

Linux Buffer Overflow

이제 우리는 꽤 혼란스러운 이론을 마쳤으므로 Linux 대상에서 실행되는 버퍼 오버플로 공격의 기본 예를 살펴보겠습니다. 먼저 기본 버퍼 오버플로에 대해 대상이 올바르게 설정되었는지 확인하겠습니다. 최신 운영 체제에는 이러한 공격을 방지하기 위한 검사 기능이 있지만 학습하는 동안에는 이를 꺼야 합니다. 이 책에서 제공하는 Linux 대상 이미지를 사용하는 경우 이미 올바르게 설정되어 있지만 확인하려면 randomize_va_space여기에 표시된 대로 0으로 설정되어 있는지 확인하세요.

 

실습 이미지 다운로드 

 

georgia@ubuntu:~$ sudo nano /proc/sys/kernel/randomize_va_space

 

randomize_va_space, 1 또는 2로 설정하면 대상 시스템에서 ASLR이 켜집니다. 기본적으로 Ubuntu에서는 무작위화가 켜져 있지만 이 예에서는 이 기능을 꺼야 합니다. 파일에 값 0이 포함되어 있으면 모든 준비가 완료된 것입니다. 그렇지 않은 경우 파일 내용을 0으로 변경하고 저장하십시오.

 

A Vulnerable Program

아래 예제와 같이 스택 기반 버퍼 오버플로에 취약한 Overflowtest.c 라는 간단한 C 프로그램을 작성해 보겠습니다.

 

Example 16-1. Simple exploitable C program

 georgia@ubuntu:~$ nano overflowtest.c

  #include <string.h>
  #include <stdio.h>

❶ void overflowed() {
          printf("%s\n", "Execution Hijacked");
  }

❷ void function1(char *str){
          char buffer[5];
          strcpy(buffer, str);
  }
❸ void main(int argc, char *argv[])
  {
          function1(argv[1]);
          printf("%s\n", "Executed normally");
  }

 

우리의 간단한 C 프로그램은 그다지 많은 일을 하지 않습니다. 두 개의 C 라이브러리 stdio.h와 string.h. 이를 통해 처음부터 빌드할 필요 없이 C의 표준 입력/출력 및 문자열 생성자를 사용할 수 있습니다. 우리는 프로그램에서 문자열을 사용하고 텍스트를 콘솔에 출력하려고 합니다.

 

위의 실습 소스코드에는 3개의 함수가 정의되어 있습니다. 

- overflowd

- function1

- main

만약 overflowd 함수가 호출되면, "Execution Hijacedt"라는 글자가 콘솔에 출력됩니다.

만약 function1 함수가 호출되면, buffer라는 5글자 크기의 문자열 타입의 지역변수를 선언합니다. function1으로 전달된 변수의 내용은 buffer로 복사됩니다.

 

기본적으로 프로그램이 시작되면 main 함수가 호출됩니다. 그리고 바로 function1을 호출됩니다.이때 프로그램이 전달받은 첫번째 명령어 인수Argument가 function1으로 전달됩니다. function1 호출이 종료되어 반환되면 곧 이어서 콘솔 화면에 "Executed normally"라는 글자가 출력됩니다.

 

정상적인 상황이라면 "overflowd" 함수는 절대 호출되지 않습니다. 따라서 "Execution Hijacked"라는 글자는 콘솔에 출력되지 않습니다. 

 

아래 명령어를 이용하여 실제 프로그램을 실행시켜봅니다.

georgia@ubuntu:~$ gcc -g -fno-stack-protector -z execstack -o overflowtest overflowtest.c

 

위와 같이 C 코드를 컴파일하기 위해 우분투에 기본적으로 내장되어 있는 GNU 컴파일러 컬렉션인 GCC를 사용합니다.  -g옵션은 GNU 디버거인 GDB에 대한 추가 디버깅 정보를 추가하도록 GCC에 지시합니다. 우리는 이 -fno-stack-protector플래그를 사용하여 GCC의 스택 보호 메커니즘을 끄는데, 이는 켜진 채로 두면 버퍼 오버플로를 방지하려고 시도합니다. 컴파일러 -z execstack옵션은 스택을 실행 가능하게 만들고 다른 버퍼 오버플로 방지 방법을 비활성화합니다. 옵션 overflowtest-o 을 사용하여 overflowtest.c호출되는 실행 파일로 컴파일하도록 GCC에 지시합니다 .

 

main 함수는 프로그램에 전달되는 첫 번째 명령줄 인수를 가져와서 function1에 전달합니다. 이렇게 전달되는 값은  5글자 크기의 지역 변수-buffer-에 복사됩니다. 여기에 표시된 대로 - AAAA - 명령줄 인수를 사용하여 프로그램을 실행해 보겠습니다. 필요하다면 리눅스 명령어 chmod를 이용하여 overflowtest 파일을 실행 가능하도록 만들어야 합니다. 문자열이 널 바이트로 끝나기 때문에 5개가 아닌 4개의 A 를 사용합니다 . 기술적으로 5개의 A 를 사용했다면 단 한 문자만 있어도 이미 버퍼 오버플로가 발생했을 것입니다.

 

georgia@ubuntu:~$ ./overflowtest AAAA
Executed normally

 

위에 출력을 보면 프로그램은 우리가 예상한 대로 수행되었습니다. 즉, main을 호출하고 function1,  function1복사하고 , 실행을 에 반환 하고, 프로그램이 종료되기 전에 콘솔에 "Executed normally"을 인쇄합니다. 아마도 예상치 못한 입력값을 전달하며 주면 버퍼 오버플로를 일으키는 데 도움이 되는 방식으로 동작하도록 강제할 수 있습니다.

 

Causing a Crash

이제 여기에 표시된 것처럼 프로그램에 A 의 긴 문자열을 인수로 제공해 보겠습니다 .

georgia@ubuntu:~$ ./overflowtest AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault

 

이번에는 프로그램이 분할 오류segmentation fault와 함께 충돌합니다. 우리 프로그램의 문제는 function1에서 사용하는 strcpy 구현에 있습니다. strcpy 함수는 한 문자열을 가져와서 다른 문자열에 복사하지만, 지정한 인수가 대상 문자열 변수에 맞는지 확인하기 위해 경계 검사bounds checking를 수행하지 않습니다. strcpy 함수는 세 글자, 다섯 글자, 심지어 수백 글자를 대상 문자열에 복사하려고 시도할 것입니다. 만약 프로그램 소스코드에 정의된 문자열이 다섯 글자이고 우리가 100자로 복사한다면, 다른 95개는 스택의 인접한 메모리 주소에서 데이터를 덮어쓰게 될 것입니다.

 

잠재적으로 function1의 스택 프레임의 나머지 부분과 더 높은 주소 영역의 메모리를 덮어쓸 수 있습니다. 해당 스택 프레임의 베이스base 바로 뒤에 메모리 주소에 무엇이 있는지 기억하십니까? 프레임을 스택에 푸시하기 전에 메인은 반환 주소를 스택에 푸시하여 함수1이 반환되면 실행을 계속해야 할 위치를 지정했습니다. 버퍼에 복사하는 문자열이 충분히 길면 버퍼에서 EBP로, 반환 주소를 통해 심지어 메인의 스택 프레임의 내용까지 덮어쓰게 됩니다.

 

strcpy가 overflow test의 첫 번째 인수를 버퍼에 넣으면 function1은 main으로 돌아갑니다. 스택 프레임은 스택에서 Pop Off되고 CPU는 리턴 주소가 가리키는 메모리 위치에서 명령을 실행하려고 합니다. 리턴주소를 아래 그림에서 보여준 것과 같이 A의 긴 문자열로 덮어썼기 때문에 CPU는 메모리 주소 41414141(A의 16진수값이 4번 반복되는 표현)에서 명령을 실행하려고 합니다.

그러나 우리 프로그램은 완전한 혼란을 야기할 수 있기 때문에 메모리의 원하는 곳에서 읽거나 쓰거나 실행할 수 없습니다. 메모리 주소(41414141)는 우리 프로그램의 범위를 벗어났으며 이 섹션의 처음에서 보았던 세그멘테이션 오류segmentation fault와 함께 프로그램에 오류가 발생합니다.

다음 섹션에서는 프로그램이 충돌할 때의 상황에 대해서 자세히 살펴보겠습니다. 다음에 논의되는 GDB에서는 maintenance info 섹션을 사용하여 어떤 메모리 영역이 프로세스에 매핑되는지 확인할 수 있습니다.

728x90

'Security > 보안기초' 카테고리의 다른 글

웹보안 기초(01)  (0) 2023.11.15
Memory Theory(02)  (0) 2023.11.14
Operation Aurora  (0) 2023.11.12
취약점 찾기  (0) 2023.11.12
APT 실습 첫번째  (0) 2023.11.12