Dev Study
← OWASP Top 10 の群 — 何のリストで、どう作られ、どう読むか
レッスン 8 / 12·約 9 分·第27期 OWASP Top 10 の群

配った実行ファイルは、受け取った人が読める — Mobile 2024 M1

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

同じ「Top 10」でも数えたものが違います。今回はモバイルの一覧が何を数えたかを読んでから、その1位の中身を手元のバイナリで確かめます。

まず結論

OWASP Mobile Top 10 2024 の M1 は、アプリのコードや設定ファイルに埋め込んだ資格情報(hardcoded credentials)を脆弱性の第一の兆候に挙げます。配布したバイナリは受け取った全員の手元にあり、埋めた鍵は汎用の文字列抽出ツール1つで平文のまま出てきます。

strip も minify も鍵を消さず、XOR で隠しても復号手順ごと配っています。鍵を埋めない限り、読める人の範囲は変わりません。

? 読む前に予測

鍵を文字列としてコードに書き、`gcc -O2` でビルドしてから `strip` でシンボルを削った実行ファイルがあります。`strings` で文字列を抜き出すと、鍵はどう見えるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

この一覧は何を数えたか、そして1位は何を言うか

番号を読む前に、何を数えた一覧かを確かめます。Mobile Top 10 の方法論は、自分を a data-based approach and unbiased sources によるものと説明し、データ源を incident reports, vulnerability databases, and security assessments と書きます。つまり事故報告・脆弱性データベース・診断結果です。具体例の列挙には Surveys of security professionals or application developers も入っています。分析に使いうる指標としては、発生頻度・影響の深刻さ・悪用の難しさなどを挙げます。ただし、集めた件数や項目ごとの値は公開されていません(確認範囲: index.md・tab_updates.md・2023-risks/methodology.md)。各項目に付いている EASY・COMMON・SEVERE は段階のラベルで、数値ではありません。

公開は二段構えです。方法論の Progress Report の表は Initial Release の日付を Aug 2, 2023 とし、2023-risks/methodology.md の同じ表は Final Release の日付を Jan 22, 2024 とします。トップページに Final release 2024 の表記が入ったのは、git の履歴で 2024-01-20 です。項目の本文は今も 2023-risks/ の下にあります。

その1位、M1: Improper Credential Usage は、脆弱性の兆候の第一にこれを挙げます。

Hardcoded Credentials - If the mobile app contains hardcoded credentials within the app's source code or any configuration files, this is a clear indicator of vulnerability.

理由は技術というより配布の構造にあります。配ったバイナリは、受け取った全員の手元にある同じ物です。 鍵を埋めれば、その鍵を読める人は「アプリを入手した全員」になります。秘密ごとに読める人を一覧にする見方は、第24期で扱います。

埋めた鍵は、配布物と一緒に全員へ届く
ソースに鍵を書くM1 の兆候の第一
ビルド(strip・minify・XOR を足しても)
配布物バイナリ/バンドルに鍵が入ったまま
配布
利用者A
利用者B
調べたい人汎用ツールで抜き出せる
  • ・直す向き: 共有の鍵はサーバに置き、端末には取り消せる値だけを渡す(M1 の How Do I Prevent の方向。本回では実装していない)
具体実物

埋めた鍵を、strings で抜き出す

環境は macOS 26.5.2(arm64)上の Docker 28.4.0、イメージ gcc:13(gcc 13.5.0・GNU strings と objdump 2.40)です。鍵は実在しないダミー文字列です。同じ手順を2回走らせ、出力は完全に一致しました。

embedded.c(鍵を埋め、実行時にヘッダへ載せる)設定ファイル
#include <stdio.h>
/* DUMMY credential embedded at build time (NOT a real secret) */
static const char *API_KEY = "DUMMYKEY-live_9f8b7c6d5e4a3210_DO_NOT_USE";
int main(int argc, char **argv) {
/* realistic: the app puts the key in a request header at runtime */
printf("Authorization: Bearer %s\n", API_KEY);
(void)argc; (void)argv;
return 0;
}

gcc -O2 でビルドし、strip -o embedded_stripped embedded でシンボルを削った版も作ります。それぞれから strings で文字列を抜き出し、grep で探します。

gcc:13 での生ログ(RUN 1 の一部。$ の行と (grep exit=…) は実行スクリプトが出した表示。strings の全出力は数百行)コマンド出力
$ ./embedded
Authorization: Bearer DUMMYKEY-live_9f8b7c6d5e4a3210_DO_NOT_USE
 
$ strings embedded | grep -n DUMMY
11:DUMMYKEY-live_9f8b7c6d5e4a3210_DO_NOT_USE
(grep exit=0)
 
$ strings embedded_stripped | grep -n DUMMY
11:DUMMYKEY-live_9f8b7c6d5e4a3210_DO_NOT_USE
(grep exit=0)
 
$ stat -c "%s %n" embedded embedded_stripped
70480 embedded
67560 embedded_stripped

11: は strings 出力の行番号です。strip の前も後も、同じ11行目に同じ鍵が出ました。 ファイルは 2920 バイトしか減っていません。削られたのはシンボルで、プログラムが実行時に使う文字列は残ります。使う以上、消せません。

✓ ここまでの確認

strip の前後でファイルは 70480 バイトから 67560 バイトになり、鍵は同じ行に残りました。ここから言えることはどれですか。

抽象原理

隠す加工は、読める人の範囲を変えない

この一覧の限定も確かめておきます。トップページは、初回の一覧に入らなかったものとして Hardcoded Secrets を挙げています。

Vulnerabilities that didn't make the place on the initial release list, but in the future, we may consider them.

ただし M1 の本文は hardcoded credentials を正面から扱っています。**「独立の項目としては入らなかった」**までが言えることで、「Top 10 で扱われていない」とは書けません。

では、鍵を読みにくく加工すればよいのでしょうか。M7: Insufficient Binary Protection は、難読化などの追加の防御についてこう書きます。

If there are additional protective measures, like obfuscation, encoding of secrets in native code (for Android) or similar, successful attacks become harder to achieve but never impossible.

同じ M7 は no fully reliable mechanisms to prevent binary attacks とも書きます。難しくはできても、不可能にはできないという位置づけです。アプリ自身が鍵を使う以上、使える形に戻す手順も同じ配布物に入っています。

逆向きの落とし穴もあります。鍵を使わないコードを -O2 でビルドすると、コンパイラが未使用の文字列を捨て、grep が空振りします(gcc:13 で確認)。この回の検証でも、最初の XOR 版は復号結果を使わない作りだったため、符号化した並びごと消えていました。「grep に出ない」は「鍵を埋めていない」の証明になりません。 確かめる対象は配布物ではなく、M1 が言うとおりソースと設定ファイルです。

具体実物

実行時に読む版と、XOR で隠した版

同じ環境で、鍵を埋めない2つの版を比べます。1つ目は getenv("API_KEY") で実行時に環境から読む版、2つ目は同じ鍵を XOR 0x5A で符号化して埋め、実行時に復号して embedded.c と同じくヘッダに載せる版です。復号した結果を実際に使う作りにしてあります。

gcc:13 での生ログ(RUN 1 の一部。$ の行と (grep exit=…) は実行スクリプトが出した表示)コマンド出力
$ strings envbased | grep -n DUMMY
(grep exit=1)
$ strings envbased | grep -n API_KEY
13:API_KEY
 
$ strings obfuscated | grep -n DUMMY
(grep exit=1)
 
$ objdump -s -j .rodata obfuscated
 
obfuscated: file format elf64-littleaarch64
 
Contents of section .rodata:
400720 01000200 00000000 41757468 6f72697a ........Authoriz
400730 6174696f 6e3a2042 65617265 72202573 ation: Bearer %s
400740 0a000000 00000000 00000000 00000000 ................
400750 1e0f1717 03111f03 7736332c 3f05633c ........w63,?.c<
400760 62386d39 6c3e6f3f 6e3b6968 6b6a051e b8m9l>o?n;ihkj..
400770 15051415 0e050f09 1f00 ..........
 
$ ./obfuscated
Authorization: Bearer DUMMYKEY-live_9f8b7c6d5e4a3210_DO_NOT_USE

実行時に読む版のバイナリには、変数名 API_KEY しか入っていません。ただし、これは配布物に鍵が無いというだけです。配った端末で動かすなら、その値を渡される先はやはり受け取った人の手元です。 実行時に読む形が効くのは、実行する場所が自分のサーバのときです。

XOR 版は平文の grep を逃れました。しかし objdump で読み取り専用データ(.rodata)を出すと、0x400750 からの 42 バイトがソースの符号化した並び(鍵 41 文字と終端の 0)とそのまま一致します。実行すれば平文の鍵が出ます。隠した並びも、戻す手順も、同じ実行ファイルに入っています。

✓ ここまでの確認

鍵を XOR 0x5A で符号化した版は、strings と grep では平文が見つからず、実行すると平文の鍵を載せたヘッダを出力しました。M7 の原文に照らした読み方として適切なのはどれですか。

⇄ 2つを見比べる
具体例 A

ネイティブのバイナリ

C で書いて gcc -O2 でビルドし、strip した実行ファイル。strings と grep で、strip の前後とも同じ行に鍵が出ました。

gcc:13 の生ログ(抜粋)コマンド出力
$ strings embedded_stripped | grep -n DUMMY
11:DUMMYKEY-live_9f8b7c6d5e4a3210_DO_NOT_USE
(grep exit=0)
具体例 B

Web のフロントエンドのバンドル

同じ鍵を JS に書き、esbuild 0.28.2 の --bundle --minify で束ねた 148 バイトのバンドル(node:24-alpine、node v24.21.0)。変数名は縮みましたが、鍵は平文のまま残りました。

node:24-alpine の生ログ(抜粋。$ の行は実行スクリプトが出した表示)コマンド出力
$ wc -c < bundle.js
148
$ cat bundle.js
var e="DUMMYKEY-live_9f8b7c6d5e4a3210_DO_NOT_USE";function a(){return fetch("/api",{headers:{Authorization:"Bearer "+e}})}export{a as callBackend};

モバイルアプリとブラウザ向けの JS。実行する場所も言語も違う2つに共通するのは何でしょうか。

その配布物のコピーを誰が持っているかを考えてみてください。

この章のまとめ

  • Mobile Top 10 2024 は事故報告・脆弱性データベース・診断結果を集めたと書くが、件数や項目ごとの値は公開していない。公開は初版 2023-08-02、方法論の表で Final Release は 2024-01-22。
  • M1 は hardcoded credentials を脆弱性の兆候の第一に挙げる。Hardcoded Secrets は独立の項目としては入らなかった。
  • 配布物は受け取った全員の手元にある。strip 後も同じ行に鍵が出て、minify したバンドルにも平文で残った。
  • XOR で隠しても、符号化した並びと復号手順が同じ実行ファイルに残る。M7 は harder to achieve but never impossible。難読化は補助。
  • grep の空振りは証明にならない。未使用の文字列は -O2 で消える。確かめる対象はソースと設定。
  • 直す向きは、共有の鍵はサーバに置き、端末には取り消せるトークンだけを渡すこと。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

Mobile Top 10 2024 での Hardcoded Secrets の扱いとして、原文に合うのはどれですか。

用語集(3語)
hardcoded credentials
コードや設定ファイルに書き込まれた資格情報。M1 は脆弱性の兆候の第一に挙げ、予防策の見出しを Avoid Using Hardcoded Credentials とする。
strip
実行ファイルからシンボル情報を削るコマンド。プログラムが使う文字列データは残る。
難読化 (obfuscation)
配布物を読みにくくする加工。M7 は、攻撃を難しくするが不可能にはしない補助として扱う。