Lord of the BufferOverflow Fedora Core3[ Level4_Hell_fire -> Evil_wizard ]
이 글에는 PLT/GOT에 대한 간단한 설명과 ROP에 대한 설명이 포함되어 있습니다.
|
이번 문제부터는 드디어 ROP를 이용한 풀이를 진행합니다. 개인적으로 말로만 듣던 ROP에 대한 환상을 가지고 있었기에 소스를 보고 들뜬 마음으로 풀이하였습니다.. 하하
그럼 시작합니다. 문제부터 보도록 하죠.
지금까지의 FC3 문제에서는 ppr gadget이 없었기 때문에 어떤 식으로 ROP를 진행 할 지 매우 궁금했었는데, 아예 함수에 ppr gadget을 박아주는군요.. 이런식으로 가젯을 넘겨 줄 것이라고는 생각도 못했습니다..
코드 중간에는 setreuid, setregid를 이용하여 권한을 주는 것을 보니 마지막엔 system 함수만 호출해주면 끝날 것 같습니다.
그럼 ROP에 대한 설명부터 간단하게 하고 진행하겠습니다.
ROP란 Return Oriented Programming에 대한 약자로, RTL이 라이브러리 단으로 넘어가서 실행을 한다면 ROP는 말 그대로 프로그래밍 단으로 넘어가서 실행을 합니다. 프로그램을 실행하기 위해서는 컴파일을 통해 프로그래밍 언어에서 어셈블리어를 거쳐 기계어로 변환시켜 바이너리를 만들어야 합니다. 이 과정 중 어셈블리어로 작성되는 코드는 흔히 16진수로 나타내는데, 이러한 16진수들을 특정한 규칙을 정해 명령어(operation)로 사용한다는 것을 우리 모두 알고 있을 것입니다. 이 특정 명령어들 중 일부를 가져다 씀으로써 프로그램을 루틴을 변형시켜 BOF를 하는 기법이 바로 ROP입니다. 또한, ROP는 Chaining RTL Calls라고도 불리웁니다. 이유는 아래쪽에서 다시 설명하도록 하겠습니다.
자, 그럼 이 문제에서 ROP를 할 때 필요한 것은 무엇이 있는지 하나씩 알아보도록 하죠.
1. strcpy 함수의 PLT 2. printf 함수의 PLT, GOT
3. system 함수의 주소
4. '/bin/sh'의 주소 5. pop-pop-ret gadget |
ROP를 위해서는 위와 같이 5가지의 정보가 필요합니다. PLT와 GOT에 관한 정보는 LOB redhat 과정을 거칠 때 작성한 문서에 기록을 해 두었으나, 현재 블로그에는 업로드 하지 않았기 때문에 간단하게 설명하도록 하겠습니다.
리눅스 환경의 프로그램에서는 함수를 호출 할 때 libc에서 가져온다는 것을 알고 계실겁니다. 하지만 프로그램은 우리가 원하는 함수를 libc에서 바로 가져오지는 못합니다. 그래서 처음 함수를 만났을 때 그 함수의 PLT에서 함수의 실제 주소를 알아내기 위한 작업을 실행합니다.
위의 그림과 같은 형태로 진행된다고 볼 수 있습니다.
함수를 처음 만나게 되면 그 함수의 PLT에서 GOT를 참조하는데, 이 때 GOT에 있는 값은 PLT에서 GOT로 jmp하는 코드 다음에 있는 operation입니다. 여기서 특정한 값을 push 해주고 PLT의 시작 지점으로 다시 jmp합니다. 이 과정은 _dl_runtime_resolve라는 함수의 과정입니다. 이 과정을 거치고 나면 GOT의 값이 바뀌게 되는데, 이 때 바뀐 GOT의 값이 바로 라이브러리에 있는 실제 함수의 주소입니다. 이러한 과정을 통해 함수의 진짜 주소를 찾아내는거죠.
하지만, 여기서 함수를 처음 만났을 때 _dl_runtime_resolve 함수를 진행하지 않고 바로 GOT에 함수의 주소가 들어 있다면 어떨까요?
그렇습니다. 그 GOT에 들어 있는 주소(함수)를 바로 실행하게 됩니다. GOT를 덮어씌워 다른 함수로 return 시키는것, 이게 바로 GOT Overwrite 기법입니다.
여기서 궁금할 수가 있는데, PLT를 바로 Overwrite 시키면 되지 왜 굳이 GOT를 Overwrite 시키느냐? 라는 질문이 나올 수가 있습니다.
저도 정확히 기억은 안나는데, PLT는 write 권한이 없고 GOT에만 write 권한이 있기 때문이었던 것 같습니다. 자세한 내용을 알고 싶다면 구글링!
자, ROP는 이 GOT Overwrite를 통해 이루어집니다. 이 과정에서 필요한 또 다른 기법이 RTL이구요. 지금까지 설명한 PLT와 GOT의 개념을 토대로 필요한 정보를 찾아내도록 하죠.
먼저, ppr(pop-pop-ret) 가젯의 주소를 알아보겠습니다.
objdump를 이용하여 ppr 가젯은 0x804854f로 확인하였습니다.
ppr gadget : 0x804854f
다음으로, strcpy의 plt입니다.
strcpy@plt : 0x8048494
다음은 system 함수의 주소입니다.
system addr : 0x7507c0
다음으로, '/bin/sh'의 위치를 찾는 find.c 소스입니다. system 함수 내부적으로 /bin/sh를 실행하므로 그 곳에 있는 /bin/sh를 찾는 원리입니다.
/bin/sh : 0x833603
이제, 위에서 설명했던 PLT와 GOT를 살펴 볼 차례입니다.
printf@plt : 0x8048424
이 printf를 GOT Overwriting을 이용하여 system 함수로 바꾸기 위해 GOT의 주소를 구해줍니다.
여기서 주의 깊게 살펴 볼 점은, PLT/GOT 설명 할 때 언급했던 _dl_runtime_resolve입니다. 위 그림에서는 1-2-3-4의 순서로 진행이 되는데, 1번의 주소가 바로 PLT의 GOT이고, 이 처음의 GOT는 3번인 0X804842a를 가리키고 있습니다. 그리고 0x18을 push 한 후 PLT 의 도입 부분인 0x80483e4로 점프하면서 GOT의 주소가 printf의 실제 주소로 바뀌는거죠. 하지만 우리는 이 printf의 GOT를 system 함수의 주소로 overwrite 하여 printf 함수를 호출하면 system 함수로 넘어가게끔 만들어야 합니다.
여기서 필요한 개념이 RTL입니다. RTL을 이용하여 strcpy 함수를 불러내서 GOT의 바이트 하나 하나씩 다른 값으로 변경시켜주는것입니다. 이 작업에서의 핵심은 RTL 시 return address 다음 4byte인 '이전 함수를 마친 이후의 return address'입니다. 이 위치에 바로 ppr gadget을 넣어준다면 어떻게 될까요?
pop 명령어는 스택의 esp 부분에 있는 값을 꺼내어 해당 레지스터에 저장 한후 esp를 +4만큼 이동시키죠. 이 작업이 2번 일어나기 때문에 esp는 8byte 늘어나게 됩니다. 그 후 ret 명령어가 있기 때문에 pop eip;jmp eip 로 인해 다른 함수를 연속적으로 호출 할 수 있게 됩니다. 이 작업을 두고 Chaining RTL Calls라고 부릅니다.
자, 그럼 ROP에 대한 기본적인 지식은 모두 설명하였습니다. 이제 나머지 필요한 정보를 조금 더 모은 뒤에 payload를 설명해보도록 하죠.
위 그림을 통해 system 함수의 각 byte가 메모리 상 어디에 존재하는 지 파악하였습니다.
c0 : 0x8048420
07 : 0x8048154
75 : 0x80482c8
00 : 0x8049840
지금까지 설명한 내용을 종합해 보도록 하죠.
printf 함수의 GOT를 strcpy 함수를 이용하여 한 바이트 씩 system 함수의 주소로 바꿔줍니다.
그리고 printf 함수를 호출하면 system 함수가 실행되겠죠.
실행 할 때 파라미터로 /bin/sh의 주소를 넣어줍니다.
짜잔! ROP가 완성되었습니다!
payload :
A*268 strcpy ppr printf@got+0 0x8048420
strcpy ppr printf@got+1 0x8048154
strcpy ppr printf@got+2 0x80482c8
strcpy ppr printf@got+3 0x8049840
printf@plt AAAA /bin/sh
위와 같이 payload를 작성하고 실행시켜주면 system 함수가 실행되면서 shell을 획득 할 수 있습니다.
References
- Return_Oriented_Programming_basics_cd80 (cd80)