# Heap Exploitation

### 前言

笔者最近跟学委吴一起接了软件安全测试中的 Heap Exploitation Project，借此 blog 梳理学习思路。

### 什么是堆？

借 stackoverflow & geeksforgeeks 的回答，先大致了解一下堆是个什么东西，与我们所熟悉的栈又有怎样的区别和联系。

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2Fe7k1Aah5v45EcTw5YIQo%2Fimage.png?alt=media&#x26;token=08c670f7-beb1-4ece-ae44-9b49d8819da1" alt=""><figcaption><p><a href="https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap">https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap</a></p></figcaption></figure>

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FF7b4slUvMz3VE7qFReVC%2Fimage.png?alt=media&#x26;token=692a2921-fd74-494c-84d1-1eae3ec2ff7e" alt=""><figcaption><p><a href="https://www.geeksforgeeks.org/stack-vs-heap-memory-allocation/">https://www.geeksforgeeks.org/stack-vs-heap-memory-allocation/#</a></p></figcaption></figure>

通过以上两张图片，相信读者已经能建立起对堆的初步印象。接下来就可以愉快地讨论堆的实现方式与利用。再次声明，以下讨论的堆，均指**运行内存中的堆区域**，该区域由程序员手动申请、手动释放，被同一个进程中的所有线程全局共享。

### 堆的实现有哪些？

从古到今，笔者了解到的堆实现方式有以下七种：

```
dlmalloc  – General purpose allocator
ptmalloc2 – glibc 
jemalloc  – FreeBSD and Firefox
tcmalloc  – Google
libumem   – Solaris
nedmalloc
Hoar
```

堆多种的实现方式，也间接提示我们 heap exploitation 不是件轻松活。那么 heap exploitation 究竟难在什么地方呢？

> Heap-based vulnerabilities can be very dependent on how the internal implementation of the heap allocator actually works.

因此为了“不耍流氓”，定义讨论的范围为采用 **ptmalloc2** 的 **glibc**，接下来让我们去一睹 glibc 堆管理的实现吧！

### glibc 实现

#### chunk 介绍

堆的最小单元为 chunk，由 metadata 与 userdata 组成。metadata 指下图中的 **PREV\_SIZE & CHUNK SIZE**，用于记录 userdata 的元信息，方便 glibc 进行内存管理。

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FJHrV8oTiTdalpZJFF84Q%2Fimage.png?alt=media&#x26;token=e7502d07-1c84-4bd8-81c2-d78921bc514f" alt=""><figcaption><p>glibc 对 chunk 的介绍</p></figcaption></figure>

metadatas 中 PREV\_SIZE 的作用：

1. 给前一块 chunk 作为 userdata 存储数据使用（见 Fig.2）；
2. 在 bin 中表示前项 chunk 的大小。

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2Fn7VllUxCklXYoQwNITJy%2Fimage.png?alt=media&#x26;token=96918f7a-2dfb-4258-98a7-0ef4949f9733" alt=""><figcaption><p>chunk 示意图</p></figcaption></figure>

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FYhZO6zoGvQSmXLYR7m4Y%2Fimage.png?alt=media&#x26;token=6b8d5748-4e57-4cb3-a8c0-3c92d3907d1d" alt="" width="375"><figcaption><p>Fig.2</p></figcaption></figure>

metadata 中 CHUNK SIZE 记录了整个 chunk 的大小，其计算方式如下图所示。需要注意的是，glibc 中规定了 alignment 是 double word，即在 32bits 机器中要求 8bytes 地址对齐，64bits 机器中要求 16bytes 地址对齐。

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FRP4EiCgxcO6AdiD2mOOq%2Fimage.png?alt=media&#x26;token=94611959-47e5-464e-9dc4-922bafc63e57" alt=""><figcaption><p>32bits 中的 chunk 组成</p></figcaption></figure>

#### chunk 释放策略与细节

glibc 对于被 free 掉的 chunk 都丢给了“小弟”——bin去管理。bin 要考虑的问题是：该把这块 chunk 改造成什么样，丢到哪里去，才方便后续分配嘞？

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2Fpy3Lr6rTKZfIurXMa0HJ%2Fimage.png?alt=media&#x26;token=6b55ca5e-16b5-4c4f-8fb2-1919ea8b64c0" alt=""><figcaption><p>chunk 在 bin 中的样子</p></figcaption></figure>

bin 根据用途分为了五类：small bin、large bin、unsorted bin、fast bin、tcache bin。其中发挥基础作用的是 small bin 和 large bin，small bin 实现对 512bytes 以下的 chunk 管理，large bin 实现对 512bytes 及以上的 chunk 管理。unsorted bin、fast bin 和 tcache bin 充当 cache layer 的作用。具体可参考下图以及联想存储器多级缓存的架构理解：

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FYRLcpZtWdqVTKqT6uq1Q%2Fea24ea8df1743e04ae7c96a6437f521.jpg?alt=media&#x26;token=498a525d-2115-4541-a8d2-80c97f6a7fa8" alt="" width="375"><figcaption></figcaption></figure>

**先从高空俯瞰一下 chunk 释放的策略：**

> if pointer is NULL “do nothing” else convert the pointer back to a chunk
>
> → Perform sanity checks on the chunk, and abort when fail
>
> → chunk fits into a *tcache* bin, store it there.
>
> → chunk has the *M* bit set, give it back to the os via *munmap*.
>
> → Otherwise we obtain the arena heap lock and then:&#x20;
>
> 1. fits into a fastbin, put it on the corresponding fastbin, and we’re done.
> 2. size > 64KB, consolidate the fastbins, put the resulting merged chunks → unsorted bin.
> 3. Merge the chunk with neighboring freed chunks in the small, large, and unsorted bins.
> 4. Merge the result chunk if neighboring the top chunk.
> 5. Otherwise store it in the *unsorted bin*

第一次看到这些策略，肯定是一头雾水，这时候就需要结合细节去理解。

**sanity checks：**

> 1. A check that the allocation [is aligned](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4182) on an 8-byte (or 16-byte on 64-bit) boundary, since *malloc* ensures all allocations are aligned.
> 2. A check that the chunk’s size field isn’t impossible–either because it is [too small](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4318), too large, not an aligned size, or [would overlap the end of the process’ address space](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4175).
> 3. A check the chunk lies [within the boundaries of the arena](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4318).
> 4. A check that the chunk is [not already marked as free](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob;f=malloc/malloc.c;h=6e766d11bc85b6480fa5c9f2a76559f8acf9deb5;hb=HEAD#l4182) by checking the corresponding “P” bit that lies in the metadata at the start of the next chunk.

**五类 bin 的具体实现：**

* bin 的整体管理是将各类 bin 放到一个数组中，包括 62 small bins, 63 large bins, 1 unsorted bin, 10 fast bins and 64 tcache bins per thread

  <figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FOVeB4TKpnR7w3UMSKTFP%2Fimage.png?alt=media&#x26;token=5e724002-8a9e-4517-b29e-756f66348a8d" alt=""><figcaption></figcaption></figure>
* small bin

  * *Every chunk less than 512 bytes on 32-bit systems (or than 1024 bytes on 64-bit systems) has a corresponding small bin. (the size of allocated memory is stable)*&#x20;

  <figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FQw53xnl5eQQRmPu1Zm2L%2Fimage.png?alt=media&#x26;token=716db596-c3b2-444c-adb9-712aef152b6c" alt=""><figcaption><p>small bin</p></figcaption></figure>
* large bin

  * 基于用户分配的空间趋向于小块的事实，large bin 倾向于作为 small bin 的补充。注意下图中 BETWEEN 是表 AMONG 的意思。

  <figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2F6kBdFVX63swVi2nR4Psh%2Fimage.png?alt=media&#x26;token=8b5930a3-6599-4724-b242-3e837ee58231" alt=""><figcaption><p>large bin</p></figcaption></figure>
* unsorted bin

  * cluster free & malloc 聚集在某一个时间段时，将其放入 unsorted bin，即拿即用。

  <figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2F7nFzniefZ8LuAfTJHNeX%2Fimage.png?alt=media&#x26;token=1762db80-33c8-4982-8ca0-0b9db7f6ef17" alt="" width="190"><figcaption><p>unsorted bin</p></figcaption></figure>
* fast bin

  * 为了减少内存碎片对系统的影响，fast bin 用于管理程序释放掉的较小的内存块（小于 88bytes）。

  <figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FCKXXSKEcF018fyPqU9JH%2Fimage.png?alt=media&#x26;token=1c94d957-f8d9-417d-a24a-160858019a52" alt=""><figcaption><p>fast bin</p></figcaption></figure>
* tcache bin

  * 因为堆是全局共享的，对于多线程的情况，则不可避免会遇上需要等待别的线程释放锁，才能申请资源的情况。为了避免此类开销，设计了 tcache。
  * *Each bin contains a maximum of* [*7 same-size chunks*](https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=2527e2504761744df2bdb1abdc02d936ff907ad2;hb=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc#l323) *ranging from* [*24 to 1032 bytes on 64-bit systems and 12 to 516 bytes on 32-bit systems*](https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=2527e2504761744df2bdb1abdc02d936ff907ad2;hb=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc#l315)*.*

  <figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2F9mP6iTJkzgLSo6CIOOao%2Fimage.png?alt=media&#x26;token=4b38a2ad-6c95-4d1a-976b-c559ef004191" alt=""><figcaption><p>tcache bin</p></figcaption></figure>

#### chunk 分配策略与细节

为了避免频繁的系统调用，在程序第一次 malloc 过后（即便申请很小的内存，如 malloc(0)），os 会分配一块 0x21000 大小的内存给 glibc，并拍了拍 glibc 的肩膀说，“小老弟，拿去耍吧。这些应该足够你应付来申请内存的小鬼了，如果实在不够再来找我”。glibc 捧着这块 0x21000 大小的内存，屁颠屁颠得开始了它的打工生活——替 os 处理内存申请与释放的工作。

**老规矩，先瞅一眼 chunk 分配的策略：**

> tcache&#x20;
>
> → mmap
>
> → obtain arena heap lock → fast bin → small bin
>
> → unsorted bin，if suitable stop，otherwise put into small / large bin
>
> → large bin
>
> → merge unsorted bin、fast bin、small bin、large bin
>
> → sbrk&#x20;
>
> → mmap
>
> → return NULL

想必你初见觉得晦涩难懂。没事，咱们 step by step 来瞅瞅看这究竟是怎么回事。

**chunk 分配机制**

1. *If there is a previously-freed chunk of memory, and that chunk is big enough to service the request, the heap manager will use that freed chunk for the new allocation.*                                                  ![](https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FaXBAkguOFCUx8ICMraNl%2Fbins-simple.png?alt=media\&token=83236ea5-5365-4e3b-a130-f867833ab564)
2. *Otherwise, if there is available space at the top of the heap, the heap manager will allocate a new chunk out of that available space and use that.*     ![](https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2F8GnQCngkesss1FVmCcgv%2Fheap-chunks-top.gif?alt=media\&token=0582d99c-a82f-4e0a-88ad-7191418bda54)
3. *Otherwise, the heap manager will ask the kernel to add new memory to the end of the heap, and then allocates a new chunk from this newly allocated space.* ![](https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FW9kYiVpQi9MmOxSvwYaY%2Fprocess-memory-heap-gif-1.gif?alt=media\&token=12273b06-320f-4350-977f-b071d390b228)
4. *`mmap` provides greater flexibility, can map files or anonymous memory, and can map memory to non-contiguous addresses. This helps overcome some limitations of `brk`, such as heap fragmentation.*                                                                           ![](https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FIHI2Wx4khQO2ivxqqST2%2Fheap-arenas-CS.png?alt=media\&token=cfdc01f2-1ba2-43fb-afc7-925378e0fbde)
5. *If all these strategies fail, the allocation can’t be serviced, and malloc returns NULL.*

### UAF

有了以上的基础，就可以愉快尝试一下 heap exploitation。笔者近期了解到的 heap exploitation 如下：

* **Heap Overflow（堆溢出）：** 攻击者通过向动态分配的堆内存写入超出分配的内存空间的数据，覆盖相邻内存的内容，可能导致程序崩溃或执行恶意代码。
* **Use-After-Free（释放后使用）：** 攻击者利用程序中已经释放的堆内存块，尝试重新使用这些内存块来执行恶意操作。
* **Double Free（重复释放）：** 这种攻击涉及重复释放相同的堆内存块，从而可能导致内存管理数据结构被破坏，以及潜在的漏洞利用机会。
* **Off-by-One Error（溢出一个字节）：** 攻击者试图溢出一个字节的内存，通常用于更细微的堆利用攻击。
* **Fastbin Attack（快速分配块攻击）：** 这种攻击针对 glibc 中的 fastbins，攻击者试图伪造 fastbin 链表，以便获取堆上的未分配内存块。
* **Tcache Attack（Tcache攻击）：** 这种攻击利用glibc 的 tcache 机制，攻击者试图伪造 tcache 链表，以获取未分配内存块。
* **Unlink Attack（取消链接攻击）：** 这种攻击涉及伪造一个被取消链接的堆块，以触发漏洞利用机会。
* **Poison Null Byte Attack（毒零字节攻击）：** 这种攻击利用零字节终止字符串，攻击者试图修改内存块的大小字段，以实现漏洞利用。

以下借 ctf-wiki 的[示例](https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/use-after-free/)介绍 **Use-After-Free(UAF)**。

UAF 的实现机理是观察堆的分配与释放，构造合法的内存写入，利用 dangling pointer 访问原先分配的地址，通过这种未定义行为发起恶意攻击（如劫持控制流），其本质是如下 L4 所述的 “arbitrary effects”。

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FbcpYxLZkQYM3pn5MRT5y%2Fimage.png?alt=media&#x26;token=2ae073d9-e04e-43de-96b4-15057bf96ac7" alt=""><figcaption><p>free in malloc.c</p></figcaption></figure>

示例[源码](https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/use_after_free/hitcon-training-hacknote)可通过链接获取，注意将 `Makefile` 修改成如下所示，将 PIE 保护关闭。温馨提示： `exp.py`中的 magic 变量值是通过反汇编（如，将可执行文件拖入 IDA） ，查看函数的起始位置得到的。其它具体的实现过程参照示例即可。

```makefile
hacknote:hacknote.c
        gcc hacknote.c -m32 -no-pie -fstack-protector -o hacknote 
```

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2Fo9Q4JLFSolFsszs0eOLv%2Fimage.png?alt=media&#x26;token=eb3f0f7d-ae79-43e8-b9cb-c80d6acbd56c" alt=""><figcaption></figcaption></figure>

示例代码架构与利用步骤如下：

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FF4QU8DbzgbhyaOqHyUH3%2Fimage.png?alt=media&#x26;token=0a2327ac-a687-47e5-862b-cf8fa999970f" alt=""><figcaption></figcaption></figure>

UAF 在此题的体现：首先申请两个 node，然后删除两个 node。两个 node 共计四块 chunk 被加入到 tcache 中管理。此时再申请一个新的 node，如果申请的 size 得当，glibc 就会将 node0 与 node1 的两块 **printnote** 分配给新的 node 使用，注意到 printnote 是一个函数指针！此时 content2 是从标准输入读入的，即可以被用户所控制的。那么先往 content2 写入 magic 函数地址，再利用 node0（此时为 dangling pointer）访问 content2 这块区域，RIP 就会访问 magic，成功实现控制流的劫持。&#x20;

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FfuHU6xH9ldm9xxZJhDhn%2Fimage.png?alt=media&#x26;token=e805dc2c-dc08-4636-99b1-b6c108b3ca5b" alt=""><figcaption></figcaption></figure>

### **附：Linux kernel heap feng shui in 2022 学习笔记**

*该文内容较杂，因此采用分点记录的方式。*

1. 该文总结了 allocator 的实现机制对堆利用的影响；
2. kernel 中两种常见的动态分配机制如下:
   1. General-purpose allocations performed using `kmalloc/kzalloc/...` API
   2. Special-purpose allocations via `kmem_cache_create/kmem_cache_alloc`
3. Slab 是 Linux 内核中的一种内存分配机制，用于高效地管理内核数据结构和对象的内存分配和释放。Slab 分配器的主要目标是减少内存分配和释放的开销，以提高系统性能。**Slab 分配器通过预分配一定大小的内存块并将它们组织成 slabs，然后在需要时分配这些内存块，从而减少了内存碎片和频繁的内存分配操作。**&#x53;lab 分配器通常管理多个 slab 缓存，每个缓存用于不同大小的内存块。这些缓存通常包括三种类型：
   1. Slab缓存：用于保存已分配的内存块。
   2. Slab未分配缓存：用于保存已分配但尚未使用的内存块，以便后续分配。
   3. Slab空闲缓存：用于保存完全未使用的内存块。
4. **对齐问题的潜在利用**：攻击者可以利用对齐问题来修改数据结构的大小，进而改变其行为。例如，他们可能故意构造一个对象，以便它与某个通用缓存别名化，从而可以操纵通用缓存并导致不安全操作。但安全机制近年来也在不断地更新：
   * `cred_jar` 在 kernel version > 4.4 的情况下加入 SLAB\_ACCOUNT 标签，使得其 standalone 免受其攻击。
   * 安全的函数（`copy_to_user` / `copy_from_user`）被逐步引入，更加严格的检测机制使漏洞利用难以下手。

     <figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FC5PvxlNevrckHbeKRpHI%2Fimage.png?alt=media&#x26;token=6d80dfba-2041-46d2-b17d-67f0779e00ba" alt="" width="497"><figcaption></figcaption></figure>
   * *A freelist pointer randomisation `CONFIG_SLAB_FREELIST_RANDOM` was introduced in 4.8 and is now enabled by default on most modern distributions.*

### References

1. [系统理解 Arm Heap Exploitation](https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/)
2. [Understanding glibc malloc](https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/)
3. [glibc malloc source code](https://github.com/iromise/glibc/blob/master/malloc/malloc.c)
4. [了解 linux kernel heap](https://www.blackhat.com/presentations/bh-usa-07/Ferguson/Whitepaper/bh-usa-07-ferguson-WP.pdf)
5. [对以往 linux heap 的研究总结](https://duasynt.com/blog/linux-kernel-heap-feng-shui-2022)
6. [ctfwiki-UAF](https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/use-after-free/)
7. [深入理解 heap exploitation](https://github.com/shellphish/how2heap)

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FtUZW4KjHBAzw5iIU1qfL%2Fimage.png?alt=media&#x26;token=bbd04f36-2072-42f6-b116-348d0872ad6a" alt=""><figcaption></figcaption></figure>

<figure><img src="https://4115421394-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FphkhzXjy1VCjm6rtX0Zs%2Fuploads%2FkdY4HHbiUmHImr0cCgiU%2Fimage.png?alt=media&#x26;token=bdcbce54-7049-48be-bcf0-8697149c3ea7" alt=""><figcaption><p>first contribution</p></figcaption></figure>
