[dreamhack]_rtld_global
프로그램 종료과정에서 사용된다.
직접 한번 보자
// Name: rtld.c
// Compile: gcc -o rtld rtld.cint
main() {
return 0;
}
실행을 해서 main 함수가 종료되는 부분부터 보면
__libc_start_main+231 로 리턴한다.
이후 내부에서 __GI_exit 를 call 한다.
계속 보다보면 __run_exit_handlers 함수를 호출한다.
함수의 코드 크기가 크기 때문에 라이브러리 코드로 봐야 한다.
/* Call all functions registered with `atexit' and `on_exit',
in the reverse of the order in which they were registered
perform stdio cleanup, and terminate program execution with STATUS. */
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
__call_tls_dtors ();
/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
while (*listp != NULL)
{
struct exit_function_list *cur = *listp;
while (cur->idx > 0)
{
const struct exit_function *const f =
&cur->fns[--cur->idx];
switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);
case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (onfct);
#endif
onfct (status, f->func.on.arg);
break;
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (atfct);
#endif
atfct ();
break;
case ef_cxa:
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
cxafct (f->func.cxa.arg, status);
break;
}
}
*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free (cur);
}
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());
_exit (status);
}
코드를 보면 exit_function 구조체 멤버에 따라서 함수 포인터를 호출한다.
enum
{
ef_free, /* `ef_free' MUST be zero! */
ef_us,
ef_on,
ef_at,
ef_cxa
};
struct exit_function
{
/* `flavour' should be of type of the `enum' above but since we need
this element in an atomic operation we have to use `long int'. */
long int flavor;
union
{
void (*at) (void);
struct
{
void (*fn) (int status, void *arg);
void *arg;
} on;
struct
{
void (*fn) (void *arg, int status);
void *arg;
void *dso_handle;
} cxa;
} func;
};
return 으로 프로그램을 종료한다면 _dl_fini 함수를 호출한다.
이 프로그램은 return 으로 프로그램을 종료하기 때문에 _dl_fini 함수를 호출한다.
call rdx를 하는데
_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:
#endif
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));
로더에 존재하는 _dl_fini
함수의 일부이다.
_dl_load_lock
을 인자로 하고 __rtld_lock_lock_recursive
함수를 호출한다.
매크로를 보면 dl_rtld_lock_recursive
라는 함수 포인터이다.
이 매크로는 뮤텍스에 대한 재귀적인 lock 을 얻기 위해 dl_rtld_lock_recursive
함수를 호출하는 것이다.
해당 함수 포인터는 _rtld_global
구조체의 멤버 변수이다.
해당 구조체는 매우 방대하기 때문에 함수 포인터와 전달되는 인자인 dl_load_lock
만을 살펴보자.
gdb 로 _rtld_global
구조체를 보면
아래쪽에
_dl_rtld_lock_recursive
함수 포인터에는 rtld_lock_default_lock_recursive
함수 주소를 가지고 있다.
_rtld_global
구조체의 _dl_rtld_lock_recursive
멤버의 주소를 vmmap 으로 확인해보면 쓰기 권한이 있는 것을 알 수 있다.
_rtld_global 초기화
static void
dl_main (const ElfW(Phdr) *phdr,
ElfW(Word) phnum,
ElfW(Addr) *user_entry,
ElfW(auxv_t) *auxv)
{
GL(dl_init_static_tls) = &_dl_nothread_init_static_tls;
#if defined SHARED && defined _LIBC_REENTRANT \
&& defined __rtld_lock_default_lock_recursive
GL(dl_rtld_lock_recursive) = rtld_lock_default_lock_recursive;
GL(dl_rtld_unlock_recursive) = rtld_lock_default_unlock_recursive;
프로세스를 로드할 때 호출되는 dl_main
코드의 일부인데 _rtld_global
구조체의 dl_rtld_lock_recursive
함수 포인터가 초기화되는 것을 볼 수 있다.
overwrite _rtld_global
ld_base
libcbase 를 구하고 로더의 베이스 주소를 알아내야 한다.
vmmap 으로 확인하면 쉽게 알아낼 수 있다.
ld_base 도 구했다면 _rtld_global
구조체를 계산해야 되는데 링커를 제공하는 문제의 경우 patchelf 로 특정 링커를 사용할 수 있다.
patchelf
patchelf 를 사용하면 바이너리가 특정 링커를 사용하도록 패치할 수 있다.$ patchelf --set-interpreter ./ld-2.27.so ./ow_rtld
이런 식으로 사용할 수 있다.
_rtld_global 주소
_rtld_global 구조체를 계산해야 되는데 rtld_global 구조체의 심볼 주소를 더해서 해당 구조체의 주소를 알아낼 수 있다._dl_load_lock
과 _dl_rtld_lock_recursive
함수 포인터의 주소를 구한다.
_rtld_global
구조체 내 멤버 변수의 오프셋을 알아내려면 구조체 이름, 멤버 변수 정부를 담고 있는 디버깅 심볼이 필요하다.
libc 와 ld 의 glibc 버전을 알아내고 상응하는 디버깅 심볼을 다운로드 해야한다.
버전을 알아내면 해당 버전의 libc6-dbg
패키지를 다운로드 하면 된다.libc6-dbg 2.27-3ubuntu1
이런식으로 구글링 해서 찾아서 다운로드 하면 된다.
다운로드한 .deb 파일을 대상으로 다음의 명령어를 사용해서 패키지에 포함된 파일을 추출해서 현재 디렉토리에 저장한다.
$ dpkg -x libc6-dbg_2.27-3ubuntu1_amd64.deb ./
그런 다음 압축을 해제하면 usr/lib/debug/lib/x86_64-linux-gnu/ld-2.27.so
이런 파일이 있다.
해당 파일을 gdb 로 열어서 _rtld_global
구조체의 멤버 변수 _dl_load_lock
과 멤버 변수 _dl_rtld_lock_recursive
의 오프셋을 얻을 수 있다.
이렇게 된다고 한다.
그럼 오프셋은 2312, 3840 이렇게 구할 수 있다.
ld_base 에 _rtld_global
오프셋을 더하면 _rtld_global
을 구할 수 있고 여기에 위에서 구한 오프셋을 더해주면 _dl_load_lock
과 _dl_rtld_lock_recursive
의 주소를 계산할 수 있다.
overwrite
이렇게 구한 주소들을 이용해서 got overwrite 하듯이 overwrite 만 해주면 되는데 다음과 같다.
프로그램이 종료하는 과정에서 _rtld_global
구조채의 _dl_load_lcok
을 인자로 _dl_rtld_lock_recursive
함수 포인터를 호출한다.
따라서 dl_load_lock
에 “/bin/sh” 이나 “sh” 를 삽입하고 dl_rtld_lock_recursive
를 system
함수로 덮어쓰면 셸을 획득할 수 있다.
glibc2.34 이전 버전에서만 사용 가능하다고 한다.
이런 공격 기법처럼 hook 이나. 이런 함수 포인터들을 이용해서 공격할 수 있다는 것이 교훈이다.
이 글은 옵시디언을 이용해서 작성되었습니다.
'TOOR' 카테고리의 다른 글
[TOOR] 11.3. Holymoly write up (0) | 2023.10.04 |
---|---|
[TOOR] 15.2. fsb write up (0) | 2023.10.03 |
[TOOR] 17. Race Condition (0) | 2023.10.02 |
[TOOR] 16.1 OOB (Out of Bound) (0) | 2023.10.02 |
[TOOR] 15.1 Format String Bug (0) | 2023.10.02 |