0. はじめに
Android 上で動作する Chrome/Chromium を自動操作する手段としては、一般的に Appium を利用し、ADB 経由でブラウザを制御する方法が広く知られている。これらは主に自動テストを目的とした仕組みであり、テスト実行環境から対象アプリやブラウザを操作することを前提としている。
一方で、端末上で動作するアプリがブラウザ操作をトリガーし、その結果を取得したいケースなどを実現したいと考えた。しかし、Appium を使うChromiumの操作は外部のテストランナーからの制御を前提としているため、このようなアプリ内からのブラウザ制御には適さない。
本記事では、Android アプリから Chrome/Chromium を自動操作するというユースケースに着目し、その実現方法や技術的な制約、利用可能なアプローチについて検証する
1. 概要
確認した内容:
- Android 端末にインストール済みの Chromium を CDP(Chrome DevTools Protocol) 有効で起動
- 実行時のCDP通信についてはADBを利用しない
- PoC アプリは CDP 経由で Chromium を操作して YouTube 動画を再生する
- Android端末(エミュレータ)で成功を確認
調査結果:
- Chrome DevTools Protocol(CDP)は Android の Intent から有効化することはできない。CDP を有効化するためのコマンドラインスイッチは、Chromium プロセスの起動時にのみ指定可能
- Android 版 Chromium は、一般的な TCP ポートではなく、@chrome_devtools_remote という abstract UNIX socket 上で DevTools 接続を待ち受ける
- Android のセキュリティモデルでは、通常のアプリから別アプリが公開している DevTools 用ソケットへ接続することは、SELinux ポリシーによって禁止される
- ただし、2 つのアプリを 同一の sharedUserId と同一の署名鍵でビルドし、同じ Linux UID を共有させた場合、SELinux の MCS(Multi-Category Security)カテゴリも一致する。その結果、Unix Domain Socket に対する connect 権限が許可され、DevTools ソケットへの接続が可能になる
- 配布済みの Chromium APK をそのまま PoC アプリと同一 UID 化することはできない。再ビルドを避けたい場合、この手法(同一 UID 化)を検証するには、Chromium APK を 再パッケージ(repackage)して再署名する作業が必要となる
2. 検証環境(再現用の確定情報)
| 項目 | 値 |
|---|---|
| ホスト OS | macOS (Darwin 25.5.0), Apple Silicon |
| Android SDK | ~/Library/Android/sdk |
| エミュレータ image |
system-images;android-36;google_apis;arm64-v8a(google_apis = userdebug) |
| AVD 名 | cdp_userdebug |
| Android バージョン | 16 (SDK 36) |
| fingerprint | google/sdk_gphone64_arm64/emu64a:16/BE2A.250530.026.F3/13894323:userdebug/dev-keys |
ro.debuggable |
1(重要: これがないと CDP を有効化できない) |
| ABI | arm64-v8a |
| Chromium |
org.chromium.chrome v151.0.7889.0(ChromePublic.apk arm64) |
| Chromium 入手元 |
https://commondatastorage.googleapis.com/chromium-browser-snapshots/Android_Arm64/<LAST_CHANGE>/chrome-android.zip(検証時 LAST_CHANGE=1645721) |
| Chromium 起動 Activity | com.google.android.apps.chrome.Main |
| PoC アプリ |
com.example.androidcdpapp, minSdk 33 / targetSdk 36, AGP 9.2.1(Kotlin 内蔵) |
| 共有 UID 設定 |
sharedUserId="com.example.cdpshared"、両 APK を ~/.android/debug.keystore で署名 → appId=10216
|
重要な前提:
- CDP を利用するには、google_apis の userdebug イメージなど、root 権限やデバッグ機能が有効な Android イメージを使用する
- Google Play 対応イメージでは ro.debuggable=0 に設定されており、adb root は production build の制約によって拒否される。Chromium が起動時に参照する CDP 有効化用の command-line file を配置・変更できないため、プロセス起動後に CDP を有効化できない
3. 成果物
4. CDP に接続できない理由
通常の Android アプリから他アプリの CDP に到達できない理由は 3 層ある
4-1. CDP 有効化を Intent で渡せない
DevTools エンドポイントは Chromium の コマンドライン switch(--remote-debugging-port /
--remote-debugging-socket-name)で有効化され、これはプロセス起動時に argv / コマンドラインファイルから読まれる。Android の Intent は action / data Uri / extras しか運べず、他アプリのプロセスに argv を注入する公開手段はない。
つまり CDP はアプリの外で(コマンドラインファイル経由・root/デバッグ可ビルド)有効化するしかない。
4-2. Chromium は TCP ではなく abstract socket で待つ
Android の Chromium は TCP ポートを開かず、Linux abstract namespace の UNIX socket chrome_devtools_remote で待ち受ける。
$ adb shell cat /proc/net/unix | grep chrome_devtools
0000000000000000: 00000002 00000000 00010000 0001 01 48209 @chrome_devtools_remote
アプリの「直接 TCP 127.0.0.1:9222」接続は失敗する
4-3. SELinux / UID が abstract socket 接続を拒否
abstract socket へ繋ごうとすると、通常アプリは untrusted_app SELinux ドメイン + 独自 UID で動くため拒否される。PoC アプリの最初の実行で捕捉した 実 avc ログ:
avc: denied { connectto }
for path=006368726F6D655F646576746F6F6C735F72656D6F7465 # = "chrome_devtools_remote"
scontext=u:r:untrusted_app:s0:c214,c256,c512,c768
tcontext=u:r:untrusted_app:s0:c213,c256,c512,c768
tclass=unix_stream_socket permissive=0 app=com.example.androidcdpapp
アプリ側のスタックトレース:
java.io.IOException: Permission denied
at android.net.LocalSocketImpl.connectLocal(Native Method)
at android.net.LocalSocket.connect(LocalSocket.java:162)
at com.example.androidcdpapp.cdp.DevToolsRelay.probeAbstractSocket(DevToolsRelay.kt:77)
ポイント:
scontext も tcontext も同じ untrusted_app ドメインなのに拒否されている。違うのは末尾の MCS カテゴリ(c214 vs c213)。これは Android のアプリ毎サンドボックス分離(per-UID カテゴリ)で、異なるアプリ UID 間の unix socket connect を遮断する。つまり「同じ untrusted_app ドメインだから繋がる」のではなく、UID(とそれに紐づく MCS カテゴリ)が一致しないと繋がらない。
今回のChrome DevTools socketについてはsharedUserId化で接続できた。
5. 回避方法の全体像
| 回避策 | 仕組み | 本 PoC での扱い |
|---|---|---|
| 同一UID 化 | 2 アプリに同一 sharedUserId を宣言し同一鍵で署名 → 同一 UID → MCS カテゴリ一致 → connectto 許可 |
今回のPoCで動作確認 |
| CDP を TCP で公開 | Chromium が TCP を bind すれば SELinux の unix socket 制限を回避できるはず。ただし Android Chromium は標準では TCP を開かない | 標準Chromiumでは不可 |
| privileged / platform 署名 app + sepolicy | system image 側で untrusted_app 以外のドメインを与え、sepolicy で devtools socket への connectto を許可 |
⚠ 未検証 |
採用した 同一UID には前提条件がある:
- 接続元・接続先の両方が同じ
sharedUserIdを宣言していること - 両方が同一の署名鍵であること
-
sharedUserIdは新規インストール時にしか確定できない(既存インストールへ後付け不可)
配布されている Chromium は sharedUserId を宣言していない。したがって Chromium APK を再パッケージして sharedUserId を足し、自分の鍵で署名し直す。
再署名によって一部機能やsignature保護機能は無効化される可能性がある.
- リソースは一切触らず、コンパイル済み binary
AndroidManifest.xmlだけを元 APK に差し替える
6. 手作業の具体的ステップ(再現手順)
作業ディレクトリと変数(全 Step 共通)
- Step 1〜2 の Chromium ダウンロード・再パッケージは作業用ディレクトリ
WORK(本検証では/tmp)で行う - PoC アプリのビルド(Step 3)はプロジェクトルートで行う。以降の各 Step は下記変数を前提にする
export SDK=~/Library/Android/sdk
export BT=$SDK/build-tools/36.1.0
# Chromium の作業ディレクトリ(任意の場所でよい)
export WORK=/tmp/cdp-work
export PROJ=~/AndroidStudioProjects/AndroidCDPApp
export APP_APK=$PROJ/app/build/outputs/apk/debug/app-debug.apk
export PATH=$PATH:$SDK/platform-tools mkdir -p "$WORK"
# apktool.jar も $WORK に置く(例):
curl -fsSL -o "$WORK/apktool.jar" https://github.com/iBotPeaches/Apktool/releases/download/v2.11.1/apktool_2.11.1.jar
Step 0. root 可能な userdebug AVD を用意
量産 (user) ビルドや Play image は ro.debuggable=0 で root 不可 であり CDP を有効化できない。
google_apis(userdebug)を使う。
SDK=~/Library/Android/sdk
# userdebug な system image を入れる
$SDK/cmdline-tools/latest/bin/sdkmanager "system-images;android-36;google_apis;arm64-v8a"
# AVD 作成
$SDK/cmdline-tools/latest/bin/avdmanager create avd \
-n cdp_userdebug -k "system-images;android-36;google_apis;arm64-v8a" -d pixel_tablet
# 起動
$SDK/emulator/emulator -avd cdp_userdebug -no-snapshot-load &
adb wait-for-device
adb shell getprop ro.debuggable # => 1 であること
adb root # => uid=0 になること
Step 1. Chromium ARM64 APK を入手・インストール
cd "$WORK" # ← 作業ディレクトリで実行(以降 Step 2 まで同じ)
BASE=https://commondatastorage.googleapis.com/chromium-browser-snapshots/Android_Arm64
REV=$(curl -fsSL "$BASE/LAST_CHANGE")
curl -fsSL -o chrome-android.zip "$BASE/$REV/chrome-android.zip"
unzip -o chrome-android.zip "chrome-android/apks/ChromePublic.apk"
# package id 確認(=> org.chromium.chrome)
$BT/aapt2 dump badging chrome-android/apks/ChromePublic.apk | grep package:
非公式 APK の注意: snapshot バケットは Chromium 公式だが、第三者ミラーの APK は再署名・改変リスクがある
Step 2. Chromium に sharedUserId を足して再署名(リソースを壊さない方法)
cd "$WORK" # ← 作業ディレクトリで実行
# 2-1. apktool で manifest を編集できる形にデコード(リソースも読む)
java -jar "$WORK/apktool.jar" d -f -o chromium_decode chrome-android/apks/ChromePublic.apk
# 2-2. <manifest> に sharedUserId を追加(namespace prefix は n1)
# <manifest n1:versionCode="..." n1:sharedUserId="com.example.cdpshared" package="org.chromium.chrome" ...>
# 2-3. aapt2(build-tools 36.1) が知らない preview 属性を除去(機能に無関係)
sed -i '' 's/ n1:zygotePreloadNativeLib="libchrome.so"//g; s/ n1:nativeService="true"//g' \
chromium_decode/AndroidManifest.xml
# 2-4. コンパイル済みの AndroidManifest.xml を生成するためだけに、いったん APK 全体を再構築
java -jar "$WORK/apktool.jar" b -f chromium_decode -o chromium_shared_unaligned.apk
mkdir -p new && unzip -o chromium_shared_unaligned.apk AndroidManifest.xml -d new
cp chrome-android/apks/ChromePublic.apk chromium_patched.apk
# manifest だけ置換
( cd new && zip ../chromium_patched.apk AndroidManifest.xml )
# 2-5. align + 署名(debug 鍵)→ 成果物は $WORK/chromium_patched_aligned.apk
$BT/zipalign -p -f 4 chromium_patched.apk chromium_patched_aligned.apk
$BT/apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android \
--key-pass pass:android --ks-key-alias androiddebugkey chromium_patched_aligned.apk
Step 3. PoC アプリにも同じ sharedUserId を宣言してビルド
app/src/main/AndroidManifest.xml:
<manifest ...
android:sharedUserId="com.example.cdpshared"
tools:ignore="Deprecated">
cd "$PROJ" # ← プロジェクトルートで実行
./gradlew :app:assembleDebug # debug ビルドは ~/.android/debug.keystore で署名 = Chromium と同一鍵
# 成果物は $APP_APK (= $PROJ/app/build/outputs/apk/debug/app-debug.apk)
Step 4. 両 APK を新規インストール(鍵一致 → 同一 UID)
2 つの APK は別の場所にある(Chromium=
$WORK、アプリ=$PROJ配下)。下記は絶対パス変数を使うので、どのディレクトリからでも実行できる。
# 署名証明書が一致することを確認(同一 SHA-256 digest であること)
$BT/apksigner verify --print-certs "$WORK/chromium_patched_aligned.apk" | grep "SHA-256 digest"
$BT/apksigner verify --print-certs "$APP_APK" | grep "SHA-256 digest"
# sharedUserId は新規インストールで確定 → 既存をアンインストールしてから入れる
adb uninstall org.chromium.chrome; adb uninstall com.example.androidcdpapp
adb install -r "$WORK/chromium_patched_aligned.apk"
adb install -r "$APP_APK"
Step 5. CDP を有効化(コマンドラインファイル)
adb root
adb shell 'echo "chrome --remote-debugging-port=9222 \
--remote-debugging-socket-name=chrome_devtools_remote \
--disable-fre --no-first-run" > /data/local/tmp/chrome-command-line'
adb shell chmod 644 /data/local/tmp/chrome-command-line
Chromium は コールドスタート時にこのファイルを読む。adb shell am force-stop org.chromium.chrome してから起動すること。
7. 成功確認ステップ
7-1. UID が共有されたか
adb shell dumpsys package org.chromium.chrome | grep -m1 appId # appId=10216
adb shell dumpsys package com.example.androidcdpapp | grep -m1 appId # appId=10216 ← 一致
adb shell dumpsys package com.example.androidcdpapp | grep sharedUser
# sharedUser=SharedUserSetting{... com.example.cdpshared/10216}
7-2. CDP socket が立ったか / フラグが読まれたか
# Chromium をコールドスタートして socket を待つ
adb shell am force-stop org.chromium.chrome
adb shell am start -n org.chromium.chrome/com.google.android.apps.chrome.Main \
-a android.intent.action.VIEW -d "https://m.youtube.com/watch?v=LWYjKvX5bQw"
# abstract socket が現れること
adb shell cat /proc/net/unix | grep chrome_devtools_remote # => @chrome_devtools_remote
# フラグが読まれたこと
adb logcat -d | grep "COMMAND-LINE FLAGS"
# cr_CommandLine: COMMAND-LINE FLAGS: [chrome, --remote-debugging-port=9222, ...]
7-3. CDP がホストから応答するか(任意の独立確認)
adb forward tcp:9222 localabstract:chrome_devtools_remote
curl -fsS http://127.0.0.1:9222/json/version
# {"Browser":"Chrome/151.0.7889.0", "webSocketDebuggerUrl":"ws://127.0.0.1:9222/devtools/browser", ...}
adb forward --remove tcp:9222
7-4. PoC アプリ単独での end-to-end 確認
adb shell am start -n com.example.androidcdpapp/.MainActivity
# 「OPEN AND PLAY YOUTUBE」ボタンを押す(CLI なら座標タップ)
adb shell input tap 1280 451
# ログを監視
adb logcat | grep -E "AndroidCDPApp|DevToolsRelay"
# SELinux 拒否が 0 件であること
adb logcat -d | grep -c 'avc:.*denied.*chrome_devtools' # => 0
8. まとめ
AndroidアプリからCDP経由でChromiumを操作してYouTube を自動再生可能であることを示した。
今回は Chromium をスクラッチからビルドし直す時間を省くために、同一UIDをPoCアプリとChromiumにもたせて、PocアプリからCDP経由でChromiumを操作できるようにした。
しかし、この方法は緊急避難的な対応なので、本来は Androidネイティブアプリから CDP 経由で Chromium を操作したい場合は、CDP の待受を UNIX Socket ではなく、HTTP(WebSocket) か Binder でアクセスできるように修正するのが良いだろう。
ところで、CDPは後方互換性を保証するAPIではなく変更される可能性が高いインターフェイスであり、Chromiumのバージョン更新時には追従確認が必要だ。CDP経由でChromiumを操作するAndroidアプリを開発する場合は、継続的に最新の Chromium への追従し続けるかを計画しなければならない。
