SlideShare a Scribd company logo
#DroidKaigi_room5
アプリをエミュレートするアプリの登場と
その危険性
2018/02/09 DroidKaigi 2018
エムスリー株式会社 星川貴樹 (@oboenikui)
#DroidKaigi_room5
# 自己紹介
## 名前
星川貴樹 Takaki Hoshikawa
## ニックネーム
@oboenikui
## 会社
エムスリー株式会社(新卒1年目)
主にAndroidアプリ担当
## 趣味・最近気になってること
野球観戦、CTF、minifyされたJSから隠し機能を見つけること
講演中に「OK Google」言ったらテロ起こせる説
#DroidKaigi_room5
# 本日の内容
- アプリを「エミュレートする」アプリとは
- マルチアカウントアプリの仕組み
- マルチアカウントアプリは安全?危険?
※ マルチアカウントアプリ ≒ アプリをエミュレートするアプリ
#DroidKaigi_room5
# アプリを「エミュレートする」アプリとは
- 一般には「マルチアカウントアプリ」として知られる
- LINEやソシャゲなど、マルチアカウントの仕組みを持た
ないアプリの別アカウントを作れることがウリ
- 2016年ごろから急増
マルチアカウントアプリの例
Parallel Space GO Multiple 2Accounts
#DroidKaigi_room5
# マルチアカウントアプリの種類
## APKを改竄してインストールするタイプ
- アプリケーションIDを変更して自己署名するタイプ
- 一般に証明書のチェックで弾ける
- App Clonerなどが有名
## 別のアプリとしてインストールせずに動かすタイプ
- 「マルチアカウントアプリ」の中で動かすタイプ
- あたかも正常にインストールされたかのように動かされ
るため、動かされる側のアプリには判定が少し厳しい
- 今回はこちらの話
#DroidKaigi_room5
# 最初に抱いた感想……
これ、やばいアプリでは……
#DroidKaigi_room5
# 最初に抱いた感想……
これ、やばいアプリでは……
↓
紐解いていきます
#DroidKaigi_room5
# 用語
- マルチアカウントアプリ上で動くアプリのこと
➔ ゲストアプリ
- マルチアカウントアプリのこと
➔ ホストアプリ
#DroidKaigi_room5
# 関連するものの本日深くは解説しないこと
- Androidのシステムがどう動いているか
- 普通のアプリがどうやって動いているか
- APKファイルの構造
- Linuxの超基礎知識
#DroidKaigi_room5
マルチアカウントアプリの仕組み
#DroidKaigi_room5
# VirtualAppの紹介
- GitHub上でソースが公開されているマルチアカウントア
プリ
- VirtualAppは商用アプリのため、勝手にソースを組み
込むことを禁止している
https://github.com/asLody/VirtualApp/
#DroidKaigi_room5
# AndroidManifest.xmlから挙動を読み解く
#DroidKaigi_room5
# AndroidManifest.xmlから挙動を読み解く
uses-permissionの記述 (186個)
StubActivityを継承したActivity
(100個)
StubContentProviderを継承した
ContentProvider (50個)
#DroidKaigi_room5
# AndroidManifest.xmlから挙動を読み解く
## AndroidManifestから読み解けること
- パーミッションは考えうる全てのものをVirtualApp側
に記載
- システムの欠陥を突いて……とかではない
- StubActivityは各アプリを動かしている……?
#DroidKaigi_room5
# AndroidManifest.xmlから挙動を読み解く
## AndroidManifestから読み解けること
- パーミッションは考えうる全てのものをVirtualApp側
に記載
- システムの欠陥を突いて……とかではない
- StubActivityは各アプリを動かしている……?
↑ ※違います!! (後で解説)
#DroidKaigi_room5
# ざっくりとした起動までの流れ
0. 動かしたいアプリのAPKファイルを読み込みインストール
1. ゲストアプリプロセスの起動
2. Activityを起動
#DroidKaigi_room5
# ざっくりとした起動までの流れ
0. 動かしたいアプリのAPKファイルを読み込みインストール
1. ゲストアプリプロセスの起動
2. Activityを起動
#DroidKaigi_room5
# アプリのインストール処理
- android.content.pm.PackageParser※
を使ってパッ
ケージ情報を取得
- /data/data/io.virtualapp/virtual/data以下
に、各アプリのデータ領域にあたるディレクトリなどを
作成
- APKファイルからlib/**/*.soなどをコピー
#DroidKaigi_room5
# PackageParserについて
- AndroidManifest.xmlをパースするクラス
- JavaDocで@hide指定された隠蔽クラスのため、SDKに
は含まれず通常使用ができない
/**
...
* @hide
*/
public class PackageParser {
...
#DroidKaigi_room5
# 隠蔽クラス/メソッドの使用
- @hide指定による隠蔽クラスやメソッドは、Javaのリフ
レクションを用いるとアプリからもアクセス可能
- ただしAndroidバージョンごとに大幅に仕様変更がある
ので通常使用すべきではない
- 解説するメソッドの多くは隠蔽メソッドなのでその度に
言及は特にしない
#DroidKaigi_room5
# リフレクションとは
- privateメソッドを実行する
- privateなメンバ変数を取得・書換する
- クラス名文字列から対応するクラスのインスタンスを生
成する(コンストラクタに引数を渡す場合)
- メソッド実行前後に処理を挟む、実行結果を変える
(Proxy)
などが可能なJavaの機能(いわゆる黒魔術)
#DroidKaigi_room5
# privateメンバ変数の取得
Field f = activity.getClass()
.getDeclaredField("mMainThread");
f.setAccessible(true);
ActivityThread mainThread =
(ActivityThread) f.get(activity);
#DroidKaigi_room5
# 名前からインスタンス生成 (LayoutInflaterの処理)
Class<? extends View> clazz = context.getClassLoader()
.loadClass(name)
.asSubclass(View.class);
Constructor<? extends View> constructor =
clazz.getConstructor(constructorSignature);
constructor.setAccessible(true);
View view = constructor.newInstance(args);
#DroidKaigi_room5
# Proxyの例
Map proxyInstance = (Map) Proxy.newProxyInstance(
SomeTest.class.getClassLoader(),
new Class[] { Map.class },
(proxy, method, args) -> {
if (method.getName().equals("get")) return 42;
throw new UnsupportOperationException();
});
proxyInstance.get(); // 42
proxyInstance.put(1, 1); // error
#DroidKaigi_room5
# インストール後のdataディレクトリ
/data/data/io.virtualapp/virtual/data内
- app/[application id]/
∗ 各アプリのネイティブバイナリ・パッケージ
キャッシュを保存
- user/0/[application id]
∗ データディレクトリ
(/data/data/[application id]) に相当
#DroidKaigi_room5
# ざっくりとした起動までの流れ
0. 動かしたいアプリのAPKファイルを読み込みインストール
1. ゲストアプリプロセスの起動
2. Activityを起動
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
VMのロード
& JNIをフック
ActivityThread改竄
(Application作成)
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
VMのロード
& JNIをフック
ActivityThread改竄
(Application作成)
#DroidKaigi_room5
# ContentProviderとは
コンテンツ プロバイダは、データの中央リポジトリへのア
クセスを管理します。(中略)
これは、プロバイダと他のアプリケーションとの間のイン
ターフェースになります。
https://developer.android.com/guide/topics/pr
oviders/content-provider-creating.html?hl=ja
- ContentUrisという仕組みでSQLiteのデータをやりと
りしたりする
- ContentProviderを含むプロセスはkillされにくい!
#DroidKaigi_room5
# StubContentProviderの起動
- ContentProviderは別プロセスで動かせるため、この
プロセスをアプリプロセスに見立てて動かす
- ランタイムのセットアップに関する処理は
StubContentProvider上で行う
- ゲストアプリごとに別プロセスの
StubContentProviderを起動する
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
VMのロード
& JNIをフック
ActivityThread改竄
(Application作成)
#DroidKaigi_room5
# ART/DalvikVMをロードする
- ネイティブ側でlibart.soもしくはlibdvm.soをロード
(dlopen)する
- dlopenにもパスを変更する処理が追加されている
- JNIのフックについては後述
#DroidKaigi_room5
# ART/DalvikVMをロードする (補足)
- システムプロパティに
persist.sys.dalvik.vm.lib.2が存在
→ 通常はlibart.soのパスがセットされている
persist.sys.dalvik.vm.libが存在
→ 通常はlibdvm.soのパスがセットされている
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
VMのロード
& JNIをフック
ActivityThread改竄
(Application作成)
#DroidKaigi_room5
# ActivityThreadの略説
- アプリケーションを動かしてる一番重要な部分
- public static void main(String[] args)があるところ
- ブレークポイントを貼るとコールスタックの初めの方に出てくる
やつ
- Looperが回ってるところ
- HというHandlerを継承したクラスがあり、そこでライフ
サイクルイベントやActivity起動などを処理している
- 詳しくはAndroidを支える技術〈Ⅱ〉の5章を読んでね!
#DroidKaigi_room5
# ActivityThreadの改竄
- ContentProviderの生成と同時にActivityThreadも
生成される
- ゲストアプリを動かす際、ActivityThread内で持つ情
報をゲストのものに改竄する必要があるので改竄を行う
- Proxyを使ったメソッドの処理改竄などもここで行う
#DroidKaigi_room5
# ActivityThreadの改竄
## ActivityThread#mBoundApplication(AppBindData)の改竄
- ActivityThreadから取得し、情報を改竄
- appInfo, providers → PackageParserで取得
- processName → パッケージ名
- instrumentationName → パッケージ名などから生成
#DroidKaigi_room5
# ActivityThreadの改竄
## AppBindData#info (LoadedApk) の生成
- Context#createPackageContext(packageName)を
用いて、対象のアプリのContextを取得
- ContextImpl#mPackageInfo (LoadedApk) を取得
#DroidKaigi_room5
# ActivityThreadの改竄
## Applicationの生成
- 生成したLoadedApkのインスタンスを用いて
LoadedApk#makeApplication()を実行し生成
- ActivityThread#mInitialApplicationに作成した
Applicationをセット
- 生成後ライフサイクル処理を走らせる
#DroidKaigi_room5
# ざっくりとした起動までの流れ
0. 動かしたいアプリのAPKファイルを読み込みインストール
1. ゲストアプリプロセスの起動
2. Activityを起動
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
Context#
startActivity
Intent差し替え
Intentから
Activityを生成
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
Context#
startActivity
Intent差し替え
Intentから
Activityを生成
#DroidKaigi_room5
# Activityの起動
- ManagerプロセスからstartActivity(intent)で
StubActivityを起動しようとする
- StubActivityとStubContentProviderは同じプロセスで動
くようAndroidManifestで定義されている
- Androidの機能でゲストプロセス側にIntentが飛ぶ
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
Context#
startActivity
Intent差し替え
Intentから
Activityを生成
Intent
Target: io.virtualapp/.StubActivity
Extra: Intent(x.guest/.MainActivity)
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
Context#
startActivity
Intent差し替え
Intentから
Activityを生成
#DroidKaigi_room5
# ActivityThreadの略説
- アプリケーションを動かしてる一番重要な部分
- public static void main(String[] args)があるところ
- ブレークポイントを貼るとコールスタックの初めの方に出てくる
やつ
- Looperが回ってるところ
- HというHandlerを継承したクラスがあり、そこでライフ
サイクルイベントやActivity起動などを処理している
- 詳しくはAndroidを支える技術〈Ⅱ〉の5章を読んでね!
#DroidKaigi_room5
# Activityの起動
- ゲストアプリプロセスでIntentを受信後、
H#dispatchMessage(msg)が呼ばれる
- 通常は受信したIntentからActivity生成処理が走る
#DroidKaigi_room5
# Handlerクラスの実装
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null
&& mCallback.handleMessage(msg)) {
return;
}
handleMessage(msg);
}
}
#DroidKaigi_room5
# Handlerクラスの実装
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null
&& mCallback.handleMessage(msg)) {
return;
}
handleMessage(msg);
}
}
mCallbackがセットされていて、
実行の結果trueが返ってきたら終了
#DroidKaigi_room5
# Handlerクラスの実装
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null
&& mCallback.handleMessage(msg)) {
return;
}
handleMessage(msg);
}
}
Handlerのコンストラクタに渡す
コールバックで、Hではnull
Hでは通常ここの処理でActivity生成
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
VMのロード
& JNIをフック
ActivityThread改竄
(Application作成)
ゲストプロセスの
ActivityThread#mH#mCallbackに
処理を追加
Context#
startActivity
…
#DroidKaigi_room5
# Activityの起動
- ゲストアプリプロセスでIntentを受信後、
H#dispatchMessage(msg)が呼ばれる
- この中で通常は受信したIntentからActivity生成処理が走る
- H#mCallbackにセットした処理により、Intentの宛先
がゲストアプリの起動したいActivityに置き換わる
- このIntentを基にActivityを生成し起動する
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
Context#
startActivity
Intent差し替え
Intentから
Activityを生成
Intent
Target: io.virtualapp/.StubActivity
Extra: Intent(x.guest/.MainActivity)
#DroidKaigi_room5
# プロセスの状態
ホストアプリプロセス Managerプロセス
ゲストアプリプロセス
(ContentProvider)
Activity起動要求
ゲスト起動処理
プロセス起動
Context#
startActivity
Intent差し替え
Intentから
Activityを生成Intent
Target: x.guest/.MainActivity
#DroidKaigi_room5
# なぜStubActivityを経由?
- 別アプリから起動する場合を考慮した結果
#DroidKaigi_room5
# 別アプリから起動する場合
別アプリ
ゲストアプリプロセス
(ContentProvider)
startActivity
ContentProvider
起動
mCallbackで
Intent差し替え
Intentから
Activityを生成
StubActivity起動
Intent
Target: io.virtualapp/.StubActivity
Extra: Intent(x.guest/.MainActivity)
#DroidKaigi_room5
# AndroidManifest.xmlの答え合わせ
StubActivity (100個)
StubContentProvider (50個)
Activityの起動を仲介する
ダミーのActivityたち
ゲストアプリの
プロセスになる奴ら
#DroidKaigi_room5
# 補足:メソッドのフック
## なぜ必要?
- Intentの処理やCameraのインストールなどがパッケー
ジ名に依存している
- PackageCacheのパスを変えないとPermission
Deniedになる
- Binder#getCallingUid()の返すUidを偽装
- その他パッケージ改竄検出をすり抜けるため
#DroidKaigi_room5
# Javaのリフレクション (Proxy) で改竄する処理
- startActivityなどIntent処理
- 改ざんしないと本物が起動してしまうため
- バックアップなど多くのメソッドの挙動を削除
- 動作しては困るものは削除してしまう
- オーディオ周りなどの挙動をホストの処理に置き換え
- ネイティブ周りなどの関係でそのままでは動かないので
などなど
#DroidKaigi_room5
# JNIのメソッドをフックして行うこと
- UIDを偽装
- ゲストのdataディレクトリを偽装
- Cameraの初期化
- AudioRecordのパーミッションを取得
- PackageCacheに使うパスを変更
詳しくはスライド最後の付録で
#DroidKaigi_room5
マルチアカウントアプリの危険性
#DroidKaigi_room5
# 広告をオーバーライドされるなどの問題
- 広告フレームワークの処理を変え、マルチアカウントア
プリ作者に収益が入るようにすることも技術的には可能
- Parallel Space上のAdMobでは変更されていないことを確認
したが、他のアプリはどうかわからない
- その他、Playストアでの課金をしたかのように振る舞わ
せるなども可能と考えられる
#DroidKaigi_room5
# 過剰にパーミッションを与えてしまう危険性
- マルチアカウントアプリに非常に多くのパーミッション
が使われているため、ゲストアプリにも同様のパーミッ
ションを与えてしまう危険性がある
- ゲストアプリに悪意がある場合、AndroidManifestで
はパーミッションを指定せず、マルチアカウントアプリ
上で動く時のみパーミッションを用いる、などが可能
- そもそもマルチアカウントアプリ自体が大変危険なアプ
リかもしれない
#DroidKaigi_room5
# アカウントデータ・パスワード漏洩の危険性
- マルチアカウントアプリに悪意がある場合、容易にパス
ワード入力情報から内部のデータまで全て盗むことが可
能
- ゲストアプリが悪意のあるものの場合も、他のゲストア
プリの保存データを取得可能
↓ VirtualApp上の端末エミュレータアプリでLINEの情報を確認できた
#DroidKaigi_room5
# 応用すると……
- 知らぬ間にアプリが置き換わっていることがあるかも……
- いろいろ頑張ればユーザーが気付かないようにアプリをアンイン
ストールさせることが可能
- 代わりに悪意のあるアプリケーション上で動かすショートカット
を作っておけば、ユーザーは置き換わっていることに気付くのは
困難
⇒ 対策した方がよいかも……
#DroidKaigi_room5
# マルチアカウントアプリの対策
- 単純な証明書のチェックはNG
- マルチアカウントアプリ上では証明書を取得するメソッドも書き
換わっており、正しい証明書を得てしまう
- パッケージ名やUIDのチェック等もNG
- これも完全にホストのものに差し替えられると確認が困難
#DroidKaigi_room5
# 2018年現在最適なチェック方法!!
- psコマンドで確認(一部省略)
$ ps
USER PID NAME
u0_a51 8561 ps
u0_a51 29616 com.lbe.parallel.intl:mdserver
u0_a51 29699 parallel.monitor
u0_a51 30703 jackpal.androidterm
u0_a51 30889 /system/bin/sh
自分のアプリでは発生し得ないプロセスが同じユーザーとして実行され
ていることをチェックできる
#DroidKaigi_room5
# 最後に(対策する前に)
- マルチアカウントアプリが使われるアプリ
≒ ユーザーはアカウント切り替え機能を求めている
⇒ マルチアカウント機能の実装を検討すべきでは?
- 現在マルチアカウントアプリの紹介記事の中で危険性を
伝える記事はないので、正しく周知していくことが必要
- 必ずしも危険なものではないが、危険性は知るべき
#DroidKaigi_room5
付録:JNIのフック処理の詳細
参考(中国語):https://www.jianshu.com/p/052b6dd45659
#DroidKaigi_room5
# JNIの基礎知識
JNIからJavaのメソッドを呼ぶ機能がある
// C++
jclass clazz =
env->FindClass("SomeJavaClass");
jmethodID methodId =
env->GetStaticMethodID(clazz, "javaMethod", "()V");
env->CallStaticVoidMethod(clazz, methodId);
#DroidKaigi_room5
# JNIの基礎知識
JNIからJavaのメソッドを呼ぶ機能がある
// C++
jclass clazz =
env->FindClass("SomeJavaClass");
jmethodID methodId =
env->GetStaticMethodID(clazz, "javaMethod", "()V");
env->CallStaticVoidMethod(clazz, methodId);
#DroidKaigi_room5
# JNIのフック
jmethodIDはMethod構造体のポインタ
// C++
struct Method {
ClassObject* clazz; // クラスを表す
u4 accessFlags; // publicか、nativeか、などのフラグ
…
DalvikBridgeFunc nativeFunc; // ネイティブ関数のポインタ
…
};
#DroidKaigi_room5
# JNIのフック
jmethodIDはMethod構造体のポインタ
// C++
struct Method {
ClassObject* clazz; // クラスを表す
u4 accessFlags; // publicか、nativeか、などのフラグ
…
DalvikBridgeFunc nativeFunc; // ネイティブ関数のポインタ
…
}; これを書き換えればOK
#DroidKaigi_room5
# JNIのフック
jmethodIDはMethod構造体のポインタ
// C++
struct Method {
ClassObject* clazz; // クラスを表す
u4 accessFlags; // publicか、nativeか、などのフラグ
…
DalvikBridgeFunc nativeFunc; // ネイティブ関数のポインタ
…
};
環境によって構造が異なる
#DroidKaigi_room5
# JNIのフック
jmethodIDはMethod構造体のポインタ
// C++
struct Method {
ClassObject* clazz; // クラスを表す
u4 accessFlags; // publicか、nativeか、などのフラグ
…
DalvikBridgeFunc nativeFunc; // ネイティブ関数のポインタ
…
};
環境によって構造が異なる
ここのオフセットを知
る必要あり
#DroidKaigi_room5
# JNIのフック
ダミーのメソッドをJava, C++両側に定義
// Java
static native void nativeMark();
// C++
void mark() {}
#DroidKaigi_room5
# JNIのフック
ダミーメソッドのポインタに一致するオフセットを探索
// C++
size_t start = (size_t) env->GetStaticMethodID(
clazz, "nativeMark", "()V");
size_t target = (size_t) mark;
int offset = 0;
while (true) {
if (*((size_t *)(start + offset)) == target)
break;
offset += 4;
}
#DroidKaigi_room5
# JNIのフック
このオフセットを使って他のメソッドを置き換える
// C++
size_t method = (size_t) someMethodId;
void **nativeFuncPtr = (void **) (method + offset);
*nativeFuncPtr = (void*) hookFunc;

More Related Content

アプリをエミュレートするアプリの登場とその危険性 / How multi-account app works