Ubuntu 10.04.4 LTS에서 버퍼오퍼플로 취약점 점검
앞선 섹션에서 말했듯이 %EIP 레지스터를 통해 function() 함수 루틴을 마친 후 본래 실행하려고 했던 main() 함수의 실행 라인으로 복귀하기 위한 RETurn 주소를 조작해야 할 순서다. 그러면 먼저 이 복귀 주소가 정확히 메모리 데이터 어디에 위치하는지 계산해야 한다. fundamental principle of EIP control
계산방법은 정상적인 'AAAA' 문자열 복사 시 이 문자열이 function() 함수의 메모리 데이터 중 어느 번지에 기록되었는지와 기본적으로 function() 함수의 호출 후 바로 push 하는 RETurn 주소를 역어셈블 명령을 통해 찾아서 이 두 번지 간에 몇 byte의 메모리 데이터가 떨어져 있는지 알아내면 된다.
위의 화면에서 'x/16xw $rsp' 명령을 통해 두 번째 중단점(function() 함수 내부의 strcpy가 실행되기 직전)까지의 %RSP(ESP) 내용을 확인하였고, 'x/1xw $rbp' 명령을 통해 %RBP(EBP)가 어디서 시작되는지 확인했다. 그리고 %RSP 레지스터 레이블에 해당하는 '0x7fffffffe2a0'의 데이터 내용 첫 번째에 '0x41414141'이 기록되어 있는 것으로 보아 여기에 'AAAA'문자열이 기록되었다는 것도 알 수 있다.
그러면 여기서부터 RETurn 주소인 '0x004005b7'까지 몇 byte가 떨어져 있는지의 계산(메모리 데이터 하나당 16진수 4 byte가 기록되어 있으므로 눈으로는 20 byte 정도('AAAA'문자열까지 포함하면 24 byte)가 떨어져 있는 것으로 확인된다.)이 맞는지 확인해 보기 위해 오버플로우 크래쉬를 유도해보자.
일단 설정되어 있는 중단점(Break point) 1과 2를 해제해야 한다. 왜냐하면 프로그램의 각 function별 메모리 데이터 내용을 확인하면서 strcpy가 실행되기 전에는 메모리 상태가 변하지 않기 때문이다. 위의 화면처럼 'delete 1', 'delete 2'로 중단점 1,2를 지우고 펄을 이용한 오버플로우 디버깅 테스트를 시도한다(run $(perl -e 'print "A" x 24 . "B" x 4'). 즉 문자 'A'를 24개 복사시키고 이후 연달아 문자 'B'를 4개 복사시켜서 우리가 알고 있는 RETurn 주소 자리에 'BBBB'라는 문자(16진수로 '42424242')가 기록되는지 확인해본다. 명령어 입력 후 디버깅이 시작되었었는데 처음부터 다시 시작할 건지 물어본다. 'y'를 눌러서 실행시킨다.
남아있는 중단점 전까지의 메모리 내용을 살펴보면 위의 화면의 'x/16xw $rsp' 명령을 통해 리턴 주소가 위치한 메모리 데이터 부분이 'BBBB'의 16진수 표기로 '42424242'로 기록되어있다는 것을 볼 수 있다. 그리고 'continue' 명령으로 다시 실행시키면 네 개의 'B' 문자열의 메모리 주소를 실행하려다 실행이 좌초될 것이다. 바로 여기가 메모리 영역 바깥의 지점인데, 이를 통해 적어도 프로그램을 실행하는 지점의 주소(스택 메모리의 번지)를 알게 되었다.
일단 4 byte의 문자열로 실행하는 지점의 주소를 덮어쓰게 되는 위치를 확인했으니 이제 여기에 공격자가 원하는 소스(악성코드)를 실행시킬 수 있는 주소로 덮어쓸 차례이다. RETurn 주소는 function() 함수가 호출될 때 %EIP(지시 포인터) 레지스터에 의해 형성되고 %EDI(목적지 인덱스) 레지스터에 저장되어 이후 main() 함수로 복귀 시 실행된다는 것을 알고 있어야 한다. 그리고 이번 실습에서는 프로그램 내부의 overflowed() 함수 영역이 악성코드의 역할을 대신하기로 했었다. 그러기 위해서는 overflowed() 함수를 호출하면 메모리의 어디로 불러들여지는지 'dissass overflowed' 명령을 통해 확인한다.
'diss overflowed' 명령을 내리면 바로 첫 번째 들어오는 메모리 데이터 번지 수가 overflowed() 함수의 첫 명령어를 담고 있다는 것을 알 수 있다. 프로그램을 여기로 방향 전환시키면 함수 내에서 모든 명령을 실행하게 된다(조지 와이드먼, 2016). 이후 run $(perl -e 'print "A" x 24 . "\x00\x00\~\x05\x64"') 명령을 통해 테스트했었던 "BBBB" 자리에 overflowed() 함수를 호출하는 주소인 "0x0~0400564"로 대체시켰다. 이때 CPU 아키텍처의 종류에 따라 번지수 입력 순서가 달라져야 하는데 지금은 보이는 순서대로 두 자리씩 끊어서 디버깅을 실행시켰다. 하지만 맨 아래 결과가 바로 'Segmentaion fault'로 오버플로우 에러 결과(crash)만 뜨고, 예상할 수 있는 overflowed() 함수의 내부 루틴 실행 결과인 'Execution Hijacked.'는 출력되지 않았다.
그러면 overflowed() 함수가 제대로 호출되지 않았다는 것을 짐작할 수 있고 위의 화면에서 'x/16xw $rsp' 명령으로 확인한 복귀 주소에는 '0x00400564'가 아닌, '0x00640540'가 덮어져 있음을 볼 수 있다. 이 두 16진수 문자를 자세히 뜯어보니, 단지 데이터의 각 두 자리의 숫자가 뒤바뀌어 있다는 것을 볼 수 있다. 이것을 리틀 엔디안(LSB)* 방식이라고 부르는데, 인텔 아키텍처(IA)-32를 따르는 운영체제인 경우는 이 방식대로 16진수 표기를 해야 컴파일할 때 우리가 생각하는 순서대로 컴퓨터가 알아먹는다. 쉽게 말해 순서대로 입력한 방식은 '빅 엔디안'을 따르는 운영체제에서 먹히고 지금은 IA-32 아키텍처를 따르는 리눅스 우분투이기 때문에 '리틀 엔디안' 방식대로 입력해야 한다.
리틀 엔디안 방식으로 디버깅 명령을 입력하면, run $(perl -e 'print "A" x 24 . "\x64\x05\x40\~\x00"')이다. continue 명령으로 마지막 종단점 이후 프로그램 디버깅 실행을 마치자, 호출되었던 overflowed() 함수 내부의 출력문 결과가 떴다. 이제 실행을 가로챘다. 즉 %EIP 레지스터의 제어권을 조종할 수 있게 되었고 실제 해커들은 여기에 악성코드 주소로 덮어써서 이후 목표(관리자 권한 획득)로 하는 공격을 전개할 수 있다.
디버거 외의 리눅스 커맨드 라인 단에서 결과를 보려면 다음과 같이 조작한 RETurn 주소('0x00400564')를 포함하는 문자열을 인자로 하여 overflowtest를 실행하면 된다.
overflowed() 함수가 실행되고 나면 본래는 main() 함수 내부에서 다음 루틴을 실행하는 것이 본래의 프로그램 로직인데 조작한 리턴 주소 다음의 스택에 나오는 4 바이트를 실행하려다 'Illegal instuction'이라는 메시지와 함께 프로그램 실행이 멈춘다. 스택 세그먼테이션 단에서는 overflowed() 함수가 스택에서 호출된 다음에 '0x00000000'라는 메모리 데이터가 조작한 리턴 주소 바로 다음에 팝 업(pop up)된다.
원리만 알면 일단 프로그램이 공격자가 의도한 대로 실행할 수 있도록 속일 수 있는 과정은 간단하지 않은가?
이번 섹션까지는 C 언어를 이용하여(안전하지 않은 strcpy 이라는 문자열 복사 함수를 사용함으로써) 배열의 끝을 확인하지 않는 오버 플로 취약점으로 인해 인접한 메모리 영역을 덮어쓸 수 있는 원리에 대해 실습해봤다. 이 취약점의 핵심은 프로그램이 기대하는 것보다 더 긴 문자열을 커맨드 라인으로 입력함으로써 시스템 취약점을 공략한다는 것이다. 여기서 함수의 리턴 주소를 임의대로 한 입력값으로 덮어써서 프로그램의 실행을 가로챘다. 또한 원본 프로그램 안에 포함된 다른 함수(overflowed)를 실행시킬 수 있었다(조지 와이드만, 2016).
다음 새로운 섹션 편부터는 버퍼 오버플로 버그의 원리를 통해 윈도 SEH(Structured Exception Handling) 단에서 제어권을 가로채는 공격을 시도해보겠다. 그전에 공격을 전개하기 위한 백도어(backdoor), 익스플로잇(exploit) 등 파이썬 코드를 통해 윈도 운영체제에서도 가능한 여러 가지 공격 루트를 알아보겠다.
* 리틀 엔디안(LSB, Least Significant Bit) : 가장 낮은 자리가 먼저 저장되어야 한다는데 반해, 빅 엔디안(MSB, Most Significant Bit)은 가장 높은 자리부터 저장되어야 한다. 각 방식을 따르는 CPU 아키텍처의 종류가 다르고 운영체제 역시 각 CPU 아키텍처에 맞춰서 저장되기 때문에 우분투 리눅스가 아닌 OS인 경우는 빅 엔디안인지 리틀 엔디안인지 확인해야 한다.
참조 서적 원문
Weidman, G. (2014). Penetration testing: A hands-on introduction to hacking. San Francisco: No Starch Press.