주요 내용으로 건너뛰기

64bit Linux Binary 취약점 분석 - (1) Basic BOF

회사에서 어쩌다 보니 취약점 분석 업무의 필요성을 느끼게 되었다. 이럴 줄 알았으면 진작 해킹 공부해둘걸.. 

초보자를 위한 입문서 책이나 블로그를 뒤져보면 대부분 32bit 기준으로 설명하고 있다. 그런데 내가 지금 당장 필요한 건 64bit 바이너리 분석이다.

어쩔 수 없이 혼자 삽질을 통해 배워가는 수밖에 없다.. 걷기도 전에 뛰려고 하는 무모한 시도같이 보이기도 하지만 그래서 적기 시작한 이 포스팅은 나처럼 32bit 환경에서의 pwnable 경험이 없는 사람도 이해할 수 있을 만큼 되도록 자세히 적으려 한다.

여기에서 사용하는 예제는 다른 곳에서 일부 인용하여 수정한 형태이다. 풀이 및 설명에도 여러 국내외 문서 및 블로그를 참고하였으며 관련 링크는 하단부에 있다.


1. 준비물

1) Ubuntu 16.04

여기에서 사용할 Linux는 Ubuntu 16.04 Desktop (64-bit)이며 kernel 버전은 4.4.0-130-generic이다. 그 외에 Kali-Linux-2018.2-vmware-amd64에서도 동일하게 작동하는 것을 확인했지만 어차피 둘 다 Debian 계열이므로, 그 외의 다른 Linux에서는 방법이 약간 상이할 수도 있다. 물론 Intel 기반이며, ARM 아키텍처에 관해서는 논외로 한다.

2) PEDA

PEDA는 Python Exploit Development Assistance for GDB의 약자로, 리눅스에서 gdb를 통해 디버깅할 때 디스어셈블 코드, 레지스터, 메모리 정보 등을 확인하는 가시성을 확보해주는 도구이다. 그 외에 checksec 등 여러 가지 다양한 기능을 자체적으할로 내장하고 있어서 편리하다. 사실 나도 모르고 있었는데 다른 블로거가 이걸로 설명한 거 따라 하면서 써보니까 굉장히 유용했다.

설치는 아래와 같이 git으로 간단히 할 수 있다.

그럼 이후로는 gdb만 키면 자동으로 gdb-peda$ 쉘로 진입된다.

3) ASLR 끄기

앞으로 이 시리즈를 얼마나 계속 쓸 수 있을지는 모르지만 현재로써는 되도록 힘이 닿는 데까지 써볼 생각이다. 그런 의미에서 part1에 해당하는 이번 포스팅에서는 모든 메모리 보호 기법을 전부 다루기엔 역부족일 것이며 향후 주제로 남겨두자. 그래서 지금은 우선 ASLR 등을 OFF 할 것이다. 루트 계정 권한이 필요하며, 아래의 명령어를 수행하면 ASLR이 꺼진다.

(ASLR 우회하는 건 한 part4쯤 가야 가능할 듯..ㅋ)


2. 취약한 프로그램 작성

시작부터 realworld에 있는 바이너리를 제낄 수는 없다. 우선 '의도적으로' 취약하게 설계된 프로그램을 직접 작성하고, 그것을 exploit 하는 방식으로 tutorial을 진행해보자. 아래 코드를 c언어로 작성하자.

vuln() 함수를 보면 버퍼의 크기는 80인데, read() 함수를 통해 길이 400만큼을 사용자로부터 입력받아서 buf에 저장하려고 하고 있다. Buffer Overflow 취약점이 존재할 것으로 보인다.

컴파일은 아래와 같이 수행한다. 이때 다음과 같은 옵션을 지정한다. (ASLR과 마찬가지로, 난이도를 낮추는 작업이다. 실제 realworld 의 프로그램 개발 시에는 되도록이면 보안 기능을 적극 이용하는 것이 좋다.)

  • -fno-stack-protector : 메모리 보호기법인 Stack Smashing Protector를 적용하지 않는다.
  • -z execstack : 스택에 실행 권한을 부여함으로써, 스택에 명령어를 삽입시키면 그대로 실행되게 함.

이 프로그램을 실행시키면 어떻게 될까? 먼저 이해를 돕기 위해 Stack의 구조를 그림으로 나타내면 아래와 같다.

이미지 설명을 입력하세요.

3. 취약점 분석

이제 컴파일된 바이너리 vuln을 분석해보자. 위에서 언급했듯이, 400 길이 안에 내용을 마구마구 채워 넣어서 RIP를 덮어 씌우는 payload를 주입하면 될 것으로 예상된다. 일단 무작정 A로만 400번을 채워보자.

이미지 설명을 입력하세요.

Segmentation fault가 발생하며 종료되었다.

디버깅 도구인 gdb를 통해 내부적으로 무슨 일이 벌어졌는지 더 자세히 볼 수 있다. 위에서 gdb-peda를 설치했다면 자동으로 peda 쉘이 실행될 것이다.

이미지 설명을 입력하세요.

A를 무작정 400개를 구겨 넣었더니 프로그램이 crash 되긴 했지만, 우리가 정확히 알고 싶은 것은 RIP이다. RIP 에 접근해야만 우리가 원하는 exploit 및 쉘 획득을 할 수 있다. 우선 RSP 의 위치를 확인해보자. x64 CPU는 RSP 레지스터를 스택 포인터 겸 스택 프레임 포인터로 사용하기 때문이다.

rsp가 A(41)로 잘 덮여있다. 그런데 집어넣은 A가 400개나 되니, 정확한 어느 위치에서 RSP를 덮은 것인지 알 수 없다.

내용을 출력해보면 아래와 같이 peda가 생성한 장황한 문자열이 나온다.

이미지 설명을 입력하세요.

사람 눈으로 보기에는 뭐가 무슨 차이인지 잘 모르겠지만 사실 peda는 각 값들의 위치를 다 기억하고 있다. 그래서 이를 통해 RSP에 들어간 값이 무엇인지 확인한 후 peda가 알고 있는 문자열의 몇 번째 위치에 속하는 값과 일치하는지를 비교하면 offset을 알 수 있게 된다.

pattern을 대입했을 때 rsp값이 어떻게 설정되어있냐면..

그렇다면 아까 생성한 pattern에서 0x41413741은 어디에 있는 것이냐 하면..

104다! 즉 아래의 그림과 같다. 

이미지 설명을 입력하세요.

즉, Return Address 까지 접근해야 할 길이는 104이다. 이를 통해, payload를 만들 때 104개를 기준으로 나누어 생각해볼 수 있겠다. 아래의 파이썬 코드(rip.py)는 "A"를 104번 입력한 후 0x424242424242(즉, BBBBBB)를 덧붙여서 in.txt 파일로 저장한다.

이렇게 만들어진 rip.py를 구동하여 in.txt를 생성할 수 있다. 파일 내용 안에는 AAAA(104번) BBBBBB가 들어있다.

다시 gdb로 vuln 바이너리를 수행하고, 이번에는 disass vuln을 통해 vuln 함수의 내부 어셈블리 구조를 살펴보자.

우선 vuln 함수의 ret 부분에 break point를 지정하자.

아까 만든 in.txt를 input으로 넣어 실행한다.

이미지 설명을 입력하세요.

peda가 이미 잘 보여주고 있듯이, leave 명령이 끝나고 *vuln+63의 ret에 break 걸린 상태에서 rsp레지스터를 보면 0x7fffffffdc28을 가리키고 있고, 그 주소에 적재된 값은 0x424242424242이다. 즉 BBB..B가 입력된 것인데 이를 그림으로 나타내면 아래와 같다.

이미지 설명을 입력하세요.
leave 명령어 수행 이후의 register들
leave 명령어 수행 이후의 register들
leave 이후의 stack
leave 이후의 stack

이제 break point를 지나서 ret 명령어가 실행되도록 step into 해보자.

이미지 설명을 입력하세요.

수행 결과 RIP 가 0x424242424242로 덮여있으며, Invalid $PC address: 0x424242424242이라는 메시지와 함께 정지되었다. 이 말인즉슨 이러한 방식으로 RIP를 원하는 값으로 조작할 수 있다는 것이다. 그리고 이는 Program counter를 조작하여 다음에 실행할 명령어를 바꾸는 것이 가능하다는 것이다.

그렇다면 이제 0x424242424242 대신에, 사용하고자 하는 명령어가 위치된 주소를 지정하면 된다. 참고로 Hacking: The Art of Exploitation, 2nd Edition(해킹 : 공격의 예술, 에이콘 출판 역간) 책에서는 쉘 코드를 export 하여 환경변수로 등록한 뒤에 getenvaddr를 통해 해당 환경변수의 주소 값을 가져와서 지정하는 방식을 안내하고 있다.

하지만 여기에서는 이미 사용자에게 주어진 buf를 이용하여 여기에 쉘 코드를 주입하고, 그 buf의 시작 위치로 RIP를 지정하는 방식을 사용하고자 한다.

*vuln+52에서 buf의 내용을 출력하려는 printf 함수가 호출된다. 이 과정을 관찰하기 위해 해당 위치에 break point를 설정하자.

그리고 아까 생성한 in.txt 파일을 입력으로 다시 한번 전달해본다.

이미지 설명을 입력하세요.

Breakpoint에서 중단되었는데, 이때 stack 의 내용을 보면 0x7fffffffdbc0부터 'A'(41) 값이 입력되기 시작하는 것을 볼 수 있다.

이미지 설명을 입력하세요.

즉, buf 배열의 시작 지점은 0x7fffffffdbc0인데, 이 위치에 쉘 코드를 넣고 RIP로 이곳을 가리키게 하면 이 위치에 있는 내용을 실행하게 될 것이다. (참고로 메모리 주소 값은 프로그램이 수행되는 환경마다 상이하며 심지어 같은 컴퓨터로 작업할 때에도 경우에 따라 값이 변하게 된다. 따라서 만약 이 작업을 따라 하는 분이 계시다면 값 자체에 의미를 두지 말고 확인된 본인의 값을 토대로 진행하기 바란다.)


4. 쉘코드 생성

쉘 코드는 exploit 하려는 프로그램에 주입한 후 실행되도록 의도된 일련의 명령어(instruction) 집합이다. 쉘 코드에 대한 구체적인 이론을 처음부터 끝까지 설명하는 것은 본 포스팅의 범위를 벗어난다. 내가 참고한 이론적 배경은 The Shellcoder's Handbook: Discovering and Exploiting Security Holes 2nd Edition이며, Chapter 3에서 상세히 설명하고 있다.

하지만 쉘 코드를 만드는 방법을 당장은 몰라도 일단 진행은 할 수 있다. 어셈블리 언어로 직접 코딩하여 자신만의 쉘 코드를 제작하고 자랑스럽게 게시하는 수많은 의인들 덕분에, 나 같은 초보들은 그저 그들의 코드를 구글에서 잘 찾아서 인용하기만 하면 된다. 

/etc/passwd 파일을 불러오는 Linux 64bit Shellcode이다. 제작자는 Mr.Un1k 0d3r - RingZer0 Team으로 기록되어 있으며, 82 bytes 길이의 쉘 코드이다.

http://shell-storm.org/shellcode/files/shellcode-878.php

해당 링크에서 설명하고 있는 쉘 코드 작성 방법인데, 굳이 따라해볼 필요 없이 결과물만 가져와도 된다.

간략히 설명하자면, buf 배열 안에 우선 /etc/passwd 의 쉘 코드를 배치한다. 그리고 원래 RIP 의 offset이 104였으므로, 쉘 코드가 앞에서 차지하고 있는 길이만큼을 감안하여 A의 개수를 조절해준다. (도합 104개를 유지해야 함) 그리고 뒷부분에는 RIP를 덮어씌워야 할 주소인 0x7fffffffdbc0을 위치시켜 준다.

이미지 설명을 입력하세요.

그렇다면 이제 프로그램이 실행될 때 RIP 레지스터가 Shellcode의 주소인 0x00007fffffffdbc0을 가리킬 것이고, 그곳에 적재된 명령어를 수행하고자 할 때 우리가 심어놓은 쉘 코드가 작동하게 될 것으로 예상할 수 있다.

이미지 설명을 입력하세요.


5. Exploit 실행

해당 파이썬 파일을 실행하여 in.txt를 생성한 뒤, gdb로 vuln 바이너리를 열어 in.txt 의 내용을 주입한다.

/etc/passwd 파일의 내용이 성공적으로 출력된다!

이미지 설명을 입력하세요.

gdb가 아닌 바이너리에서 직접 하면? 당연히 될 거라고 생각했지만 큰 오산이었다.(다 된 줄 알고 신났다가 막판에 기분 잡침ㅋ) exploit 개발은 정말 골치 아프고 성가신 일 투성이인 듯 ;

결론적으로 말하면 디버깅 모드로 작동할 때와 일반 모드로 실행할 때 참고하는 주소 영역 계산 방식이 다르다고 한다. (참고 : https://stackoverflow.com/questions/17775186/buffer-overflow-works-in-gdb-but-not-without-it )

자세한 설명은 패스하기로 한다. 참고로 나의 경우에는 gdb시 0x7fffffffdbc0 였던 주소가, 일반 실행으로 할 때에는 0x7fffffffdbe0으로 바뀌어서 shellcode payload를 아래와 같이 수정하였다.

cat 명령어를 사용하여 pipe(|)로 출력을 vuln 에 전달해 주었다. 성공적으로 /etc/passwd 파일이 조회된다.


+. 추가

보통 CTF Write-up을 보면 대부분 python에서 pwntools를 사용하여 Exploit 하여 쉘 획득 후 interactive 모드로 진입하는 방식으로 시연을 하고 있다. 위의 내용에서 쉘 코드를 /bin/sh 띄우는 것으로 변경한 후 pwntools로 짜면 아래와 같다.


python 명령어로 해당 파이썬 코드를 수행하면 자동으로 process가 실행되면서 결과가 나오고, /bin/sh 획득에 성공하면 interactive 모드가 전개된다.


6. 결론

가장 기본적인 stack smashing attack을 64-bit linux 에서 수행하였다. 본 exploit이 잘 작동되어서 다행이지만, 여기에서 기뻐하기는 이르다. 서두에서 언급하였듯이 초급자의 진입장벽을 낮추기 위해 NX, ASLR 등의 보호 기법을 모두 해제한채로 실습했기 때문이다. 앞으로 이어질 글에서는 NX (stack의 명령어가 수행되는 것을 방지하는 보호기법)에 대한 우회기법을 터득하기 위해 일명 ret2libc 관련 내용을 진행해 보도록 하겠다.


7. 참고문헌

  1. Aleph, One. "Smashing the stack for fun and profit." http://www. shmoo. com/phrack/Phrack49/p49-14 (1996).
  2. Koziol, Jack, et al. The shellcoder's handbook: discovering and exploiting security holes. New York: Wiley, 2004.
  3. Hacking: The Art of Exploitation, 2nd Edition(해킹 : 공격의 예술, 에이콘 출판 역간)
  4. http://shell-storm.org/shellcode/files/shellcode-878.php
  5. Techorganic's blog 
  6. 64 Bits Linux Stack Based Buffer Overflow

정보보안에 관심이 많은 대학원생, 소프트웨어 엔지니어/서버관리자

CPUU 님의 창작활동을 응원하고 싶으세요?

댓글

SNS 계정으로 간편하게 로그인하고 댓글을 남겨주세요.