Android のアンチリバース防御

ルート検出のテスト

概要

アンチリバースの文脈では、ルート検出の目的はルート化されたデバイス上でアプリを実行することをもう少し難しくすることで、その後、リバースエンジニアが使用したいツールやテクニックを妨げます。他のほとんどの防御と同様に、ルート検出はそれ自体に高い効果はありませんが、いくつかのルートチェックをアプリにちりばめることで改竄対策スキーム全体の有効性が向上します。
Android では、用語「ルート検出」をより広く定義し、カスタム ROM の検出などを含みます。例えば、デバイスが製品版の Android ビルドであるか、もしくはカスタムビルドであるかを確認します。

共通ルート検出手法

以下のセクションでは、よく見かけるいくつかのルート検出手法を記します。OWASP Mobile Testing Guide に添付されている crackme サンプル [1] で実装されているチェックがいくつかあります。
SafetyNet
SafetyNet はソフトウェアとハードウェアの情報を使用してデバイスのプロファイルを作成する Android API です。このプロファイルは Android 互換性テストに合格したホワイトリスト化されたデバイスモデルのリストと比較されます。Google はこの機能を「不正使用防止システムの一環として付加的な多層防御シグナル」として使用することを推奨しています [2] 。
SafetyNet が正確に中で何をしているかは十分に文書化されておらず、いつでも変更される可能性があります。この API を呼び出すと、サービスは Google はデバイス検証コードを含むバイナリパッケージをダウンロードし、リフレクションを使用して動的に実行されます。John Kozyrakis の分析によると、SafetyNet により実行された検査はデバイスがルート化されているかどうかを検出しようとしますが、これがどのくらい正しいかは不明確です [3] 。
この API を使用するには、アプリは the SafetyNetApi.attest() メソッドが Attestation Result の JWS メッセージを返し、それから以下のフィールドをチェックします。
  • ctsProfileMatch: "true" の場合、デバイスプロファイルは Android 互換性テストに合格した Google のリスト化されたデバイスのひとつと一致します。
  • basicIntegrity: アプリを実行しているデバイスはおそらく改竄されてはいません。
attestation result は以下のようになります。
1
{
2
"nonce": "R2Rra24fVm5xa2Mg",
3
"timestampMs": 9860437986543,
4
"apkPackageName": "com.package.name.of.requesting.app",
5
"apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the
6
certificate used to sign requesting app"],
7
"apkDigestSha256": "base64 encoded, SHA-256 hash of the app's APK",
8
"ctsProfileMatch": true,
9
"basicIntegrity": true,
10
}
Copied!
プログラムによる検出
ファイルの存在チェック
おそらく最も広く使用されている手法はルート化されたデバイスに通常見つかるファイルをチェックすることです。一般的なルート化アプリのパッケージファイルや関連するファイルおよびディレクトリなどがあります。
1
/system/app/Superuser.apk
2
/system/etc/init.d/99SuperSUDaemon
3
/dev/com.koushikdutta.superuser.daemon/
4
/system/xbin/daemonsu
Copied!
検出コードはデバイスがルート化されたときに一般的にインストールされるバイナリも検索します。例として、busybox の存在チェックや、su バイナリを別の場所で開こうとしていることをチェックすることなどがあります。
1
/system/xbin/busybox
2
3
/sbin/su
4
/system/bin/su
5
/system/xbin/su
6
/data/local/su
7
/data/local/xbin/su
Copied!
代わりに、su が PATH にあるかどうかを確認することもできます。
1
public static boolean checkRoot(){
2
for(String pathDir : System.getenv("PATH").split(":")){
3
if(new File(pathDir, "su").exists()) {
4
return true;
5
}
6
}
7
return false;
8
}
Copied!
ファイルチェックは Java とネイティブコードの両方で簡単に実装できます。以下の JNI の例では、stat システムコールを使用してファイルに関する情報を取得します (rootinspector [9] から改変したコード例)。ファイルが存在する場合、1 を返します。
1
jboolean Java_com_example_statfile(JNIEnv * env, jobject this, jstring filepath) {
2
jboolean fileExists = 0;
3
jboolean isCopy;
4
const char * path = (*env)->GetStringUTFChars(env, filepath, &isCopy);
5
struct stat fileattrib;
6
if (stat(path, &fileattrib) < 0) {
7
__android_log_print(ANDROID_LOG_DEBUG, DEBUG_TAG, "NATIVE: stat error: [%s]", strerror(errno));
8
} else
9
{
10
__android_log_print(ANDROID_LOG_DEBUG, DEBUG_TAG, "NATIVE: stat success, access perms: [%d]", fileattrib.st_mode);
11
return 1;
12
}
13
14
return 0;
15
}
Copied!
su および他のコマンドの実行
su が存在するかどうかを判断する別の方法は、Runtime.getRuntime.exec() で実行を試みることです。su が PATH にない場合、IOException がスローされます。同じ方法を使用して、ルート化されたデバイス上によく見つかる他のプログラムを確認することができます。busybox や一般的にそれを指すシンボリックリンクなどがあります。
実行中のプロセスの確認
Supersu は最も人気のあるルート化ツールであり、daemonsu という名前の認証デーモンを実行します。そのため、このプロセスが存在することはルート化されたデバイスのもうひとつの兆候です。実行中のプロセスは ActivityManager.getRunningAppProcesses() および manager.getRunningServices() API、ps コマンドで列挙でき、/proc ディレクトリで閲覧できます。例として、rootinspector [9] では以下のように実装されています。
1
public boolean checkRunningProcesses() {
2
3
boolean returnValue = false;
4
5
// Get currently running application processes
6
List<RunningServiceInfo> list = manager.getRunningServices(300);
7
8
if(list != null){
9
String tempName;
10
for(int i=0;i<list.size();++i){
11
tempName = list.get(i).process;
12
13
if(tempName.contains("supersu") || tempName.contains("superuser")){
14
returnValue = true;
15
}
16
}
17
}
18
return returnValue;
19
}
Copied!
インストール済みのアプリパッケージの確認
Android パッケージマネージャを使用するとインストールされているパッケージのリストを取得できます。以下のパッケージ名は一般的なルート化ツールに属します。
1
com.thirdparty.superuser
2
eu.chainfire.supersu
3
com.noshufou.android.su
4
com.koushikdutta.superuser
5
com.zachspong.temprootremovejb
6
com.ramdroid.appquarantine
Copied!
書き込み可能なパーティションとシステムディレクトリの確認
sysytem ディレクトリに対する普通とは異なるアクセス許可は、カスタマイズまたはルート化されたデバイスを示します。通常の状況下では、system および data ディレクトリは常に読み取り専用でマウントされていますが、デバイスがルート化されていると読み書き可能でマウントされることがあります。これはこれらのファイルシステムが "rw" フラグでマウントされているかどうかをチェックすることでテストできます。もしくはこれらのディレクトリにファイルを作成してみます。
カスタム Android ビルドの確認
デバイスがルート化されているかどうかを確認するだけでなく、テストビルドやカスタム ROM の兆候を確認することも役に立ちます。これを行う方法のひとつは、BUILD タグに test-keys が含まれているかどうかを確認することです。これは一般的にカスタム Android イメージを示します [5] 。これは以下のように確認できます [6] 。
1
private boolean isTestKeyBuild()
2
{
3
String str = Build.TAGS;
4
if ((str != null) && (str.contains("test-keys")));
5
for (int i = 1; ; i = 0)
6
return i;
7
}
Copied!
Google Over-The-Air (OTA) 証明書の欠落はカスタム ROM のもうひとつの兆候です。出荷版の Android ビルドでは、OTA アップデートに Google の公開証明書を使用します [4] 。

ルート検出のバイパス

JDB, DDMS, strace やカーネルモジュールを使用して実行トレースを実行し、アプリが何をしているかを調べます。通常はオペレーティングシステムとのすべての種類の疑わしいやり取りを表示します。su の読み込みやプロセスリストの取得などがあります。これらのやり取りはルート検出の確実な兆候です。ルート検出メカニズムを一つ一つ特定し非アクティブにします。ブラックボックスの耐性評価を実行している場合は、ルート化検出メカニズムを無効にすることが最初のステップです。
多くのテクニックを使用してこれらのチェックをバイパスできます。これらのほとんどは「リバースエンジニアリングと改竄」の章で紹介されています。
  1. 1.
    バイナリの名前を変更する。例えば、場合によっては単に "su" バイナリの名前を変更するだけで、ルート検出を無効にできます (あなたの環境を壊さないようにします) 。
  2. 2.
    /proc をアンマウントして、プロセスリストの詠み込みなどを防止する。往々にして、proc が利用できないだけでそのようなチェックを無効にできます。
  3. 3.
    Frida や Xposed を使用して、Java やネイティブレイヤーに API をフックする。これを行うことにより、ファイルやプロセスを隠したり、ファイルの実際の内容を隠したり、アプリが要求するすべての種類の偽の値を返したりできます。
  4. 4.
    カーネルモジュールを使用して、低レベル API をフックする。
  5. 5.
    アプリにパッチを当て、チェックを削除する。

有効性評価

ルート検出メカニズムが存在するかどうかを確認し、以下の基準を適用します。
  • 複数の検出手法がアプリ全体に分散されている (ひとつの手法にすべてを任せてはいない)
  • ルート検出メカニズムは複数の API レイヤ (Java API、ネイティブライブラリ関数、アセンブラ/システムコール) で動作する
  • そのメカニズムはある程度の独創性を示している (StackOverflow や他のソースからコピー&ペーストしたものではない)
ルート検出メカニズムのバイパス手法を開発し、以下の質問に答えます。
  • RootCloak などの標準ツールを使用してそのメカニズムを簡単にバイパスできますか?
  • ルート検出を処理するにはある程度の静的/動的解析が必要ですか?
  • カスタムコードを書く必要はありましたか?
  • それをうまくバイパスするにはどれくらいの時間がかかりましたか?
  • 難易度の主観的評価はいくつですか?
より詳細な評価を行うには、「ソフトウェア保護スキームの評価」の章の「プログラムによる防御の評価」に記載されている基準を適用します。

改善方法

ルート検出が欠落しているか、または非常に簡単にバイパスされてしまう場合は、上記の有効性基準に沿って提案を作成します。これには、より多くの検出メカニズムを追加すること、または既存のメカニズムを他の防御とより良く統合することが含まれます。

参考情報

OWASP Mobile Top 10 2016

OWASP MASVS

  • V8.3: "アプリは二つ以上の機能的に依存しないルート検出方式を実装しており、ユーザーに警告するかアプリを終了することでルート化デバイスの存在に応答している。"

CWE

N/A

その他

ツール

アンチデバッグのテスト

概要

デバッグはアプリのランタイム動作を解析する非常に効果的な方法です。これはリバースエンジニアがコードをステップ実行し、任意の箇所でアプリの実行を停止し、変数の状態を検査し、メモリを読み取りおよび変更し、さらに多くのことを可能にします。
「リバースエンジニアリングと改竄」の章で述べたように、Android では二つの異なるデバッグプロトコルを扱う必要があります。JDWP を使用した Java レベルと、ptrace ベースのデバッガを使用したネイティブレイヤーのデバッグが可能です。したがって、優れたアンチデバッグスキームでは両方のデバッガタイプに対して防御を実装する必要があります。
アンチデバッグ機能は予防型または反応型にできます。この名前が示すように、予防型アンチデバッグトリックはまず第一にデバッガがアタッチすることを防ぎます。反応型トリックはデバッガが存在するかどうかを検出し、何らかの方法でそれに反応させようと試みます (アプリの終了やなんらかの隠された動作のトリガなど) 。「多ければ多いほど良い」ルールが適用されます。効果を最大限にするため、防御側では、さまざまな API レイヤーで動作しアプリ全体に分散されている、複数の予防と検出の手法を組み合わせます。

アンチ JDWP デバッグの例

「リバースエンジニアリングと改竄」の章では、デバッガと Java 仮想マシンとの間の通信に使用されるプロトコルである JDWP について説明しました。また、Manifest ファイルにパッチを当てて任意のアプリを容易にデバッグ可能にできることや、ro.debuggable システムプロパティを変更することであらゆるアプリをデバッグ可能にできることがわかりました。開発者が JDWP デバッガを検出ないし無効にするために行ういくつかのことを見てみます。
ApplicationInfo のデバッグ可能フラグの確認
すでに何度か android:debuggable 属性が出てきました。アプリマニフェストのこのフラグは JDWP スレッドがアプリに対して起動されるかどうかを決定します。その値はアプリの ApplicationInfo オブジェクトを使用してプログラムで決定できます。このフラグが設定されている場合、これはマニフェストが改竄されてデバッグ可能になっていることを示します。
1
public static boolean isDebuggable(Context context){
2
3
return ((context.getApplicationContext().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
4
5
}
Copied!
isDebuggerConnected
Android Debug システムクラスはデバッガが現在接続されているかどうかをチェックする静的メソッドを提供します。このメソッドは単にブール値を返します。
1
public static boolean detectDebugger() {
2
return Debug.isDebuggerConnected();
3
}
Copied!
同じ API をネイティブコードから呼ぶことが可能です。DvmGlobals グローバル構造体にアクセスします。
1
JNIEXPORT jboolean JNICALL Java_com_test_debugging_DebuggerConnectedJNI(JNIenv * env, jobject obj) {
2
if (gDvm.debuggerConnect || gDvm.debuggerAlive)
3
return JNI_TRUE;
4
return JNI_FALSE;
5
}
Copied!
タイマーチェック
Debug.threadCpuTimeNanos は現在のスレッドがコードの実行に費やした時間量を示します。デバッグはプロセスの実行を遅くするため、実行時間の違いを利用して、デバッガがアタッチされているかどうかを推測することができます [2] 。
1
static boolean detect_threadCpuTimeNanos(){
2
long start = Debug.threadCpuTimeNanos();
3
4
for(int i=0; i<1000000; ++i)
5
continue;
6
7
long stop = Debug.threadCpuTimeNanos();
8
9
if(stop - start < 10000000) {
10
return false;
11
}
12
else {
13
return true;
14
}
Copied!
JDWP 関連のデータ構造への干渉
Dalvik では、グローバル仮想マシンの状態は DvmGlobals 構造体を介してアクセス可能です。グローバル変数 gDvm はこの構造体へのポイントを保持します。DvmGlobals には JDWP デバッグに重要なさまざまな変数やポインタが含まれており、改竄可能です。
1
struct DvmGlobals {
2
/*
3
* Some options that could be worth tampering with :)
4
*/
5
6
bool jdwpAllowed; // debugging allowed for this process?
7
bool jdwpConfigured; // has debugging info been provided?
8
JdwpTransportType jdwpTransport;
9
bool jdwpServer;
10
char* jdwpHost;
11
int jdwpPort;
12
bool jdwpSuspend;
13
14
Thread* threadList;
15
16
bool nativeDebuggerActive;
17
bool debuggerConnected; /* debugger or DDMS is connected */
18
bool debuggerActive; /* debugger is making requests */
19
JdwpState* jdwpState;
20
21
};
Copied!
例えば、gDvm.methDalvikDdmcServer_dispatch 関数ポインタに NULL を設定すると JDWP スレッドがクラッシュします [2] 。
1
JNIEXPORT jboolean JNICALL Java_poc_c_crashOnInit ( JNIEnv* env , jobject ) {
2
gDvm.methDalvikDdmcServer_dispatch = NULL;
3
}
Copied!
gDvm 変数が利用できない場合でも、ART で同様の技法を使用してデバッグを無効にできます。ART ランタイムは JDWP 関連のクラスの vtable の一部をグローバルシンボルとしてエクスポートします (C++ では、vtable はクラスメソッドのポインタを保持するテーブルです) 。これには JdwpSocketState と JdwpAdbState を含むクラスの vtable を含んでいます。これら二つはネットワークソケットと ADB を介した JDWP 接続をそれぞれ処理します。デバッグランタイムの動作はこれらの vtable のメソッドポインタを上書きすることにより操作できます。
これを行うための方法のひとつは "jdwpAdbState::ProcessIncoming()" のアドレスを "JdwpAdbState::Shutdown()" のアドレスで上書きすることです。これによりデバッガは直ちに切断されます [3] 。
1
#include <jni.h>
2
#include <string>
3
#include <android/log.h>
4
#include <dlfcn.h>
5
#include <sys/mman.h>
6
#include <jdwp/jdwp.h>
7
8
#define log(FMT, ...) __android_log_print(ANDROID_LOG_VERBOSE, "JDWPFun", FMT, ##__VA_ARGS__)
9
10
// Vtable structure. Just to make messing around with it more intuitive
11
12
struct VT_JdwpAdbState {
13
unsigned long x;
14
unsigned long y;
15
void * JdwpSocketState_destructor;
16
void * _JdwpSocketState_destructor;
17
void * Accept;
18
void * showmanyc;
19
void * ShutDown;
20
void * ProcessIncoming;
21
};
22
23
extern "C"
24
25
JNIEXPORT void JNICALL Java_sg_vantagepoint_jdwptest_MainActivity_JDWPfun(
26
JNIEnv *env,
27
jobject /* this */) {
28
29
void* lib = dlopen("libart.so", RTLD_NOW);
30
31
if (lib == NULL) {
32
log("Error loading libart.so");
33
dlerror();
34
}else{
35
36
struct VT_JdwpAdbState *vtable = ( struct VT_JdwpAdbState *)dlsym(lib, "_ZTVN3art4JDWP12JdwpAdbStateE");
37
38
if (vtable == 0) {
39
log("Couldn't resolve symbol '_ZTVN3art4JDWP12JdwpAdbStateE'.\n");
40
}else {
41
42
log("Vtable for JdwpAdbState at: %08x\n", vtable);
43
44
// Let the fun begin!
45
46
unsigned long pagesize = sysconf(_SC_PAGE_SIZE);
47
unsigned long page = (unsigned long)vtable & ~(pagesize-1);
48
49
mprotect((void *)page, pagesize, PROT_READ | PROT_WRITE);
50
51
vtable->ProcessIncoming = vtable->ShutDown;
52
53
// Reset permissions & flush cache
54
55
mprotect((void *)page, pagesize, PROT_READ);
56
57
}
58
}
59
}
Copied!

アンチネイティブデバッグの例

ほとんどのアンチ JDWP トリックは (おそらくタイマーベースのチェックは安全だが) 旧来の ptrace ベースのデバッガをキャッチしないため、この種のデバッグを防ぐには別の防御が必要です。多くの「従来の」Linux アンチデバッグトリックがここでは採用されています。
TracerPid のチェック
プロセスへのアタッチに ptrace システムコールを使用すると、デバッグされたプロセスのステータスファイルの "TracerPid" フィールドにアタッチプロセスの PID が表示されます。"TracerPid" のデフォルト値は "0" (他のプロセスはアタッチしていない) です。したがって、そのフィールドに "0" 以外のものを見つけることは、デバッガやその他の ptrace のいたずらの兆候です。
以下の実装は Tim Strazzere's Anti-Emulator project [3] から得ました。
1
public static boolean hasTracerPid() throws IOException {
2
BufferedReader reader = null;
3
try {
4
reader = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/self/status")), 1000);
5
String line;
6
7
while ((line = reader.readLine()) != null) {
8
if (line.length() > tracerpid.length()) {
9
if (line.substring(0, tracerpid.length()).equalsIgnoreCase(tracerpid)) {
10
if (Integer.decode(line.substring(tracerpid.length() + 1).trim()) > 0) {
11
return true;
12
}
13
break;
14
}
15
}
16
}
17
18
} catch (Exception exception) {
19
exception.printStackTrace();
20
} finally {
21
reader.close();
22
}
23
return false;
24
}
Copied!
Ptraceのバリエーション*
Linux では、ptrace() システムコールは別のプロセス ("tracee") の実行を監視および制御し、tracee のメモリとレジスタを調査および変更するために使用されます [5] 。それはブレークポイントデバッグとシステムコールトレースを実装する主な手段です。多くのアンチデバッグトリックは何かについえ ptrace を使用します。一度にプロセスにアタッチできるのはひとつのデバッガだけであるという事実をよく利用します。
簡単な例として、以下のようなコードを使用して、子プロセスをフォークし、それをデバッガとして親プロセスにアタッチすることで、プロセスのデバッグを防ぐことができます。
1
void fork_and_attach()
2
{
3
int pid = fork();
4
5
if (pid == 0)
6
{
7
int ppid = getppid();
8
9
if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0)
10
{
11
waitpid(ppid, NULL, 0);
12
13
/* Continue the parent process */
14
ptrace(PTRACE_CONT, NULL, NULL);
15
}
16
}
17
}
Copied!
子がアタッチされると、何かしらがさらに親に接続しようとする試みは失敗します。これを確認するには、JNI 関数のコードをコンパイルし、デバイス上で実行するアプリにパックします。
1
[email protected]:/ # ps | grep -i anti
2
u0_a151 18190 201 1535844 54908 ffffffff b6e0f124 S sg.vantagepoint.antidebug
3
u0_a151 18224 18190 1495180 35824 c019a3ac b6e0ee5c S sg.vantagepoint.antidebug
Copied!
親プロセスに gdbserver でアタッチしようとすると、エラーで失敗します。
1
[email protected]:/ # ./gdbserver --attach localhost:12345 18190
2
warning: process 18190 is already traced by process 18224
3
Cannot attach to lwp 18190: Operation not permitted (1)
4
Exiting
Copied!
しかしこれは、子を終了し、追跡から親を「解放」することにより、容易に回避されます。実際には、通常、複数のプロセスやスレッド、さらには改ざんを防ぐための監視など、より緻密なスキームがあります。一般的な方法は以下のとおりです。
  • 互いに追跡する複数のプロセスをフォークします。
  • 子が生存し続けていることを確認するために実行中のプロセスを追跡し続けます。
  • /proc/pid/status の TracerPID など /proc ファイルシステムの値を監視します。
上記の方法を簡単に改良してみます。初期の fork() の後、子のステータスを継続的に監視する親の追加スレッドを実行します。アプリがデバッグモードとリリースモードのいずれでビルドされたか (マニフェストの android:debuggable による) に従って、子プロセスは以下のいずれかの方法で動作することが期待されます。
  1. 1.
    リリースモードでは、ptrace への呼び出しは失敗し、子はセグメンテーションフォルト (exit code 11) で直ちにクラッシュします。
  2. 2.
    デバッグモードでは、ptrace への呼び出しは機能し、子は無期限に実行されます。結果として、waitpid(child_pid) への呼び出しは決して戻らないでしょう。もし戻るのであれば、何かが怪しく、私たちはプロセスグループ全体を終了します。
これを JNI 関数として実装する完全なコードは以下のとおりです。
1
#include <jni.h>
2
#include <string>
3
#include <unistd.h>
4
#include <sys/ptrace.h>
5
#include <sys/wait.h>
6
7
static int child_pid;
8
9
void *monitor_pid(void *) {
10
11
int status;
12
13
waitpid(child_pid, &status, 0);
14
15
/* Child status should never change. */
16
17
_exit(0); // Commit seppuku
18
19
}
20
21
void anti_debug() {
22
23
child_pid = fork();
24
25
if (child_pid == 0)
26
{
27
int ppid = getppid();
28
int status;
29
30
if (ptrace(PTRACE_ATTACH, ppid, NULL, NULL) == 0)
31
{
32
waitpid(ppid, &status, 0);
33
34
ptrace(PTRACE_CONT, ppid, NULL, NULL);
35
36
while (waitpid(ppid, &status, 0)) {
37
38
if (WIFSTOPPED(status)) {
39
ptrace(PTRACE_CONT, ppid, NULL, NULL);
40
} else {
41
// Process has exited
42
_exit(0);
43
}
44
}
45
}
46
47
} else {
48
pthread_t t;
49
50
/* Start the monitoring thread */
51
52
pthread_create(&t, NULL, monitor_pid, (void *)NULL);
53
}
54
}
55
extern "C"
56
57
JNIEXPORT void JNICALL
58
Java_sg_vantagepoint_antidebug_MainActivity_antidebug(
59
JNIEnv *env,
60
jobject /* this */) {
61
62
anti_debug();
63
}
Copied!
再び、これを Android アプリにパックして、それが機能するかどうかを確認します。前と同様に、アプリのデバッグビルドを実行すると、二つのプロセスが表示されます。
1
[email protected]:/ # ps | grep -i anti-debug
2
u0_a152 20267 201 1552508 56796 ffffffff b6e0f124 S sg.vantagepoint.anti-debug
3
u0_a152 20301 20267 1495192 33980 c019a3ac b6e0ee5c S sg.vantagepoint.anti-debug
Copied!
但し、子プロセスを終了すると、親プロセスも終了します。
1
[email protected]:/ # kill -9 20301
2
130|[email protected]:/ # cd /data/local/tmp
3
[email protected]:/ # ./gdbserver --attach localhost:12345 20267
4
gdbserver: unable to open /proc file '/proc/20267/status'
5
Cannot attach to lwp 20267: No such file or directory (2)
6
Exiting
Copied!
これを回避するには、アプリの動作を少し修正する必要があります (最も簡単なのは _exit への呼び出しを NOP でパッチするか、libc.so の関数 _exit をフックすることです) 。現時点では、よく知られた「軍拡競争」に入ります。この防御をより複雑な形で実現することは常に可能であり、それを回避する方法は常にあります。

デバッガ検出のバイパス

例によって、アンチデバッグを回避する一般的な方法はありません。これはデバッグを防止または検出するために使用される特定のメカニズムや、全体的な保護スキームのその他の防御に依存します。例えば、整合性チェックがない場合、またはすでに無効化している場合には、アプリにパッチを当てるのが最も簡単な方法です。他の場合には、フックフレームワークやカーネルモジュールを使用するほうが望ましいかもしれません。
  1. 1.
    アンチデバッグ機能をパッチアウトします。単純に NOP 命令で上書きすることで不要な動作を無効にします。アンチデバッグメカニズムが十分に検討されている場合には、より複雑なパッチが必要になることに注意します。
  2. 2.
    Frida または Xposedを使用して、Java およびネイティブレイヤの API をフックします。isDebuggable や isDebuggerConnected などの関数の戻り値を操作し、デバッガを隠蔽します。
  3. 3.
    環境を変更します。Android はオープンな環境です。それ以外の何も機能しないのであれば、オペレーティングシステムを変更して、アンチデバッグトリックを設計する際に開発者が行った想定を覆すことができます。
バイパスの例: UnCrackable App for Android Level 2
難読化されたアプリを扱う場合、開発者はネイティブライブラリのデータや機能を意図的に「隠す」ことがよくあります。"UnCrackable App for Android" のレベル2にこの例があります。
一見すると、コードは以前のチャレンジと似ています。 "CodeCheck" と呼ばれるクラスはユーザーが入力したコードの検証を担当します。実際のチェックはメソッド "bar()" で行われているようです。これは native メソッドとして宣言されています。
-- TODO [Example for Bypassing Debugger Detection] --
1
package sg.vantagepoint.uncrackable2;
2
3
public class CodeCheck {
4
public CodeCheck() {
5
super();
6
}
7
8
public boolean a(String arg2) {
9
return this.bar(arg2.getBytes());
10
}
11
12
private native boolean bar(byte[] arg1) {
13
}
14
}
15
16
static {
17
System.loadLibrary("foo");
18
}
Copied!

有効性評価

アンチデバッグメカニズムの有無を確認し、以下の基準を適用します。
  • JDB および ptrace ベースのデバッガはアタッチに失敗するか、アプリを終了または機能を停止する
  • 複数の検出手法がアプリ全体に分散されている (すべてを単一のメソッドや関数につぎ込んではいない)
  • アンチデバッグ防御は複数の API レイヤ (Java、ネイティブライブラリ関数、アセンブラ/システムコール) で動作する
  • メカニズムはある程度の独創性を示す (StackOverflow や他のソースからのコピー/ペーストではない)
アンチデバッグ防御のバイパスに取り組み、以下の問いに答えます。
  • 単純な手法を使用してメカニズムをバイパスすることは可能か? (例えば、単一の API 関数をフックするなど)
  • 静的および動的解析を使用してアンチデバッグコードを特定することはどの程度困難か?
  • 防御を無効にするカスタムコードを書く必要はあるか?どの程度の時間を費やす必要があったか?
  • 難易度の主観的評価は何か?
より詳細な評価を行うには「ソフトウェア保護スキームの評価」の章の「プログラムによる防御の評価」に記載されている基準を適用します。

改善方法

アンチデバッグが欠落しているか、非常に簡単にバイパスされる場合、上記の有効性基準に沿って提案します。これにはより多くの検出メカニズムの追加や、さらに既存のメカニズムと他の防御の統合を含みます。

参考情報

ファイル整合性監視のテスト

概要

ファイル整合性に関連するトピックは二つあります。
  1. 1.
    アプリケーションソース関連の整合性チェック 「改竄とリバースエンジニアリング」の章では、Android の APK コード署名チェックについて説明しました。また、リバースエンジニアがアプリを再パッケージおよび再署名することで、このチェックを簡単に回避できることも説明しました。このプロセスをより複雑にするために、アプリのバイトコードやネイティブライブラリ、重要なデータファイルの CRC チェックを使用して、保護スキームを拡張できます。これらのチェックは Java とネイティブの両方のレイヤで実装できます。この考えは、コード署名が有効であっても、変更されていない状態でのみ正しく実行されるように、追加のコントロールを用意することです。
  2. 2.
    ファイルストレージ関連の整合性チェック ファイルがアプリケーションにより SD カードまたはパブリックストレージに格納される場合、またはキー・バリューペアが SharedPreferences に格納される場合、それらの整合性は保護される必要があります。

サンプル実装 - アプリケーションソース

整合性チェックでは選択したファイルに対してチェックサムやハッシュを計算することがよくあります。一般的に保護されているファイルは以下のとおりです。
  • AndroidManifest.xml
  • クラスファイル *.dex
  • ネイティブライブラリ (*.so)
Android Cracking Blog [1] の以下のサンプル実装では classes.dex に対して CRC を計算し、期待値と比較します。
1
private void crcTest() throws IOException {
2
boolean modified = false;
3
// required dex crc value stored as a text string.
4
// it could be any invisible layout element
5
long dexCrc = Long.parseLong(Main.MyContext.getString(R.string.dex_crc));
6
7
ZipFile zf = new ZipFile(Main.MyContext.getPackageCodePath());
8
ZipEntry ze = zf.getEntry("classes.dex");
9
10
if ( ze.getCrc() != dexCrc ) {
11
// dex has been modified
12
modified = true;
13
}
14
else {
15
// dex not tampered with
16
modified = false;
17
}
18
}
Copied!

サンプル実装 - ストレージ

ストレージ自体に整合性を提供する場合。Android の SharedPreferences のようにキー・バリューペアを介して HMAC を作成することも、ファイルシステムが提供する完全なファイルに対して HMAC を作成することもできます。 HMAC を使用する場合、bouncy castle 実装を使用して指定されたコンテンツまたは AndroidKeyStore を HMAC にして、後でその HMAC を検証します。処理をするにはいくつかのステップがあります。 暗号化が必要な場合。[2] で説明されているように暗号化してから HMAC することを確認してください。
BouncyCastle で HMAC を生成する場合:
  1. 1.
    BounceyCastle または SpongeyCastle がセキュリティプロバイダとして登録されていることを確認します。
  2. 2.
    HMAC をキーで初期化します。キーはキーストアに格納します。
  3. 3.
    HMAC を必要とするコンテンツのバイト配列を取得します。
  4. 4.
    HMAC とバイトコードで doFinal を呼び出します。
  5. 5.
    手順3のバイト配列に HMAC を追加します。
  6. 6.
    手順5の結果を格納します。
BouncyCastle で HMAC を検証する場合:
  1. 1.
    BounceyCastle または SpongeyCastle がセキュリティプロバイダとして登録されていることを確認します。
  2. 2.
    メッセージと hmacbytes を個別の配列として抽出します。
  3. 3.
    データに対して hmac を生成する手順1-4を繰り返します。
  4. 4.
    ここで抽出された hmacbytes を手順3の結果と比較します。
Android キーストアに基づいて HMAC を生成する場合、Android 6 以降でのみこれを行うことが最適です。その場合、[3] で説明されているように hmac のためのキーを生成します。 AndroidKeyStore なしでの便利な HMAC 実装を以下に示します。
1
public enum HMACWrapper {
2
HMAC_512("HMac-SHA512"), //please note that this is the spec for the BC provider
3
HMAC_256("HMac-SHA256");
4
5
private final String algorithm;
6
7
private HMACWrapper(final String algorithm) {
8
this.algorithm = algorithm;
9
}
10
11
public Mac createHMAC(final SecretKey key) {
12
try {
13
Mac e = Mac.getInstance(this.algorithm, "BC");
14
SecretKeySpec secret = new SecretKeySpec(key.getKey().getEncoded(), this.algorithm);
15
e.init(secret);
16
return e;
17
} catch (NoSuchProviderException | InvalidKeyException | NoSuchAlgorithmException e) {
18
//handle them
19
}
20
}
21
22
public byte[] hmac(byte[] message, SecretKey key) {
23
Mac mac = this.createHMAC(key);
24
return mac.doFinal(message);
25
}
26
27
public boolean verify(byte[] messageWithHMAC, SecretKey key) {
28
Mac mac = this.createHMAC(key);
29
byte[] checksum = extractChecksum(messageWithHMAC, mac.getMacLength());
30
byte[] message = extractMessage(messageWithHMAC, mac.getMacLength());
31
byte[] calculatedChecksum = this.hmac(message, key);
32
int diff = checksum.length ^ calculatedChecksum.length;
33
34
for (int i = 0; i < checksum.length && i < calculatedChecksum.length; ++i) {
35
diff |= checksum[i] ^ calculatedChecksum[i];
36
}
37
38
return diff == 0;
39
}
40
41
public byte[] extractMessage(byte[] messageWithHMAC) {
42
Mac hmac = this.createHMAC(SecretKey.newKey());
43
return extractMessage(messageWithHMAC, hmac.getMacLength());
44
}
45
46
private static byte[] extractMessage(byte[] body, int checksumLength) {
47
if (body.length >= checksumLength) {
48
byte[] message = new byte[body.length - checksumLength];
49
System.arraycopy(body, 0, message, 0, message.length);
50
return message;
51
} else {
52
return new byte[0];
53
}
54
}
55
56
private static byte[] extractChecksum(byte[] body, int checksumLength) {
57
if (body.length >= checksumLength) {
58
byte[] checksum = new byte[checksumLength];
59
System.arraycopy(body, body.length - checksumLength, checksum, 0, checksumLength);
60
return checksum;
61
} else {
62
return new byte[0];
63
}
64
}
65
66
static {
67
Security.addProvider(new BouncyCastleProvider());
68
}
69
}
Copied!
整合性を提供する他の方法には、取得されるバイト配列への署名があります。署名の生成方法については [3] を確認してください。署名を元のバイト配列に追加することを忘れないでください。

ファイル整合性監査のバイパス

アプリケーションソースの整合性チェックをバイパスしようとする場合
  1. 1.
    アンチデバッグ機能にパッチを当てます。それぞれのバイトコードまたはネイティブコードを NOP 命令で上書きするだけで望まれない動作を無効にします。
  2. 2.
    Frida または Xposed を使用して Java およびネイティブレイヤ上のファイルシステム API をフックします。改変されたファイルの代わりに元のファイルへのハンドルを返します。
  3. 3.
    カーネルモジュールを使用して、ファイル関連システムコールを傍受します。プロセスが改変されたファイルを開こうとすると、代わりに改変されていないバージョンのファイルのファイル記述子が返ります。
パッチ、コードインジェクション、カーネルモジュールの例については、「改竄とリバースエンジニアリング」のセクションを参照ください。
ストレージの整合性チェックをバイパスしようとする場合
  1. 1.
    デバイスバインディングのセクションで記載されているように、デバイスからデータを取得します。
  2. 2.
    取得されたデータを変更し、ストレージに戻します。

有効性評価

アプリケーションソースの完全性チェックの場合 変更されていない状態でデバイス上でアプリを実行し、すべてが機能することを確認します。次に、アプリパッケージに含まれている classes.dex とすべての .so ライブラリに簡単なパッチを適用します。「セキュリティテスト入門」の章で説明されているようにアプリを再パッケージおよび再署名し、実行します。アプリは変更を検出して、何らかの方法で応答する必要があります。少なくとも、アプリはユーザーに警告したり、アプリを終了したりする必要があります。防御をバイパスするように作業し、以下の質問に答えます。
  • 単純な手法を使用してメカニズムをバイパスすることは可能か? (例えば、単一の API 関数をフックするなど)
  • 静的および動的解析を使用してアンチデバッグコードを特定することはどの程度困難か?
  • 防御を無効にするカスタムコードを書く必要はあるか?どの程度の時間を費やす必要があったか?
  • 難易度の主観的評価は何か?
より詳細な評価を行うには「ソフトウェア保護スキームの評価」の章の「プログラムによる防御の評価」に記載されている基準を適用します。
ストレージの完全性チェックの場合 同様のアプローチをここで考え、以下の質問に答えます。
  • 単純な手法を使用してメカニズムをバイパスすることは可能か? (例えば、ファイルまたはキー・バリューの内容を変更するなど)
  • HMAC 鍵や非対称秘密鍵を取得することはどの程度困難か?
  • 防御を無効にするカスタムコードを書く必要はあるか?どの程度の時間を費やす必要があったか?
  • 難易度の主観的評価は何か?

参考情報

OWASP Mobile Top 10 2016

OWASP MASVS

-- V8.3: "アプリは実行ファイルや重要なデータの改竄を検出し応答している。"

CWE

  • N/A

その他

リバースエンジニアリングツールの検出のテスト

概要

リバースエンジニアは多くのツール、フレームワーク、アプリを使用し、このガイドで遭遇した多くのリバースプロセスを支援します。結果として、デバイス上にそのようなツールが存在することは、ユーザーがアプリをリバースエンジニアリング使用としているか、少なくともそのようなツールをインストールすることによるリスクが増大していることを示している可能性があります。

検出手法

一般的なリバースエンジニアリングツールは、変更されていない形式でインストールされている場合、関連するアプリケーションパッケージ、ファイル、プロセス、またはその他のツール固有の修正やアーティファクトを探すことにより検出できます。以下の例では、このガイドで広く使用されている frida 計装フレームワークを検出するさまざまな方法を示します。Substrate や Xposed などの他のツールは同様の手段を使用して検出できます。DBI/インジェクション/フックツールはランタイムの完全性チェックによって暗黙的に検出されることもあります。以下で個別に説明します。
例: Frida を検出する方法
frida や類似のフレームワークを検出する明白な方法は、パッケージファイル、バイナリ、ライブラリ、プロセス、一時ファイルなどの関連するアーティファクトの環境をチェックすることです。一例として、fridaserver について考えます。これは TCP を介して frida を公開するデーモンです。fridaserver が動作しているかどうかを確認するために実行中のプロセスリストをたどる Java メソッドを使用できます。
1
public boolean checkRunningProcesses() {
2
3
boolean returnValue = false;
4
5
// Get currently running application processes
6
List<RunningServiceInfo> list = manager.getRunningServices(300);
7
8
if(list != null){
9
String tempName;
10
for(int i=0;i<list.size();++i){
11
tempName = list.get(i).process;
12
13
if(tempName.contains("fridaserver")) {
14
returnValue = true;
15
}
16
}
17
}
18
return returnValue;
19
}
Copied!
これは frida がデフォルト設定で動作している場合に機能します。おそらくリバースエンジニアリングの最初のほんの小さな一歩を行う一部のスクリプトキディを困惑させるには十分です。しかし、fridaserver バイナリの名前を "lol" や別の名前に変更することで簡単にバイパスできるので、もっと良い方法を見つけるべきです。
デフォルトでは、fridaserver は TCP ポート 27047 にバインドするので、このポートが開いているかどうかを確認することもひとつの考えです。ネイティブコードでは、以下のようになります。
1
boolean is_frida_server_listening() {
2
struct sockaddr_in sa;
3
4
memset(&sa, 0, sizeof(sa));
5
sa.sin_family = AF_INET;
6
sa.sin_port = htons(27047);
7
inet_aton("127.0.0.1", &(sa.sin_addr));
8
9
int sock = socket(AF_INET , SOCK_STREAM , 0);
10
11
if (connect(sock , (struct sockaddr*)&sa , sizeof sa) != -1) {
12
/* Frida server detected. Do something… */
13
}
14
15
}
Copied!
この場合も、デフォルトモードの fridaserver を検出しますが、リスニングポートはコマンドライン引数で簡単に変更できるため、これをバイパスすることは非常に簡単です。この状況は nmap -sV をプルすることで改善できます。fridaserver は D-Bus プロトコルを使用して通信するため、開いているすべてのポートに D-Bus AUTH メッセージを送信し、答えをチェックします。fridaserver の期待は自身を公開することです。
1
/*
2
* Mini-portscan to detect frida-server on any local port.
3
*/
4
5
for(i = 0 ; i <= 65535 ; i++) {
6
7
sock = socket(AF_INET , SOCK_STREAM , 0);
8
sa.sin_port = htons(i);
9
10
if (connect(sock , (struct sockaddr*)&sa , sizeof sa) != -1) {
11
12
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "FRIDA DETECTION [1]: Open Port: %d", i);
13
14
memset(res, 0 , 7);
15
16
// send a D-Bus AUTH message. Expected answer is “REJECT"
17
18
send(sock, "\x00", 1, NULL);
19
send(sock, "AUTH\r\n", 6, NULL);
20
21
usleep(100);
22
23
if (ret = recv(sock, res, 6, MSG_DONTWAIT) != -1) {
24
25
if (strcmp(res, "REJECT") == 0) {
26
/* Frida server detected. Do something… */
27
}
28
}
29
}
30
close(sock);
31
}
Copied!
私たちは fridaserver を検出する非常に安定した手法を持っていますが、目立った問題がいくつかあります。最も重要なこととして、frida は fridaserver を必要としない代替の操作モードを提供しています。それらをどのように検出しますか。
frida のすべてのモードでの共通のテーマはコードインジェクションです。したがって、frida が使用されるときはいつでも、frida 関連のライブラリがメモリにマップされていることが期待できます。それらを検出する簡単な方法は、ロードされているライブラリのリストを調べて、疑わしいものをチェックすることです。
1
char line[512];
2
FILE* fp;
3
4
fp = fopen("/proc/self/maps", "r");
5
6
if (fp) {
7
while (fgets(line, 512, fp)) {
8
if (strstr(line, "frida")) {
9
/* Evil library is loaded. Do something… */
10
}
11
}
12
13
fclose(fp);
14
15
} else {
16
/* Error opening /proc/self/maps. If this happens, something is of. */
17
}
18
}
Copied!
これは名前に "frida" を含むライブラリを検出します。表面上ではこれは機能しますが、いくつかの大きな問題があります。
  • fridaserver と呼ばれる fridaserver に頼るのは良い考えではなかったことを覚えていますか。同じことがここに当てはまります。frida に小さな変更を加えることで、frida エージェントライブラリは簡単に名前を変更できます。- 検出は fopen() や strstr() などの標準ライブラリコールに依存します。本質的には、あなたが察するように frida で簡単にフックできる関数を使用して frida を検出しようとしています。明らかにこれはあまり強固な戦略ではありません。
課題番号一は古典的なウイルススキャナ風の戦略を実装することで対応できます。frida のライブラリにある「ガジェット」が存在するかどうかメモリをスキャンします。私はすべてのバージョンの frida-gadget と frida-agent に存在すると思われる文字列 "LIBFRIDA" を選択しました。以下のコードを使用して、/proc/self/maps にリストされているメモリマッピングを繰り返し、各実行可能セクション内の文字列を検索します。簡潔にするために瑣末な機能は除外していることに注意します。それらは GitHub にあります。
1
static char keyword[] = "LIBFRIDA";
2
num_found = 0;
3
4
int scan_executable_segments(char * map) {
5
char buf[512];
6
unsigned long start, end;
7
8
sscanf(map, "%lx-%lx %s", &start, &end, buf);
9
10
if (buf[2] == 'x') {
11
return (find_mem_string(start, end, (char*)keyword, 8) == 1);
12
} else {
13
return 0;
14
}
15
}
16
17
void scan() {
18
19
if ((fd = my_openat(AT_FDCWD, "/proc/self/maps", O_RDONLY, 0)) >= 0) {
20
21
while ((read_one_line(fd, map, MAX_LINE)) > 0) {
22
if (scan_executable_segments(map) == 1) {
23
num_found++;
24
}
25
}
26
27
if (num_found > 1) {
28
29
/* Frida Detected */
30
}
31
32
}
Copied!
通常の libc ライブラリ関数の代わりに my_openat() などを使用することに注意します。これらは Bionic libc と同様に機能するカスタム実装です。それぞれのシステムコールの引数を設定し、swi 命令を実行します (下記参照) 。これによりパブリック API の依存がなくなり、典型的な libc フックの影響を受けにくくなります。完全な実装は syscall.S にあります。以下は my_openat() のアセンブラ実装です。
1
#include "bionic_asm.h"
2
3
.text
4
.globl my_openat
5
.type my_openat,function
6
my_openat:
7
.cfi_startproc
8
mov ip, r7
9
.cfi_register r7, ip
10
ldr r7, =__NR_openat
11
swi #0
12
mov r7, ip
13
.cfi_restore r7
14
cmn r0, #(4095 + 1)
15
bxls lr
16
neg r0, r0