ステップ3: 脆弱性を探す|Tech Book Zone Manatee

マナティ

セキュリティコンテストチャレンジブック

ステップ3: 脆弱性を探す

書籍『セキュリティコンテストチャレンジブック』の2章「pwn」の記事より「ステップ3: 脆弱性を探す」を掲載します。

はじめに

この連載では『セキュリティコンテストチャレンジブック』から2章「pwn」の記事より2.1~2.3節を掲載していきます! 前回の記事『ステップ2: 下調べ』はこちら

続きのステップ4: エクスプロイトステップ5:pwn! pwn! pwn!
書籍『セキュリティコンテストチャレンジブック』でお読みいただけます。

ステップ3: 脆弱性を探す

環境や実行ファイルの設定等の項目を確認し終えたら、その結果を踏まえて脆弱性を探していきます。

3.1 方針

脆弱性を探すステップでの1つの目標はプログラムが落ちるような入力を探すことです。プログラムが落ちた場合、書き込み不可能なアドレスに書き込もうとしていたり、実行不可能なアドレスを実行しようとしていたりすることがあるため、対象アドレスをうまく操作すると自由にプログラムを制御できることがあります。特にプログラムの実行位置 (IP = Instruction Pointer) を自由に変更できる状態のことを EIP(RIP) を奪った と言うことがあります。

もう1つの目標は脆弱性の存在する箇所にプログラムを実行して辿り着くことです。 Pwn の問題でもゲームをクリアすると攻撃させてもらえるといった Reversing に近い形式の問題がよく出題されます。逆アセンブル結果を読んで確実に脆弱性が存在するとわかっていても辿り着けなくては解くことができませんから、 Reversing 問題を解く力も要求されることがあります。

脆弱性の存在する場所に辿り着き、EIP を奪うことができたら脆弱性を探すステップはほぼ終わりです。高難易度問題では複数の脆弱性を組み合わせることがありますが、一旦はシェルを奪取できるかどうかを考えるためにステップ 3 へと移りましょう。

3.2 ユーザー入力を扱う関数

scanffgets などのユーザー入力を扱う関数では入力をメモリ上に配置するため、入力がバッファサイズを超えてしまうとバッファオーバーフローの脆弱性に繋がることがあります。

例えば、次のようなソースコードを考えてみましょう。

bof.c
#include <stdio.h>

int main(int argc, char *argv[]) {
    char buffer[100];
    fgets(buffer, 128, stdin);
    return 0;
}

4行目で宣言された buffer 変数のサイズは 100 バイト分ですが、 5行目で標準入力から読み込まれる文字列は最大 128 バイトになっています。

それではこのソースコードを SSP 無効の状態で実行してバッファオーバーフローを発生させてみましょう。

$ gcc -m32 -fno-stack-protector -o bof bof.c
$ python -c 'print("CTF for Beginners")' | ./bof
$ python -c 'print("A"*128)' | ./bof
Segmentation fault

CTF for Beginnersという入力に対しては正常に終了していることが確認できます。一方で、Aを128回繰り返した文字列を入力して与えた場合には Segmentation fault というエラーメッセージが表示されています。

どのような問題でプログラムが停止したかを簡単に確認するために、 strace コマンドを用いて詳細を表示してみます。 strace コマンドに -i オプションを付加して実行するとログの表示時にプログラムの IP を追加で表示してくれます。

$ python -c 'print("A"*128)' | strace -i ./bof
[00007f9ba0c27337] execve("./bof", ["./bof"], [/* 48 vars */]) = 0
[ Process PID=51655 runs in 32 bit mode. ]
[f77cbd89] brk(0)                       = 0x9bfe000
[f77cd8b1] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[f77cd983] mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = fffff77b3000
[f77cd8b1] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[f77cd7b4] open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
[f77cd73d] fstat64(3, {st_mode=S_IFREG|0644, st_size=68096, ...}) = 0
[f77cd983] mmap2(NULL, 68096, PROT_READ, MAP_PRIVATE, 3, 0) = fffff77a2000
[f77cd92d] close(3)                     = 0
[f77cd8b1] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[f77cd7b4] open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
[f77cd7f4] read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\340\233\1\0004\0\0\0"..., 512) = 512
[f77cd73d] fstat64(3, {st_mode=S_IFREG|0755, st_size=1754876, ...}) = 0
[f77cd983] mmap2(NULL, 1759868, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = fffff75f4000
[f77cd983] mmap2(0xf779c000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a8000) = fffff779c000
[f77cd983] mmap2(0xf779f000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = fffff779f000
[f77cd92d] close(3)                     = 0
[f77cd983] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = fffff75f3000
[f77b6e05] set_thread_area(0xff911290)  = 0
[f77cda04] mprotect(0xf779c000, 8192, PROT_READ) = 0
[f77cda04] mprotect(0x8049000, 4096, PROT_READ) = 0
[f77cda04] mprotect(0xf77d6000, 4096, PROT_READ) = 0
[f77cd9c1] munmap(0xf77a2000, 68096)    = 0
[f77b5440] fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[f77b5440] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = fffff77b2000
[f77b5440] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 129
[41414141] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---
[????????????????] +++ killed by SIGSEGV +++
Segmentation fault

先頭の [41414141] の部分が IP を示しています。

[41414141] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---

Segmentation fault が発生した時には IP = 0x41414141 となっていて、これは A の文字コードですから、128文字入力した A の一部分が IP に現れていることがわかります。

入力した文字列で IP の値を変更できる状態ですから、これは EIP を奪った 状態であると言えます。

3.3 printf系関数の書式文字列

printf などの書式文字列を扱う関数では、書式文字列部分にユーザーの入力を用いてしまうと書式文字列攻撃の脆弱性に繋がります。

例えば次のコードは書式文字列攻撃の脆弱性を含んでいます。

format.c
#include <stdio.h>

int main(int argc, char *argv[]) {
    char str[128];
    fgets(str, 128, stdin);
    printf("Hello, ");
    printf(str);
    return 0;
}

書式文字列を指定する部分にユーザーの入力を入れてしまうと、入力に書式指定子が存在している時にその部分がフォーマットされて出力されてしまいます。

$ gcc -m32 -o format format.c
$ ./format 
%x,%x,%x,%x,%x,%x,%x,%x,%x 
Hello, 80,f76f4c20,ff8eb904,0,0,0,252c7825,78252c78,2c78252c

入力した %x が展開されて表示されました。 printf 自体の引数には書式文字列部分しか指定されていないのに何らかの数値が表示されています。これは x86 では引数はスタックを使って渡すため、 printf が書式指定子の数に応じてスタックからデータを読み込んでしまっていることが原因で発生しています。

書式文字列攻撃はメモリの読み出しと書き込みを同時に行うことができるので、直接 EIP を奪えるわけではありませんが強力な脆弱性です。メモリ操作を行う方法はステップ4 で詳しく解説します。
(※続きは書籍でお読みいただけます)

著者プロフィール

SECCON実行委員会/CTF for ビギナーズ(著者)
コンピュータセキュリティ技術を競う競技であるCTF (Capture The Flag) の初心者を対象とした勉強会。CTFに必要な知識を学ぶ専門講義と実際に問題に挑戦してCTFを体験してもらう演習を行っている。