Translated ['src/binary-exploitation/libc-heap/unsorted-bin-attack.md']

This commit is contained in:
Translator 2025-08-28 17:03:43 +00:00
parent ff95809f3f
commit 96520c6c19

View File

@ -2,55 +2,111 @@
{{#include ../../banners/hacktricks-training.md}}
## Basic Information
## 기본 정보
Unsorted bin이 무엇인지에 대한 자세한 정보는 페이지를 확인하세요:
Unsorted bin이 무엇인지에 대한 자세한 정보는 다음 페이지를 확인하세요:
{{#ref}}
bins-and-memory-allocations.md
{{#endref}}
Unsorted 리스트는 chunk`bk` 주소에 `unsorted_chunks (av)`의 주소를 쓸 수 있습니다. 따라서 공격자가 unsorted bin 내의 chunk에서 **`bk` 포인터의 주소를 수정**할 수 있다면, 그는 **그 주소를 임의의 주소에 쓸 수 있게 되어** Glibc 주소를 유출하거나 일부 방어를 우회하는 데 도움이 될 수 있습니다.
Unsorted 리스트는 청크`bk` 주소에 `unsorted_chunks (av)`의 주소를 쓸 수 있습니다. 따라서 공격자가 unsorted bin 내부의 청크에서 `bk` 포인터의 주소를 **수정할 수 있다면**, 그는 그 주소를 **임의의 주소에 쓸 수 있게** 되고 이는 Glibc 주소를 leak 하거나 일부 방어를 우회하는 데 도움이 될 수 있습니다.
기본적으로 이 공격은 **임의의 주소에 큰 숫자를 설정**할 수 있게 해줍니다. 이 큰 숫자는 주소로, 힙 주소나 Glibc 주소일 수 있습니다. 일반적인 목표는 **`global_max_fast`**로, 더 큰 크기의 fast bin을 생성할 수 있게 해줍니다(unsorted bin 공격에서 fast bin 공격으로 넘어갈 수 있습니다).
요약하면, 이 공격은 **임의의 주소에 큰 숫자(큰 값)를 설정할 수 있게** 해줍니다. 이 큰 숫자는 힙 주소나 Glibc 주소일 수 있습니다. 전통적인 타깃은 **`global_max_fast`** 로, 이를 통해 더 큰 크기의 fast bin을 만들 수 있게 하여 (unsorted bin 공격에서 fast bin 공격으로 전환하는 데) 사용되었습니다.
- Modern note (glibc ≥ 2.39): `global_max_fast` became an 8bit global. Blindly writing a pointer there via an unsorted-bin write will clobber adjacent libc data and will not reliably raise the fastbin limit anymore. Prefer other targets or other primitives when running against glibc 2.39+. See "Modern constraints" below and consider combining with other techniques like a [large bin attack](large-bin-attack.md) or a [fast bin attack](fast-bin-attack.md) once you have a stable primitive.
> [!TIP]
> [https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#principle](https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#principle)에서 제공된 예제를 살펴보면, chunk 크기로 0x400과 0x500 대신 0x4000과 0x5000을 사용하면 **현재** 오류 **`malloc(): unsorted double linked list corrupted`**가 발생하는 것을 볼 수 있습니다.
> T> aking a look to the example provided in [https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#principle](https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#principle) and using 0x4000 and 0x5000 instead of 0x400 and 0x500 as chunk sizes (to avoid Tcache) it's possible to see that **nowadays** the error **`malloc(): unsorted double linked list corrupted`** is triggered.
>
> 따라서 이 unsorted bin 공격은 이제 (다른 체크와 함께) 이중 연결 리스트를 수정할 수 있어야 하며, `victim->bk->fd == victim`이거나 `victim->fd == av (arena)`가 아니어야 합니다. 이는 우리가 쓰고자 하는 주소가 가짜 chunk의 `fd` 위치에 가짜 chunk의 주소를 가져야 하며, 가짜 chunk의 `fd`가 arena를 가리켜야 함을 의미합니다.
> Therefore, this unsorted bin attack now (among other checks) also requires to be able to fix the doubled linked list so this is bypassed `victim->bk->fd == victim` or not `victim->fd == av (arena)`, which means that the address where we want to write must have the address of the fake chunk in its `fd` position and that the fake chunk `fd` is pointing to the arena.
> [!CAUTION]
> 이 공격은 unsorted bin을 손상시킵니다(따라서 small과 large도 마찬가지입니다). 따라서 이제 **fast bin에서 할당만 사용할 수 있습니다**(더 복잡한 프로그램은 다른 할당을 수행하고 충돌할 수 있습니다), 이를 트리거하기 위해서는 **같은 크기를 할당해야 하며, 그렇지 않으면 프로그램이 충돌합니다.**
> Note that this attack corrupts the unsorted bin (hence small and large too). So we can only **use allocations from the fast bin now** (a more complex program might do other allocations and crash), and to trigger this we must **allocate the same size or the program will crash.**
>
> **`global_max_fast`**를 덮어쓰는 것이 이 경우에 도움이 될 수 있으며, fast bin이 exploit이 완료될 때까지 모든 다른 할당을 처리할 수 있다고 믿습니다.
> Note that overwriting **`global_max_fast`** might help in this case trusting that the fast bin will be able to take care of all the other allocations until the exploit is completed.
[**guyinatuxedo**](https://guyinatuxedo.github.io/31-unsortedbin_attack/unsorted_explanation/index.html)의 코드는 이를 매우 잘 설명하고 있지만, mallocs를 수정하여 Tcache에 끝나지 않도록 충분히 큰 메모리를 할당하면 앞서 언급한 오류가 발생하여 이 기술을 방지하는 것을 볼 수 있습니다: **`malloc(): unsorted double linked list corrupted`**
The code from [**guyinatuxedo**](https://guyinatuxedo.github.io/31-unsortedbin_attack/unsorted_explanation/index.html) explains it very well, although if you modify the mallocs to allocate memory big enough so don't end in a Tcache you can see that the previously mentioned error appears preventing this technique: **`malloc(): unsorted double linked list corrupted`**
### 실제 쓰기가 발생하는 방식
- Unsortedbin 쓰기는 free 시, 해제된 청크가 unsorted 리스트의 머리에 삽입될 때 트리거됩니다.
- 삽입 과정에서 allocator는 `bck = unsorted_chunks(av); fwd = bck->fd; victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;` 를 수행합니다.
- 만약 `free(victim)`을 호출하기 전에 `victim->bk``(mchunkptr)(TARGET - 0x10)`으로 설정할 수 있다면, 마지막 문장이 쓰기를 수행합니다: `*(TARGET) = victim`.
- 이후 allocator가 unsorted bin을 처리할 때, 무결성 검사로 `bck->fd == victim``victim->fd == unsorted_chunks(av)` 같은 검사들이 실행되며, 그렇지 않으면 `malloc(): unsorted double linked list corrupted`로 abort합니다. 삽입 시 이미 `bck->fd`(우리의 `TARGET`)에 `victim`을 썼기 때문에, 이 쓰기가 성공했다면 이러한 검사들은 만족될 수 있습니다.
## 최신 제약 (glibc ≥ 2.33)
현재 glibc에서 unsortedbin 쓰기를 신뢰성 있게 사용하려면:
- Tcache 간섭: tcache에 해당하는 사이즈의 경우 free가 거기로 우회되어 unsorted bin에 도달하지 않습니다. 따라서
- 요청을 MAX_TCACHE_SIZE보다 큰 사이즈로 하거나(기본적으로 64비트에서 ≥ 0x410),
- 해당 tcache bin을 채워(7개 항목) 추가 free가 global bin으로 가게 하거나,
- 환경을 제어할 수 있다면 tcache를 비활성화(예: GLIBC_TUNABLES glibc.malloc.tcache_count=0)하세요.
- Unsorted 리스트에 대한 무결성 검사: 다음 할당 경로에서 unsorted bin을 검사할 때 glibc는 (단순화하여) 다음을 확인합니다:
- `bck->fd == victim``victim->fd == unsorted_chunks(av)`; 그렇지 않으면 `malloc(): unsorted double linked list corrupted`로 abort합니다.
- 이는 목표로 하는 주소가 두 번의 쓰기를 견뎌야 함을 의미합니다: 먼저 free 시 `*(TARGET) = victim`이 쓰이고; 그 후 청크가 제거될 때 allocator가 `*(TARGET) = unsorted_chunks(av)`로 다시 씁니다. `bck->fd`를 bin 머리로 되돌리기 때문입니다. 단순히 큰 비영(非0) 값을 강제하는 것만으로도 유용한 타깃을 선택하세요.
- 현대 익스플로잇에서의 일반적인 안정적 타깃
- 애플리케이션 또는 전역 상태에서 "큰" 값을 플래그/한계로 처리하는 곳.
- 간접적인 프리미티브(예: 이후의 [fast bin attack]({{#ref}}fast-bin-attack.md{{#endref}})을 위한 준비 또는 이후의 writewhatwhere를 피벗할 수 있는 준비).
- 새로운 glibc에서 `__malloc_hook`/`__free_hook`은 2.34에서 제거되었으므로 피하세요. `global_max_fast`는 ≥ 2.39에서 피하세요(다음 노트 참조).
- `global_max_fast`에 관하여(최근 glibc)
- glibc 2.39+에서는 `global_max_fast`가 8비트 전역으로 변경되었습니다. 여기에 포인터를 무분별하게 쓰면 인접한 libc 데이터가 훼손되고 fastbin 한계를 신뢰성 있게 올릴 수 없습니다. 다른 전략을 권장합니다.
## 최소 익스플로잇 절차 (최신 glibc)
목표: unsortedbin 삽입 프리미티브를 사용하여 임의의 주소에 힙 포인터 하나를 안정적으로 쓰는 것(프로그램을 크래시시키지 않음).
- Layout/grooming
- tcache를 우회할 만큼 충분히 큰 사이즈로 A, B, C를 할당(예: 0x5000). C는 top chunk와의 합쳐짐(consolidation)을 방지합니다.
- Corruption
- A에서 B의 청크 헤더로 오버플로우하여 `B->bk = (mchunkptr)(TARGET - 0x10)`으로 설정합니다.
- Trigger
- `free(B)`를 수행합니다. 삽입 시 allocator는 `bck->fd = B`를 실행하므로, 결과적으로 `*(TARGET) = B`가 됩니다.
- Continuation
- 계속해서 할당을 진행할 계획이고 프로그램이 unsorted bin을 사용한다면, allocator가 나중에 `*(TARGET) = unsorted_chunks(av)`로 다시 쓸 것임을 예상하세요. 두 값 모두 일반적으로 큰 값이며, "큰" 값만 확인하는 타깃의 크기/한계 의미론을 변경하기에 충분할 수 있습니다.
Pseudocode skeleton:
```c
// 64-bit glibc 2.352.38 style layout (tcache bypass via large sizes)
void *A = malloc(0x5000);
void *B = malloc(0x5000);
void *C = malloc(0x5000); // guard
// overflow from A into Bs metadata (prev_size/size/.../bk). You must control B->bk.
*(size_t *)((char*)B - 0x8) = (size_t)(TARGET - 0x10); // write fake bk
free(B); // triggers *(TARGET) = B (unsorted-bin insertion write)
```
> [!NOTE]
> • If you cannot bypass `tcache` with size, fill the `tcache` bin for the chosen size (7 frees) before freeing the corrupted chunk so the free goes to `unsorted`.
> • If the program immediately aborts on the next allocation due to `unsorted-bin` checks, reexamine that `victim->fd` still equals the bin head and that your `TARGET` holds the exact `victim` pointer after the first write.
## Unsorted Bin Infoleak Attack
사실 이것은 매우 기본적인 개념입니다. unsorted bin의 chunk는 포인터를 가질 것입니다. unsorted bin의 첫 번째 chunk는 실제로 **`fd`**와 **`bk`** 링크가 **주요 arena (Glibc)**의 일부를 가리키게 됩니다.\
따라서 unsorted bin에 chunk를 **넣고 읽거나** (use after free) **포인터 중 적어도 하나를 덮어쓰지 않고 다시 할당**하여 **읽으면**, **Glibc 정보 유출**을 얻을 수 있습니다.
이것은 사실 매우 기본적인 개념이다. `unsorted bin`에 있는 청크들은 포인터를 갖고 있다. `unsorted bin`의 첫 번째 청크는 실제로 **`fd`**와 **`bk`** 링크가 **main arena (Glibc)의 일부를 가리키게 된다**.\
따라서, 만약 당신이 **청크를 `unsorted bin`에 넣고 그것을 읽을 수 있다**(use after free)거나 **적어도 하나의 포인터를 덮어쓰기하지 않고 다시 할당해서** 읽을 수 있다면, **Glibc info leak**을 얻을 수 있다.
이 [**writeup에서 사용된 유사한 공격**](https://guyinatuxedo.github.io/33-custom_misc_heap/csaw18_alienVSsamurai/index.html)은 4개의 chunk 구조(A, B, C 및 D - D는 top chunk와의 통합을 방지하기 위해서만 사용됨)를 악용하여 B에서 null byte overflow를 사용하여 C가 B가 사용되지 않았음을 나타내도록 했습니다. 또한 B에서 `prev_size` 데이터를 수정하여 크기가 B의 크기 대신 A+B가 되도록 했습니다.\
그런 다음 C가 해제되고 A+B와 통합되었지만 B는 여전히 사용 중이었습니다. 크기 A의 새 chunk가 할당된 후 libc 유출 주소가 B에 기록되어 유출되었습니다.
유사한 [**attack used in this writeup**](https://guyinatuxedo.github.io/33-custom_misc_heap/csaw18_alienVSsamurai/index.html)에서는 4개 청크 구조(A, B, C, D - D는 top chunk와의 consolidation을 방지하기 위한 것)를 남용했다. B에서의 null byte overflow를 이용해 C가 B가 미사용이라고 표시하도록 만들었고, 또한 B의 `prev_size` 데이터를 수정해 B의 크기 대신 A+B가 되도록 했다.\
후 C가 해제되어 A+B와 통합되었고(하지만 B는 여전히 사용 중이었다), 크기 A의 새 청크가 할당되었고 libc leaked addresses가 B에 쓰여져서 거기서 유출되었다.
## References & Other examples
- [**https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#hitcon-training-lab14-magic-heap**](https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/unsorted_bin_attack/#hitcon-training-lab14-magic-heap)
- 목표는 4869보다 큰 값으로 전역 변수를 덮어쓰는 것이므로 플래그를 얻을 수 있으며 PIE는 활성화되지 않습니다.
- 임의의 크기의 chunk를 생성할 수 있으며 원하는 크기로 힙 오버플로우가 발생합니다.
- 공격은 3개의 chunk를 생성하는 것으로 시작됩니다: chunk0는 오버플로우를 악용하고, chunk1은 오버플로우되며, chunk2는 top chunk가 이전 chunk와 통합되지 않도록 합니다.
- 그런 다음 chunk1이 해제되고 chunk0가 chunk1의 `bk` 포인터를 가리키도록 오버플로우됩니다: `bk = magic - 0x10`
- 그런 다음 chunk1과 동일한 크기로 chunk3가 할당되어 unsorted bin 공격이 트리거되고 전역 변수를 수정하여 플래그를 얻을 수 있게 됩니다.
- The goal is to overwrite a global variable with a value greater than 4869 so it's possible to get the flag and `PIE` is not enabled.
- It's possible to generate chunks of arbitrary sizes and there is a `heap overflow` with the desired size.
- The attack starts creating 3 chunks: `chunk0` to abuse the overflow, `chunk1` to be overflowed and `chunk2` so top chunk doesn't consolidate the previous ones.
- Then, `chunk1` is freed and `chunk0` is overflowed to the `bk` pointer of `chunk1` points to: `bk = magic - 0x10`
- Then, `chunk3` is allocated with the same size as `chunk1`, which will trigger the `unsorted bin` attack and will modify the value of the global variable, making possible to get the flag.
- [**https://guyinatuxedo.github.io/31-unsortedbin_attack/0ctf16_zerostorage/index.html**](https://guyinatuxedo.github.io/31-unsortedbin_attack/0ctf16_zerostorage/index.html)
- merge 함수는 두 인덱스가 동일할 경우 재할당하고 해제하여 해제된 영역에 대한 포인터를 반환하므로 취약합니다.
- 따라서 **2개의 chunk가 생성됩니다**: **chunk0**는 자기 자신과 병합되고 chunk1은 top chunk와의 통합을 방지합니다. 그런 다음 **merge 함수가 chunk0**에 대해 두 번 호출되어 use after free가 발생합니다.
- 그런 다음 **`view`** 함수가 인덱스 2(사용 후 해제된 chunk의 인덱스)로 호출되어 **libc 주소를 유출**합니다.
- 바이너리에 **`global_max_fast`**보다 큰 크기만 malloc할 수 있는 보호가 있으므로 fastbin이 사용되지 않고 unsorted bin 공격이 사용되어 전역 변수 `global_max_fast`를 덮어씁니다.
- 그런 다음 인덱스 2(사용 후 해제된 포인터)로 edit 함수를 호출하고 `bk` 포인터를 `p64(global_max_fast-0x10)`를 가리키도록 덮어씁니다. 그런 다음 새 chunk를 생성하면 이전에 손상된 해제 주소(0x20)를 사용하여 **unsorted bin 공격**이 트리거되어 `global_max_fast`를 매우 큰 값으로 덮어씁니다. 이제 fast bin에서 chunk를 생성할 수 있습니다.
- 이제 **fast bin 공격**이 수행됩니다:
- 우선 **`__free_hook`** 위치에서 크기 200의 fast **chunk**로 작업할 수 있음을 발견합니다:
- The `merge` function is vulnerable because if both indexes passed are the same one it'll `realloc` on it and then `free` it but returning a pointer to that freed region that can be used.
- Therefore, **2 chunks are created**: **`chunk0`** which will be merged with itself and `chunk1` to prevent consolidating with the top chunk. Then, the **`merge` function is called with `chunk0`** twice which will cause a `use after free`.
- Then, the **`view`** function is called with index 2 (which the index of the use after free chunk), which will **leak a libc address**.
- As the binary has protections to only `malloc` sizes bigger than **`global_max_fast`** so no `fastbin` is used, an `unsorted bin` attack is going to be used to overwrite the global variable `global_max_fast`.
- Then, it's possible to call the edit function with the index 2 (the use after free pointer) and overwrite the `bk` pointer to point to `p64(global_max_fast-0x10)`. Then, creating a new chunk will use the previously compromised free address (0x20) will **trigger the `unsorted bin` attack** overwriting the `global_max_fast` which a very big value, allowing now to create chunks in `fast bins`.
- Now a **fast bin attack** is performed:
- First of all it's discovered that it's possible to work with fast **chunks of size 200** in the **`__free_hook`** location:
- <pre class="language-c"><code class="lang-c">gef➤ p &__free_hook
$1 = (void (**)(void *, const void *)) 0x7ff1e9e607a8 <__free_hook>
gef➤ x/60gx 0x7ff1e9e607a8 - 0x59
@ -59,16 +115,20 @@ gef➤ x/60gx 0x7ff1e9e607a8 - 0x59
0x7ff1e9e6076f <list_all_lock+15>: 0x0000000000000000 0x0000000000000000
0x7ff1e9e6077f <_IO_stdfile_2_lock+15>: 0x0000000000000000 0x0000000000000000
</code></pre>
- 이 위치에서 크기 0x200의 fast chunk를 얻으면 실행될 함수 포인터를 덮어쓸 수 있습니다.
- 이를 위해 크기 `0xfc`의 새 chunk가 생성되고 병합 함수가 그 포인터로 두 번 호출되어 fast bin에서 크기 `0xfc*2 = 0x1f8`의 해제된 chunk에 대한 포인터를 얻습니다.
- 그런 다음 이 chunk의 edit 함수를 호출하여 이 fast bin의 **`fd`** 주소를 이전 **`__free_hook`** 함수로 가리키도록 수정합니다.
- 그런 다음 크기 `0x1f8`의 chunk가 생성되어 fast bin에서 이전의 쓸모없는 chunk를 가져오고, 또 다른 크기 `0x1f8`의 chunk가 생성되어 **`__free_hook`**에서 fast bin chunk를 가져오고, 이 chunk는 **`system`** 함수의 주소로 덮어씌워집니다.
- 마지막으로 문자열 `/bin/sh\x00`을 포함하는 chunk가 delete 함수를 호출하여 해제되어 **`__free_hook`** 함수가 호출되고, 이 함수는 `/bin/sh\x00`을 매개변수로 하여 system을 가리킵니다.
- If we manage to get a fast chunk of size `0x200` in this location, it'll be possible to overwrite a function pointer that will be executed
- For this, a new chunk of size `0xfc` is created and the merged function is called with that pointer twice, this way we obtain a pointer to a freed chunk of size `0xfc*2 = 0x1f8` in the fast bin.
- Then, the edit function is called in this chunk to modify the **`fd`** address of this fast bin to point to the previous **`__free_hook`** function.
- Then, a chunk with size `0x1f8` is created to retrieve from the fast bin the previous useless chunk so another chunk of size `0x1f8` is created to get a fast bin chunk in the **`__free_hook`** which is overwritten with the address of **`system`** function.
- And finally a chunk containing the string `/bin/sh\x00` is freed calling the delete function, triggering the **`__free_hook`** function which points to `system` with `/bin/sh\x00` as parameter.
- **CTF** [**https://guyinatuxedo.github.io/33-custom_misc_heap/csaw19_traveller/index.html**](https://guyinatuxedo.github.io/33-custom_misc_heap/csaw19_traveller/index.html)
- 1B 오버플로우를 악용하여 unsorted bin에서 chunk를 통합하고 libc 정보 유출을 얻은 다음 fast bin 공격을 수행하여 malloc hook을 one gadget 주소로 덮어쓰는 또 다른 예입니다.
- Another example of abusing a 1B overflow to consolidate chunks in the `unsorted bin` and get a libc infoleak and then perform a fast bin attack to overwrite malloc hook with a one gadget address
- [**Robot Factory. BlackHat MEA CTF 2022**](https://7rocky.github.io/en/ctf/other/blackhat-ctf/robot-factory/)
- 우리는 `0x100`보다 큰 크기의 chunk만 할당할 수 있습니다.
- Unsorted Bin 공격을 사용하여 `global_max_fast`를 덮어씁니다(ASLR로 인해 1/16의 확률로 작동하며, 12비트를 수정해야 하지만 16비트를 수정해야 합니다).
- 전역 chunk 배열을 수정하기 위한 Fast Bin 공격. 이는 임의의 읽기/쓰기를 가능하게 하여 GOT를 수정하고 일부 함수를 `system`을 가리키도록 설정할 수 있습니다.
- We can only allocate chunks of size greater than `0x100`.
- Overwrite `global_max_fast` using an `Unsorted Bin` attack (works 1/16 times due to ASLR, because we need to modify 12 bits, but we must modify 16 bits).
- Fast Bin attack to modify the a global array of chunks. This gives an arbitrary read/write primitive, which allows to modify the GOT and set some function to point to `system`.
## References
- Glibc malloc `unsorted-bin` integrity checks (example in 2.33 source): https://elixir.bootlin.com/glibc/glibc-2.33/source/malloc/malloc.c
- `global_max_fast` 및 관련 정의 in modern `glibc` (2.39): https://elixir.bootlin.com/glibc/glibc-2.39/source/malloc/malloc.c
{{#include ../../banners/hacktricks-training.md}}