フロムスクラッチ開発者ブログ

from scratch Engineers' Blog

CTFのバイナリ解析から学ぶセキュリティとハッキング vol.1

 こんにちは、フロムスクラッチにこの春入社した新人エンジニアの遠藤です!

 フロムスクラッチでは、データを取り扱う会社として、定期的にセキュリティに関する社内勉強会を実施しております。今回はこの勉強会で得た学びを記事にしたいと思います。社内勉強会ではマイナビ出版の『セキュリティコンテストチャレンジブック CTFで学ぼう情報を守るための戦い方』を参考にしており、今回は「CTFのバイナリ解析から学ぶセキュリティとハッキング」というテーマでお話できればと思います。
 今回学びが非常に多く、分量が大きくなり過ぎたので、「CTFとバイナリ解析の基本」と「CTFのPwn問題の基本的な攻略法」の2回に分けて投稿していきます。

1.CTFとは?

 CTFとは情報技術に関する問題に対して適切な形で対処し、それに応じて得られた得点で勝敗を決める大会です。”Capture The Flag”の頭文字をとってCTFといいます。CTFでは得点となる答えの文字列のことを「フラグ(Flag)」と呼び、これを得ること で得点となるのが基本です。
 そこで、例えばこんなクイズが出るかもしれません。
 
 フラグはなんでしょう?

LRGM OY [LXUSYIXGZIN]


答え

FLAG IS [FROMSCRATCH]

 これはカエサル(シーザー)暗号といってアルファベットを特定の文字数ずらすことによる暗号化です。今回は6文字ずらして暗号化しています。

f:id:takuya-endo:20180420110954p:plain

 CTFではこのように暗号などを解いて答えを送る問題出題型の競技形式以外に、攻防型のものもあります。チームごとに与えられたシステムに存在する脆弱性や問題について攻防を行います。国内ではSECCON CTF(https://2017.seccon.jp/)が有名です。
 後者の攻防型の競技においては、いかにソースコードのない実行プログラムを解析するかであったり、いかにシステムの脆弱性を見つけて攻撃するかが重要になってきます。まさにセキュリティの勉強になりますね。

2.バイナリについて

 バイナリという言葉は「2値の、2進数の」といった意味を持つ英単語で、IT用語として端的に述べると「コンピュータが扱える0と1の羅列として表現されたデータや表現形式」を意味します。
 今回の勉強会では更に定義を狭めて、バイナリファイルを「コンピュータが実行可能な形式を持ったデータファイル」とし、そのなかでも実行バイナリ(または実行ファイル)を扱いました。
 ここでバイナリファイルとは、例えばコンパイラ言語であるC言語などのファイルから生成された実行ファイルにあたります。WindowsではPEファイル(.exe)、LinuxではELFファイルとして生成されます。このバイナリは、機械語で書かれているため人間には読むことのできないファイルになっています。
 実際にCTFではpwmという脆弱性を攻略することが中心となる問題分野があり、バイナリを解析する必要が出てきます。

3.バイナリ解析-基本

 ここからはpwnの問題バイナリ解析の簡単な流れに沿って進めていきたいと思います。
 pwn問題の流れとしては、

 1.環境を調べる(下調べ)
 2.脆弱性を探す
 3.エクスプロイト
 4.シェルの奪取もしくはフラグの読み出し

 この流れの中で、今回は1の下調べなどで扱う操作を紹介します。
 まずはバイナリに対してファイルの判別し、実行ファイルの詳細を知る必要があります。 file コマンドによって確認を行うことができます。

$ file bitmap.exe
Bitmap.exe: PE32 executable (console) Intel 80386 stripped to external PDB), for MS Windows

このときは
 ・ファイルフォーマット
   PE(Portable Executable)
   Windows 上で用いられる実行ファイル形式
 ・CPUアーキテクチャ
   Intel 80386
 ・対象OS
   MS Windows向け
 

$ file bof1
bof1: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c3a38cf9fb86d513f7f6cdb35622fd1688d3a2cf, not stripped 

このときは
 ・ファイルフォーマット
    ELF フォーマット
    LinuxやBSDベースのOSで用いられる実行ファイル形式
 ・エンディアン
    LSB(Least Significant Byte) : リトルエンディアン
 ・リンカ
    dynamically linked : 動的リンク

4.実行バイナリ解析

 次に実行ファイルの解析を考えます。コンパイラ言語は、図のようにソースコードをコンパイルしてオブジェクトコードを生成します。オブジェクトコードは完全に機械語であるため人間が理解するのは困難です。しかしながら解析ではこのオブジェクトコードを読み取る必要があります。

f:id:takuya-endo:20180420110957p:plain

 ここで逆コンパイルをすることでソースコードに戻して読む手法もありますが、一般的に逆コンパイルをすることでデータが欠損したりしてしまい結局読むことができなくなることが多いです。
 そこで、逆コンパイルではなく、オブジェクトコードを逆アセンブルすることでアセンブリコードを生成します。アセンブリコードは慣れない人にとっては読みにくいかもしれませんが、人間が読むことのできる低級言語で、逆アセンブルの過程においても情報が落ちにくいため、実行ファイルの解析には逆アセンブルするためのソフトである逆アセンブラを用います。
 例えば”Hollo World!”をC言語で記述すると下記のようになります。

#include <stdio.h>

int main() {
  
  printf(“Hello World!\n”)
    
  return 0;
}

 これをコンパイルして実行ファイル(今回はa.outというファイル)を生成します。このファイルに対して逆アセンブルを実行します。実行コマンドは、objdumpです。また、-dでdisassembleを指定します。

$ objdump -d a.out

 このコマンドを実行して得られるアセンブリは次のようになります。

a.out:     ファイル形式 elf64-x86-64
セクション .init の逆アセンブル:

00000000004003e0 <_init>:
  4003e0:       48 83 ec 08             sub    $0x8,%rsp
  4003e4:       48 8b 05 0d 0c 20 00    mov    0x200c0d(%rip),%rax   # 600ff8 <_DYNAMIC+0x1d0>
  4003eb:       48 85 c0                test   %rax,%rax
  4003ee:       74 05                   je     4003f5 <_init+0x15>
  4003f0:       e8 3b 00 00 00          callq  400430 <__gmon_start__@plt>
  4003f5:       48 83 c4 08             add    $0x8,%rsp
  4003f9:       c3                      retq   

 かなり長いコードが得られたので先頭の方だけ記載させていただきます。
 新人の自分にはなかなか解読が難しいですが、このアセンブリも人間が読める言語で、慣れれば簡単に読めるようになるそうです。
 また、オブジェクトコードはもともとある関数を呼び出しながら実行されることが多いです。
 関数の呼び出しは、OSが備えているカーネルが実行する関数を呼び出す場合とライブラリが実行する関数を呼び出す場合の2種類があります。関数を呼び出すというのは、例えばC言語では標準出力に書式付きで出力する際に printf() を使います。この printf() の関数はもともとライブラリにある関数で、実行するときに呼び出されています。

f:id:takuya-endo:20180420111001p:plain

 その関数をトレースすることで実行バイナリの解析に活用することができます。カーネルへの呼び出しに対してはstraceを、ライブラリの呼び出しにはltraceを使うことで呼び出しをトレースすることができます。取り出したい情報に応じてどちらをトレースした結果を利用するかが決まります。例えば

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

int main() {

  char buf[32];
  char key[] = "d3m0_pr0gr4m_k3y";
  puts("Please input tha passphrase.");
  fgets(buf, sizeof(buf), stdin);
  strtok(buf, "\n");
  if (!strcmp(buf, key)) {
    puts("Congratulations! Your flag is [FROM SCRATCH]");}
  else {
  puts("Invalid inputs.");
  }

return 0;
}

 このように、決められた文字列が入力されたときに、Congratulation!…と返してくる、C言語で記述されたソースコードがあるとします。これをコンパイルしたオブジェクトコードは人間が読むことができません。この実行ファイルのファイル形式をfileコマンドで確認した上で ltrace を実行してみます。するとこのような結果が得られます。

$ ltrace ./a.out
__libc_start_main(0x40064d, 1, 0x7ffd0d870ae8, 0x4006e0 <unfinished ...>
puts("Please input tha passphrase."Please input tha passphrase.
)
fgets(das
"das\n", 32, 0x7f76ac022640)
strtok("das\n", "\n") = "das"
strcmp("das", "d3m0_pr0gr4m_k3y")
puts("Invalid inputs."Invalid inputs.
)
+++ exited (status 0) +++

 この結果から、オブジェクトコードがわからずとも、例えば

strcmp("das", "d3m0_pr0gr4m_k3y") 

 ここで値の比較を行っているなどの情報を読み取ることができます。
CTFではこういったことをヒントに対象とする欲しい情報を取り出したり、システムの脆弱性を探し当てることを競技として行っています。

5.おわりに

 次回は、脆弱性という観点でより踏み込んで、CTFの基本的な攻略法やセキュリティについて、お話したいと思います。勉強会でもここからが面白いところでしたのでぜひ次回もご覧いただけたらと思います。