linux_0x04 RTL
RTL(Return to Library)
안녕하세요. Sulla입니다!
이번에 알아볼 내용은 RTL(Return to Library)입니다. 먼저 RTL이 뭔지를 알아보도록 하겠습니다.
RTL(Return to Library)
지난 포스팅에서 메모리 보호 기법에 대해 간단하게 봤습니다. 그 중 ASLR은 봤고 DEP/NX bit는 언급만 하고 넘어갔었죠. 메모리 내부에서 코드실행을 방지하기 위한 메모리 보호 기법 중 하나라고 알려 드렸습니다.
메모리에서의 코드 실행이 막혔기 때문에(Shellcode 실행 차단) 다른 방법을 찾아야 합니다. /bin/sh 를 직접 실행할 수 있다면 가능할 듯 합니다.
그럼 어떻게 해야할까요? 첫 포스팅에서 메모리 구조를 설명 드릴 때 위 그림을 보여 드렸었습니다.[스택 - 공유라이브러리 - 힙 - BSS/Data - Code]으로 이뤄지며, 공유 라이브러리는 메모리에 미리 필요한 중요 함수들을 저장해두는 장소라고 간단하게 설명 드렸었죠. 저 공유 라이브러리를 사용하여 /bin/sh을 실행할 수 있지 않을까???라는 생각으로 RTL이 연구되었다 생각되네요….(뇌피셜)
공유 라이버르러리를 간단하게 정리하자면 다음과 같습니다.
- 여러 프로세스에서 동시에 사용 가능한 라이브러리
- 프로그램이 시작될 때 메모리에 적재됨
우리는 저 공유 라이브러리 중에서 system() 함수를 사용하겠습니다. 해당 함수는 입력받은 문자열을 실행시키는 함수입니다. 즉, 인자값으로 /bin/sh 를 입력한다면 system(“/bin/sh”);과 같은 형태가 되며 system()이 /bin/sh를 실행하게 됩니다.
RTL의 흐름을 위와 같이 표현했습니다. libc(공유라이브러리)의 함수(system())를 사용해서 8byte 뒤에 /bin/sh 의 주소를 박아두면 system()이 /bin/sh 를 실행하여 shell을 따내게 됩니다. 즉, 버퍼 + sfp + system() + AAAA + /bin/sh 처럼 표현 가능할 듯 합니다.
여기서 왜 system() 뒤에 인자값으로 바로 /bin/sh/가 아닌 잉여값 “AAAA“을 입력할까요? 그냥 바로 인자값을 입력하면 인식을 못할까요?
system(“/bin/sh”) 이 입력된 상태에서 함수가 종료되면 에필로그 과정을 가집니다. 에필로그 과정 중 ret(pop eip) 과정을 거치며 system() 함수의 주소를 eip에 옮기게 되죠. 이 과정에 pop 명령을 사용 했기에 스택에 공간이 생깁니다.(가운데) 그런 다음 system() 함수의 프롤로그 과정을 가지며 push ebp, mov ebp, esp 과정을 거치며 새롭게 메모리의 영역을 확보하죠. 즉, AAAA의 위치는 system 함수가 끝난 뒤의 ret 구역이 되며 잉여값을 배치해주고, 다음 공간에 원하는 주소값을 위치시켜 줍니다. 좀더 확실하게 알아보도록 하죠. (참고로 root로 생성된 실행파일은 디버깅이 안되니 /tmp/ 하위에 다른 폴더로 복사해서 분석해주세요!!)
위 사진에서와 같이 system() 내부를 들여다 보면 프로로그 과정을 거치고 EBP에서 8byte 떨어진 주소를 참조합니다. EBP에서 4byte를 가지고 우리는 남은 4byte만(RET) 채워준 뒤 원하는 주소를 입력해준다면 인자값을 정상적으로 인식하게 됩니다.
Return to Library라는 이름을 아주 성실하게 이행하는 모습이네요. 그럼 직접 시작해보곘습니다. 먼저 찾아야 할 주소는 아래와 같습니다.
- system() 주소 = ????
- /bin/sh 주소 = ????
1번 system 주소부터 확인 하도록 하겠습니다.
먼저 브레이크를 main 함수에 잡아주고 구동 시킵니다.bp에 멈추고 나면 print 명령을 사용해 system()의 주소를 확인 하시면 됩니다.(system의 주소를 못 찾는 심볼을 찾을수 없다는 에러가 뜨는 경우 static 옵션을 뺴고 컴파일 해주세요….개고생 했습니다…..)
- system() 주소 = 0x4203f2c0
- /bin/sh 주소 = ????
이제 /bin/sh의 주소를 구해야 합니다. 실제로는 /lib/libc.so.6 라이브러리 파일 내의 /bin/sh와 system 의 오프셋을 구해서 최종적인 문자열 주소를 찾을수 있지만 아래의 반복문을 통해서 쉽게 구해보겠습니다.
01 #include <stdio.h>
02
03 int main(){
04 long shell=0x4203f2c0; //system()의 주소를 입력해 주시면 됩니다.
05 while(memcmp((void*)shell,("/bin/sh"),8))
06 shell++;
07 printf("%p\n",shell);
08 }
코드의 중요 라인인 5라인만 설명 드리자면 memcmp 함수를 사용해서 바이트 데이터를 비교 하는 과정입니다. memcmp(인자1, 인자2, 사이즈)의 기본형을 가지며 인자 1의 첫 바이트와 인자 2의 첫 바이트를 사이즈 만큼 비교하는 것이며, 이 과정을 통해서 system()내부에 /bin/sh 문자열을 찾아내며 결과적으로 주소값으로 리턴 해줍니다. 돌려줍시다~
- system() 주소 = 0x4203f2c0
- /bin/sh 주소 = 0x42127ea4
최종적으로 system() 과 /bin/sh 의 주소값을 찾아냈습니다. 이제 찾아낸 정보를 바탕으로 공격 페이로드를 작성해 보겠습니다.
./bof1 `python -c 'print "AAAA"*11 + "\xc0\xf2\x03\x42" + "AAAA" + "\xa4\x7e\x12\x42"'`
마지막으로 페이로드를 정리 하고 직접 때려보도록 하겠습니다.
먼저 버버 크기(40byte)와 sfp(4byte) 를 채워 주고 공유 라이브러리에 위치한 system() 함수를 저장시킵니다. 다음 위치에는 AAAA의 4byte를 채워 줍니다. 이유는 위에서 알려 드렸듯 system()의 인자값은 8byte 뒤에 위치하기에 그 거리만큼 벌려 주고 우리가 필요로 하는 /bin/sh을 위치 시켜 줌으로서 system()이 인자값으로 /bin/sh을 정상적으로 받을수 있게 설계해준겁니다.
설명은 이쯤 해두고 직접 떄려보도록 하겠습니다.
shell이 떨어지네요. 지금 한 방식처럼 system() 내부의 /bin/sh 문자열을 찾는 방법도 있고 전 포스팅에서 했던 환경변수를 이용 하는 방법도 있습니다. 빠르게 보고 넘어가죠.
위에서 설명 드렸듯 bof라는 환경변수에 /bin/sh 문자열을 저장하고 해당 환경변수의 주소를 확인합니다. 아래쪽은 bof 환경변수의 동작 여부를 확인 했습니다.
system()의 주소는 알고 있으니 환경변수의 주소만 추가해서 바로 페이로드를 작성하고 떄려봅시다
shell은 마찬가지로 잘 떨어집니다만…우리가 원하는 root 권한이 아니라서 아쉽습니다…..ㅠ 아쉬움을 달래봅시다.
우선 system()의 근본적인 문제를 알아 봅시다.
system()의 경우 내부적으로 /bin/sh -c argument를 실행합니다. 따라서 /bin/sh -c /bin/sh 로 처리가 되며, root 권한을 얻는것은 system()의 내부에서 /bin/sh를 실행 후 다시 /bin/sh을 실행하기에 불가능합니다. 따라서 setuid()함수 를 0으로 셋팅해주고 system()가 실행되도록 하여 미리 실행전에 root 권한으로 준비시킨 후 system()을 실행시킨다면 가능하며 system() 함수의 주소가 아닌 다른 함수가 필요한데 그것이 execl()함수입니다. 먼저 setuid()를 셋팅해 줄 소스부터 작성 해보겠습니다.
01 #include <stdio.h>
02
03 int main(){
04 setuid(0);
05 system("/bin/sh");
06 return 0;
07 }
해당 파일을 컴파일해주시고 마찬가지로 환경변수에 저장 해줍니다. execl()의 주소도 위에서 system() 주소값을 찾았던 방식으로 찾아주시면 됩니다.
- execl() 주소 = 0x420acaa0
- 새로 작성한 /tmp/bof1/rootsh 주소 = ??
이제 페이로드를 짜야 하는데 한가지 주의점은 execl()은 끝에 null로 끝나야 합니다. execl(const char *path, const char *arg0, const char *arg1, const char *arg2,…const char *argn, (char *)0);의 형태로 구성 되있는데 조금더 보기 쉽게 하자면 execl(경로, 인자1, 인자2….인자n, null) 이라고 이해하시면 됩니다. 마지막은 인자의 끝을 의미로 null값이 위치하게 됩니다.
./bof1 `python -c 'print "AAAA"*11 + "execl주소" + "AAAA" + "rootsh 주소"'`
공격 페이로드는 위와 같이 구성 될것입니다.
페이로드 전에 execl()을 사용하기 위해 메모리 상태를 봐야합니다. 이유는 끝이 null값으로 끝나는 적당한 지점을 골라야 하기 때문입니다. 직접 보는게 이해하기 빠를겁니다.
위에서 그림과 같이 BBBB는 execl()의 주소가 되며(파란색) CCCC는 불필요한 값이며(초록색) DDDD가 새로 작성한 rootsh의 주소(노란색)가 됩니다. 위에서 execl()의 인자값의 형태를 봤습니다. 경로 + 인자1 + 인자2 +인자n + NULL의 형태를 띄우며 DDDD 하나는 경로에 들어가며 DDDD가 하나더 들어가면 인자 1에 들어갑니다. 그렇다는 것은 마지막이 마지막으로 DDDD의 뒤의 한 바이트는 NULL이 되어야 4바이트가 전부 NULL로 자리를 잡게 됩니다. 다음 그림과 위 그림의 하늘색 박스의 1byte값인 2c를 확인해보면 무슨 말인지 이해 되실거라 생각 됩니다.
상단에 입력한 페이로드를 보면 DDDD를 2 했으며 아래쪽에 하늘색 박스가 2c가 아닌 00으로 바뀌었음을 확인 간으합니다. 즉 DDDD DDDD가 들어간 다음 1byte는 NULL로 바꾸게 됩니다. 이제 DDDD3을 입력하여 우리가 원하는 0xbffffe4a0(보라색)의 값이 바뀌는지 확인하도록 합시다.
우리가 원하는 모양으로 메모리 값이 바뀌는걸 확인 했으니 페이로드를 다시 정리 해보겠습니다.
./bof1 "`python -c 'print "AAAA"*11 + "\xa0\xca\x0a\x42" + "AAAA" + "rootsh 주소"*3'`"
이제 rootsh 의 주소를 알아내고 *3만 붙여주면 될 듯 합니다. 고지가 보이네요.
위에서 작성하고 컴파일한 rootsh를 주소가 변하지않는 값을 찾아서 심볼릭 링크를 걸어줘야 합니다. 변하지 않는 곳은 Data segment 영역입니다.
Data segment 영역의 주소는 0x08049000 부터 시작합니다. 해당 주소를 dbg를 이용해서 열어보도록 하겠습니다.
0x08049014 주소의 값이 0x01(=0x00000001)을 갖고있습니다. 다른 값을 쓰셔도 됩니다만 입력에 있어서 편안한 간단한 값을 선택 합니다. 0x08049014 = 0x01 이라는것을 알아 두시면 됩니다. 이제 rootsh 코드와 0x01의 주소를 심볼릭 링크 시켜줍니다.
자 이제 준비가 끝났습니다. 위에서 찾아본 내용을 바탕으로 페이로드를 재구성 해보겠습니다.
- execl() 주소 = 0x420acaa0
- /tmp/bof1/rootsh 심볼릭 링크 주소 = 0x08049014
[AAAA(44byte)] + [execl() 주소] + [잉여값(4byte)] + [rootsh sl 주소] 과 구성되며 실제 공격 페이로드는 아래와 같습니다.(공격 페이로드는 root 권한의 파일을 대상으로 확인하셔야 합니다.)
../bof/bof1 "`python -c 'print "AAAA"*11 + "\xa0\xca\x0a\x42" + "AAAA"
+ "\x14\x90\x04\x08"*3'`"
이때 주의할 점은 페이로드 전체를 ““로 감싸야 합니다. \x0a를 \x00으로 인식하는 문제가 있어서 “python -c 'print .......'
” 이런식로 감싸주셔야 똑바로 값이 들어갑니다. 꼭 ““으로 감싸주세요!
짜잔….개…root 권한으로 shell을 땄습니다. 이번에도 역시나 간단하게 하고 싶었는데 고생도 하느라 이것저것 실수 했던것들 다 담느라 많이 길어졌네요…..ㅂㄷㅂㄷ….
RTL이란 기법은 나중에 알아볼 ROP를 위한 초석입니다. 꼭 잘 숙지 해주셔야 합니다….. :(
다음 포스팅은 Chaining RTL이란 것을 정말…간단히 볼 수 있도록 해보겠습니다.
뿅!