🤖

DchaService を使ってみよう

2024/09/11に公開

今回は、あのチャレンジパッドに含まれている、DchaService の機能、依存関係ライブラリの作り方、使い方について解説していきます。

前提

DchaService は、アプリ であり、
BenesseExtension は、フレームワーク です。
また、BenesseExtension は、CT302.00.000 以降及び、CTX(TAB-A05-BD), CTZ(TAB-A05-BA1) に存在しています。
BenesseExtension については、こちらの記事をご覧ください。

DchaService

DchaService とは、Benesse がチャレンジパッドの動作補助目的で開発したシステムアプリです(多分)。
端末やビルドによって動作が異なる事が有るので管理は面倒です。
パッケージIDは jp.co.benesse.dcha.dchaservice です。

それでは、機能を紹介していきます。

void cancelSetup()

doCancelDigichalized() をコールします。
dcha_state3 の場合( もしくは /data/data/jp.co.benesse.dcha.dchaservice/update.log が存在する場合)はスルーされます。
それ以外の場合、以下の処理を行います。

  • DchaDataBoxWIPE コマンドを送信
    DataBox 内のファイルを消します
  • 全てのユーザーアプリをアンインストール
  • dcha_state0 に変更

boolean checkPadRooted()

CT2 のある特定のビルド以降では、ただ false を返すだけの関数になりました。
本来は、suコマンドが実行可能かどうかを真偽値で返す関数でした。

void clearDefaultPreferredApp(String)

引数にパッケージIDを入れ、そのアプリの規定(優先アクティビティマッピング)を解除します。
例えば、Nova Launcher(com.teslacoilsw.launcher) を既定のランチャーにしている場合、clearDefaultPreferredApp("com.teslacoilsw.launcher") と実行する事で、規定の状態が解除され、次回ホーム画面を開こうとすると、どのランチャーを開くかを問われます。

boolean copyFile(String, String)

ビルドによっては false を返すだけなので注意。
src(一つ目の引数。コピー元)と dst(二つ目の引数。コピー先)の二つを絶対パスに変換し、それらが外部ストレージかどうかを確認し、そうでない場合は false を返す。
ファイルのコピーに成功した場合は true を返す。
dst はディレクトリを指定する事も出来る。

例:copyFile("/storage/sdcard1/src.apk", "/storage/sdcard1/dst.apk")

boolean copyUpdateImage(String, String)

究極の脆弱性。
本来、ファームウェアの更新を行う為の関数であり、cacheパーティションにアクセスするためにシステム権限を用いているのだが、なんとこれは絶対パスを取得せずに相対パスでもいけてしまうので、/data/dev 等にもアクセス出来てしまう。
開発者はアホ。

例(脆弱パターン):copyUpdateImage("/sdcard/src.apk", "/cache/../data/local/tmp/dst.apk")
こっちは copyFile() と違い、ディレクトリの指定は不可。ファイルでないと無理。

boolean deleteFile(String)

copyFile()と同じく、ビルドによっては false を返すだけなので注意。
copyFile()のコピー機能がファイル削除になっただけ。
ディレクトリの削除も可能。
絶対パスを取得して外部ストレージかどうかを確認する部分も一緒。
例:deleteFile("/storage/sdcard1/file.apk")

void disableADB()

ADB を無効化します。
Secure テーブルの adb_enabled の値を 0 にします。

String getForegroundPackageName()

現在フォアグラウンドで実行しているアプリのパッケージIDを取得します。

int getSetupStatus()

DchaService が保有している DigichalizedStatus の値を返します。
想定外のバグが無ければ、普通に dcha_state の値を返します。

int getUserCount()

システムサービスの "user" の個数を返します。
めっちゃ解り易く言い換えると、Android のユーザープロファイルの個数を返します。
CT2 を想像すると解り易いかも?

void hideNavigationBar(boolean)

ナビゲーションバーの表示状態を変更します。
引数にtrueを入れると 非表示、falseを入れると表示されます。
これは、System テーブルの hide_navigation_bar の値を変えており、boolean ? 1 : 0です。
このコードの意味が解らない人は「三項条件演算子」などで調べてください。

boolean installApp(String, int)

APK をインストールします。
一つ目の引数にはAPKのフルパスを入れ、二つ目の引数に 2 を入れると、デバッグ版に限りダウングレードを許可します。
二つ目の引数に関してですが、普通は 0 で大丈夫です。
例:installApp("/data/local/tmp/base.apk", 0)

boolean isDeviceEncryptionEnabled()

これまたビルドによっては false を返すだけです。
システムプロパティの ro.crypto.state が "encrypted" かどうかを真偽値で返します。
要は、端末が暗号化されているかを返します。

void rebootPad(int, String)

一つ目の引数によって処理が変わります。
0:端末を再起動します。
1:リカバリーに初期化コマンド(rebootWipeUserData)を投げます
2:リカバリーにパッケージ(ファームウェア)のインストールを要求します
二つ目の引数は、ファームウェアのインストール時に参照するパスを入力します。
普通は /cache/update.zip です。
例(再起動):rebootPad(0, null)
(ファームウェアアップデート):rebootPad(2, "/cache/update.zip")

void removeTask(String)

引数にはパッケージIDを入れます。
アプ3リ履歴にあるアクティビティのタスクを終了します。
引数は null にする事も可能であり、その場合、アプリ履歴にあるすべてのタスクが終了されます。
例(特定のパッケージ):removeTask("com.android.deskclock")
(全タスク):removeTask(null)

void sdUnmount()

SDカードをソフトウェア単位で取り出します。

void setDefaultParam()

以下を順にコール (処理が多過ぎるので一部抜粋)

  • setInitialSettingsWirelessNetwork()
    • Wi-Fi を有効化
    • 詳細ログを無効化
    • アクセスポイント(ホットスポット)を無効化
    • オープンネットワークの通知を無効化
    • Wi-Fi のスリープ状態を切り替える機能を無効化
    • Bluetooth を無効化
    • Bluetooth のスキャンを無効化
    • 機内モードを無効化
  • setInitialSettingsTerminal()
    • 画面の明るさの自動調整を無効化
    • 無操作からスリープまでの時間を900秒(15分)に変更
    • スクリーンセーバーを無効化
    • 文字の倍率を 1.0 に変更
    • 画面の自動回転を無効化
    • 通知の優先度を変えたり、サイレント(無効化)にしたり、視覚効果を無効にしたりする(多分)
    • dcha_state3 でない場合、AppOps の設定をリセットする
    • ユーザーアプリの状態を変更
      • 通知を無効化
      • アプリを有効化
      • READ_EXTERNAL_STORAGE の権限が宣言されている場合、強制的に昇格
    • AppOps で何らかの2つの権限を昇格
    • 着信音、通知音、アラーム音を全て未設定の状態に変更
    • 音声効果を有効化し、それを読み込み
    • USB接続したときの状態をPTPに変更
    • セーフブートを無効化
    • バッテリーセーバーを無効化
  • setInitialSettingsUser()
    • 位置情報を無効化
    • パスワードの表示を有効化
    • 不明なソースからのアプリのインストールを無効化
    • 画面固定機能を無効化
  • setInitialSettingsAccount()
    • 端末の言語及び地域を日本に変更
    • 物理(ハードウェア)キーボードが接続されている時に仮想(ソフトウェア)キーボードを表示する機能を無効化
    • スペルチェッカーを無効化
    • 自動入力サービスを無効化
  • setInitialSettingsSystem()
    • 日時時刻の自動補正を有効化
    • タイムゾーンを Asia/Tokyo に変更
    • 時間表示を12時間方式に変更
    • アクセシビリティ群を無効化
    • 長押し判定を 0.5秒 または 0.4秒に変更
  • setInitialSettingsDevelopmentOptions()
    • 開発者向けオプションを無効化
    • バッテリー接続時に画面を表示し続ける機能を無効化
    • QSタイルを色々修正
    • Bluetooth の HCI スヌープログを無効化
    • ADBを無効化
    • 電源オプションからバグレポートのショートカットを削除
    • AppOps にて特定の権限が宣言されている場合は却下
    • レイアウトのデバッグを無効化
    • デバッグアプリを削除
    • デバッガーを待機する機能を無効化
    • ADB経由でインストールされたアプリの検証を無効化
    • スクリーンショットを無効化
    • ログバッファのサイズを 256K に変更
    • タップの視覚表示を無効化
    • タップデータのオーバーレイ表示を無効化
    • RTLレイアウトを無効化
    • ウィンドウアニメ/トランジションアニメ/Animator 再生時間 スケールを規定値に変更
    • デスクトップモードの強制を無効化
    • ハードウェア層の更新の表示を無効化
    • 画面の更新を表示する機能を無効化
    • ハードウェア層の更新を表示する機能を無効化
    • GPUオーバードローのデバッグを無効化
    • 非短形クリップ操作のデバッグを無効化
    • 4x MSAA の適用を無効化
    • 色空間シミュレートを無効化
    • USBオーディオルーティング機能を無効化を無効化
    • CPUプロセスの表示を無効化
    • バックグラウンドANRの表示を無効化
    • HWUIレンダリングのプロファイル作成を無効化
    • 通知チャネルの警告の表示を無効化
    • Bluetooth デバイスを名前なしで表示する機能を無効化
    • 再度、開発者向けオプションを無効化
    • これでも尚無効化出来ていない場合はもう一度無効化を試みる

void setDefaultPreferredHomeApp(String)

引数にはパッケージIDを入れる。
対象のアプリにHOMEカテゴリがある場合は、そのアプリを既定のランチャーとして設定する。
例:setDefaultPreferredHomeApp("com.teslacoilsw.launcher")

void setPermissionEnforced(boolean)

なんかのアプリ(恐らくフォアグランドで実行されているアプリ)でREAD_EXTERNAL_STORAGEが宣言されている場合、booleanの値に基づいて権限を修正する。
ただし上手く動作せずDchaServiceが死ぬ。

void setSetupStatus(int)

DchaService が保有している DigichalizedStatus の値を変更します。
dcha_state も直接変更します。
例:setSetupStatus(3)

void setSystemTime(String, String)

一つ目の引数に設定する日付の値、二つ目の引数に日付のフォーマットを入力します。
例:setSystemTime("2024/09/13 03:06:00", "yyyy/MM/dd HH:mm:ss")
フォーマットの詳細は以下のページを参照してください
SimpleDateFormat (Java Platform SE 8 )

boolean uninstallApp(String, int)

一つ目の引数はパッケージIDで、二つ目の引数はフラグです。
フラグの効果が良く解らないので、1 を入れましょう。
例:uninstallApp("jp.co.benesse.touch.setuplogin", 1)

boolean verifyUpdateImage(String)

引数にはファームウェアのパスを入れます。
リカバリーのverifyPackageを召喚して、exceptionが発生しなければ正しい署名かつ正しいパッケージであるとされます。
例:verifyUpdateImage("/data/local/tmp/a05ba-ota-01.03.000_user_full.zip")

ライブラリの作り方

  1. DchaService を使用している別のアプリを用意する
  2. dex を class 化する
    dex2jar を使って JAR 化し、unzip すれば抽出可能
  3. jp/co/benesse/dcha/dchaservice/IDchaService.class をディレクトリごと抽出する
    $Stub$Stub$Proxy も含める
  4. jp ディレクトリをルートとしてZIPに圧縮
  5. ファイルの拡張子を .jar にして完了

ライブラリの使い方

マニフェストにDchaServiceの使用を宣言

AndroidManifest.xml に DchaService の権限の使用を宣言します。

app/src/main/AndroidManifest.xml
<uses-permission android:name="jp.co.benesse.dcha.permission.ACCESS_SYSTEM" />

これが無いと使えません。

依存関係の使用を宣言

Gradle(Groovy形式) の場合、以下の形式で使用を宣言します。

app/build.gradle
dependencies {
    implementation files('libs/DchaService.jar')
}

先程生成したライブラリを app/libs/DchaServices.jar に配置している前提です。

Javaコード内での使い方

ちょっと面倒です。

app/src/main/java/package/DchaTestActivity.java
import static android.content.pm.PackageManager.*;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

import jp.co.benesse.dcha.dchaservice.IDchaService;

public class DchaTestActivity extends Activity {
    IDchaService mDchaService;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // DchaService をバインド
        if (bindService(new Intent("jp.co.benesse.dcha.dchaservice.DchaService").setPackage("jp.co.benesse.dcha.dchaservice"), new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                mDchaService = IDchaService.Stub.asInterface(iBinder);
            }
            @Override
            public void onServiceDisconnected(ComponentName componentName) {
                unbindService(this);
                Toast.makeText(getApplicationContext(), "DchaService から切断されました", Toast.LENGTH_LONG).show();
                finishAndRemoveTask();
            }
        }, Context.BIND_AUTO_CREATE)) {
            try {
                // DchaState を 3 に変更
                mDchaService.setSetupStatus(3);
            } catch (RemoteException ignored) {
            }
        } else {
            Toast.makeText(this, "DchaService をバインド出来ませんでした", Toast.LENGTH_LONG).show();
            finishAndRemoveTask();
        }
    }
}

はい。こんな感じで長くなります。
はっきり言って超面倒くさいです。

アプリ内で権限の昇格を要求

targetSdk が 23 以上の場合、権限をユーザーに昇格してもらう必要があります。
その際は、次のようなコードで出来ます。

app/src/main/java/package/RequestPermission.java
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;

public class RequestPermission extends Activity {
    public static final String ACCESS_SYSTEM = "jp.co.benesse.dcha.permission.ACCESS_SYSTEM";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(ACCESS_SYSTEM) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{ACCESS_SYSTEM}, 0);
            }
        }
        finish();
    }
}

2重 if なので、&& で連結しても良いかも知れませんね。

難読化の対策

ProGuard に以下を追記します

app/proguard-rules.pro
-keep class jp.co.benesse.dcha.dchaservice.IDchaService

DchaUtilService

解説するのが面倒なので、DchaServiceTesterのソースコードを見てください。
ある程度は解ると思います。

Discussion