본문 바로가기

공부/보안

[dreamhack] System Exploitation 10강

반응형

거의 다쓴거 날라가서 멘탈이 터졌으니 짧게 하고 나중에 수정하겠다.

10강 Advanced Linux Exploitation

10.1 one-shot gadget

함수 포인터를 덮어쓸 수 있지만, ROP와 같이 코드를 연계해 셸을 획득하는 것이 불가능할 때 이런 상황에서 사용할 수 있는 가젯이 원샷 가젯이다.

원샷 가젯이란 라이브러리 내에 존재하는 가젯으로, 리눅스 시스템에서 특정 조건 하에 pc를 바꾸는 것 만으로 셸을 실행시켜 주는 코드 가젯이다.

원샷 가젯은 libc.so.6 라이브러리 파일 내부에 존재해 라이브러리가 매핑된 주소를 안다면 오프셋 계산을 통해 가젯의 주소를 찾을 수 있다.

원샷 가젯들 중 하나의 디스어셈블리 결과이다. do_system 함수 내부에 존재하는 코드로, system 함수의 인자로 전달된 명령어를 실행하기 위해 사용하는 코드의 일부이다. "/bin/sh" 문자열 주소를 첫번째 인자로 사용해 execve 시스템 콜을 호출하기 때문에 실행 흐름을 바꿔 셸을 획득할 수 있다.

 

 

10.2 .init_array & .fini_array

바이너리의 여러 섹션들은 코드가 빌드될 때 컴파일러에 의해 만들어진다. .init_array와 .fini_array는 바이너리가 실행되고 종료될 때 참조되는 함수 포인터들이 저장되어 있는 섹션이다.

1
2
3
4
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);
cs

바이너리가 처음 실행될 때 호출되는 start 함수는 바이너리 실행 과정에 필요한 여러 요소들을 초기화하기 위해 __libc_start_main 함수를 호출한다. 초기화 과정에서 __libc_start_main 은 .init_array 섹션을 참조하는데, main 함수가 호출되기 전에 __libc_csu_init 함수에 의해 실행된다.

다음은 __libc_csu_init 함수의 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
  for (size_t i = 0; i < size; i++)
      (*__init_array_start [i]) (argc, argv, envp);
}
cs

.init_array 섹션 크기를 계산하여 .init_array에 저장돈 함수 포인터들을 순차적으로 호출하는 것을 확인할 수 있다. 바이너리 종료 시점에도 이와 비슷한 방식으로 .fini_array가 참조돼 사용된다.

.fini_array는 프로그램이 종료된 후 참조되는 섹션으로 RELRO 보호기법이 설정되어 있지 않고 임의 주소 쓰기가 가능하면, .fini_array 섹션을 덮어 실행 흐름을 조작할 수 있다. .fini_array을 실행하기 까지 main 함수가 리턴된 후, _GI_exit 함수를 호출한다.

1
2
3
4
void exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, truetrue);
}
cs

_GI_exit 함수는 _run_exit_handlers함수를 호출한다. 이 함수는 exit_function 구조체 멤버 변수인 flavor 값에 따라 함수를 호출하는데, 기본 상태에선 로더 라이브러리 내부에 존재하는 _dl_fini 함수를 호출한다.

exit_function구조체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
};
cs

_run_exit_handlers 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
             bool run_list_atexit, bool run_dtors)
{
  /* First, call the TLS destructors.  */
#ifndef SHARED
  if (&__call_tls_dtors != NULL)
#endif
    if (run_dtors)
      __call_tls_dtors ();
    ...
      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;
        }
    }
cs

_dl_fini 함수 내부에서 다음과 같은 명령어로 .fini_array 를 호출한다.

   0x7ffff7de7dc9 <_dl_fini+777>:	mov    r12,QWORD PTR [rax+0x8]      
   ...
   0x7ffff7de7de5 <_dl_fini+805>:	je     0x7ffff7de7e00 <_dl_fini+832>
   0x7ffff7de7de7 <_dl_fini+807>:	nop    WORD PTR [rax+rax*1+0x0]
   0x7ffff7de7df0 <_dl_fini+816>:	mov    edx,r13d
=> 0x7ffff7de7df3 <_dl_fini+819>:	call   QWORD PTR [r12+rdx*8]

rax+0x8에 위치한 값을 기준으로 call 명령을 사용해 함수 포인터를 호출하는데, rax+8은 0x600930 주소를 가리키고, 이 주소는 .fini_array를 가리킨다.

LOAD:0000000000600928                 Elf64_Dyn <1Ah, 6008C8h> ; DT_FINI_ARRAY

.fini_array를 참조해 호출하기 때문에 바이너리에 RELRO 보호기법이 적용되어 있지 않으면 이를 덮어씀으로 pc를 조작할 수 있다.

 

 

 

 

10.3 _rtld_global Overwrite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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, 020);
    setvbuf(stdout, 020);
    printf("stdout: %lp\n",stdout);
    printf("addr: ");
    scanf("%ld",&addr);
    printf("data: ");
    scanf("%ld",&data);
    *(long long *)addr = data;
    exit(0);
}
cs

위는 임의의 주소에 원하는 값을 쓰는 예제다. main 함수 마지막에 exit 함수를 사용해 종료하고 Full RELRO보호기법이 적용되어 있어 .fini_array 를 덮는 공격을 사용할 수 없다.

_GI_exit 함수는 _run_exit_handlers 함수를 호출하고, _run_exit_handlers는 ld.so에 존재하는 _di_fini 함수를 호출하게 된다.

_di_fini 함수 코드의 일부이다.

1
2
3
4
5
6
7
8
9
10
11
12
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));
cs

_di_fini 함수는 dl_load_lock을 인자로 __rtld_lock_lock_recursive함수를 호출한다. 해당 함수와 인자는 _rtld_global 멤버인데, _rtld_global 구조체가 위치한 영역에는 쓰기 권한이 있어 _rtld_lock_lock_recursive 함수 포인터와 _dl_load_lock의 값을 덮어쓸 수 있다면 system("sh")을 호출할 수 있다.

_rtld_global 구조체를 gdb로 확인한 결과이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
int main()
{
    long long addr;
    long long data;
    setvbuf(stdin, 020);
    setvbuf(stdout, 020);
    printf("stdout: %lp\n",stdout);
    printf("addr: ");
    scanf("%ld",&addr);
    printf("data: ");
    scanf("%ld",&data);
    *(long long *)addr = data;
    exit(0);
}
cs

위 C코드를 익스플로잇 하는 파이썬 스크립트를 작성할 때 유의할 점은 프로그램은 한 번 임의 주소 쓰기를 실행한 후 exit 함수를 호출해 종료하기 때문 _rtld_lock_lock_recursive 함수 포인터와 _dl_load_lock 인자를 동시에 조작할 수 없다. 그래서 _rtld_lock_lock_recursive 함수 포인터의 값을 바이너리 start로 바꿔 계속해서 임의 주소 값 쓰기 가능하다.

1
2
p.sendlineafter("addr:",str(rtld_recursive))
p.sendlineafter("data:",str(0x0000000000400580)) # start
cs

그 이후에는 _dl_load_lock의 위치에 "sh" 문자열을, _rtld_lock_lock_recursive 함수 포인터를 system 함수의 주소를 덮어써 system("sh")을 호출한다.

https://rninche01.tistory.com/entry/Linux-Binary-Execution-Flow

 

 

 

10.4 _hook Overwrite

GNU C 라이브러리는 malloc, realloc, free와 같은 함수들의 동작을 수정할 수 있도록 __malloc_hook, __realloc_hook, __free_hook 와 같은 변수를 제공한다. 이러한 변수에 후킹 함수의 주소가 저장되어 있으면 해당 함수가 호출될 때 기존 함수가 아닌 후킹 함수가 호출된다.

1
2
3
4
__int64 __fastcall malloc(__int64 a1, __int64 a2, __int64 a3)
{
  if ( _malloc_hook )
    return _malloc_hook(a1, retaddr);
cs

malloc 함수에서 __malloc_hook을 호출하는 코드로 이런 후킹 변수들은 라이브러리의 쓰기 가능한 영역에 존재해 익스플로잇 과정에서 이를 실행 흐름을 조작하는데 사용한다.

 

 

 

10.5 Master canary

SSP 보호 기법이 적용되어 있으면 함수에서 스택을 사용할 때 카나리가 생성된다. 마스터 카나리는 main 함수가 호출되기 전 랜덤으로 생성된 카나리를 스레드 별 전역 변수로 사용되는 Thread Local Storage(TLS)에 저장한다. → 모든 스레드가 하나의 카나리 값을 사용하기 위해서이다.

TLS는 tcbhead_t 구조체를 가지는데, 아래와 같은 멤버 변수들이 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct
{
  void *tcb;        /* Pointer to the TCB.  Not necessarily the
               thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;        /* Pointer to the thread descriptor.  */
  int multiple_threads;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  int gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
  int private_futex;
#else
  int __glibc_reserved1;
#endif
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
} tcbhead_t;
cs

security_init

1
2
3
4
5
6
7
8
9
10
static void
security_init (void)
{
  /* Set up the stack checker's canary.  */
  uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
#ifdef THREAD_SET_STACK_GUARD
  THREAD_SET_STACK_GUARD (stack_chk_guard);
#else
  __stack_chk_guard = stack_chk_guard;
#endif
cs

security_init 함수는 _dl_setup_stack_chk_guard 함수에서 반환한 랜덤 카나리의 값을 설정한다. THREAD_SET_STACK_GUARD 매크로는 TLS 영역의 header.stack_guard에 카나리의 값을 삽입하는 역할을 한다.

1
2
#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
cs

_dl_allocate_tls_storage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *
internal_function
_dl_allocate_tls_storage (void)
{
  void *result;
  size_t size = GL(dl_tls_static_size);
#if TLS_DTV_AT_TP
  /* Memory layout is:
     [ TLS_PRE_TCB_SIZE ] [ TLS_TCB_SIZE ] [ TLS blocks ]
              ^ This should be returned.  */
  size += (TLS_PRE_TCB_SIZE + GL(dl_tls_static_align) - 1)
      & ~(GL(dl_tls_static_align) - 1);
#endif
  /* Allocate a correctly aligned chunk of memory.  */
  result = __libc_memalign (GL(dl_tls_static_align), size);
cs

TLS 영역은 _dl_allocate_tls_storage 함수에서 __libc_memalign 함수를 호출해 할당된다.

SSP 보호기법이 적용되어 있으며 지역 변수를 사용하면 main 함수에서 카나리를 삽입하고 검사하는 루틴이 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ gdb master1
gdb-peda$ x/10i main
   0x400596 <main>:    push   rbp
   0x400597 <main+1>:    mov    rbp,rsp
   0x40059a <main+4>:    sub    rsp,0x110
   0x4005a1 <main+11>:    mov    rax,QWORD PTR fs:0x28
   0x4005aa <main+20>:    mov    QWORD PTR [rbp-0x8],rax
gdb-peda$ b *0x4005aa
Breakpoint 1 at 0x4005aa
gdb-peda$ r
gdb-peda$ i r rax
rax            0x42c9c74301cbfa00    0x42c9c74301cbfa00
 
gdb-peda$ find 0x42c9c74301cbfa00
Searching for '0x42c9c74301cbfa00' in: None ranges
Found 1 results, display max 1 items:
mapped : 0x7ffff7fd7728 --> 0x42c9c74301cbfa00 
gdb-peda$ x/100x 0x7ffff7fd7728-0x28
0x7ffff7fd7700:    0x00007ffff7fd7700    0x00007ffff7fd6010
0x7ffff7fd7710:    0x00007ffff7fd7700    0x0000000000000000
0x7ffff7fd7720:    0x0000000000000000    0xeb514bc87b676e00 
0x7ffff7fd7730:    0x8a9912a628c155d7    0x0000000000000000
cs

0x4005aa에서 rax 레지스터를 보면 카나리의 값 0x42c9c74301cbfa00을 확인했고, find 명령어를 통해 마스터 카나리의 위치를 찾을 수 있다.

카나리가 존재하는 영역을 디버거로 보면 fs:0x28이 참조하는 주소는 TLS 영역의 header.stack_guard인 것을 확인할 수 있다. 만일 공격자가 마스터 카나리를 원하는 값으로 조작한다면 랜덤 카나리 값을 모른다 해도 fs:0x28을 참조해 비교하는 SSP 보호 기법을 우회할 수 있다.

 

 

10.6 Environ ptr

라이브러리에서 프로그램의 환경 변수를 참조할 일이 있다. 이를 위해 libc.so.6에는 environ라는 프로그램의 환경 변수를 가리키는 포인터가 존재한다. environ 포인터는 로더의 초기화 함수에 의해 초기화된다.

 

 

 

 

10.7 Seccomp Filter Bypass

SECure COMPuting mode(SECCOMP)는 리눅스 커널에서 프로그램의 샌드박싱 매커니즘을 제공하는 컴퓨터 보안 기능이다. 샌드박스는 시스템 오류나 취약점으로인한 2차 피해를 막기 위해 프로그램의 권한을 분리하기 위한 매커니즘이다. 이 친구를 이용하면 프로세스가 필요하지 않지만 위험한 시스템 콜들에 대한 호출을 막을 수 있다.

 

 

10.8 SROP

리눅스에서 시그널이 들어오면 커널 모드에서 처리하는데 커널 모드에서 유저 모드로 돌아오는 과정에서 유저의 스택에 레지스터 정보들을 저장한다. rt_sigreturn 은 저장한 정보를 다시 돌릴 때 사용한다.

공격자가 rt_sigreturn 시스템 콜을 호출할 수 있고 스택을 조작할 수 있다면 모든 레지스터와 세그먼트를 조작할 수 있다. rt_sigreturn 시스템 콜을 사용해 익스플로잇하는 기법을 SigReturn Oriented Programming(SROP)라 한다.

rt_sigreturn은

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static int restore_sigcontext(struct pt_regs *regs,
                  struct sigcontext __user *sc,
                  unsigned long uc_flags)
{
    unsigned long buf_val;
    void __user *buf;
    unsigned int tmpflags;
    unsigned int err = 0;
    /* Always make any pending restarted system calls return -EINTR */
    current->restart_block.fn = do_no_restart_syscall;
    get_user_try {
#ifdef CONFIG_X86_32
        set_user_gs(regs, GET_SEG(gs));
        COPY_SEG(fs);
        COPY_SEG(es);
        COPY_SEG(ds);
#endif /* CONFIG_X86_32 */
        COPY(di); COPY(si); COPY(bp); COPY(sp); COPY(bx);
        COPY(dx); COPY(cx); COPY(ip); COPY(ax);
#ifdef CONFIG_X86_64
        COPY(r8);
        COPY(r9);
        COPY(r10);
        COPY(r11);
        COPY(r12);
        COPY(r13);
        COPY(r14);
        COPY(r15);
         ...
}
cs

COPY와 COPY_SEG 매크로를 사용해 레지스터와 세그먼트를 복원한다. SROP 기법을 사용하여 공격하기 위해서는 sigcontext 구조체를 알고 있어야 합니다.

sigcontext-32bit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct sigcontext
{
  unsigned short gs, gsh;
  unsigned short fs, fsh;
  unsigned short es, esh;
  unsigned short ds, dsh;
  unsigned long edi;
  unsigned long esi;
  unsigned long ebp;
  unsigned long esp;
  unsigned long ebx;
  unsigned long edx;
  unsigned long ecx;
  unsigned long eax;
  unsigned long trapno;
  unsigned long err;
  unsigned long eip;
  unsigned short cs, __csh;
  unsigned long eflags;
  unsigned long esp_at_signal;
  unsigned short ss, __ssh;
  struct _fpstate * fpstate;
  unsigned long oldmask;
  unsigned long cr2;
};
cs

sigcontext-64bit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct sigcontext
{
  __uint64_t r8;
  __uint64_t r9;
  __uint64_t r10;
  __uint64_t r11;
  __uint64_t r12;
  __uint64_t r13;
  __uint64_t r14;
  __uint64_t r15;
  __uint64_t rdi;
  __uint64_t rsi;
  __uint64_t rbp;
  __uint64_t rbx;
  __uint64_t rdx;
  __uint64_t rax;
  __uint64_t rcx;
  __uint64_t rsp;
  __uint64_t rip;
  __uint64_t eflags;
  unsigned short cs;
  unsigned short gs;
  unsigned short fs;
  unsigned short pad0;
  uint64_t err;
  __uint64_t trapno;
  __uint64_t oldmask;
  __uint64_t cr2;
  extension union
    {
      struct _fpstate * fpstate;
      __uint64_t __fpstate_word;
    };
  __uint64_t __reserved1 [8];
};
cs

pwntools의 SigreturnFrame은 SROP 익스플로잇을 위한 클래스이다. SROP를 이용해 공격할 때는 sigcontext 구조체에 맞게 공격 코드를 구성해야 하는 번거로움이 SigreturnFrame를 사용해 비교적 쉽게 SROP 공격 코드를 만들 수 있다.

10.9 _IO_FILE(, vtable, vtable check bypass, Arbitrary Read, Write)

_IO_FILE는 리눅스 시스템의 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체이다. 이는 프로그램이 fopen 과 같은 함수를 통해 파일 스트림을 열 때 힙이 할당된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct _IO_FILE
{
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;    /* Current read pointer */
  char *_IO_read_end;    /* End of get area. */
  char *_IO_read_base;    /* Start of putback+get area. */
  char *_IO_write_base;    /* Start of put area. */
  char *_IO_write_ptr;    /* Current put pointer. */
  char *_IO_write_end;    /* End of put area. */
  char *_IO_buf_base;    /* Start of reserve area. */
  char *_IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */
  struct _IO_marker *_markers;
  struct _IO_FILE *_chain;
  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
cs

_IO_FILE 구조체에 여러 멤버 변수들이 존재한다.

  • _flags : 파일에 대한 읽기/쓰기/추가 권한을 의미
  • _IO_read_ptr : 파일 읽기 버퍼에 대한 포인터
  • _IO_read_end : 파일 읽기 버퍼 주소의 끝을 가리키는 포인터
  • _IO_read_base : 파일 읽기 버퍼 주소의 시작을 가리키는 포인터
  • _IO_write_base : 파일 쓰기 버퍼 주소의 시작을 가리키는 포인터
  • _IO_write_ptr : 쓰기 버퍼에 대한 포인터
  • _IO_write_end : 파일 쓰기 버퍼 주소의 끝을 가리키는 포인터
  • _chain : 프로세스의 _IO_FILE 구조체는 _chain 필드를 통해 링크드 리스트를 만든다. 링크드리스트의 헤더는 라이브러이의 전역 변수인 _IO_list_all 에 저장
  • _fileno : 파일 디스트립터의 값

_flags 필드는 해당 파일이 가지고 있는 여러 성질을 플래그로 나타내는 필드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_U
cs

_flags가 0xfbad2488의 값을 가지고 있다고 하면 0xfbad0000 + 0x2000 + 0x400 + 0x80 + 0x8로 나타낼 수 있다. 각각 _IO_MAGIC, _IO_IS_FILIEBUF, _IO_TIED_PUT_GET, _IO_LINKED, _IO_NO_WRITES(파일을 열 때 읽기 모드로 열음)을 의미한다.

파일의 플래그는 파일을 열어 파일에 대한 _IO_FILE 구조체를 생성할 때 초기화된다. fopen 함수는 파일을 열 때 내부적으로 _IO_new_file_fopen 함수를 호출한다. 이 함수는 인자로 전달받은 mode에 따라 플래그 값을 지정해 파일을 연다.(mode 값에 따라 각기 다른 플래그를 설정해 주는 것을 확인할 수 있다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FILE * _IO_new_file_fopen (FILE *fp, const char *filename, const char *mode, int is32not64)
{
  int oflags = 0, omode;
  int read_write;
  int oprot = 0666;
  int i;
  FILE *result;
  const char *cs;
  const char *last_recognized;
  if (_IO_file_is_open (fp))
    return 0;
  switch (*mode)
    {
    case 'r':
      omode = O_RDONLY;
      read_write = _IO_NO_WRITES;
      break;
    case 'w':
      omode = O_WRONLY;
      oflags = O_CREAT|O_TRUNC;
      read_write = _IO_NO_READS;
      break;
    case 'a':
      omode = O_WRONLY;
      oflags = O_CREAT|O_APPEND;
      read_write = _IO_NO_READS|_IO_IS_APPENDING;
      break;
  ...
}
cs

_IO_FILE - vtable

지금까지 _IO_FILE 구조체에 대해 알아봤는데, 실제로 파일 스트림을 열 때 _IO_FILE_plus 구조체가 리턴된다. _IO_FILE_plus 구조체는 파일 스트림에서의 함수 호출을 용이하게 하기 위해 _IO_FILE 구조체에 함수 포인터 테이블을 가리키는 포인터를 추가한 구조체이다.

1
2
3
4
5
struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};
cs

_IO_jump_t에는 파일에 관련된 여러 동작 수행하는 fread, fwrite, fopen과 같은 표준 함수 포인터들이 저장되어 있다. _IO_jump_t 구조체는 여러 함수 포인터가 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_file_finish),
  JUMP_INIT(overflow, _IO_file_overflow),
  JUMP_INIT(underflow, _IO_file_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_default_pbackfail),
  JUMP_INIT(xsputn, _IO_file_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_new_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, _IO_new_file_sync),
  JUMP_INIT(doallocate, _IO_file_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
cs

fread 함수가 호출될 때 vtable 내부 함수 포인터들을 통해 저수준 동작을 수행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define fread(p, m, n, s) _IO_fread (p, m, n, s)
size_t
_IO_fread (void *buf, size_t size, size_t count, FILE *fp) {
    ...
    bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
    ...
}
size_t
_IO_sgetn (FILE *fp, void *data, size_t n)
{
  /* FIXME handle putback buffer here! */
  return _IO_XSGETN (fp, data, n);
}
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
cs

stdin, stdout, stderr 세 파일 스트림은 프로세스가 시작할 때 라이브러리에 의해 기본적으로 생성되는 파일 스트림들이다. 파일 함수가 호출되면 파일 포인터의 vtable에 있는 함수 포인터를 호출한다. vtable 주소는 파일 포인터 내 존재해 vtable을 가리키는 주소를 바꾸거나 vtable의 값을 조작할 수 있으면 원하는 주소를 호출할 수 있다. 우분투 16.04 이후의 버전에서는 _IO_vtable_check 함수가 추가되었기에 해당 방법으로는 공격할 수 없다.

 

Review(10강 Advanced Linux Exploitation)

 

one-shot gadget : 리눅스에서 원샷 가젯은 libc.so.6 라이브러리 파일에 존재해, execve 시스템 콜의 인자로 "/bin/sh" 문자열을 사용해 pc만 조작이 가능해도 코드 가젯의 연계없이 셸을 획득할 수 있다. + 함수 포인터 조작할 수 있는 경우

 

.init_array & .fini_array : .init_array는 main 함수 호출 전에 참조하는 함수 포인터의 배열이고,.fini_array는 main 함수가 끝나고 참조하는 함수 포인터 배열이다. RELRO 보호 기법이 적용되어 있지 않으면 해당 배열 쓰기 권한이 존재해 덮어쓸 수 있다. .fini_array를 덮으면 main 함수가 종료되고 _dl_fini 함수에서 참조해 pc 조작가능

 

_rtld_global Overwrite : exit 함수는 프로그램을 리턴하지 않고 종료하는 함수로 호출되면 .fini_array가 호출되는 과정과 동일하게 _dl_fini 함수가 호출된다. _dl_fini 함수 내부 _rtld_global 구조체 내에 __rtld_lock_lock_recursive 함수 포인터를 호출하는데, 공격자가 이를 덮을 수 있다면 pc를 조작할 수 있다.

 

_hook Overwrite : _hook는 디버깅 용도로 사용할 수 있는 변수다. malloc과 free 함수의 경우 _hook 변수값이 0이 아니라면 이를 함수 포인터로 호출하는데, 공격자가 _free_hook 변수를 덮으면 다음 free 함수 호출 때 pc 조작 가능

 

Master canary : 마스터 카나리는 로더에서 프로그램을 로딩할 때 TLS 영역을 할당해 생성한다. TLS는 스레드 별로 고유한 저장 공간 영역인데 스레드를 생성해 호출되는 함수에서 변수를 할당하면 해당 영역을 사용하고 버퍼 오버플로우가 발생하면 마스터 카나리를 덮을 수 있다. 스레드를 생성해 호출 함수에서 카나리를 비교할 때 TLS 영역에 존재하는 마스터 카나리를 비교하기 때문에 덮어쓴 카나리를 입력함으로써 카나리를 우회할 수 있다.

 

Environ ptr : environ 는 스택의 환경 변수 주소를 갖고 있는 라이브러리 변수이다. libc.so.6 라이브러리 내에 존재하며, 스택을 갖고 있는 변수이기 때문에 라이브러리 주소를 알면 해당 변수에 존재하는 값을 이용해 스택 주소를 알 수 있다.

 

Seccomp Filter Bypass : seccomp는 prct1 함수 인자로 PR_SET_SECCOMP 을 전달해 특정 시스템 콜의 호출을 허용하거나 않는 샌드박스 매커니즘이다. 리눅스 커널에서 시스템 골을 호출할 때 32비트 자료형을 사용해 시스템 콜 테이블을 참조해 인티저 오버플로우를 사용해 호출을 허용하지 않는 시스템 콜을 호출할 수 있다.

 

SROP : sigreturn 시스템 콜을 사용해 공격하는 기법으로 해당 시스템 콜을 유저 모드와 커널 모드의 코드를 동시에 실행하기 위해 레지스터와 세그먼트를 스택에 저장하는 용도로 사용된다. 공격자가 스택을 전부 조작하면 sigreturn 시스템 콜을 호출하여 모든 레지스터와 세그먼트를 조작할 수 있다.

 

_IO_FILE : 파일 입출력 수행할 때 사용하는 구조체로 파일 함수는 해당 구조체를 참조하여 입출력을 해 vtable과 다른 포인터 멤버 변수를 조작할 수 있다면 임의 주소 쓰기 및 읽기 취약점으로 연계할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

반응형