RTL 공격기법 원리 이해하기 예제 ( Omega Project )

2017. 11. 17. 13:20SystemHacking/LOB(BOF원정대)




RTL ( Return To Library )

스택에 실행권한을 없애 스택에 쉘 코드를 올리는 것을 막는 오버플로우 대응책을 우회할 수 있는 기법

간단히 말하면 프로세스의 RET 주소를 다른 함수의 주소로 변조시키면 해당 함수로 Jmp되어 해당 함수가 실행 된다

오메가 프로젝트에서 발전된 기법이다 예제를 통해서 알아보자


간단한 C프로그램을 하나 작성하습니다 ( #vi omega.c )

간단한 printf 함수를 호출하는 func1, func2, func3, func4 함수들을 선언했고, main함수에는 버퍼오버플로우 취약점이 존재한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
void func1()
{
        printf("Call Func1\n");
}
void func2()
{
        printf("Call Func2\n");
}
void func3()
{
        printf("Call Func3\n");
}
void func4()
{
        printf("Call Func4\n");
}
 
int main( int argc, char* argv[] )
{
        char buffer[40];
        if ( argc >= 2 ){
                strcpy( buffer, argv[1] );
        }
 
        printf("Call Main\n");
        return 0;
}
cs


< 프로그램 어셈블리 코드 >

0x8048450 <main>:       push   %ebp

0x8048451 <main+1>:     mov    %ebp,%esp

0x8048453 <main+3>:     sub    %esp,40

0x8048456 <main+6>:     cmp    DWORD PTR [%ebp+8],1

0x804845a <main+10>:    jle    0x8048471 <main+33>

0x804845c <main+12>:    mov    %eax,DWORD PTR [%ebp+12]

0x804845f <main+15>:    add    %eax,4

0x8048462 <main+18>:    mov    %edx,DWORD PTR [%eax]

0x8048464 <main+20>:    push   %edx

0x8048465 <main+21>:    lea    %eax,[%ebp-40]

0x8048468 <main+24>:    push   %eax

0x8048469 <main+25>:    call   0x8048340 <strcpy>

0x804846e <main+30>:    add    %esp,8

0x8048471 <main+33>:    push   0x8048510

0x8048476 <main+38>:    call   0x8048330 <printf>

0x804847b <main+43>:    add    %esp,4

0x804847e <main+46>:    xor    %eax,%eax

0x8048480 <main+48>:    jmp    0x8048482 <main+50>

0x8048482 <main+50>:    leave

0x8048483 <main+51>:    ret


[ 함수의 에필로그 부분 ]

0x8048482 <main+50>:    leave                     : mov esp, ebp + pop ebp

0x8048483 <main+51>:    ret                        : pop eip

* 함수의 에필로그 부분 leave , ret 을 통해서 스택에 저장해둔 복귀주소와 다음 실행명령어 주소를 이용해 프로세스가 진행된다

* RTL은 ret 명령에서 사용되는 eip 주소의 값을 변조시켜서 프로세스의 실행 과정을 바꾼다

* eip주소를 변조시켜서 main함수의 종료 후 다른 함수를 호출되도록 해보자


[ 예제 1 ]

원리는 간단하다. 우선 해당 프로그램의 스택구조를 살펴보자


[ 스택메모리 구조 ]

 buffer ( 40byte )

 A * 40

 save ebp ( 4byte )

 AAAA

 saved eip ( 4byte )

 func'2 의 시작 주소

 argc ( 4byte )

 

 argv[0] ( 4byte )

 


스택의 구조는 표의 왼쪽 과 같다. strcpy() 함수에서 오버플로우 시켜서 saved eip 주소에 func'2 주소를 넣는 다면 어떻게 될까 ?

( func2' addr : 0x08048414 )


Call Main 다음에 Call Func2 가 보이는가 ?

Main함수의 실행이 종료되고 마지막 ret 부분에서 eip에 저장된 Func2 함수로 이동 하게 된 것이다


* 추가로 알아두면 좋은 것

ebp + 4 => eip

ebp + 8 => 함수의 첫번 째 인자 ( 함수가 인자를 사용하는 경우 )

ebp + 12 => 함수의 두번 째 인자 ( 함수가 인자를 사용하는 경우 )


[ 예제 2 ]

* 추가 설명 

함수에서 함수를 호출하는 과정을 이해하고 있으면 쉽게 이해 할 수 있다

어셈블리에서 함수를 호출 할 때에는 call 명령어를 사용한다

call = push eip + jmp 실행할 함수의 주소 ( 호출하기 전에 다음으로 실행할 명령어의 주소를 스택에 저장해두고 esp를 이동한다 )

실행할 함수의 주소로 이동한다음 해당 함수의 프롤로그 부분에서 push ebp 에 의해 스택에는 복귀할 주소까지 저장해둔다

스택의 구조는 다음과 같다


[ 스택메모리 구조 ]

 saved ebp

 호출 받은 함수의 프롤로그에 의해 푸쉬된 복귀주소

 saved eip

 호출 받은 함수의 종료 후 실행할 다음 명령어 주소

 saved ebp

 호출 하는 함수의 프롤로그 부분에서 푸쉬된 복귀주소

 saved eip

 호출 하는 함수의 프롤로그 부분에서 푸쉬된 다음 명령어 주소 

 argc

 호출 하는 함수의 인자 수
 argv[0] 호출 하는 함수의 첫번 째 인자


함수는 함수의 종료한 후 다음에 실행할 명령어를 push 한 뒤, 복귀주소를 push 한다

스택의 구조는 ebp+4 지점에는 eip가 위치하게 된다. ret 명령어의 pop eip에 의해서 함수는 해당 주소로 이동하고 실행되는 구조이다


그럼 위의 예제를 통해서 해당 개념을 복습해봅시다

( func2' addr : 0x08048414 , func3' addr : 0x08048428 )


[ 예제2 스택메모리 구조 ]

 buffer

 A * 40

 saved ebp

 AAAA

 saved eip

 func'2 addr

 argc

 func'3 addr 

 argv[0]

 X

 argv[1]

 X

 argv[2]

 X


이전 예제에서 설명했던 것처럼 main함수의 eip를 func'2의 주소로 변조시켜서 func2함수가 실행되었다

그러면 func'3 가 실행되는 원리는 무엇인가 ? 위의 추가 설명 부분에서 설명한 call 명령어의 원리를 이해하고 있으면 된다


func'2 로 이동되서 func2 의 프롤로그 부분 push ebp => 스택에 ebp가 푸쉬된다

그럼 스택의 ebp + 4 지점 ( ebp 바로 다음 지점 )에는 무엇이 있겠는가 ? 오버플로우 시켜서 입력한 func3의 주소가 있다 !

그럼 func2 함수가 종료 될 때 ret 명령어에 의해서 func3 주소로 이동하게 되는 것이다

잊지말자 ! call = push eip , jmp func


[ 예제 3 ]

이번에는 호출하는 함수가 인자를 사용하는 경우를 알아보도록 하겠습니다. 소스코드는 다음과 같습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
void func1(int a)
{
        printf("%d\n",a);
}
void func2(int a)
{
        printf("%d\n",a);
}
void func3(int a)
{
        printf("%d\n",a);
}
void func4(int a)
{
        printf("%d\n",a);
}
 
int main( int argc, char* argv[] )
{
        char buffer[40];
        if ( argc >= 2 ){
                strcpy( buffer, argv[1] );
        }
 
        printf("Call Main\n");
        return 0;
}
 
 
cs


* 함수를 호출하는 경우 스택 메모리의 구조를 알고 있어야 합니다

* 함수 호출 시 실행되는 어셈블리 코드 : func( argv1, argv2 )

push argv2

push argv1

call 함수        = push eip + jmp 호출함수주소


[ 스택 메모리 구조 ]

 saved ebp

 호출 받은 함수의 복귀 주소

 saved eip

 호출 받은 함수의 종료 후 실행할 다음 명령어 주소

 argv 1

 첫번 째 인자가 위치한 주소

 argv 2

 두번 째 인자가 위치한 주소 

 saved ebp

 호출하는 함수의 복귀 주소

 saved eip

 호출 하는 함수의 다음 실행 명령어 주소


아까 설명했습니다. ebp + 8 지점에는 첫번 째 인자의 주소가 ebp + 12 지점에는 두번 째 인자의 주소가 저장됩니다

단, 함수가 인자를 사용하는 경우에만 해당합니다. 이 원리와 버퍼오버플로우를 함께 사용하도록 하겠습니다


[ 예제3 스택메모리 구조 ]

 buffer ( 40byte )

 A * 40

 save ebp ( 4byte )

 AAAA

 saved eip ( 4byte )

 func2 addr

 argc ( 4byte )

 func3 addr 

 argv[0] ( 4byte )

 func2' 인자 

 argv[1] ( 4byte )

 func3 인자



func2, func3 인자로 들어가서 출력되어 지는 것을 확인할 수 있습니다 ( 출력문자들이 왜 저런지는 잘 모르겟습니다..  )


[ 동작 순서 ]

main함수 종료 후 func2 시작주소로 이동되어 지고 func2함수는 ebp+8 지점에는 func'2인자 "9"*10 이 존재합니다

func2 ( "9"*10 ) 이 실행되고 func2 종료시 eip는 func3이기 때문에 func3이 실행됩니다

func3 실행 되면서 ebp + 8 지점에는 "7"*10이 위치해 있고, func3( "7" * 10 ) 이 실행되어 지게됩니다

마지막 func3의 eip는 "9999999999" 가 되겟지요? 따라서 알 수 없는 주소이므로 Segmentation fault 가 발생하게 됩니다



해당개념을 이용해서 LOB darkknight -> bugbear 를 풀 수 있습니다


[ 방법 1 ] 직접 /bin/sh 을 스택에 입력하여 사용한다


1> 라이브러리에 위치해 있는 system 함수의 주소를 확인합니다

(gdb) disas system

or

(gdb) print system

2> 오버플로우를 통해서 프로세스의 eip를 system함수의 주소로 변조시킨다

3> system 함수의 변수로 들어갈 " /bin/sh " 을 입력할 적당한 공간을 찾는다 ( 저는 argv[1] 을 사용합니다 )

4> /bin/sh 입력 후 /bin/sh가 위치한 주소를 system함수의 인자가 위치 해야할 주소( ebp+8 ) 에 입력한다

5> 스택 메모리 구조로 이해하기 쉽게 설명하면 다음과 같다

 buffer

 A * 40

 saved ebp

 AAAA

 saved eip

 라이브러리에 위치한 system()함수의 주소

 argc

 AAAA

 argv[0]

 argv[1]의 주소

 argv[1]

 " /bin/sh "


44byte만큼 오버플로우 시킨 뒤 eip 주소에 system함수의 주소로 변질 시킨다

=> 프로세스 종료 시 eip를 통해서 system함수가 실행된다

=> system 함수에는 인자가 필요하므로 ebp + 4 에는 인자가 위치해야 한다



[ 방법 2 ] 메모리 내에 있는 /bin/sh을 찾아서 사용한다

[ /bin/sh 찾기 프로그램 ]

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <string.h>
int main(){
  long start;
start = 0x40058ae0;
  while (memcmp( (void *) start, "/bin/sh"8) ){
    start++;
  }
  printf("/bin/sh: 0x%08x \n", start);
}
cs


[ 실행결과 ]



이하 내용은 방법 1과 같습니다

다음 포스팅에서는 직접 bugbear 문제를 풀도록 하겠습니다