/ POSTS

linux_0x02 BOF 시작

0x02 - Buffer Overflow 맛보기

안녕하세요. Sulla임돠.

오늘은 저번 시간에 다룬 내용에 이어서 기본적인 BOF 와 추가적으로 알아야 할 내용을 다루도록 하겠습니다.

먼저 이번 시간부터는 직접 gdb를 이용해서 여기저기 들쑤실 계획인데 그럴려면 gdb 조작법을 먼저 알아야 겠죠. 마찬가지로 필요한 명령어들 먼저 알아 보도록 하겠습니다. (개취)

gdb

[그림 2-1]
[그림 2-2]

그누 디버거 라는 이름으로 아주 옛부터 널리 쓰이던 디버거 입니다. 앞으로 진행될 내용에서는 이정도 명령어만 알아도 크게 무리 없이 기본적인 분석은 가능하다고 생각 됩니다. gdb는 다양한 명령어가 있으니 상황에 맞게, 취향에 맞게 추가적으로 더 찾아서 쓰시면 되겠습니다. 참고로 괄호안의 문자는 약어입니다.

그런데 막상 gdb로 이것저것 뒤져보면서 메모리의 주소값을 보다보면 이상하게 뒤죽박죽 매핑된듯 한 느낌을 받습니다.

이를테면 분명 0xABCD1234의 값이 메모리에는 34 12 CD AB와 같이 보기 불편하게 박혀있습니다. 왜 저럴까요?

리틀엔디안/빅엔디안

왜 저렇게 보기 귀찮게, 어렵게 저장하고 있냐하면 일종의 메모리 저장 방식 중 하나라고 이해 하시면 됩니다. 특정 데이터를 메모리에 저장할 때 바이트 단위로 저장을 하게 됩니다. 이 때 CPU의 아키텍쳐에 따라 이 바이트의 저장 순서에 따라 리틀엔디안, 빅엔디안 또는 두 방식 모두 지원하거나 모두 지원하지 않는 미들엔디안이 있습니다. 여기서는 미들엔디안은 제외 하고 각각의 예를 들어 보도록 하겠습니다. (4바이트)

[그림 2-3]

보시는 것처럼 빅엔디안의 경우는 우리가 평소 글을 쓰는 방향과 동일하게 저장됩니다. 반면 리틀엔디안의 경우 이상한 모습이죠. 빅엔디안과는 다르게 데이터가 역순으로 저장 됩니다.

왜 컴퓨터는 이런 짓 거리를 할까요? CPU에는 ALU라는 산술/연산을 담당하는 친구가 껴있습니다. 이 친구는 메모리를 읽을 때 낮은 주소에서 높은 주소로 읽어들이고 그래야 작업 속도가 빠른 친구입니다. 쉽게 말해 작업 처리를 더 효율적으로 하기위해 설계된 방식입니다.

빅엔디안의 경우 주로 네트워크 상에서 사용된다 생각하시면 되겠습니다. 우리 주변에 있는 대부분의 데스크톱은 리틀엔디안을 사용하며 Intel 계열의 프로세서인 리눅스, 윈도우가 이에 해당 됩니다. 앞으로 리틀엔디안을 자주 보게 될겁니다.

추가적로 ARM 프로세서들은 빅엔디안과 리틀엔디안을 선택하여 사용 가능합니다.

함수 프롤로그/에필로그

코드를 몇 번 분석하다 보면 공통적인 모습을 찾을 수 있습니다. (못 보셨을수도 있습니다.)바로 함수의 시작과 끝이 비슷한 모습을 취하고 있다는 것이죠. 왜일까요?

함수 호출 시 동작 방식은 다음과 같이 요약해 봤습니다.

  1. 함수가 사용한 인자를 스택에 저장
  2. eip값 즉, 함수 호출후 돌아올 주소를 스택(ret)에 저장 후 함수 시작 지점으로 점프(함수 호출)
  3. 함수 내에서 사용할 스택 프레임을 설정(프롤로그)
  4. 함수의 내용 수행
  5. 수행 후 처음 호출한 지점으로 돌아가기 위해 스택을 복원 (에필로그)
  6. 호출한 지점의 다음 라인으로 점프 또는 스택에 저장 된 eip값으로 복귀(다음 함수 수행)

즉, 함수를 수행하기 위한 준비 과정을 프롤로그라 하며 함수 수행을 마무리하는 과정을 에필로그라 이해하면 되겠습니다. 그사이에 실제 코드가 동작 하게는 명령어들이 자리 잡게 됩니다.

1   #include <stdio.h>
2
3   main(){
4    //프롤로그
5     int num1=1;
6     int num2=3;
7
8     sum(num1,num2); /*sum() 종료후 복귀할 주소를 ret에 저장 
					및 인자값을 스택에 저장 후 sum()으로 점프*/
9          
10     return 0;
11   //에필로그
12  }
13
14  sum(int num1, int num2){
15   //프롤로그       
16    int sum1;
17       
18 		  sum1 = num1 + num2;
19
20    return sum1;
21   //에필로그
22  }

위에 코드에서 프롤로그와 에필로그의 대략적인 위치를 보여줍니다. 또한 sum 함수 호출 시의 어떤 동작을 수행 하는지도 간략하게 확인 가능 합니다. 또한 어셉블리어로는 다음과 같이 표현됩니다.

// 프롤로그
1  push    ebp         # 이전 함수의 베이스 주소를 저장(sfp)
2  mov     ebp, esp    # 새로운 스택 프레임 생성

.......생략........

// 에필로그
3  mov     esp, ebp    # 베이스 주소를 이전의 스택으로 복구
4  pop     ebp         # 베이스 주소 복구
5  pop     eip         # eip를 ret에 저장
6  jmp     eip         # 함수 종료 후 다음 명령으로 이동

main() 함수 시작 시 프롤로그인 1~2라인이 먼저 수행 됩니다. 그 다음 작성된 코드가 수행 되고 return 값 반환 후에 에필로그 3~6라인이 수행됩니다.

이 때 3~4 라인은 leave, 5~6라인은 ret라고 표현하기도 합니다.

간단히 정리 하자면, 함수의 시작점은 프롤로그, 끝점은 에필로그라 생각하시면 됩니다. 그 과정에서 길을 잃지 않기 위해서 위와 같이 ebp와 esp의 조작 과정들을 거치는 것이죠.

그럼 sfp는 무엇이고, ret는 무엇일까요?

BOF 맛보기

sfpretBOF 맛보기와 함께 알아보도록 하겠습니다.

bof1.c
1   #include <stdio.h>
2   
3   int main(int argc, char *argv[]){
4
5      char buf[20];
6      
7      strcpy(buf, argv[1]);
8      printf("%s\n", buf);
9
10     return 0;
11  }

버퍼 20을 할당하고 사용자에게 입력값을 받아 버퍼에 저장하는 코드가 보입니다. 메모리에서는 함수가 실행 되면 4byte 단위로 메모리에 버퍼 공간을 확보합니다. 사용자가 임의의 값(AAAA)을 입력했을 때 메모리의 모습은 이런 모습입니다.

[그림 2-4]

sfp(save frame pointer)는 ebp를 바로 전에 호출한 ebp 주소를 저장해두고 나중에 함수 리턴전에 이 값을 참조하여 ebp를 복구합니다.

ret에서는 함수 종료 후 복귀할 주소가 저장됩니다. 제일 뒤에 있는 파라미터는 필요한 변수의 값들이 들어서게 됩니다.

여기서 코드 실행 시 일반적으로 사용자가 입력한 값은 버퍼20 영역에 들어가도록 설계가 되었습니다. 하지만 취약한 함수인 scanf, strcpy 등(str…..)을 사용하며, 사용자로부터 입력받은 값의 길이를 검증하지 않는 경우 버퍼를 넘어서 sfp, ret의 영역에 까지 입력값이 저장 됩니다. 이 때 ret영역에 공격 쉘코드로 덮어버리면 공격자의 코드까지 동작되는 것이죠. 예를 들어 “AAAA”*64를 입력 해서 A를 256개를 입력 받았다면 다음과 같이 할당 받은 20byte의 버퍼 공간을 넘어서 저장될 것입니다.

[그림 2-5]

대충 감이 오시나요?? 주어진 버퍼의 길이 이상의 데이터를 입력하여 sfp, ret 영역까지 덮고 함수 종료 후 ret영역의 코드가 실행되어 공격자가 원하는 행위를 하도록 하는 공격인 것이죠. 실제로는 아래와 같이 공격 페이로드를 작성해서 BOF를 시도 합니다. 우리가 노리는건 256바이트 무의미한 덩어리가 아닙니다.(물론 dos 공격의 개념으로 사용 가능합니다.) 우리는 ret 영역에 shell을 따낼수있는 코드를 구성하는 것입니다.

[그림 2-6]

이제부터는 앞에서 봤던 코드를 대상으로 BOF 시도해 봅시다. 우리 목표는 위의 그림과 마찬가지로 ret 영역에 HACK를 넣는겁니다. 주의 할 점은 코드내에서 버퍼를 20byte를 주었다 해도 실제 메모리상에서는 그 이상을 할당할 수도 있습니다. 반드시 gdb를 통해서 실제 할당 공간을 확인해야 합니다.

[그림 2-7]

위에서 봤던 bof.c 코드를 작성 후 컴파일 해야 합니다. 컴파일된 바이너리 파일을 실행하여 정상적으로 동작이 되는지까지 확인 합시다. 동작은 아래와 같습니다.

  • bof.c의 7번 라인에서 입력 받은 argv의 값을 strcpy 함수를 통하여 buf에 복사
  • bof.c의 8번 라인에서 buf에 복사된 입력값을 printf 함수를 통하여 출력

정상적으로 동작되는 것을 확인 했습니다. 이제 gdb를 사용해서 바이너리 파일 bof1을 뜯어 보도록 합시다.

[그림 2-8]

gdb로 bof1 바이너리 파일을 실행 시키고 disassemble 명령어를 통해 main 함수를 어셈블리어로 출력 해줍니다. 하지만 코드를 읽기에 익숙하지 않은 형태입니다. gdb는 기본적으로 at&t 형식을 표현합니다. 이걸 intel 형식으로 바꿔서 우리가 읽기 편하게 바꿔 봅시다.

[그림 2-9]

보기 편한 형태로 보시면 됩니다. 참고로 다시 at&t 형식으로 바꾸는 방법은 set disassembly-flavor att 입니다. 계속 진행 해보겠습니다. 위에서 짚고 넘어갔던 프롤로그/에필로그와 전 편에서 다뤘던 어셈블리 명령어, 레지스터 등이 보입니다.

여기서 중요시 봐야할 라인은 버퍼의 크기를 정하는 main+3 라인입니다. 프롤로그 후에 필요한 버퍼 크기만큼 할당하여 esp를 쭉 밀어 넣는다고 생각하시면 됩니다. 하지만 앞서 말했듯이 우린 20byte를 지정 했지만 실제로는 0x28 즉, 40byte를 할당 받았습니다. 이 유는 컴파일러마다의 차이라고 하시면 되겠습니다. 중요한 것은 할당받은 버퍼공간을 꼭 항상 확인해야 한다는 겁니다.

[그림 2-10]

지금까지 확인 된 내용은 위의 그림과 같습니다. 40byte의 버퍼와 sfp, ret 각 4byte 입니다. 그럼 이제 버퍼에 값이 정말 들어가는지 봐야겠습니다. main+31 라인은 strcpy 함수를 호출하는 라인입니다. 우리가 입력값이 저장되어있는 argv의 값을 buf에 복사하기 전입니다. 그 다음 라인 36,39 라인은 esp를 정리하는 라인이며 42라인에서 ebp를 eax에 저장하는 과정을 확인 가능합니다. 버퍼에 입력한 값들이 들어 가는지 확인해 봅시다.

잊지 말아야 하는 점은 버퍼 다음 sfp 다음 ret 영역이 자리 잡는 다는 겁니다.

[그림 2-11]

위의 사진과 같이 브레이크 포인트를 main+42 지점에 설정 해줍시다. 그리고 run 명령어를 통해서 bof1이 실행 되며, python의 -c 옵션과 print함수를 사용하여 입력값을 전달됩니다. 미리 설정한 브레이크 포인트에서 멈췄다는걸 보여줍니다. 이제 esp를 기준으로 특정 크기만큼의 메모리 상태를 확인하여 버퍼 구역에 값이 똑바로 들어갔는지 확인합니다.

[그림 2-12]

검정색으로 덮혀있는 40byte가 버퍼 구역임을 확인할 수 있습니다. 버퍼 다음엔 sfp 구역이며 이어서 ret 구역이 자리잡는다고 알려 드렸습니다. 다음의 사진을 통해 ebp의 위치를 확인 하여 붉은 박스가 sfp 영역인지 확인할 수 있습니다.

[그림 2-13]

ebp의 위치와 저장되있는 값을 확인해보니 붉은 박스가 sfp 영역임을 확인 헀습니다. 그럼 초록 박스는 ret구역이라는 것을 알 수 있습니다. 계속 해서 continue 명령을 사용해 나머지 부분을 실행 시킵니다. 입력한 “A” 40byte가 똑바로 출력 됩니다. 그렇다면 추가로 4byte를 입력하여 sfp구역의 EBP값을 넘어간 후 추가적으로 4byte를 입력 한다면 ret 위치에 저장 될 것입니다. 바로 확인 해보겠습니다.

[그림 2-14]

sfp 위치에 BBBB의 아스키 값이 저장 되있고, 우리의 타겟이었던 ret 구역(초록 박스)에 “HACK” 문자의 아스키 값이 잘 저장 되어있네요. 이로써 ret 구역에 값이 저장 되며 이를 악용해 쉘코드를 저 위치에 저장 하면 됩니다.

즉, 40byte(buffer) + 4byte(sfp)의 잉여 데이터를 채우고 ret에 공격 쉘코드를 읽을수 있는 주소값으로 바꾸면 함수 종료 후 ret에 저장된 주소값을 참조하며 쉘코드가 실행 됩니다.

지금 까지는 맛보기 BOF였습니다. 이것 저것 한 번에 알려 드려서 양이 많아졌네요. 공부 하면서 중구난방으로 있던 내용들을 무리해서라고 꽉꽉 채웠습니다. 최대한 간단하게…

다음 시간에는 직접 쉘코드를 활용해서 실제 쉘을 따보겠습니다. 갈 길이 멉니다….천천히 탄탄히 갑시다.

감사합니다.