👨‍💻

チャレンジパッドNeoのパスワードを突破した方法

2025/02/07に公開

今回は、チャレンジパッドNeo/Next において、どのようにして 開発者向けオプション のパスワードを回避したのかを解説します。

AOSPの抜け穴を探す

まずは、ブラウザが開けるかどうかを確認します。
これは、APKをダウンロードしてインストールできるかどうかを確認するためです。

確認方法としては、Wi-Fi の設定からヘルプを開くか、OSSライセンスを開くか、キーボードショートカットを使うかです。

結論としては、無理です。
と言うか、仮にブラウザを開けたとしても無理でした。

と言うのも、この 開発者向けオプション を塞ぐ機能自体、ただパスワードを掛けるだけで無く、ブラウザ、検索アプリの無効化、提供元不明のアプリの無効化が含まれています(一部)。

セットアップウィザードを解析

ブラウザが塞がれている場合、他に残ってる手段は、ホーム画面にデカデカと表示されている、ベネッセが作成したセットアップウィザードです。

ただ、ADB が使えないので、アプリそのものの解析は出来ません。
まず、現時点で出来ることを整理します。

  • Wi-Fi に接続
  • プライベート(ローカル)ネットワークに接続
  • Proxy の設定
  • Bluetooth に接続
  • USB でのファイル転送
  • Bluetooth でのファイル転送
  • 画面ロック
  • 端末管理者
  • 証明書の追加

一部ですが、これらが挙げられます。

ファイル転送が出来るということは内部ストレージが覗けますが、面白いものは何一つありません。

結論、パケットキャプチャリングが出来ます。
通信内容の傍受です。
これが出来ると、セットアップウィザードがどのタイミングでどのような通信をしているのかを見れます。

手段の一例として、今回は Charles Proxy を使用しました。

傍受手順

  1. PC に Charles をインストール
  2. プライベートネットワークとして接続
  3. Charles を開く
  4. Proxy Settings を開き、HTTP Proxy の Port を確認する(通常は 8888)
  5. SSL Proxying Settings を開き、Enable SSL Proxying にチェック
  6. 同プロパティの Include 側の Add を開き、Host を * として設定し追加
  7. Local IP Addresses を開き、PC 側のローカル IP(v4) を確認して控える
  8. Save Charles Root Certificate から Binary certificate (.cer)をエクスポート
  9. チャレンジパッド にコピー
    この際の手段として、SDカードかUSB転送が挙げられます。
  10. 画面ロックを設定
  11. コピーしたルート証明書をインストール
    必ず VPNとアプリ(規定) としてインストールしてください。
  12. PC と同じ Wi-Fi に接続します
  13. 同ネットワークのプロパティを開き、プロキシ を 手動 にし、ホスト名を 先ほど控えた PC 側の IP(v4) アドレス、ポートを 8888 と入力し保存
  14. 変更を適用するために、Wi-Fi をオフにし、再度オンにします
  15. PC 側に戻り、Charles に、Connection from [IP(v4)] のポップアップが出るので、なるべく早く Allow を押します
  16. Charles 左上の🧹のアイコンを押し、ログを消します
  17. チャレンジパッド 側に戻り、充電器を挿した状態で、セットアップウィザード を開きます
    この際、充電残量が 50% 未満の場合は続行できません。
  18. Wi-Fi のスピードテストまで進めます
  19. PC 側で、Charles に http://ctcds.benesse.ne.jp が出てくるのを確認します
    これにより、正しく傍受していることを確認します。
  20. チャレンジパッド側だそのまま続行し、ログイン画面になる前に https://townak.benesse.ne.jp にアクセスされた事を確認します
    内容を確認すると、TouchSetupLogin という APK ファイルがダウンロードされているのが確認できます。

ほぼ Charles の説明になってしまいましたが、結果として、会員にログインするためのアプリがダウンロードされている事が判明しました。

ログインアプリの改ざん

セットアップウィザードは、ログインするために、専用のアプリをダウンロードし、インストール後、アプリを開く事が分かりました。
今度は、Proxy の Rewrite 機能を使って、APK を差し替えてみます。

まず、TouchSetupLogin のパッケージ名を調べます。
デコンパイラーに APK を入れると、マニフェストに jp.co.benesse.touch.setuplogin と出てきます。
このパッケージ名と同じアプリを作っていきます。

とりあえず、権限の自動昇格のために、targetSdk は 22 にします。
MainActivity は何もいじらずに、Hello world が出る内容のままで大丈夫です。
後は、マニフェストから、MainActivity のカテゴリの LAUNCHER を DEFAULT に置き換えてください。
この状態でビルドします。
これは、TouchSetupLogin のマニフェストから確認できます。

APK をローカルサーバーでも良いので、どこかにアップロードします。

差し替え方法

  1. Charles で Rewrite settings を開き、Enable Rewrite にチェックを入れ、下部の Add を押して項目を追加します
  2. Untitled Set を開き、Type を URL にします
  3. 右上の Location の Add を押し、Protocol を HTTPS、Host を townak.benesse.ne.jp、Port を 443、Path を rel/A/sp_84/open/TouchSetupLogin.apk と入力し、OK を押します
  4. 右下の Add を押して Rewrite rule を開き、Match の Value を https://townak.benesse.ne.jp/rel/A/sp_84/open/TouchSetupLogin.apk に、Replace の Value を 偽装 APK の URL にし、Replace all にチェックを入れ、OK を押します
  5. もう一度 OK を押して、ルールを適用します

これで差し替えの準備は出来たので、実際にテストしてみます。

セットアップウィザードを最初からやり直すと、こう新中です のプログレスバーの後に、Hello world のアクティビティが立ち上がるのが確認できたと思います。

パスワードの回避

後は、パスワードを無効化する処理をアプリに組み込めれば良いわけです。
ただ、現時点ではアプリの差し替えが出来ただけで、回避するための情報は何一つありません。
このパスワード機能について解析するには、設定アプリをデコンパイルする必要があります。

残念ながら通常の方法ではここまででこれ以上何もする事は出来ません。
DchaService の copyUpdateImage 関数を使って抽出する方法もありますが、かなり難易度が高く手間も掛かります。

これはセコいのですが、最終手段として、ベネッセの会員情報を使い、Neo のファームウェアを取得しました。
そして、それを展開し、deodex して、MtkSettings をデコンパイルしました。

すると、開発者向けオプションのアクティビティの欄に、BenesseExtension の getDchaState() が 3 以外の時に、BenesseExtension の checkPassword を呼び出し、それが合っていれば開く様になっています。

public static /* synthetic */ void lambda$onCreate$0(DevelopmentSettingsDashboardFragment developmentSettingsDashboardFragment, DialogInterface dialogInterface, int i) {
    if (!BenesseExtension.checkPassword(developmentSettingsDashboardFragment.mEditText.getText().toString())) {
        developmentSettingsDashboardFragment.getActivity().finish();
    } else {
        developmentSettingsDashboardFragment.mView.setVisibility(8);
    }
}
if (BenesseExtension.getDchaState() != 3 && BenesseExtension.COUNT_DCHA_COMPLETED_FILE.exists() && !BenesseExtension.IGNORE_DCHA_COMPLETED_FILE.exists()) {
    this.mFrameLayout.addView(this.mView, 0, new ViewGroup.LayoutParams(-1, -1));
}

BenesseExtension の詳細については今回は述べません。
とにかく、getDchaState() の戻り値が 3 の時はパスワードが要らないと分かったわけです。
なら、偽装ログインアプリで getDchaState() の返り値を 3 にすれば良い訳です。
その値自体がどこに保管されているかを確認する必要があります。
フレームワークをデコンパイルすると、

サンプル
static final String DCHA_STATE = "dcha_state";
public int getDchaState() {
    long clearCallingIdentity = Binder.clearCallingIdentity();
    try {
        return getDchaStateInternal();
    } finally {
        Binder.restoreCallingIdentity(clearCallingIdentity);
    }
}
private int getDchaStateInternal() {
    return Settings.System.getInt(this.mContext.getContentResolver(), DCHA_STATE, 0);
}

このようなコードが確認できます。
つまり、System ネームスペース にあると言う事が確認できました。
このコードを putInt に置き換え偽装アプリにコピーし、Hello world が表示されたタイミングで実行されるようにします。
System ネームスペースの値を変えるには、android.permission.WRITE_SETTINGS の権限が必要となるので、マニフェストに追記します。

この状態でビルドし、差し替え APK を上書きし、セットアップウィザードをやり直すと、また Hello world のアプリが立ち上がります。
見た目こそ変わりませんが、一旦ホームに戻って設定アプリを開きます。
この際、セットアップ途中でホーム画面に戻ると、本来であれば UI が元に戻りアプリが全て削除されるのですが、これも dcha_state の挙動によって制御されています。

設定アプリの タブレット情報 から ビルド番号 を7回タップし、開発者向けオプション を表示させます。 一つ前の画面に戻り、開発者向けオプション を開くと、無事開きました!
そのまま USB デバッグ を有効にして、PC 経由で ADB の接続を試みると、あっさり接続出来ました。

ここから Neo/Next の魔改造の冒険が始まったわけですね。


以下はオマケです

ファイルを置いて回避

getDchaState() の戻り値を 3 にする以外に、

BenesseExtension.COUNT_DCHA_COMPLETED_FILE.exists() && !BenesseExtension.IGNORE_DCHA_COMPLETED_FILE.exists()

このように書いてありました。
定数を確認すると、

public static final File IGNORE_DCHA_COMPLETED_FILE = new File("/factory/ignore_dcha_completed");
public static final File COUNT_DCHA_COMPLETED_FILE = new File("/factory/count_dcha_completed");

このように記載があります。
と言うことは、COUNT_DCHA_COMPLETED_FILE が作成されなければ、もしくは、IGNORE_DCHA_COMPLETED_FILE を作成すれば大丈夫な訳です。
ただし、IGNORE_DCHA_COMPLETED_FILE はファイルの UID が 0 (root) かどうかの確認が入ります。

private boolean getDchaCompletedPast() {
    boolean z = false;
    if (getUid(BenesseExtension.IGNORE_DCHA_COMPLETED_FILE) != 0) {
        z = BenesseExtension.COUNT_DCHA_COMPLETED_FILE.exists();
    }
    return z;
}
private int getUid(File file) {
    if (file.exists()) {
        return FileUtils.getUid(file.getPath());
    }
    return -1;
}

/factory/ 自体、root 権限が無いと書き込めません。
なので、ファイルの作成も削除もできません。

そして、COUNT_DCHA_COMPLETED_FILE が作成されるタイミングについてなのですが、dcha_state が初めて 3 になったタイミングです。
ファイルの中身は数字になっており、dcha_state が 3 になるたびに1ずつ加算されていきます。
初期化しても消えません。

とにかく、IGNORE_DCHA_COMPLETED_FILE を置けば良いだけなのですが、置くには root が必要です。
CT3 は mtk-su が使えるので大したことは無いですが、Neo/Next は、BLU 確定です。

このファイルの有無で回避すると、単にパスワード保護が消えるだけでなく、ブラウザ等が使えるようになったり、提供元不明のアプリもインストールできるようになります。

パスワードは何?

結局パスワードは何なのか。
インターネットを接続していない時にも出てくるのですからローカルから呼び出しているはずです。

結論はこちら

CT3
private static final byte[] DEFAULT_HASH = "9b66c16d267c7c3331acafd4cb449219118998678205e8843b5e1094a9b14237".getBytes();
Neo/Next
private static final byte[] DEFAULT_HASH = "a1e3cf8aa7858a458972592ebb9438e967da30d196bd6191cc77606cc60af183".getBytes();

SHA-256 でハッシュ化されている為、総当りによる解析(?)が必要です。
はっきり言って無理です。
恐らく、今までのパスワードの傾向から、パターンは [0-9][a-z][A-Z] の十文字だと考えています。
もはや今となっては当てても意味ないんですがね...

と言うのも、このパスワードが分かったところで、本当に開発者向けオプションが開くだけで、他になんの変化もありません。
ブラウザ等は無効のままです。

パスワードを変更

なんと、このパスワードは変えることができます。

static final String DCHA_HASH_FILEPATH = "/factory/dcha_hash";

ここに、好きな文字列の SHA-256 のハッシュ値を入れれば良いです。

echo -n "" | sha256sum | cut -c-64

この結果は、e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 です。
これを、DCHA_HASH_FILEPATH に書き込み、最低 004(o+r) の権限モードにすると適用されます。
開発者向けオプションを開き、空文字なので何も入力せず OK を押すと開きます。

パスワードの関数の詳細

static final String BC_PASSWORD_HIT_FLAG = "bc_password_hit";
static final String DCHA_HASH_FILEPATH = "/factory/dcha_hash";
private static final byte[] DEFAULT_HASH = "a1e3cf8aa7858a458972592ebb9438e967da30d196bd6191cc77606cc60af183".getBytes();

public boolean checkPassword(String str) {
    MessageDigest messageDigest;
    if (str == null) {
        return false;
    }
    byte[] bArr = new byte[64];
    byte[] bArr2 = null;
    try {
        FileInputStream fileInputStream = new FileInputStream(DCHA_HASH_FILEPATH);
        if (fileInputStream.read(bArr) != 64) {
            bArr = (byte[]) DEFAULT_HASH.clone();
        }
        $closeResource(null, fileInputStream);
    } catch (IOException e) {
        bArr = (byte[]) DEFAULT_HASH.clone();
    }
    try {
        messageDigest = MessageDigest.getInstance(HASH_ALGORITHM);
    } catch (NoSuchAlgorithmException e2) {
        messageDigest = null;
    }
    if (messageDigest != null) {
        messageDigest.reset();
        byte[] digest = messageDigest.digest(str.getBytes());
        bArr2 = new byte[64];
        for (int i = 0; i < digest.length && i < bArr2.length / 2; i++) {
            int i2 = i * 2;
            bArr2[i2] = this.HEX_TABLE[(digest[i] >> 4) & 15];
            bArr2[i2 + 1] = this.HEX_TABLE[digest[i] & 15];
        }
    }
    boolean equals = Arrays.equals(bArr, bArr2);
    Log.i(TAG, "password comparison = " + equals);
    if (equals) {
        putInt(BC_PASSWORD_HIT_FLAG, 1);
    }
    return equals;
}

ここで新しい関数と定数が一つずつ出てきましたね。
putInt は、BenesseExtension 内で定義されている System ネームスペースの値を変更できます。
そして、BC_PASSWORD_HIT_FLAG は、端末起動時に ADB を自動的に無効化しないためだけに使われます。

private boolean changeAdbEnable() {
    if (getAdbEnabled() == 0 || BenesseExtension.getDchaState() == 3 || !getDchaCompletedPast() || getInt(BC_PASSWORD_HIT_FLAG) != 0) {
        return false;
    }
    Settings.Global.putInt(this.mContext.getContentResolver(), "adb_enabled", 0);
    return true;
}

ここで出てくる getInt に関しても、BenesseExtension 内専用のものとなっています。

制限の詳細

全てのトリガーは、COUNT_DCHA_COMPLETED_FILE が作成されたことによって発動されます。
では、開発者向けオプションが塞がれる以外にどのような制限がされるのでしょうか。
全てまとめます。

  • 開発者向けオプションにパスワード制限が掛かる
  • ブラウザ、検索アプリが無効化される
  • 提供元不明のアプリが無効化される
  • セーフモードが無効化される
  • システムトレースが無効化される(但し dcha_state が 0 の時は使用可能)

これらが挙げられます。
提供元不明のアプリが塞がれるのが致命的でしたね。
端末所有者が使えないとすごく不便だったかもしれません。

Discussion