티스토리 뷰

Security/시스템 해킹

RTL (Return to Libc)

on1ystar 2019. 2. 5. 21:45
728x90
반응형

RTL (Return to Library)


 

이 기법은 라이브러리의 함수로 리턴해서 그 함수를 실행할 수 있습니다. 때문에 임의로 짠 바이너리에 시스템함수가 없어도 라이브러리의 시스템함수를 호출해서 그 바이너리에 쓸 수 있게 됩니다.


방금 알아 봤듯이 printf, puts, gets, 이런 함수들은 우리가 만든 사용자 함수가 아닌 외부 라이브러리에서 가져와 사용하는 함수들입니다. 가져오는 방법은 plt got를 공부하면서 알아봤습니다.


그 중 system 함수를 담고 있는 라이브러리를 찾아 ret 주소에 이 system 함수의 주소로 변조시켜 주는 겁니다. 그러면 ret가 변조된 함수가 리턴될 때 해당 라이브러리를 참조해 system 함수를 실행시킬 수 있게 됩니다.


Dynamic link의 공유 라이브러리를 이용한다는 개념버퍼오버플로우에 접목시킨다고 생각해 볼 수 있을 것 같습니다.


그렇다면 저번에 실습했던 버퍼오버플로우 방법과 어떤 차이가 있고 왜 이런 기법을 사용하느냐 ? 이는 NX bit라는 메모리 보호 기법 때문입니다. (참고로 윈도우에서는 DEP = Data Execution Prevention 이라고 합니다.)

 


NX bit ( Never eXecute bit)


직역해보면 실행 방지 비트로 NX 특성이 지정된 메모리 구역은 데이터 저장을 위해서만 사용되며, 프로세서 명령어가 이 구역에 상주하지 못하게 합니다.


예를 들어 buffer라는 문자열을 저장하는 목적의 변수가 있고, 이에 NX bit 보호 기법을 사용했습니다. 그렇게 되면 이 buffer의 메모리 구역은 오로지 데이터를 저장하는 용도로 제한이 됩니다.


때문에 이 buffer에 프로그램을 실행하게 하는 쉘 코드를 삽입하게 되면 세그멘테이션 오류가 발생하게 됩니다.


따라서 직접적으로 쉘 코드를 실행할 수 없게 되니까 이를 우회해 system 함수를 직접 호출하여 쉘을 실행시키도록 하는 것입니다.

 

 


버퍼오버플로우– RTL 기법 실습


 

<실습 환경>

가상 환경 :VMware Workstation 10

OX :Linux-Ubuntu 16.04.02-64bit

Setting :echo 0 > /proc/sys/kernel/randomize_va_space (ASLR 해제)

 

실습해볼 소스 코드는 이렇습니다.

 

1

2

3

4

5

6

7

8

9

10

11

12

#include <stdio.h>

#include <string.h>

 

int main()

{

    char buff[40];

    printf("[+]your input : ");

    gets(buff);

    printf("[+]your input is %s !\n",buff);

 

    return 0;

}

Colored by Color Scripter

cs

 

입력 값을 받고 그대로 출력해주는 간단한 코드입니다.

저번 strcpy() 처럼 gets() 함수에도 버퍼오버플로우를 유발시키는 취약점이 존재합니다.

 

gets() 취약점

이 함수 역시 따로 문자열의 사이즈를 고려하지 않고, 엔터가 입력(\n)될 때 까지 입력된 문자열을 버퍼에 저장하는 함수입니다. 때문에 입력 값으로 버퍼를 넘치게 해서 다른 메모리영역을 침투하는 것 역시 가능합니다.


이 함수를 대체하는 방법으로는 fgets() 함수를 사용하는 것입니다.

이 함수는 fgets(char *s, int n, FILE *stream)로 정의되어 문자열의 길이를 인자 값으로 받기 때문에 입력 값의 길이를 제한

할 수 있습니다.

다시 본론으로 넘어와 이 소스 코드의 바이너리를 실행시켜 보겠습니다.


이런 식으로 실행이 되고, 버퍼의 주소를 넘치게 입력하면,


44개의 문자열을 입력하니까 세그멘테이션 폴트가 발생합니다.


계산해보면, buff(40) + SFP(4) + RET 니까 45개의 문자가 입력되면 RET를 침범합니다. 입력한 문자열은 44개인데 사실상 엔터키(\n)를 포함시켜야 하므로 45개의 문자열을 입력한 것이 맞습니다.

 

NX 설정 여부

RTL 기법 실습이기 때문에 NX가 설정되어 있겠지만 이를 확인해 볼 수 있습니다.

$ readelf -all 파일명                 

을 입력하면 elf파일의 정보들이 나오는데 그 중에서 GNU_STACK Flg 값이 RW라면 NX가 적용된 바이너리입니다.


 

스택의 상태 (gets 함수)



시스템 함수의 주소와 인자 값을 찾기 전에 먼저 printf 함수가 호출 된 후 이 함수의 스택 구조를 보겠습니다.


왼쪽이 스택의 Top부분이고 오른쪽으로 갈 수록 높은 주소 값이 될 겁니다.


우리의 목적은 gets() 함수가 buff를 입력 받은 뒤 ret를 통해 main으로 돌아가는 게 아니라 buff를 넘치게 해서 ret system() 함수 주소 값을 넣어 이를 실행시킵니다. 그리고, system() 함수의 파라미터 값으로 /bin/sh를 주기 위해 dummy (4 바이트) 뒤에 /bin/sh를 넣어 줍니다.


그러면 결국 system(/bin/sh)가 실행이 되면서 쉘을 실행시킬 수 있게 됩니다.


참고로 저 dummy 값에는 연속적 함수 호출을 위한 그 다음 리턴 어드레스가 담겨있습니다. 지금은 system() 함수 하나만 호출하면 되기 때문에 일단 쓰레기 값 4바이트를 넣어주면 된다만 알고 넘어가겠습니다.

구조도 알았으니까 이제 system() 함수의 주소 값을 찾아보겠습니다.

 

system() 주소 값


간단하게 main에 브레이크 포인트를 걸어준 다음 실행시켜 줍니다.

p system을 입력하면 바로 system() 함수의 주소 값을 알 수 있습니다.



system() 함수는 참고로 libc라는 공유 라이브러리에 존재하는 함수입니다. 처음에 main에 브레이크 포인트를 걸어준 다음 실행시킨 이유도 libc 라이브러리가 main 함수가 호출되기 전에 libc_start_main 등의 호출에 의해 링크되게 하기 위함입니다.(사실 이 부분은 제 머릿속에서 나와서 확실치 않습니다)


system() address = 0xf7e41da0

 

system()의 파라미터 값 /bin/sh


이제 system()에 들어갈 문자열 /bin/sh를 넣어주면 system(/bin/sh)를 실행시킬 수 있습니다. 그럼 어떻게 넣어줄 것인가를 고민하게 되는데,


처음에 그냥 문자열 /bin/sh를 입력하면 되지 않을까를 생각했습니다. 근데 이 부분을 좀만 찾아보니 바보 같은 생각이라는 것을 금방 알 수 있습니다

왜냐하면 이 문자열을 넣을 스택 공간이 파라미터 값의 주소 값을 저장하는 공간이니까요. 당연히 주소 값을 저장하겠죠... /bin/sh가 그대로 전달된다는 생각은 너무 낭만적이었습니다.


그렇다면 "/bin/sh"라는 문자열의 주소를 어떤 식으로 구해서 넣을까를 찾아보니, 여러 방법 중 system함수 내부를 이용하는 방법이 있었습니다.


system() 함수는 내부적으로 execve 함수를 사용합니다. 이는 저번 쉘 코드를 작성해 볼 때 공부했던 내용입니다. execve 함수는 "/bin/sh"을 이용하기 때문에 이 문자열을 담고 있습니다

따라서 system()함수 내부에서 "/bin/sh" 문자열 주소를 알아낼 수가 있게 됩니다.

 

이를 알아내기 위한 간단한 C코드를 작성해 보겠습니다.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

#include <stdio.h>

#include <string.h>

 

int main()

{

    long system = 0xf7e41da0;

    

    while (memcmp((void*)system, "/bin/sh\x00"8))

    {    system++;    }

 

    printf("/bin/sh : %x\n",system);

 

    return 0;

}

Colored by Color Scripter

cs


참고로 /bin/sh뒤에 붙은 \x00는 널 바이트입니다.

새로 알아야 되는건 memcmp()라는 함수입니다. 이 함수는

int memcmp(const void *s1, const void *s2, size_t n);

 

식으로 선언되어 있습니다

두 메모리 영역을 처음 n바이트 비교하는 함수인데, s1 s2보다 크면 0보다 큰 정수를 반환합니다. 같으면 0을 반환, 작으면 0보다 작은 정수를 반환합니다.

memcmp() 0이 아닌 정수를 반환해 system()함수의 주소 값을 ++해주면서 루프를 돌게 됩니다.


그러다가 "/bin/sh\00xx" 문자열을 만나면 0을 반환하면서 while문을 종료시키고 system 변수에는 "/bin/sh\00xx"가 있는 주소 값이 담기게 됩니다. 그럼 그 주소 값을 출력하면 됩니다.


계속 세그멘테이션 폴트가 뜨는데 이유를 모르겠습니다 ; 일단 제쳐두고


다른 방법을 찾아 봤는데 커맨드라인 입력으로 libc 라이브러리에서 string을 찾는 방법이 있습니다.


이렇게 입력하면 이 바이너리가 사용하는 라이브러리들이 나오는데요, 우리가 사용할 system() 함수의 라이브러리는 libc.so.6. 입니다. 이 라이브러리의 경로를 그대로 가져와서


이렇게 입력하면 16진수 형태로 "/bin/sh"가 위치한 주소 값을 출력해 줍니다.


그런데 이게 다가 아니라 offset 주소이기 때문에 계산이 필요합니다.( 이 부분은 offset에 대한 개념이 확실해야 하는데 모호해서 일단 따라해봤습니다. https://shayete.tistory.com/entry/Lv1-Gate )



"/bin/sh"의 문자열 주소 - system 주소 = offset

offset = 0x120c6b


system 주소 + offset = "/bin/sh"의 문자열 주소

이런저런 계산을 해서 나온 주소 값을 열어 보니까 정말 "/bin/sh" 문자열이 있습니다 ! 신기할 따름이네요...

"/bin/sh" 문자열 주소 값 = 0xf7f62a0b

 

페이로드 작성


ax44 (buff + sfp) + ret(system() address = 0xf7e41da0) + ax4(dummy) + 인자 값("/bin/sh" 문자열 주소 값 = 0xf7f62a0b)


물론 리틀 엔디안 방식으로 입력해야 합니다.


 

참조 :    https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/

           https://expointer.tistory.com/13

           https://shayete.tistory.com/entry/4-Return-to-Library-RTL


728x90
반응형

'Security > 시스템 해킹' 카테고리의 다른 글

Easy CrackMe 리버싱  (1) 2019.05.08
어셈블리로 구구단 짜기(nasm)  (1) 2019.04.02
리버싱 실습(Easy_ELF)  (0) 2019.01.24
버퍼오버플로우 실습  (0) 2019.01.23
쉘 코드(/bin/sh  (0) 2019.01.16
댓글