CTFするぞ

CTF以外のことも書くよ

angrをいろいろ使ってみる【yoshi-camp備忘録】

はじめに

yoshikingがセキュリティキャンプに落ち、私もチューターに落ちたのでチームinsecureと阪大からの2人ほどで08/17と08/18にyoshi-campという名の勉強会を開いています。 やる場所が定まらなくて困っていたのですが、つてを辿って阪大の良いお部屋をお借りすることができました。阪大の人々および教授のご厚意に感謝です! 今日はふるつきによるangr講座があり、主にCTFのrev問での使い方を演習をはさみつつ詳しく説明してもらいました。 講義では基本から応用までいろいろ扱ったのですが、眠たいので特に面白かった点だけまとめます。

angrで関数をフックする

フックには大きく分けて2パターンあって、関数の処理全体を変更したいときと、部分的に変更したり制約を加えたいときに使えるフックがあるようです。 (というかフック機能すら使ったことがなかった。) 関数の処理全体を変更できるフックは、特定の条件のときにフラグを出力してくれる、といったバイナリに対して非常に有効だと思います。 あとfindとかavoidは出力内容から決定できるという便利機能がありました。 例えば次のようなバイナリがあったとき、

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int main()
{
  char flag[32];
  scanf("%31s", flag);
  
  if (strcmp(flag, "flag{test}") == 0 && time(NULL) == 0xdeadbeef) {
    puts("Okay!");
  } else {
    puts("Nope!");
  }
  return 0;
}

こんな感じのコードで"Okay"まで到達できます。

import angr
import claripy
from logging import getLogger, WARN

getLogger("angr").setLevel(WARN + 1)

p = angr.Project("./crackme", load_options={"auto_load_libs": False})
state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)

class MyTime(angr.SimProcedure):
    def run(self, arg1):
        return claripy.BVV(0xefbeadde, 32)

def pokemon(state):
    if b"Okay" in state.posix.dumps(1):
        return True
    return False

def digimon(state):
    if b"Nope" in state.posix.dumps(1):
        return True
    return False

simgr.explore(find=pokemon, avoid=digimon)
p.hook_symbol("time", MyTime())

try:
    found = simgr.found[0]
    print(found.posix.dumps(0))
except IndexError:
    print("Not Found")

hook_symbol は特定のシンボルの関数が呼ばれた時のフックで、関数の処理全体を書き換えられます。 stripされているときは hook にしてアドレスを渡すといい感じにフックしてくれました。 このメソッドにはSimProcedureを継承したクラスのインスタンスを渡します。 runメソッドの戻り値は元の関数の戻り値なのですが、整数とかはBVVで渡さないといけない点と、BVVの値はビッグエンディアンで渡すという点に注意が必要です。 あと出力を見たいときは posix.dumps(1) のようにfdを変えます。

static linkとか

angrは中にlibcとかよく使われるライブラリの代替機能を備えており、libc関数が呼ばれるとそっちを利用することで高速化しているらしい。 実際、static linkするとリンクされた関数の中まで呼んでしまうので非常に遅くなる。 講義では次のように、(使われている)リンクされた関数とangr中の代替機能を繋げることで解析を大幅に高速化した。

p.hook_symbol("__libc_start_main", angr.SIM_PROCEDURES["glibc"]["__libc_start_main"]())
p.hook_symbol("printf", angr.procedures.libc.printf.printf())
p.hook_symbol("__isoc99_scanf", angr.procedures.libc.scanf.scanf())
p.hook_symbol("strcmp", angr.procedures.libc.strcmp.strcmp())
p.hook_symbol("puts", angr.procedures.libc.puts.puts())

stripされているときはhookを使えば良いと思う。

angrを殺す手法の考案

講義の中でangrの中の仕組みや解けない条件みたいなのをざっくり説明してもらったので、それを元にrev問をangrで解けなくする機構を考えました。 もちろん1日の講義中に考えたアイデアなので簡単に回避できますが、「脳死angrで解かせたくない」という時に使えるテクニックみたいな感じで。

とりあえず講義中に思いついたのが、PIE有効のときのangrの挙動を使った手法です。 PIEが有効なとき、angrは標準で0x400000をベースアドレスとします。 一方x64では通常もっと大きな値がベースアドレスとして取られるので、次のような関数 __anti_angr を適当な場所に入れておくことでangrで解けなくなります。

void __anti_angr()
{
  unsigned long pos;
  asm goto("call %l0;"::::tabi);
 tabi:
  asm volatile("popq %rax;");
  asm volatile("movq %%rax, %0": "=g"(pos));
  if (pos <= 0x500000) exit(1);
}

この手法はベースアドレスを変更するだけで回避できます。 プロセス間通信とか使ってプログラムを2つ起動してアドレスが変わってるか見れば強くなると思います。

なんか他にもいろんな方法を書きたかったですが、ねむねむなのでおやすみ......

その他

講義中に解決しなかった点とか

  • int3はangrでも落ちたので検出には使えなさそう。
  • ptrace系のdebugとかは上記と同様にangrで回避できる
  • Veritestingがよくわからない

さいごに

明日はマルウェア解析&フォレンジックの続きを講義するので頑張ります。