こんにちは。AIR GOの開発を担当しているKIM TAEWOOです。このブログではAIR GOを紹介したいと思います。
AIR GOとは一言で言うとAndroid/iOSのビルドバイナリ(APKファイル/IPAファイル)を分析し、脆弱性を含むリスクを発見するツールです。オープンソースツールであるSanDroidなどを利用したことがある方はイメージしやすいと思います。最近社内では、アプリをリリースする際にAIR GOを活用して、リリース前のアプリに含まれるリスクを除去するようにしています。
社内の方であれば誰でもAIR GOにアクセスして利用することができますし、外部の方も、https://airgo.line.meにアカウントを登録すれば利用することが来ます。(1回/1日)
過去にコーポレートページでセキュリティサービス「AIR」を紹介(https://linecorp.com/ja/pr/news/ja/2018/2126)していますが、今回はAIRを構成するツールの一つであるAIR GOについて、エンジニアの観点から紹介します。
AIR GOの仕組み
AIR GOは、パッケジングされたAPK/IPAファイルを入力とした場合、解凍処理をした後、必要な情報を抽出して、難読化の状況、脆弱性、ライセンス、悪性コードなどをチェックします。また、URLを入力とした場合は、悪性コードがそのURLに含まれているかをチェックします。
このセッションでは、AIR GOがAPKファイルを入力とした場合のどのような仕組みで脆弱性チェックを行うかについて説明します。
APKファイルの脆弱性のチェックは、①コードのデコンパイル処理→②デコンパイルされたコードのパーシング処理→③パーシングされたデータと脆弱性パータンとの比較より→④結果の出力処理の順番で行われます。
コードのデコンパイルやデコンパイルされたコードのパーシング処理は、脆弱性チェックに必要なデータを抽出して利用可能な形へ変換するプロセスです。
Androidアプリのバイナリ(APKファイル)を解凍すると多様なフォーマットのファイルが存在しますが、AIR GOでは、DEX、SO、DLL、XMLファイルを対象に診断に必要な情報を抽出します。
この中でclasses.dexファイルはDalvik VM上で動く中間言語として存在するため、 smaliコードに変更して人間が読める形にします。このプロセスをdisassembly(bytecodeからinstruction listへの変換)と言い、AIR GOではsmaliコードに基づいて脆弱性をチェックします。
smaliコードを理解するため、以下のコードを見てみましょう。Toastを利用して"Toast Hello"というメッセージを出力するJavaコードとsmaliコードになります。
▼Javaコード
String message = "Toast Hello";
Toast toast = Toast.makeText(this, message, Toast.LENGTH_LONG);
toast.show();
▼smaliコード
const-string v2, "Toast Hello"
const/4 v3, 0x1
invoke-static {p0, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v1
invoke-virtual {v1}, Landroid/widget/Toast;->show()V
まず、Javaコードの1~2行目が、smaliコードの1~4行目に該当し、Javaコードの3行目が、smaliコードの5行に該当します。
Javaコードの2行目では、ToastクラスのmakeTextメソッドを利用して、toastというインスタンスを生成していますが、これが、smaliコードの3行目と4行目に該当していて、v1レジスタにそのインスタンスが格納されるようになります。
最後にJavaコードの3行目では、showメソッドを呼び出していますが、smaliコードの5行目に該当し、invoke-virtual(Dalvik opcode)を利用してv1レジスタを呼び出しています。
smaliコードの5行目をより詳しく説明しますと、ご覧のとおりです。
上記のようにsmaliコードは、インスタンスや文字列を格納するためのレジスタ、クラスのパス、メソッドの名前、メソッドの引数、メソッドのリターンタイプなどで構成されるため、
AIR GOでは、脆弱なJavaコードに該当するsmaliコードを前もってパターン化し、脆弱性チェックの際はそのパターンと比較する処理を行います。
AIR GOの脆弱性チェックの項目には、GoogleのApp security improvement programで定めた項目や、よく知られているCVE項目などが含まれています。
次は、AIR GOの脆弱性チェック項目の一つであるInsecure Hostname Verificationを例に説明します。
脆弱性) Insecure Hostname Verification
この脆弱性は、クライアントアプリがリモートホストへの HTTPS 接続する際に、すべてのホスト名を許可してしまうため、中間者攻撃(Man-in-the-middle attack)によりHTTPS通信の盗聴や改ざんができるものです。
JavaではHttpsURLConnectionクラスのsetHostnameVerifierメソッドを提供しており、verifyメソッドをoverrideすることでサーバーのホスト名の検証を制御することが可能になっていますが、以下のコードのようにreturn trueになっていれば、サーバーのホスト名を検証しないことを意味します。
▼Javaコード
URL url = new URL("https://example.org/");
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
// Set Hostname verification
urlConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// Ignore host name verification. It always returns true.
return true;
}
});
AIR GOでは上記のJavaコードを以下のようにsmaliコードに変換します。
▼smaliコード
# virtual methods
.method public verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z
.locals 1
.param p1, "hostname" # Ljava/lang/String;
.param p2, "session" # Ljavax/net/ssl/SSLSession;
.prologue
.line 62
const/4 v0, 0x1
return v0
.end method
p1, p2 がverifyメソッドの引数を意味しており、p1のhostnameを検証せず、trueを意味する0x1を格納したv0をリターンしています。AIR GOではこのようにverifyメソッドの引数を検証なしでtrueをリターンするsmaliコードのパターンを脆弱な項目として検知しています。
以下はhostnameを検証するJavaコードです。
▼Javaコード
URL url = new URL("https://example.org/");
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
// Set Hostname verification
urlConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
HostnameVerifier hv =
HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify("example.com", session);
}
});
(https://developer.android.com/training/articles/security-ssl.htmlより)
▼smaliコード
# virtual methods
.method public verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z
.locals 2
.param p1, "hostname" # Ljava/lang/String;
.param p2, "session" # Ljavax/net/ssl/SSLSession;
.prologue
.line 21
invoke-static {}, Ljavax/net/ssl/HttpsURLConnection;->getDefaultHostnameVerifier()Ljavax/net/ssl/HostnameVerifier;
move-result-object v0
.local v0, "hv":Ljavax/net/ssl/HostnameVerifier;
const-string/jumbo v1, "example.com"
invoke-interface {v0, v1, p2}, Ljavax/net/ssl/HostnameVerifier;->verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z
move-result v1
return v1
.end method
使ってみましょう
以下の画面は、https://air.line.me/airgoにログインしたあとの画面です。(email登録をすればアカウントが作れます。)
左側にAPKファイルとIPAファイルをアップロードすれば脆弱性チェックが可能です。(ファイルの容量は512MBまで) また、右側にURLを入力すれば悪性コードがあるかチェックすることが可能です。
パシング→分析を減って数秒あるいは数分以内にチェック結果を見ることができます。またチェック結果は後でも確認することもできます。
次の画面はAndroidアプリ(APKファイル)をアップロードした結果になります。パッケージ名、バージョン、Platformなどの基本情報から、難読化の状況、脆弱性、ライセンス、悪性コードなどの検知結果がそれぞれのタブで表示されます。
難読化のタブ(Obfuscation)では、難読化の状況を表示します。難読化が必要なファイルが難読化されていない場合は赤い色で表示して難読化の適用を推奨します。
脆弱性のタブ(Vulnerability)では、Googleので定めた項目を含めて、AIR GOの項目(脆弱なライブラリのチェック、SSL証明書エラーの検証不備、安全ではない暗号化のアルゴリズムのチェック、AndroidのコンポネントであるActivity, Receiver, Providerのexport状況など)をチェックした結果を表示します。(ガイドを含む)
また脆弱性を、CRITICAL, WARNING, NORMAL, SAFE といった4つのレベルで分けて表示します。それぞれの意味は以下の通りです。
- CRITICAL :
- OpenSSLの脆弱性(Critical, High severity)
- CVSS v3.0 Ratingsで7.0以上の脆弱性が一つ以上発見された場合あるいは、悪性コードが発見された場合
- GoogleのApp security improvement programで定めた項目の中でRemediation Deadlineがある脆弱性
- WARNING :
- OpenSSLの脆弱性(Moderate severity)
- CVSS v3.0 Ratings で4.0-6.9の脆弱性が一つ以上発見された場合
- GoogleのApp security improvement programで定めた項目の中でRemediation Deadlineがない脆弱性
- NORMAL :
- OpenSSLの脆弱性(Low severity)
- CVSS v3.0 Ratings で0-3.9の脆弱性が、一つ以上発見された場合
- その他recommend相当の場合
- SAFE : 上記のレベルの脆弱性が発見されなかった場合
ライセンスのタブ(License)では、アプリに使われたライブラリのライセンス情報を表示し、ソースコードを公開する必要があるものに対しては公開義務(reciprocal)あり、なしで表示します。
悪性コードのタブ(Malware)では、悪性コードが検知されたコードと悪性コードの情報を表示します。
証明書のタブ(Certification)では、署名に使用した証明書の詳細情報を表示します。
構成のタブ(Structure)では、APKファイル、IPAファイルのディレクトリ構成とファイルの中身を表示します。
AIR GOの活用事例
皆さんご存知かもしれませんが、LINEは、自社アプリでクライアントアップデートが必要な脆弱性が発見されるとユーザが最新バージョンへアップデートするようにjvn公表とともに自社ホームページで告知を行っています。
7月には、LINE MUSIC AndroidアプリでのSSLサーバー証明書エラーの検証不備の問題を社内で発見し、以下のようなアナウンスを行いました。
https://linecorp.com/ja/security/article/181 (日本語) (https://linecorp.com/en/security/article/182 (英語))https://jvn.jp/jp/JVN16933564/index.html (日本語)(https://jvn.jp/en/jp/JVN16933564/index.html (英語))この脆弱性は、クライアントアプリがSSLの証明書エラーを無視するため、MITM攻撃により、サーバーの証明書が変わってもサーバーとのHTTPS通信を行うものです。
これは、AIR GOで検知できる項目でもあり、再発防止のため、アプリをビルドしたあとは、必ずAIR GOを利用して脆弱性がないかチェックするように活用しています。
詳しく説明しますと、アプリの開発において、テストなどの理由でオレオレ証明書を用いる場合があり、以下のようにTrustManagerのcheckServerTrustedメソッドをoverrideして証明書エラーを無視したい場合があります。
▼Javaコード
// SSL setting
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{ new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// client certification check
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// server certification check
}
@Override
public final X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}, null);
この脆弱性がAIR GOで検知された場合、以下のように、'TrustManager Verification' 項目として表示されます。detect_countが2となっているのは2箇所が検知されていることを意味していて、GoogleHttpClientクラス(https通信用のカスタムクラス)のaメソッド(caller)からSSLContextクラスのinitメソッド(callee)が呼ばれていることが確認できます。またinitメソッドの2番目の引数(argument index)は、GoogleHttpClientクラスのaメソッドで実装されたcheckServerTrustedメソッドを意味し、証明書エラーをチェックするメソッドが実装されていない(no exist api)ことを表示します。
最後に
本文で説明したように、AIR GOでは、難読化の状況、脆弱性、ライセンス、悪性コードなどをチェックする機能があり、動作仕組みや利用方法について紹介しました。
AIR GOは、以下のような特徴を持つツールであり、このブログを読んだ方は、ぜひ一度は使っていただければと思います。
ソースコードではないビルドバイナリ(APKファイル/IPAファイル)を入力としています。企業あるいは、開発者がアプリに存在するリスクをチェックする立場になった場合、ソースコードを渡す必要がありません。
ビルドバイナリに含まれている様々な種類のファイル(.dex、.dll、.so)の難読化の状況をチェックします。最近は、リリース前にデコンパイルを難しくする難読化を行う場合が多いですが、難読化されていないファイルがあるか確認できます。(AIR ARMORから難読化を行うことができます。)
リリース前にセキュリティリスクを把握します。既にGoogleは、App security improvement programでいくつの脆弱な項目を定めています。アプリをリリースする際にこのようなリスクを事前にチェックすることは企業あるいは、開発者にとって役に立ちます。