Proxyで傍受可能なオレオレ証明書を通せるAndroidアプリの作り方

人間誰しも1度はProxyを通して通信内容をデバッグしたい時がある。
要はオレオレ証明書を発行して間に挟めば傍受も改ざんもやりたい放題なのだが、Android 7以降からは一筋縄では実現できない。

In Android Nougat, we’ve changed how Android handles trusted certificate authorities (CAs) to provide safer defaults for secure app traffic. Most apps and users should not be affected by these changes or need to take any action. The changes include:

  • Safe and easy APIs to trust custom CAs.
  • Apps that target API Level 24 and above no longer trust user or admin-added CAs for secure connections, by default.
  • All devices running Android Nougat offer the same standardized set of system CAs—no device-specific customizations.

https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html

上記のとおり、AOSPで設定されたCA以外は信頼されず、ユーザが独自にインストールしたCAすらもデフォルトで無効になる。
しかしこれらをオプトインで利用することも当然可能になっており、またアプリ内にderファイルを同梱させ利用させることも可能だ。

これらは設定ファイルを記述して自分のアプリに組み込めば実現できる。

今回目指すこと

今回は、

  • Proxyサーバが、アプリが要求した https://example.com の内容を傍受できるようにする
  • 上記の内容を改竄し、アプリがへ届けられるようにする

の2つの実現を目指す。
ただし、傍受や改竄自体は便利なアプリが複数存在する(たとえば Burp Suite など)ので、実際に行うのは独自CAの許可のみだ。

以降で実装手順を解説する。実際のコードはGitHubに掲載した。

github.com

1. Response bodyを表示する簡単なアプリを作る

まずは挙動が確認できないことには始まらないので、通信した結果を表示するだけの画面を作る。
ざっくり以下のようになるはずだ:

class MainActivity : AppCompatActivity() {

    companion object {

        private const val ENDPOINT = "https://example.com"

    }

    private val client = OkHttpClient()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.button)
            .setOnClickListener {
                val req = Request.Builder()
                    .url(ENDPOINT)
                    .build()

                GlobalScope.launch(Dispatchers.IO) {
                    val result = client.newCall(req).execute().let {
                        it.body()?.string() ?: it.code().toString()
                    }

                    GlobalScope.launch(Dispatchers.Main) {
                        findViewById<TextView>(R.id.text)
                            .text = result
                    }
                }
            }
    }
}

このような表示を期待する。

f:id:S64:20190424173808p:plain:w250

2. 構成ファイルを作成・配置する

今回の設定はアプリ内でのみ有効になる。試しに

  • 全ての通信において、プリインストールのCAとres/raw/myca.derを信頼する

という設定にし、src/main/res/xml/nsc.xml に配置した:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="system" />
            <certificates src="@raw/myca" />
        </trust-anchors>
    </base-config>
</network-security-config>

3. CAを追加する

前項で設定したとおり、src/main/res/raw/myca.derとして追加する。

4. Manifestに設定する

nsc.xmlというファイル名自体には意味がなく、これを AndroidManifest.xml から参照する必要がある。

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    ...

    <application
            ...
            android:networkSecurityConfig="@xml/nsc">
        ...
    </application>

</manifest>

<application/> 要素に android:networkSecurityConfig で設定する。

以上で、独自証明書を用いた通信の傍受や改竄が可能になったはずだ。