Notes

Do not read codes but patch binary.

objtestについて

3月からお仕事でmalware解析やるので、役立ちそうなbinary解析ツールを作っている。

その中で、objtestというコマンドが割と自分の中でいいものができたなっという気がするので、 簡単に紹介。

qemuやllvmなど、大きなプロジェクトのソースを読む場合って、それの簡単なunit test書いたりするのすら一見、結構面倒だったりする。 静的にlinkした場合、ものによってはbuildがやたら遅くなったり、動的にlinkする場合でも、わざわざテストしたい.oを.soに替えてlinkし、.soの依存するlibraryを全部loadしなければならない。 大きなプロジェクト程、個々のobject file間の依存性が大きくなり、依存性が大きくなる程、小さな単位のテストが難しくなる。

そこで、build時に生成される静的relocation前のrelocatable objectを依存する関数をmock化することでテストすればいいんじゃないかっと前々から常々思っており、やってみたら、かなり小さい分量である程度の所まで作れた。(https://github.com/Hiroshi123/bin_tools/blob/master/src/tools/objtest.c)

発想としては、何かをcompileする。これは、cでもc++でもrustでもgoでもよい。そして、取り敢えず、relocation前のobjectが欲しい。relocation前とはtargetマシンの命令語の羅列なのだけど、calleeのアドレスが後で埋めて下さいっとブランクになっている形。

通常この後、実行binaryを作る過程で、linkerがこのblankを埋める作業を行うのだけれど、objtestは、その前のrelocatableなobject(test対象と, test & mock用の2つ)を使い、それを"強引に"実行する。"強引"な実行とはつまる所は以下のコード

  asm("call *%0" : : "r"(get_current_meta_addr));
  asm("mov %rax,%rdi");
  asm("call *%0" : : "r"(r));
  asm("mov %rax,%rbx");
  asm("call *%0" : : "r"(get_current_meta_addr));
  asm("mov %rbx,(%rax)");

部分的に書くと分かりにくいのだが、3行目のcallで変数rのアドレスに%eipを設定。このrには、object fileのsymbol tableから見つけた、テスト対象の関数が入る。 その前と後の部分は、テスト関数に、引数(%rdi)のポインタを与え(1,2行目)、テスト関数の戻り値(%rip)を獲得する(5,6行目)。 inline assemblerだと、変数とregister間の受け渡しが出来ないみたいなので、一回heapメモリに渡したい値を置いて、それで、特定のレジスタ(この場合,%rdiと%rax)を読み書きしている。

ところが、relocationしていないと、テスト対象の関数に制御を移したはいいものの、所々が0xe8 0x00 0x00 0x00 0x00みたいなままなので、0x00に%eipを移してしまい、場合によってはfailする。つまり、線路は大枠あるのだが、所々に橋が架かっていない上での見切り発車みたいな雰囲気。 というわけで、橋を架けるのが、relocationだが、ここで、全部正しく橋を架ける(別の正規なobject fileを持ってくる)と、多大な費用(build時間)が掛かるので、適当にでっちあげるのがmock化。 この作業なんだけど、基本的に、relocation前のobject fileが橋を架けてないlistを持っていて、その部分を"適当"に"つぎはぎ"して、その上で実行すればok!(この部分は興味があればcodeを読んで下さいm__m)

因みに、関数のmock化だけでなく、c++などobject指向言語のclassのinstanceのmock化も行える。c++のメンバ変数は、第一引数が、classへのpointerになっており、c言語でそいつを呼び出す場合は、そこを既にallocateされた領域の先頭にし、その先頭からobjectのサイズ分好きな値を書いて関数callの引数に渡せば、関数内のメンバ変数アクセスは全てそいつらを見に行く。

-----test subject ----

class A {
   int a = 1;
   void f1() {return a;}
}

---- test function----

int* a = 0x??;already allocated area
*a = 2;
f1(a); needs to be mangled in reality to discern to which class it belongs

この方法で、テストを書くと大変なのが、全てのcallee分だけmock関数を用意する処。しかし、それが面白い処でもまたある。

これが、個人的には、究極的に柔軟なソフトウェアのデバッグ手法だと思っている。