読者です 読者をやめる 読者になる 読者になる

ADB (Android Debug Bridge) のプロトコル 前編

Android Kotlin

概要

ADB の Java クライアントを試作してみようと取り組んでみたら深みにはまっていったお話。 とても長いので前後編に分けました。

雑談

USB ケーブルがなくっても使える ADB

ADB (Android Debug Bridge) は Android アプリ開発者にとってはおなじみのツール。 普段、ADB を使用するときは USB で PC とスマホを接続する。 ADB は USB だけでなく TCP で接続することもできる。 TCP で接続すれば、PC からスマホだけでなく、スマホからスマホにネットワーク経由で接続することができる。 さらに言えば、スマホからスマホ自身に接続することができる。 そんなことをして何が嬉しいのかって?

ADB を使えば空だって飛べる

eligor13.hatenablog.jp

こちらは、スマホからスマホ自身に接続している例。 パッケージの無効化をやっている。 プリセットアプリが邪魔だと思ったことはありませんか。

他にも色々とできます。 例えば、input コマンド。

先ほどの例では、スマホに adb コマンドがインストールされていることが前提となっている。 しかし、Xperia Z5 には adb コマンドはインストールされていない。 そんなときは Terminal Emulator。 Terminal Emulator を使えばコマンドを使える。

play.google.com

input コマンドを Terminal Emulator で使ってみる。

$ input tap 100 300

input コマンドはタップ操作をエミュレートすることができる。 上記の例では、座標 (100, 300) の位置がタップされる。 左上が (0, 0) なので、Terminal Emulator のコマンド入出力画面がタップされる。 空が飛べるというのは冗談として、うまく使えば自動操作くらいはできる。

ところが、どの座標でもタップされるわけではない。 ソフトウェアキーボードの領域を input コマンドでタップさせてみてもコマンドは機能せず、Terminal Emulator の領域のみ機能する。 コマンドを実行する際には権限が影響してくる。 Terminal Emulator では一般ユーザー権限としてコマンドを実行する。 一般ユーザー権限では使える機能が制限される。

そこで登場するのが ADB。

スマホには ADB がインストールされていないので、PC から接続してみる。 Android アプリ開発者にはおなじみの方法。 スマホChrome を立ち上げた状態で、下記のコマンドを PC で実行してみる。

$ adb shell input tap 100 100

Chrome の画面左上のホームボタンがタップされた。 ADB Shell を使った場合には、ソフトウェアキーボードの領域であろうがタップできる。 ADB Shell を使った場合の権限は一般ユーザー権限よりも多くの権限を与えられている。 root 権限であれば全てを操作できるが、全ての操作は行えていないので root 権限ではない。

ADB を使えば、アプリからは使えない機能だって使える。

Terminal Emulator の裏側

Terminal Emulator のソースコードを読んだわけではないが、想像するに次のようなことを行っていると思われる。

Runtime.getRuntime().exec("input 1000 100")

Runtime (ProcessBuilder の方がよいかも) を使えば、コマンドを実行できる。

しかし、上記の処理を実行して自アプリ以外の領域 (例えば、ソフトウェアキーボード) の領域をタップすると、 android.permission.INJECT_EVENTS 権限のエラーが表示される。 AndroidManifest.xml に INJECT_EVENTS 権限を加えてみても解決はしない。 権限についてはセキュリティのための仕掛けがあり、普通のアプリでは自アプリ以外の領域に input コマンドによるタップイベントは送れない。

blog.onpu-tamago.net

input コマンドを使って発生させるタップイベントは

  • 自アプリからは自アプリの領域のみに送れる
  • ADB からはどこの領域でも送れる

スマホだって ADB を使いたい

では、ADB を自アプリから接続したらどうだろう。 はじめに話したとおり、ADB は TCP 接続も可能になっている。 但し、一度は USB で接続する必要がある。 USB で接続して次のコマンドを実行する。

$ adb tcpip 5555

5555 はポート番号。スマホ側はこのポート番号で ADB の通信を待ち受けるようになる。 上記だけでスマホ側 (サーバー) の準備は完了。

次は、サーバーに接続するクライアント。 自作するのは面倒なので Java のライブラリを探すと JADB なるライブラリがみつかる。

github.com

いざ使ってみると動かない。おや?使い方が悪いのか? ソースを読むと送信しているデータが随分とシンプルに思える。

ADB のプロトコル文書を探すと、こちらも見つかる。ラッキー。

github.com

そんなに複雑ではなさそうなので、これなら自作できるか。

いざ作り始めると、はまる。 まず、CRC32 と書いてあるから CRC32 を理解するために調べてみた。 CRC32 はデータの破損を検出するための技術。

CRC32 を生成する処理を実装してみた。 配布されている ADB でコマンドを実行し、ADB とスマホの通信を WireShark でキャプチャーする。 キャプチャーしたパケットのデータと CRC32 を取得。 実装した処理でデータの CRC32 を生成してみるが、キャプチャーした CRC32 と一致しない。 何が違う?

ADB Test - Google グループ

文書に書かれている CRC32 は嘘だった。 ただのチェックサムだ。 データの各バイトを加算するだけという簡単な仕様。 いずれは CRC32 にするつもりなのか。 実装を CRC32 からチェックサムに置き換えたところ、キャプチャーしたパケットと一致。 無事に解決。

次のハマりポイントは認証。仕組みは次のとおり。

  1. サーバーはクライアントにトークンを送信
  2. クライアントは RSA秘密鍵とトークンを使って署名を生成
  3. クライアントはサーバーに署名を送信
  4. サーバーは署名を検証
  5. 検証した結果、既知のクライアントであると識別できたら接続を確立

サーバーが署名を検証するためには、サーバーに公開鍵をあらかじめ渡す必要がある。 処理が加わる。

  1. サーバーは署名を検証できなかったときに、再びトークンを送信
  2. クライアントは RSA の公開鍵をサーバーに送信
  3. サーバーはユーザーに確認を求める
  4. ユーザーが許可すると接続を確立

送信する公開鍵の後ろには user@host を付与する。 4096 ビットで生成した鍵を送付したところ受け付けられた。 ハマりポイントは最後に 0 (文字ではなく、数値として) を付与する必要がある。 後から思えば不思議ではないが、トークンや署名はバイト列だから最後に 0 は不要、公開鍵は文字列なので 0 が必要という仕様。 C 言語で書くことを思えば納得。

公開鍵のデータは、~/.android/adbkey.pub (macOS 環境の場合) ファイルの内容を送ればよいということだ。

Socket のコンストラクタでは、スマホの IP とポート番号 (adb tcpip コマンドで指定した番号) を指定。 プロトコル文書に従って構築したパケットを送信することで、 クライアントとサーバーの接続が完了して、コマンドを実行する準備が整った。

後編に続く。