상세 컨텐츠

본문 제목

0x05 - RTL Chaining & ROP (테스트용)

Linux System_BOF/x86

by REDTEAM 2020. 5. 25. 16:30

본문

이번 시간부터는 지난 포스팅에서 진행된 환경보다 비교적 최신의 환경에서 진행 해보겠습니다.

지금까지 알아본 Stack관련 내용은 모두 오늘 포스팅을 위해 알아본것입니다. 그럼 먼저 이번에는 RTL Chaining에 대해 알아 보겠습니다. RTL Chaining은 원하는 라이브러리 함수를 여러번 호출하는 기법입니다. 어떻게 여러번 호출 하느냐 전에 Gadget 이라는 개념을 알아야 합니다.

Gadget 의 사전 의미는 "(작고 유용한) 도구" 의미입니다만, 우리가 다루는 bof에서의 GadgetRET로 끝나는 연속된 함수를 지칭 합니다. 대표적으로 POP POP RET(PPR)를 사용합니다. PPR의 용도는 "함수에 사용된 인자들을 정리하기 위해 사용되는 것" 으로 알아두시면 됩니다.(POP의 역할을 생각해보시면 쉽습니다!)

그럼 어떻게 PPR을 사용해서 원하는 행위를 유도하는지 알아 보도록 하죠. 위에서 RTL을 설명드릴 때 RET 구역에 system()의 주소가 위치하고 바로 이어서 무의미한 값을 채웠습니다. 그 다음 /bin/sh 의 주소를 위치 시켰었죠. 지금은 RET구역 다음에 위치하던 무의미한 값을 채우는것이 아닌 Gadget을 위치시키도록 합니다. 이 때 RET에 위치하는 함수가 사용하는 인자의 갯수에 맞춰 PPR의 모양이 달라집니다.

함수가 사용하는 인자가 2개라면 PPR, 1개라면 PR, 5개라면 PPPPPR이 됩니다. 우리는 system() 함수를 사용하며 이 친구는 1개의 인자를 사용합니다. 따라서 PR(POP RET)이 되겠습니다.

함수가 동작 되면 POP으로 함수의 인자를 정리하고 RET에 원하는 라이브러리 함수를 위치시켜서 원하는 함수로 다시 호출하는 방식입니다. 이것을 반복하면 원하는 함수를 여러번 계속 해서 호출이 가능해 집니다. 조금 쉽게 표현 하자면 학교 끝나고 에만 갔었는데(RTL) 이제는 학교 끝나고 편의점도 들리고 PC방도 들리고 데이트도 하고나서 에 도착하는 행위가 가능해진 겁니다.(RTL Chaining)

즉, 공격자의 입맛대로 공격 페이로드를 디잔인할 수 있는 것이죠. 그림으로 비교 해보겠습니다.

위의 그림이 앞 포스팅에서 함께 봤던 일반적인 RTL입니다. execl()을 실행 시켜서 4byte 뒤에 위치시켜둔 인자를 받아내고 RET 위치위 AAAA로 이동 후 error를 발생 시키고 종료 합니다.(error 발생이 싫으신 분들은 exit() 함수를 위치 시켜주시면 종료 됩니다.) 이 작업을 여러번 chain처럼 연결 시켜 연속적으로 RTL을 사용하는 RTL Chaining이 완성 됩니다. 다음은 RTL Chaining을 보도록 하겠습니다.

RTL Chaining

쉬운 이해를 위해서 함수를 단순화 했습니다. func1()함수는 두 개의 인자를, func2()는 한 개의 인자를 사용합니다. 따라서 func1()이 끝나고 func2()로 이어기 위해선 PPR이 필요합니다.

func1()이 종료되면 RET 위치에 위치한 PPR동작합니다. POP POP 작업으로 두개의 인자가 정리되고 ret가 수행되며 func2()로 넘어갑니다. func2()의 동작이 끝나고 ret가 수행될 것입니다.

RTL Chaining은 이런식으로 ret 영역에 PPR(함수 인자수에 맞춰서)을 위치 시켜 원하는 함수를 계속적으로 추가 수행 하도록 하는 방식입니다.

결과적으로 공격자는 RTL Chaining을 이용해서 필요한 함수를 원하는 만큼 계속해서 활용 가능합니다. 아래쪽 내용을 보시면 어떤 의미인지 조금더 확실하게 아실수 있습니다.

다음으로 PLTGOT라는것을 알아보겠습니다.

PLT & GOT

PLTProcedure Linkage Table의 약자로 외부 프로시저를 연결해주는 테이블 입니다. 이놈을 이용해 다른 라이브러리의 프로시저를 호출합니다.

GOTGlobal Offset Table의 약자로 PLT가 참조하는 테이블입니다. 프로시저들의 실제 주소가 있습니다.

그림에서 보는 과정처럼 함수를 처음 호출 할 경우 PLT에서 GOT를 참조하며 GOT는 호출된 함수의 실제 주소를 찾기위해 여러 과정을 거칩니다.(PLT > GOT > resolve > fixup > symbol > fixup > resolve > return) 최종적으로 resolve에서 호출 함수의 주소 리턴하며 실행 됩니다.

위 그림에서 처럼 함수가 한 번 호출 되고 나면 GOT에 해당 함수의 주소가 저장 됩니다. 그 후에 다시 해당 함수를 호출할 경우 첫 함수 호출 때 처럼 복잡한 과정은 생략되고 바로 GOT에 저장된 주소를 참조하여 호출 됩니다.

네비게이션에서 목적지의 경로를 찾기 위해서 여러 검색 과정을 거치는 과정이 함수의 첫 호출 과정이고 같은 목적지의 경로를 다시 갈 경우 복잡하게 목적지를 등록할 필요 없이 이미 저장되어있는 목적지로 바로 안내가 시작 되는 것을 함수 재호출 과정이라 생각하시면 될 것 같습니다.

그런데 왜 PLT와 GOT를 알아봤을까요???

공격자가 GOT에 저장된 주소를 특정 주소로 바꾼다면???GOT에서 호출되는 주소는 호출 함수의 주소가 아닌 지정된 임의의 주소로 호출이 가능하게 됩니다.

위 그림처럼 표현 해봤습니다. GOT Overwrite 라는 기법입니다. 그림에서 보이듯 GOT에 공격자가 원하는 함수의 주소를 저장 시켜놓고 해당 함수를 호출하여 동작하게 되는 시나리오가 그려집니다.

이제는 대충 뭘 어떻게 해야할지 그림이 그려질 것이라 믿고 ROP를 진행 하겠습니다.

(bof.c 의 코드는 변경 없이 그대로 진행됩니다.)

ROP(Return Oriented Programming)

  • buffer 크기 :
  • strcpy() 주소 :
  • execve() 주소 :
  • PPR 주소 :
  • .BSS 주소 :
  • puts@plt 주소 :
  • puts@got 주소 :

먼저 ROP진행을 위해 위 내용들이 필요 합니다. 그런데 puts()라는 처음 보는 아이가 보이네요. 저 아이는 printf() 함수와 같은 문자열 출력 역할을 합니다. 약간의 차이는 있습니다만 쉽게 생각해 printf()보다 단순화(한 개의 인자만 처리) 및 빠른 처리가 가능한 문자열 출력 함수라 생각하시면 됩니다. 그런데 왜 puts() 저놈이 보이느냐면 상위 버전의 linux 환경에서는 한 개의 인자만 사용해서 printf()를 사용할 경우 알아서 더 효율적인 puts()함수로 사용 하게 됩니다.

어찌됫건 우리는 저 puts()의 got 주소를 원하는 함수로 바꿔야 합니다.(printf()이건 puts()건 결과는 같습니다.)

먼저 사용할 함수의 주소를 찾아보도록 하겠습니다. 전 포스트를 참고하셔서 execve()주소를 아래처럼 찾아 주시면 됩니다.

그리고 strcpy()puts@pltputs@got도 다음과 같이 뽑아줍시다.

다음은 리눅스 기본 툴인 objdump를 사용하여 PPR 의 주소를 찾아보겠습니다. 명령어는 다음중 아무거나 사용하셔도 상관 없습니다. 결과는 같습니다.

objdump -d bof1 | grep -B 4 ret

objdump -d bof | grep "pop" -A3

strcpy()는 두 개의 인자를 사용하니 PPR의 주소를 챙겨 둡니다.

다음은 .bss의 주소를 찾아 보겠습니다. .bss는 첫 포스팅에서 메모리 구조를 표현할때 나왔던 녀석입니다. 데이터 세그먼트의 한 부분이며 사용자에게 쓰기 권한이 있고 주소값이 변하지 않는 영역입니다.(초기화되지 않은 변수들이 저장되는 전역변수 영역) 쉽게 말해 공격자에게 사랑받는 영역입니다.(쓰기 가능/고정 주소) .bss는 gdb 내에서 info files 명령어를 사용하여 찾아줍니다.

.bss의 시작 주소는 0x0804a020입니다만 겹침을 방지하기 위해 16byte 정도 뒤의 주소를 사용합니다. 따라서 16byte를 더한 주소값인 0x0804a030이 되겠습니다.

이제 마지막으로 Buffer의 사이즈를 확인 해보겠습니다.

이전 포스팅까지는 해당 위치의 EBP+ 값으로 버퍼의 크기를 확인 가능했지만 지금은 ESP가 자리 잡고 있으며 다른 방법으로 버퍼의 크기를 구해야 합니다.

먼저 버퍼의 크기가 할당된 다음인 +25 라인에 브레이크를 걸어주고 실행 시키킵니다. 그 후에 EBP에서 EAX의 값을 빼주면 해당 값이 버퍼의 크기가 됩니다. 프로그램을 run 시켜주고 브레이크에 잡혀있는 main+25의 위치에서 EBPEAX를 확인후 앞서 말헀듯 EBP - EAX를 해줍시다

0xbf96a168 - 0xbf96a14c = 0x1c입니다. 1c28이며 잉여값 4를 플러스 해 줍니다. 최종적으로 Buffer의 크기는 28 + 4하여 32byte가 되겠습니다.

그럼 지금까지 찾은 정보들을 확인해보겟습니다.

  • buffer 크기 : 32byte
  • strcpy() 주소 : 0x08048310
  • execve() 주소 : 0x43814550
  • PPR 주소 : 0x080484fe
  • .BSS 주소 : 0x0804a030
  • puts@plt 주소 : 0x08048320
  • puts@got 주소 : 0x0804a010
  • /tmp/sh\x00 주소 :

이제 필요한 정보들을 어느정도 모았습니다. 이 정보들을 활용해서 shell을 따도록 합시다.

먼저 data영역의 puts@gotexecve() 함수의 주소를 저장해줘야 합니다. execve()의 주소는 0x43814550이지만 그대로 저장시키면 안되며 각 주소의 4byte씩을 나눠서 저장해야 합니다. 즉 0x 43 81 45 50이런식으로 잘라서 각각 4byte의 주소값을 별도로 구해야 합니다.

무슨 개똥같은 말이냐면 \x43이라는 문자열의 주소 \x81이라는 문자열의 주소와 같이 각 자리를 문자열로 취급하여 해당 문자열의 주소값을 찾아야 하는겁니다. 같은 방식으로 "/tmp/sh\x00"도 각각의 문자열의 주소를 찾아야 하니 참고하시면 됩니다.('/', 't', 'm', 'p', 's', 'h', '\x00' 과 같이 구해야 합니다......)

앞에서 info files 에서 나왔던 첫 주소값부터 .bss 주소값까지를 사용하셔도 되고 /proc/self/maps의 주소값을 찾으셔도 됩니다. 주의점은 위 그림처럼 나오는 주소중에 똑바로 안되는 주소도 있으며 오른쪽에 추가적인 내용이 없는 주소를 사용 하시면 됩니다.(저는 info files로 확인한 주소를 사용했습니다.)

또한 execve()의 주소값을 puts@got에 저장하기 위해 페이로드 작성시 기존의 리틀엔디안으로 입력했던 방식과는 다르게 이번엔 두 번 뒤집어주면 됩니다. 리틀엔디언의 리틀엔디언으로 입력해주시면 됩니다. 쉽게말해 execve()의 주소값인 0x43814550의 각 4byte 주소값을 입력할 때는 다시 역순으로 해주시면 됩니다. 50의 주소를 먼저 다음은 45 81 43 순으로 입력하는것이죠. 다시 말하지만 페이로드 작성할 때를 말씀드리는 겁니다. 계속 해서 각각의 주소값을 구하면 다음과 같습니다.

puts@gotexecve()의 주소를 저장하기 위한 준비가 끝났습니다. 실제로 해당 got 위치에 execve()의 주소가 박히는지 확인해 보겠습니다.

우선 페이로드는 위 그림처럼 구성을 하시면 되겠습니다. 위에서 설명했던 RTL Chaining 을 활용해서 첫 strcpy가 끝나면 다음 strcpy로 넘어가고, 두 번째 수행이 끝나면 세 번째 strcpy 함수로 쭉쭉 진행하게 됩니다. 진행하면서 puts@got[0]~[3]까지 공간에 execve() 함수의 주소값을 의미하는 문자열의 주소값이 저장됩니다. 다시 조금더 보기 편하게 각 칸에 주소값을 입력해보겠습니다.

  • buffer 크기 : *32byte ("AAAA"8)
  • strcpy() 주소 : 0x08048310
  • PPR 주소 : 0x080484fe
  • puts@got 주소 : 0x0804a010
  • execve() 주소 : 0x43814550 (0x08048018, 0x08048001, 0x0804805d, 0x08048277)

페이로드는 이렇게 구성 되겠습니다. 직접 때려보고 gotexecve()의 주소값 0x43814550이 잘 박혀있나 확인 해보겠습니다.

puts@got시작 주소인 [0] 주소를 까보면 execve()의 주소값인 0x43814550이 잘 박혀있는것을 확인할 수 있습니다.

이제 70%정도 끝났습니다. 계속해서 나머지 30% 채워서 root shell까지 함께 따보겠습니다.

이번엔 .bss영역에 이전 포스팅에서 작성했던 setuid를 세팅해주는 스크립트의 경로와 마지막 null값까지 포함한 '/tmp/sh\x00'의 각 문자의 주소를 따보도록 하겠습니다. 주소를 따는 방법은 위에서와 동일 하지만 리틀엔디안의 리틀엔디안같은 똥같은 짓은 안하셔도 됩니다. 기존에 하던 방식처럼 리틀엔디언을 한 번만 생각해주시면 됩니다.

각 문자열의 주소값은 위 그림과 같습니다. 다시 말씀드리지만 주소값 찾는 방법은 위에서 찾은 방법과 동일합니다. 다만, 제가 찾은 문자열의 주소와 다를 경우 안되는 경우도 있습니다. 이런 노가다성 작업을 하고서는 최종 페이로드 작성하고 공격했는데 실해하면 상당히 멘붕입니다. 별다른 단서가 없기에 다시 노가다 하는 방법밖에 없죠....(툴이 있긴 합니다. 편하고 편한....) 저와 같은 삽질은 없으시길 바랍니다.(연속된 삽질로 숙련된 삽퍼가 되긴 합니다. 의도하지 않은 삽질로 이것저것 이해가 되는....비추입니다.)

페이로드는 위와 같이 구성되며 마찬가지로 각 주소값을 입력하면 다음과 같습니다.

  • buffer 크기 : *32byte ("AAAA"8)
  • strcpy() 주소 : 0x08048310
  • PPR 주소 : 0x080484fe
  • .BSS 주소 : 0x0804a030
  • /tmp/sh\x00 주소 : 0x08048154, 0x080480f6, 0x0804825f, 0x0804824a, 0x08048154, 0x08048162, 0x080480d8, 0x08048007

실제 페이로드는 위와 같으며 이것도 마찬가지로 /tmp/sh\x00이 잘 박혀있나 확인 해봅시다.

.bss의 첫 추소를 검색해보면.....나이스ㅠ.....\x00은 null로 인식되어 우리가 원하는 /tmp/sh 문자열이 잘 박혀있습니다.

이제 99%입니다. 1% 채우고 개...아니 root shell 따고 종료 합시다!!!!

이제 남은건 puts@plt를 호출하고 execve()의 인자값으로 "/tmp/sh"를 넘겨주면 뙇씌!!!(작업이 끝나감에 신이 났습니다.) 바로 하도록 하겠습니다. 위에서 만든 두개의 페이로드를 합쳐 주시고 제일 뒤쪽에 puts@plt 호출 및 인자 전달을 위한 .bss의 첫 주소를 박아주면 되겠습니다.

전체적인 페이로드는 이런 모습으로 구성 될 것입니다. 제일 아래쪽 puts@plt 라인을 추가하여 puts@plt를 호출하여 puts@got에 저장된 execve()의 주소값을 호출하며 execve()의 인자값으로 .bss[0]의 주소를 넘겨주어 최종적으로 /tmp/sh가 실행 되도록 하는 구성입니다. (execve()를 이용해서 /tmp/sh를 실행시키는 과정입니다.)

  • buffer 크기 : *32byte ("AAAA"8)
  • strcpy() 주소 : 0x08048310
  • PPR 주소 : 0x080484fe
  • puts@got 주소 : 0x0804a010
  • execve() 주소 : 0x43814550 (0x08048018, 0x08048001, 0x0804805d, 0x08048277)
  • .BSS 주소 : 0x0804a030
  • /tmp/sh\x00 주소 : 0x08048154, 0x080480f6, 0x0804825f, 0x0804824a, 0x08048154, 0x08048162, 0x080480d8, 0x08048007
  • puts@plt 주소 : 0x08048320

최종 페이로드 구성입니다. 설명도 위쪽에 이미 전부 했기에 바로 떄리고 확인 해보겠슴돠! (그나저나 알록달록 이쁘게 뽑힌듯 합니다...흐뭇...)

root 계정이 아닌 일반 계정으로 바꾼 후 위의 페이로드를 떄리겠습니다.

짜잔....uid가 개...root로 잡혀있는것을 확인 가능합니다.(Sulla는 그냥 제 닉이라서 제가 좋하는 주황색으로 그어봤습니닿)

지금까지 진행한 내용은 오늘 포스팅한 ROP를 위해 알아야 하는 사전 지식의 개념이었으며 오늘 해본 ROP는 다른 여러 ROP의 기본 개념이 됩니다. 기회가 된다면 다른 ROP도 포스팅 하겠습니다.(믿지마세요.....) 그리고 지금까지 한 Stack BOF는 Heap 공부하기 위한 준비였습니다...

다음 포스팅부터는 모두가 좋아하는 Hip....Heap BOF에 대해 또 천천히 가보도록 하겠습니다.

고생하셨슴돠!!!

References 참고 자료

https://shayete.tistory.com/

https://www.lazenca.net/display/TEC/02.TechNote

'Linux System_BOF > x86' 카테고리의 다른 글

0x00 - Buffer Overflow 시작에 앞서 (테스트용)  (0) 2020.05.25

관련글 더보기