2011年08月09日

root 化ずみ端末に対応した Android アプリを書く方法

はてなブックマークに登録

Android 端末を root 化するユーザが増えています。
一部の開発者向け製品以外では root 権限を取得するための公式の手段は提供されていないので、その方法を自分で探索するにせよ誰かが開拓した手順をトレースするにせよすべては自己責任であるわけですが、一方で root 権限を必要とするアプリはマーケットにもあれこれ出回っており、つまりはこの件に関する安全性と利便性のトレードオフに満足できない向きが世界中に大勢いるということでしょう。

その状況自体もいろいろ興味ぶかいのですが、ところで、その「root 権限を利用するアプリ」というものはどうやって書けばいいのでしょう?Google 公式の開発者向け資料は言うに及ばず、その他のリソースにも今のところほとんどこの話題に関する情報は見当たりません。そこで今回は、実際に手元のアプリを root 権限での実行に対応させる試みを通じて得た情報やノウハウを紹介したいと思います。


どういうものを作ればいいか?

まず、「root 権限を利用するアプリ」がどのような形のものであるかを明らかにする必要がありますね。 ここでは前提となるふたつの話題を確認しながらそれを整理してみます。

■ 前提 1. Android アプリの uid について

一件の Android アプリの uid は、それを端末へインストールした時点でシステムにより自動的に割り当てられます。この uid は端末においてアプリごとに固有です(※)。このようなしくみであるため Android ではシステムが割り当てた uid 以外でアプリを実行することはできず、root 化ずみの端末であってもインストールずみのアプリケーション本体を root 権限で実行することはできません。

(※)例外としてアプリ開発時に AndroidManifest.xml での sharedUserId 指定により意図的に複数の自作アプリで uid を共有することができます

■ 前提 2. "su" コマンドと "Superuser" アプリについて

Superuser」 はその筋での実質標準として大変有名なアプリケーションなのでの多くの方がご存知でしょう。 端末の root 化は Andorid の Linux 環境に owner=root で設置した "su" コマンドに setuid ビットを立てることで実現しますが、 そこでは ChainsDD (Adam Shanks) 氏による su コマンドのバイナリを、同じく同氏の開発した Android アプリである Superuser とペアで導入することが一般的です。この組合せにより、ユーザはアプリごとに root 権限付与の可否を対話的に設定することが可能となります。

root 権限を利用するアプリを書くためには、この 「su + Superuser」 の組合せがどのように機能しているのか、そのしくみを把握しておく必要があるでしょう。概要を以下に示します。

su と Superuser の処理の流れ

ChainsDD 氏の su は、何かのプロセスから実行要求を受けると、そのプロセスの uid を調査し要求されたコマンドラインと uid の情報をもとに所管のホワイトリストデータベースを照会する。該当するレコードが存在すればそのレコードの "allow", "deny" の属性に応じて呼び元からの要求を許可または拒否し、該当レコードがなければ要求情報一式を action="com.noshufou.android.su.REQUEST" のインテントに整形してシステムへブロードキャストする。
一方、Superuser は Activity としての実装以外にブロードキャストレシーバとしての実装を備えており上記ブロードキャストの発生時にシステムによりキックされる。そして、当該アプリの su 実行可否を GUI でユーザに確認しその結果をホワイトリストへ保存+ユーザの選択をプロセス間通信で su コマンド側へ伝える、という振る舞いをする。

■ つまり、こういうものを作ればよさそうです

以上の話題にもとづき、root 権限を利用するアプリを開発する上で留意すべき点を挙げてみます。

a) root 権限で実行できるのはアプリ本体ではなく、Android の Linux 環境上の "su" コマンド経由で生成するプロセスである

b) そのため、root 権限の必要な処理はアプリ本体に記述するのではなく、Android の Linux 環境において実行可能な独立した実行形式に記述する必要がある

c) アプリ本体は、必要な場面で上記の実行形式を "su" コマンド経由で実行することによって root 権限での処理の効果を得る

上記 a) は、当該アプリが root 権限での実行に求めるものが Linux のシェルそのものであったり、あるいは標準的なコマンドの機能の範囲で収まる場合には b) で触れた別の実行形式は必要ないということも意味しています。それにあてはまる典型的な例はターミナルエミュレータアプリでしょう。

上記 b) の「実行形式」は実行単位としてはアプリ本体から独立した存在ですが、あくまでも処理全体の文脈の一部であるためアプリ本体と物理的には分離せず、必要な場面で切り出して使用できるようにリソース等の形でアプリへ埋め込んでおくことが自然でしょう。

上記 c) は、uid !=root の親プロセス(アプリ本体)と uid = root の子プロセス(su 経由のプロセス)の所作を適切に協調させる必要があることを意味します。多くの場合、その制御は、子プロセスよりも低い権限で動作中の親プロセスであるアプリ側が担うことになるでしょう。


どのように作ればいいか?

目標とするプログラムの形が前項でほぼ明らかになりました。ご覧の通り考え方そのものはごくシンプルなのであとはアプリの要件に応じてを肉づけを行えばよいでしょう。以下、実装を行う上で参考になりそうなポイントをいくつか控えておきます。

■ su コマンドの実行に関すること

アプリ内から su コマンドを実行するには java.lang.Runtime クラスの exec メソッド等を使うとよいでしょう。

Process process = Runtime.getRuntime().exec(cmdstr);

su コマンドを実行する際にはパラメータの指定に注意が必要です。
-c オプションを使えば "su -c <cmd> [args]" の要領で実行対象のプログラムとその引数をまとめて指定することができますが、その形で要求を行った場合、Superuser は "<cmd> [args]" の一式を要求コマンド情報として所管のホワイトリストへ記録します。

つまり、<cmd> へ渡す [args] の内容が可変の場合、Superuser はその都度新規の要求としてユーザに実行可否の確認を行い、さらに、その要求内容を新規のレコードとしてホワイトリストへ追加するということです。

Superuser を効果的に使うためにそういう指定は避けるべきです。コマンドラインとしては "su -c <cmd>" までの指定に留め、[args] に該当する情報は別途プロセス間通信等で受け渡すことが望ましいでしょう。 別の方法として、オプションなしの "su" を実行して uid=root のシェルプロセスを起動し、その stdin ストリームにアプリから所定のコマンド文字列を書き込んで実行させるというやり方もありますが、Android を狙うマルウェアの脅威が増大しつつある現状においては、それが短期的なものであったとしても、裸の root シェルプロセスを起動する仕様を 新規に作成するアプリへ組み込む選択は避けるべきかもしれません。

■ 「独立した実行形式」の開発について

前項に記述した su コマンド経由で実行する 独立した実行形式 は Android OS の Linux 環境上で適切に動作する内容であれば開発手順や言語は何でもよいのですが、おそらくもっとも手軽で確実なのは C 言語で処理を記述し Android NDK を使ってビルドするという方法でしょう。 NDK でスタンドロンの実行形式を作成する方法については下記の記事などが参考になると思います。

DSAS開発者の部屋:Android NDK でネイティブ CUI プログラムを書く!

■ 「独立した実行形式」のアプリへの埋め込み方と切り出し方

NDK 等で作成したスタンドアロンの実行形式は 前述 のとおりアプリ本体に埋め込んでおいて必要な時にアプリ側から切り出して使用するという扱い方がよいでしょう。

アプリへの埋め込みは、Eclipse 上で対象アプリのプロジェクトツリー上の [res] - [raw] に、当該実行形式ファイルをリソースデータとしてコピー&ペーストするのが簡単です。これで apk ファイルに実行形式が挿入されます。
apk からの切り出しには下記コードの要領で android.content.Context の getResources メソッドを使います。このコードを含むソースファイルはここにあります。切り出した実行形式には必ず実行許可を付与します。

OutputStream os = new FileOutputStream(strOutputFileName);
byte[] buf = new byte[1024];
InputStream is = mContext.getResources().openRawResource(nResourceId);
int len = 0;
while ((len = is.read(buf)) != -1) {
    os.write(buf, 0, len);
}
is.close();
os.close();

なお、「raw」データとして apk に埋め込んだリソースファイルのデフォルトの出力先は 「/data/data/[パッケージ名]/files」 というディレクトリです。このディレクトリのパスはアプリ内から次のコードで取得することができます。
[context.]getFilesDir().getAbsolutePath() + File.separator
su コマンドへ実行形式のフルパスを渡す際などにこのコードを使うことになるでしょう。

■ プロセス間通信について

su コマンド経由でひとたび起動したプロセスは当該アプリの子プロセスではあるものの親とは異なり強大な root 権限で動作しています。それでも特別な事情がない限りはユーザインターフェイスの要素を含め、プログラム全体のコントロールはあくまでも親プロセスであるアプリ側が担うことになるでしょう。

たとえば子供である root プロセスに任せる処理がごく直線的なもので、一度呼び出したらその終了を待つだけでよいような内容であればアプリ側は子プロセスの稼動状況を監視するだけで事足りるでしょう。 しかし、多くの場合シナリオはもっと複雑で、おそらく最低でもアプリは子プロセスを終了させるための指示を送る必要があるでしょう。冒頭に書いた手元のアプリの場合もそうでした。

もっとも手軽な合図の方法は子プロセスへのシグナル送信ですが、残念ながら非 root のプロセスから root のプロセスへ適切にシグナルを送ることはできませんね。それならここではプロセス間通信に何を使うのがよいか?が同僚とのやりとりでも話題となり、このケースでの実用上の秘話性確保の点でも扱い易さの点でも UNIX ドメインソケットを使うのがいいだろう、という話に落ち着きました。
アプリから子プロセス側に指示を送ることが目的なので、まず子プロセス側にサーバ機能を C 言語で実装し、次にアプリ側にクライアント機能を実装しようとしたところで Java では UNIX ドメインソケットがサポートされていないことを知りました。これはプラットフォーム依存を排する言語設計に基づくものでしょう。 ただし、Android SDK は独自拡張で UNIX ドメインソケットに対応しています。意外とそのサンプルコードが見当たらないのでクライアントのコード例を示します。(例外処理を除いています)

import android.net.LocalSocket;
import android.net.LocalSocketAddress;
         :
public int SendUnixDomainSocketMessage(String sockpath, String msg) {
  LocalSocket lsock = new LocalSocket();
  LocalSocketAddress endpoint =
    new LocalSocketAddress(sockpath,
        LocalSocketAddress.Namespace.FILESYSTEM);
  lsock.connect(endpoint);
  if (lsock != null && lsock.isConnected()) {
    OutputStream out = lsock.getOutputStream();
    byte [] bytemsg = msg.getBytes();
    out.write(bytemsg);
    out.flush();
    lsock.shutdownOutput();
    lsock.close();
  }
  return 0;
}

■ root 化ずみ環境の判定について

たとえそれが root 化ずみ端末でなければメインの機能を果たさないアプリであったとしても、無垢な端末環境で実行した際に意味不明のエラーが発生するといった事態は避けるべきでしょう。 また、アプリによっては端末が root 化されていれば特定の機能を利用可能とし、そうでなければその機能を無効化するといった実装が必要になる場合もあるでしょう。 そのためには現在の端末環境が root 化ずみであるかそうでないかの判定を行う必要がありますね。

実際にはそれはなかなか根の深い話題ですが、常に十分ではないものの一定水準の実用性のある方法として、「Superuser がインストールされているか否かを調べる」というやり方が挙げられます。指定されたパッケージ名からそのアプリがインストールずみかそうでないかを真偽値で返すコード例を示します。 Superuser のパッケージ名である "com.noshufou.android.su" を渡せばインストールの有無がわかる、というわけですね。

import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
         :
public boolean AppIsInstalled(String PackageName) {
  PackageManager pm = mContext.getPackageManager();
  try {
    ApplicationInfo ai = pm.getApplicationInfo(PackageName, 0);
  } catch (NameNotFoundException e) {
    return false;
  }
  return true;
}

(tanabe)
klab_gijutsu2 at 21:00│Comments(4)TrackBack(0)Android | win

トラックバックURL

この記事へのコメント

1. Posted by dayflower   2011年08月10日 10:58
最後のroot化済環境の判定についてですが
実害のない"su -v"あたりを実行して確かめるという実装もそこそこ広く使われているみたいです。
ただしプロセス間通信を使う場合だとホワイトリスト項目が別になってしまいますね。

ある日本の事業者の端末では
"com.noshufou.android.su"がブラックリストに載っており
パッケージ名を変えて対処しているユーザもいます。
「常に十分ではないものの一定水準の実用性のある」とお書きになっているので
ご存知かもとは思いましたが
すでにそういう実装もあるということで。
2. Posted by tanabe   2011年08月10日 12:06
dayflower さん、コメントをありがとうございます。
ご指摘の通りネット上の記事には "su -v" を使う方法も散見されるのですが、-v が実装依存であることが気になっていました。たしかに ChainsDD 版の su では stdout にバージョン情報を出力するオプションとして実装されていますが、もし Superuser とも無関係に別の実装の su が設置されていた場合はどうでしょう。その場合、コマンド実行を試行すること自体にリスクがないとは言い切れないのですよね。
(あえて手近な例をひとつ挙げると、Android SDK に含まれる Android エミュレータの Linux 環境には最初から su コマンドが設置されていますが、この su を -v つきで実行すると -v は無視されオプションなしの su と同じ挙動となります)

一方で、この記事では ChainsDD 版 su + Superuser の設置された root 化ずみ環境をひとまずターゲットとして想定しており、Superuser と協調するためのノウハウにも触れています。そのため、常に安全とは限らないコマンドの試し撃ちという方法ではなく、Superuser のインストール有無で判定を行う方法に言及しているわけです。これは、安全かつ確実に解決することの難しい話題のひとつだと思います。
3. Posted by dayflower   2011年08月10日 13:11
深いところまで配慮した内容だったのですね。
失礼いたしました。

リスク廃するか可搬性をとるかまさに「安全かつ確実に解決することの難しい」話題ですね。
4. Posted by tanabe   2011年08月10日 13:21
おっしゃる通りだと思います。お手元で良い方法など想起された折にはぜひご一報下さい。コメントをありがとうございました。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント
「最新トラックバック」は提供を終了しました。