/ POSTS

linux_0x01 BOF 준비2

0x01 - Buffer Overflow 준비

안녕하세요. Sulla입니다. 오늘부터 본격적으로 BOF에 알아보도록 하겠습니다.

BOF(Buffer Overflow)란 메모리의 동작 과정중의 오류로 인한 잘못된 동작을 유도하는 취약점입니다. 프로세스가 데이터를 버퍼에 저장 시 입력 값의 크기를 검증하지 않을 경우 주어진 버퍼를 넘어선 즉, 지정된 위치가 아닌 곳에 저장되며 인접한 메모리를 덮어 쓰게 됩니다. 인접 메모리에는 기존의 데이터가 저장되어 있으며 변수, 프로세스 흐름 제어 등의 데이터가 포함됩니다. 따라서 메모리 접근 오류로 인한 이상 동작을 하게되며 결과적으로 취약점으로 동작하게 되는것입니다.

앞으로 이 BOF에 대해서 천천히 알아보고자 합니다. 먼저 BOF를 알기 위해서는 메모리, 레지스터 등의 구조 및 개념과 앞으로 사용될 gdb의 사용법 등 본격적인 시작에 앞 서 필요한 배경 지식에 대하여 짚고 넘어가도록 하겠습니다.

메모리 구조

[그림 1-1]

먼저 BOF를 알기 위해선 메모리를 알아야 합니다.

  • Kernel : OS의 중요 코드들이 로드되며 일반 사용자는 접근 불가한 영역
  • off-limit : 사용자의 Kernel 접근을 막기 위해 설정한 영역
  • Stack : 환경변수, 파라미터, 반환되는 주소 및 지역변수 등 임시적인 데이터를 저장하는 공간이며, 높은 메모리 주소에서 낮은 메모리 주소로 저장됨
  • libc : 프로그램 내부에서 사용하는 라이브러리 함수들과 관련된 공유 라이버리 파일이 정장되는 공간
  • Heap : 엉덩…아니 필요에 의해 사용되는 동적 변수의 데이터가 저장되는 공간이며, 낮은 메모리 주소에서 높은 메모리 주소로 저장됨
  • BSS / Data : 전역변수, 정적변수, 배열, 구조체 등이 저장되는 구조이며 차이는 아래와 같다
    • BSS : 초기화 되지 않은, 0으로 초기화, Null로 초기화된 데이터의 경우 저장되는 공간
    • Data : 초기화 된 데이터가 저장 되는 공간
  • Code : 실제로 실행되는 hex, bin과 같은 파일내부에 실행되는 코드들이 저장되며 기계어 명령어, 어셈블리 코드가 실행되는 영역

메모리를 간단하게 정리한 내용입니다. 여기서 Stack과 Heap의 동작 방향이 다르다는 것이 특이한 모습을 보입니다. 왜 그럴까요?

바로 효율적인 메모리 관리와 Kernel영역의 보호를 위해 저런 모습으로 설계 되었습니다. Stack은 Kernel의 반대 방향으로 움직이고 Stack과 Heap 사이에 libc를 배치하여 메모리 관리의 효율성을 높인것이죠.

이제 메모리를 간단히 알아 보았으니 메모리 중 Stack에 관하여 간단히 짚고 넘어가도록 하겠습니다.

Stack?

Stack의 역할은 위에서 언급했으니 패스하고 동작 장식에 대하여 알아 보도록 하겠습니다. Stack은 기본적으로 후입선출(LIFO, Last in First out)의 방식으로 동작합니다.

[그림 1-2]

Stack의 동작은 위와 같습니다. 16byte의 Stack공간에 A(4byte) 데이터를 저장시 PUSH를 통하여 저장합니다. 밀어 넣는다는 거죠. 이 때 ESP -4 즉, 스택 포인터의 값을 총값에서 -4값을 저장 해둡니다. 왜일까요?

Stack은 메모리의 높은 주소값에서 낮은 주소값으로 이동하기 떄문에 16byte 에서 4byte씩 줄이는 것이죠. 높은 곳에서 낮은 곳으로.

다음으로 B 데이터를 저장시 마찬가지로 PUSH를 통하여 A데이터 위에 밀어 넣게 됩니다. ESP 값 또한 -4의 값을 저장 합니다. C 데이터까지 같은 방식으로 데이터를 저장 합니다. 그런데 다시 A 데이터를 빼고 싶다면?

앞서 말했던 Stack의 동작 방식인 후입선출에 의해 C 데이터를 POP을 통하여 뽑고 ESP의 값은 -4가 아닌 +4의 값을 저장합니다. 이어서 B데이터를 뽑은 후 A 데이터를 뽑을 수 있습니다. 이것이 Stack의 동작 방식인 후입선출입니다.

정리하자면, Stack은 메모리의 높은 주소값에서 낮은 주소값으로 데이터가 저장되며 동작 방식은 후입선출의 방식으로 동작 합니다. 위에서 나온 ESP의 경우 레지스터의 한 종류로서 레지스터는 포인터, 산술 연산, 인덱스, 세그먼트, 플래그 등이 있으며 필요한 몇가지의 레지스터만 간단하게 정리하자면 다음과 같습니다.

포인터 레지스터 : 포인터와 관련된 레지스터

  • EIP (Instruction Pointer) : CPU가 실행할 다음 명령어의 주소값을 나타냄
  • EBP (Base Pointer) : 현재 스택의 최하단 주속값을 나타냄, EBP의 다음 주소는 Return값을 나타냄
  • ESP (Stack Pointer) : 현재 스택의 최상단 주소값을 나타냄

산술 연산 레지스터 : 산술 논리 연산에 필요한 레지스터

  • EAX (Accumulator Register) : 산술 논리 연산에 필요한 상수 또는 변수값이 저장되며 함수의 리턴값 저장
  • ECX (Counter Register) : 반복문 동작시 반복문의 반복 횟수를 저장
  • EDX (Data Register) : EAX 레지스터를 보조

인덱스 레지스터 : 작업에 필요한 데이터의 주소값 저장에 사용되는 레지스터

  • ESI (Source Index) : 데이터 복사 및 조작할 때 사용하는 데이터 주소를 저장(ESI <> EDI)
  • EDI (Destination Index) : 데이터 복사시 목적지의 주소 저장 (EDI <> ESI)

레지스터의 사용 이유는 CPU의 빠른 작업 처리를 위해 사용 됩니다. CPU내부에 위치하여 메모리에 접근하는 것보다 더욱 빠른 속도로 접근 가능합니다. 단, 별도의 데이터를 저장 하기엔 용량이 작기에 위에서 알아본 특수한 목적에 맞는 특정 데이터만 저장해두며 CPU의 빠른 동작을 지원 해줍니다.

레지스터의 종류는 위의 내용 외 더욱 다양한 레지스터가 존재 하지만 현재 필요한 레지스터 및 알아두면 좋을 레지스터를 알아 보았습니다. 급하게 외우기보단 반복적으로 사용하다 보면 필요한 레지그터의 값만 빠르게 살펴보고 자연스럽게 기억이 되실겁니다.

마지막으로 어셈블리어 명령어에 대하여 알아보도록 하겠습니다. 위에서 Stack의 후입선출의 동작 방식을 알아보는 과정 중 PUSH, POP 그리고 언급은 없었지만 NOP라는 단어가 보였습니다. 이 단어들이 어셈블리어의 명령어이며, 어셈블리어란 컴퓨터 프로그래밍의 저급 언어입니다. 쉽게 말해 컴퓨터가 알아먹기 쉽도록 구성된 언어입니다. 다양한 명령어가 있으며 여기서도 필요한 또는 알아두면 나중에 좋을만한 내용들만 알아보도록 하겠습니다.

  • PUSH : 데이터를 스택에 쌓음
  • POP : 스택의 저장된 데이터를 뽑음
  • NOP : 아무 동작 안함
  • MOV : 데이터 이동
  • CALL : 특정 주소의 함수를 호출, JMP와 같이 실행의 흐름이 변경되지만 호출된 함수의 동작이 종료되면 돌아갈 리턴 주소를 Stack에 저장해 둔다는 차이점이 있음
  • JMP : 특정 주소의 함수로 이동하며 JMP 외의 다양한 조건 점프 명령어가 있음
  • ADD : 덧셈 명령을 수행
  • SUB : 뺄셈 명령을 수행

위의 리스트 외에도 곱셈, 나눗셈, AND, OR, XOR 연산 및 조건 분기 등 다양한 기능의 명령어들이 존재 합니다. 이 명령어들 또한 레지스터들과 함께 자연스럽게 익혀 나가시면 됩니다.

다음 포스트에는 간단한 BOF 공격과 공격에 필요한 내용들을 추가적으로 정리해보도록 하겠습니다.

천천히 갑시다. 조바심 내지 말고 천천히 탄탄하게.