Androidアプリに潜む脆弱性

玉木英嗣氏:はい、よろしくお願いいたします。「アプリに潜む脆弱性 -LINEの場合-」というタイトルでLINEの玉木が発表させていただきます。よろしくお願いします。

始めに自己紹介です。玉木英嗣と申します。2018年4月に入社してからLINE Androidのチャットまわりの機能開発を担当しています。LINEがGitHub上で公開しているAndroid向けの2つのOSSライブラリ、ApngDrawableとfeature-flag-androidのメンテナーをやっています。

このセッションでは、大きく5つの項目について話します。始めに、このセッションで話す内容についての簡単な説明をします。次に、脆弱性とは何かという点について簡単な補足を行います。3つ目に、LINEが社内外でどのような脆弱性の対策を行っているのかについて紹介をしていきます。4つ目に、実際にどのような脆弱性がLINE Androidの中にあったのかピックアップして紹介をします。そして、最後にまとめとなります。

それでは始めにこのセッションについてです。まず、このセッションで話すこととしては、LINEのAndroidアプリに存在した脆弱性の紹介を行います。話さないことは、アプリに脆弱性を含めないためのセキュアコーディングガイドラインのような一般的なアドバイスです。このセッションは固い内容のものではないので「あぁ~なるほど~」「へぇ~」みたいな感じで聞いていただき、コメント等でリアクションをしていただければと思います。

次に、脆弱性についての簡単な補足を行います。まず始めに、脆弱性とバグ、この2つはまったく同じものではありません。総務省のホームページからの引用ですが、脆弱性はバグや設計の不備から生じた情報セキュリティ上の欠陥のことを指します。もちろん、Androidアプリでも、きちんとした知識を持ち対策を講じないと脆弱性をアプリ内に埋め込む危険があります。

LINEで実際に取り組んでいる脆弱性対策

では、LINEでは脆弱性対策としてどのような取り組みを行っているのかについて紹介します。

まず、社内メンバーによるチェックです。毎日交わされるPRのレビュー上でのお互いのチェックにより、脆弱性の芽となるコードの混入を防いでいます。また、社内のセキュリティチームにより、API設計であったり、実装されたコード、ステージング環境でのチェックの他、社外報告者からのバグレポートの確認、開発チームへの連絡といったことが行われています。

次に、社外報告者からの報告です。LINEではLINE Security Bug Bounty ProgramというBounty Programを行っており、有効な脆弱性の報告に対し報奨金を支払っています。また、LINE AndroidはGoogle Playが実施しているBounty Programである、Google Play Security Reward Programの対象になっています。

これは1億回以上のインストールなどの条件を満たしたアプリに対してGoogle Playが脆弱性の報告を受け付けることで、Google Playのエコシステムの安全性を高めるためのものです。

3つ目に、重要な脆弱性が見つかった際の公表です。どのように発見されたのか、社内なのか社外なのかに関わらず、重要な脆弱性はIPA(情報処理推進機構)やJPCERT/CCと連携し、利用者へのアナウンスやCVE(Common Vulnerabilities and Exposures)の発番を行っています。

CVEとは共通脆弱性識別子という、アメリカのMITRE社が管理をしているもので、さまざまな製品の脆弱性にユニークなIDを付与することで、対策情報同士の相互参照や関連付けに活用できるようにしたものです。また、現在LINE Security Bug Bounty Programを行っているHackerOneというプラットフォーム上において、修正された脆弱性のレポートも順次公開をしています。

実際にLINE Androidにあった脆弱性

では、実際にLINE Androidにあった脆弱性のうち、いくつかをピックアップして紹介していきたいと思います。

まず、注意点として、ここで紹介する脆弱性はすべて修正済みのものになります。各々の脆弱性の紹介時に修正された年を併記しています。今回紹介するのはこれらの9個の脆弱性です。

始めにSQLインジェクションです。SQLインジェクションといえば、脆弱性に対する説明でよく用いられることもあり、有名なものの1つかなと思います。大まかに説明をすると、SQLクエリの生成時に不正なクエリを発行できる脆弱性です。

例としてユーザからの入力値をinputという変数に置いていると考えます。このとき、単純にこのinputをSQLクエリのwhere文の一部として埋め込んでしまうと、入力値のOR 't' = 't'の部分がSQLの構文の一部として認識されてしまいます。

このサンプルではwhere文でユーザ名によって制約を掛けているはずが、実際に実行されるSQL文に(スライドの)下の部分のようにOR 't' = 't'が追加されてしまったために、全レコードを返すクエリになってしまっています。

不正なメッセージによりSQLインジェクションを起こせる脆弱性が存在

では、LINE Android上でどのようなかたちでこの脆弱性が存在していたのでしょうか。

概要として、不正なmetaデータが埋め込まれたメッセージを処理する際に、SQLインジェクションを起こせる脆弱性がありました。不正なメッセージは、通常のLINE AndroidやLINE iOSからではなく、非公式のクライアントから送信することが可能でした。これにより受信者のアプリのクラッシュを引き起こすことが可能でした。

この脆弱性はBug Bounty経由で報告され、2018年に修正が完了しました。原因としては、コード上でSQLクエリを生成する際にAndroidのSQLiteDatabaseが提供する、「?」を使ったプレースホルダー機能を使用していなかったというものです。スライドのコードのようにwhere文を生成する部分で、直接値を挿入してしまっていました。

また、サーバ側でのバリデーションも存在していませんでした。メッセージは一度サーバに送信されますが、そのリクエスト内の値をそのまま別ユーザに送付するかたちで実装がされていました。

対策としては、まずSQLiteDatabaseクラスが提供する「?」マークを使ったプレースホルダー機能を使用するというものです。内部できちんとエスケープ処理が入るため、利用する側で気にする必要がなくなります。他に、Roomなどのフレームワークを使用するのもエスケープ処理を任せるという点では有効です。

また、今回の原因に限った話ではサーバ側でのバリデーションも有効です。アプリのHotfixのリリースには多少時間が掛かってしまうため、サーバ側に対策を入れるというのは初期のQuickfixとしては有効になります。

SQLiteのジャーナルモードが引き起こした脆弱性

次に、SQLiteのジャーナルモードというものについてです。これに関してはあまり知らない人も多いのではないでしょうか。SQLiteのジャーナルモードとはトランザクション中の操作を管理するための手法です。PRAGMA文によって設定することが可能です。例として、WALモードは、Write-Ahead Logというものを利用したトランザクションを実現するというモードです。

トランザクション開始から終了までの操作を-shmというサフィックスの付いたファイルへ、コミットが行われたら-walというサフィックスの付いたファイルに書き込みを行います。本体のデータベースファイルへはクローズをしたときに書き込まれ、読み込みは-walファイルと本体データベースファイルの両方から行われることになります。

もう1つのDELETEモードではトランザクション中の操作を-journalというサフィックスの付いたファイルへ書き込み、コミット時に本体のデータベースへ書き込むモードになります。DELETEと名が付くようにジャーナルファイルは正常に本体データベースへ書き込まれたあとに削除されます。

では、このジャーナルモードがどのような脆弱性を引き起こしたのでしょうか。

概要としては、LINEのメッセージ機能の1つである「送信取消」を行った際にメッセージの内容がデータベース上のジャーナルファイルに残ってしまうという問題でした。データベースファイル自体は内部ディレクトリにありますが、rootを取得済みのデバイスなどでは内容を読み取られる危険がありました。この脆弱性は社内から報告され、2018年に修正が完了しました。

原因としては、Android 4.1.1からSQLiteのジャーナルモードのデフォルト値がPERSISTというものになっていました。このPERSISTモードでは、DELETEモードと同じようにジャーナルファイルを使用するのですが、コミット時にジャーナルファイルを削除しないというモードです。これにより本来消えるべき内容のものがジャーナルファイルに残ってしまうという状態になっていました。

対策としては、データベースをオープンするときに明示的にジャーナルモードをDELETEモードにすることで解決しました。ただし、コミット時にファイル削除まで行うというパフォーマンス上の懸念もありますので、消さなければいけないというデータがなければやる必要はないでしょう。

WebViewからのリソースアクセス

3つ目はWebViewからのリソースアクセスについてです。概要としては、WebViewで表示しているリソースを中間者攻撃で差し替えることにより、読み込まれたJavaScriptのコードを経由してアプリ内のデータの盗聴や書き換えが可能というものでした。この脆弱性は、JPCERT/CCおよびIPAからの報告とBug Bounty経由での報告を受け、2015年に修正をしています。また、重大な脆弱性として公式サイト上でも脆弱性の修正の告知を行いました。

原因としては、WebView上でHTTPなURLのページを読み込んでいたということです。このWebViewは利用規約などのヘルプページなどを出す目的で使用されていました。また、WebView上のJavaScript関連機能が有効になっており、またその中のカスタマイズした機能の一部にCordovaのAPIを呼ぶためのブリッジが存在していました。

中間者攻撃によりリソースが差し替えられ、悪意があるJavaScriptが読み込まれた際に、Cordovaのブリッジ経由でファイルの操作であったりデバイス情報を取得するAPIなどへのアクセスが可能になっていたということです。

対策としては、まず読み込むURLのHTTPS化が挙げられます。もし何かしらの都合でHTTPが必須の場合は、別のブラウザを立ち上げるなどをして、アプリのWebViewでは開かないようにします。

さらに言ってしまえば、Android 9からはHTTPSが基本的に必須になり、HTTPを用いた通信はマニフェストに記載しないとNGになっています。さらに、不必要な箇所でのWebViewのJavaScript関連の機能の無効化、そして使われていないCordovaのプラグインの削除といった対策も行いました。

書き換え可能なファイルでのデバッグフラグ

4番目は、書き換え可能なファイルでのデバッグフラグです。ある一部のモジュールにおいてログを出すか出さないかに使用されるフラグが、リリースビルドでも書き換え可能になっていました。このファイルを書き換えると、ログチャット上にユーザのIDなどのデバッグ情報が出力されてしまうというものでした。この脆弱性はBug Bounty経由で報告され、2018年に修正が完了しました。

原因としては、/sdcardのような誰でも書き換え可能な外部領域に設定ファイルを置いていたことでした。この設定ファイルはデバッグやQAの際に使用される目的で準備された機能ですが、リリースビルドでも利用可能な状態でした。

まず対策としては、フラグを書き換え可能な状態にしないということです。productFlavorやbuildTypeによってBuildConfigなどの基本的に書き換えのできない値として生成し、使用するということです。もし、テスト目的やQA用に書き換えが必要な場合であっても、リリースビルドでは定数値となるように書くようにしましょう。

一例としては、このようにBuildConfig.DEBUGと、if文を使用するというものです。また、この例のようにする恩恵としてproguardやR8によりデバッグに関連したコードがリリースビルドからは削除されるという効果も期待できます。

TextViewでのHTMLタグでの脆弱性

5番目に、TextViewでのHTMLタグでの脆弱性を紹介します。この脆弱性の概要としては「〇〇さんがあなたを招待しています」というテキストを表示する際、〇〇の部分にユーザの名前を入れて表示をするようなTextViewにおいて、ユーザ名にHTMLのタグが含まれている場合に、その装飾が適用されてしまうという問題でした。例として、打ち消し線のための<s>の開始タグのみが名前に含まれている場合、テキスト全体が装飾されてしまいます。

TextViewのため、<script>のようなスクリプトを実行できる機能はないのですが、本来の見た目とは違うものにすることができてしまいます。この脆弱性は社内からの報告を受け、2018年に修正を行いました。

原因としては、文字列リソースにユーザ名をそのままエスケープせずに埋め込んでいたということでした。

対策としては、特殊な意味を持つ文字を置換することによりエスケープを行いました。

双方向テキストによるリンクの偽装

6番目は、双方向テキストによるリンクの偽装です。概要として、双方向テキストのための制御文字を用いることで、トークルーム内で送信できるリンクを見た目とは違うページへのリンクとすることが可能でした。双方向テキストとは、1つの文章内にLeft to Rightな文章とRight to Leftな文章が混じっている状態のテキストのことです。

例えば、Unicodeのコードポイント202E:RTLO(Right to Left override)と呼ばれる制御文字を使用すると、以降のテキストをRight to Leftなものとして認識するため、もしこの制御文字までがLeft to Rightであった場合は、制御文字より後ろのテキストが逆順になって見えるようになります。

スライドのサンプルでは、本来は拡張子が.apkとなっているはずのリンクですが、RTLO文字により見た目上では拡張子が.jpgになっているように見えてしまいます。この脆弱性はBug Bounty経由で報告され、今年に修正が完了しました。

原因としては、メッセージのテキスト内のリンクを抽出するためのロジック、正規表現に不備があり、これらの制御文字のことが考えられていなかったという点が挙げられます。昔のバージョンで、「(」が前後に含まれるテキストでも正確にURLを抜き出すために、このリンク抽出ロジックは自前で書かれたものでした。

対策としては、androidx-coreに含まれるLinkifyCompatを使用するというものです。これは、AndroidフレームワークにあるLinkifyクラスのCompat版になります。このクラスはRFC3987: 国際化URIの規格に則り、引数で渡されたSpannableオブジェクトにLink spanを設定してくれています。

このLinkifyCompatが使用しているURLのためのRegexはPatternsCompatに定義があるのですが、hide APIになってしまっているため、実質的にLinkifyCompatを使うのが必須になります。ただし、この実装ではリンクに対してさらに何かをしたい、何かを行いたいという場合には少し不向きになります。

そのため別の対策としては、制御文字を削除、もしくは置換するというものがあります。このスライドのサンプルでは、制御文字202Eを空白スペースに置き換えた状態になります。

ネイティブコードでの整数オーバーフローの脆弱性

7つ目は、ネイティブコードでの整数オーバーフローの脆弱性です。概要としては不正な加工がされた画像ファイルを読み込もうとすることで、内部で整数オーバーフローが発生してしまうというものでした。LINE Androidでは一部の画像を読み込みを高速化するためにC、C++で書かれた独自の画像読み込みライブラリを使用しており、そちらに問題がありました。この脆弱性は社内からの報告を受け、2019年に修正が完了しました。

原因としては、画像を読み込むためのバッファを確保する際に、画像のヘッダに埋め込まれた縦横ピクセル数を使用してバッファのサイズを計算するのですが、その縦横ピクセル数が極端に大きな数字が埋め込まれた際に乗算の結果がオーバーフローしてしまうというものです。サンプルコードではload_widthとload_height関数で高さと横幅を取得し、その2つを使用してバッファサイズを計算してその結果を基にmallocで領域を確保しています。

このバッファサイズがsize_tの範囲を超えてオーバーフローした際、このmallocで確保したバッファ領域が本来必要なバッファサイズより小さくなってしまうため、このバッファの配列にそのまま書き込もうとすると確保された配列外への書き込みを行ってしまうことになります。社内のテストではクラッシュを引き起こすことまでしかできませんでしたが、原理的には最悪の場合に任意コードの実行というところまで起きてしまいます。

対策としては、読み込んだ値などの外部からの値に対しては、乗算や加算の計算の前にはオーバーフローのチェックを行うということです。このサンプルのようにsize_t型の最大値であるSIZE_MAXから割った値と比較することで、乗算によるオーバーフローが発生するかどうかを確認することができます。

こちらは余談ですが、この脆弱性は一番始めに自分がメンテナーをやっているといったApngDrawableで発見されたのを皮切りにさまざまなモジュールで発見が進みました。この脆弱性はLINEのインターン生の方が見つけたもので、インターンレポートが公開されていますので、ぜひ興味のある方はご覧ください。

そして、ApngDrawableがもともとLINE内部の独自デコーダを切り抜いてOSS化したものであったため、LINEにある他のデコーダについても調査をしていたところ同様の問題が見つかり、最終的にはApngDrawableとLINE両方にCVEの番号を発行することとなりました。

パストラバーサルに関連する脆弱性

8番目はパストラバーサルに関連する脆弱性です。パストラバーサルとはファイルパスを構成する文字に「..」やnull文字といった特別な意味のある文字を入れることで本来の制限外のファイルを指し、そこへアクセスさせるという攻撃です。「..」は親ディレクトリへの参照で用いられ、null文字はCなどではそこで文字列の区切りとして扱われる文字になります。

スライドのサンプルでは/webroot下のファイルを指しているかと思ったら、実際には親ディレクトリの参照が含まれるために実際には/etc/shadowファイルを見に行ってしまうようになります。

では、この脆弱性の概要です。別のアプリからのファイル共有インデント、ACTION_SENDを受け取る箇所での問題でした。別アプリからLINEへの画像などのファイルの共有を試したことがある人は多いかと思いますが、ファイルをシェアすることでメッセージとして送信をするという機能があります。この機能において、アプリ内からしかアクセスできない領域のファイルを送信してしまうという問題でした。攻撃においては、悪意のあるアプリのインストールや、共有を行わせるというユーザの操作が必要でした。

この脆弱性は、Bug Bounty経由で報告をされ2020年に修正されましたが、同様の報告が2018年、2019年にもありました。

原因としては、外部からシェアされたURLに対するチェックが不十分でファイルパスがパストラバーサルによって内部ディレクトリを指していた場合でもそのファイルを送信してしまうというものでした。

対策としては、共有などで外部から受け取った値に対しては適切にハンドリングを行い、パストラバーサルの危険の可能性のあるパスを禁止したり、内部パスを参照するようなパスを禁止したりということが挙げられます。

また、ACTION_SEND、ACTION_SEND_MULTIPLEなどの外部からのシェアや、ACTION_GET_CONTENTによるファイルオープン処理を行っている部分において、外部アプリとの境界点であるというということを意識するということも重要です。

ZIPファイル関連の脆弱性

最後は、ZIPファイル関連の脆弱性についてです。脆弱性の概要としては、ZIPファイルを扱う処理に問題があり、内部ストレージの任意のファイルの上書きが可能になっていたというものです。この脆弱性は社内からの報告を受け、2015年に修正が完了しました。

原因としては、ZIPファイルをダウンロードし展開する処理の中で、HTTP通信を用いた中間者攻撃が可能な状態になっていたというものです。また、2つ目にJava標準のZipInputStreamを用いた展開処理に問題があったという点です。

影響としては、中間者攻撃によりZIPファイルが不正なものと差し替えられてしまった場合に、パストラバーサルによる無関係のファイルの書き換えやZIP爆弾による膨れ上がれを考慮しない実装となっていたというものです。ZIP爆弾というのは、数十キロバイトの小さなファイルであっても、展開するとテラバイト、ペタバイト級にファイルが生成されるように細工されたZIPファイルのことを指します。

対策としては、まずHTTPではなくHTTPSを使用するというものが挙げられます。そしてZIPファイルの展開時にパストラバーサルやZIP爆弾といった攻撃のチェックをするというものが挙げられます。LINE Androidでは、ZipInputStreamを拡張し、新しくSafeZipInputStreamというクラスを作成して使用しております。

このクラスではgetNextEntry()でのパストラバーサルにつながる文字が含まれていないかのチェックと、リードを行う際にそれまで読み込んだバイト数を記憶しておき、最大の読み込みバイト数以上は読み込まないというものです。

セッションのまとめ

それでは、まとめに入りたいと思います。脆弱性のレポートをしていただいたすべてのレポーターの方にお礼を申し上げます。ありがとうございます。そしてLINEからのお願いです。常に最新版のアプリを使用するようにしてください。アップデートには機能の追加だけでなく、このような脆弱性の修正も含まれます。

最後にこのセッションのまとめです。初めに、きちんと脆弱性のチェックを行うようにしましょう。社内でのチェックを行うだけでなくBounty Programなどによる社外からの報告を受け付けるという方法もあります。そして、報告を受けた脆弱性はきちんと直すようにしましょう。サービスの安定運用の妨げになったり、何より情報漏えいなどユーザが被害を被る可能性があります。

以上でこのセッションの発表は終わりとなります。ありがとうございました。