本文只是粗略的介绍 Android Hook 和使用,不会涉及到原理方面
背景 工作中遇到前辈们使用过的各种 Hook,所以这里就只是简单说说自己工作中遇到的哪些 Hook。
Android Hook 分类可以看这里:盘点Android常用Hook技术 ,本文重点讲述的是 Native Hook,更具体的说是其中的 PLT hook。
Native Hook Native Hook 可以分为很多种,其中 PLT hook 和 Inline hook 是使用最广泛,并且相对来说通用性最强的两种方式。
PLT hook
原理 针对某个函数整体进行 hook。修改调用方(caller)的导入表,或修改被调用方(callee)的导出表。以修改导入表为例:
找到被hook函数在调用方导入表中的位置。
修改该导入表项的值。同时备份原值。
使用方可以根据需要,通过上一步中备份的原值调用原函数。
难点 对于某个目标函数,如何 hook 所有对它的调用。
优点 稳定性好,hook 操作的执行速度快。可用于线上。
缺点
只能 hook 通过 PLT 执行的函数调用。
只能以函数为单位执行 hook,不能以指令为单位。
典型场景 APM 类型的 app 监控。
典型实现
bytehook:https://code.byted.org/mobile_perf_infra/bytehook
xhook:https://github.com/iqiyi/xHook
facebook plthook:https://github.com/facebookincubator/profilo/tree/master/deps/plthooks
Inline hook
原理 可以 hook 函数起始位置,也可以 hook 函数中的某条指令。以 hook 函数起始位置为例:
找到被 hook 函数的指令起始点(函数地址)。
创建 trampoline,备份函数开头的 N 条指令。因为备份后 PC 地址改变了,所以所有涉及到 pc 值计算的指令都需要修复(修改指令的部分数据,或者用其他指令序列来达到和原指令相同的效果)。最后在 trampoline 末尾添加“跳回到原函数剩余部分的跳转指令”。此时 trampoline 就可以用作调用原函数的入口。
将被 hook 函数的开头 N 条指令,修改为“跳转到新函数的指令”。(thumb2典型值 8/10/12 字节,arm32 典型值 8 字节,arm64 典型值 24 字节)
使用方可以根据需要,使用 trampoline 的地址来调用原函数。 (如果需要 hook 函数中的某条指令,还需要做寄存器备份和恢复的工作)
难点
对“被 hook 函数的开头 N 条指令”的替换操作,可能不是原子的。ptrace/suspend 在线上容易导致 anr。
指令修复的实现和测试,比较繁琐和容易出错。
优点 不受 PLT 的限制,可以 hook 动态库内部函数的项目调用,可以 hook 绝大多数的指令位置。
缺点
“稳定性”和“hook 操作的执行性能”很难同时保证。
太短的函数不能 hook。
典型场景
外挂,vip破解。
动态分析调试。
修复特定机型/os 版本的问题。
实现 java hook/热修复等。
典型实现
ele7enxxh t32/a32 inlinehook:https://github.com/ele7enxxh/Android-Inline-Hook
sandhook:https://github.com/ganyao114/SandHook
GToad t32/a32 inlinehook:https://github.com/GToad/Android_Inline_Hook
GToad a64 inlinehook:https://github.com/GToad/Android_Inline_Hook_ARM64
字节 shadowhook:https://github.com/bytedance/android-inline-hook
PLT hook 实现比较
bytehook:https://code.byted.org/mobile_perf_infra/bytehook
xhook:https://github.com/iqiyi/xHook
facebook plthook:https://github.com/facebookincubator/profilo/tree/master/deps/plthooks
3 种 PLT hook 方案比较:
bytehook
xhook
facebook plthook
hook 单个 / 部分 / 全部的调用者
✓
✓
✓
unhook
✓
✕
✓
崩溃保护
✓
✓
✓
自动完成对新加载动态库的 hook(实现方案来自:memory-leak-detector )
✓
✕
✕
可指定 hook 函数的被调用者(适配 android linker namespace)
✓
✕
✓
自动避免 hook 函数引起的“递归调用”和“环形调用”
✓
✕
✕
hook 函数兼容 unwind(可以在 hook 函数中获取完整的 backtrace)
✓
✓
✕
hook 机制对“被hook函数”的执行性能损耗
低
很低
高
PLT 实战
实战目标:Hook 获取进程中所有执行的 SQL 语句
sqlite3_profile
查看官方文档我们可以知道,在 SQLite3 中每个 SQL 执行完成后都会调用 sqlite_profiloe
中注册的回调函数。这就为我们提供了 Hook 点。
1 2 3 4 5 6 7 8 9 10 void *sqlite3_profile (sqlite3 *db, void (*xProfile)(void *,const char *,sqlite3_uint64), void *pArg) { void *pOld; sqlite3_mutex_enter(db->mutex); pOld = db->pProfileArg; db->xProfile = xProfile; db->pProfileArg = pArg; sqlite3_mutex_leave(db->mutex); return pOld; }
怎么证明执行 SQL 语句后,真的会执行我们的回调呢?我们以一个 Android 中执行插入语句的例子来说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 SQLiteDatabase$insert(...) ▼ SQLiteDatabase$insertWithOnConflict(...) ▼ SQLiteStatement$executeInsert() ▼ SQLiteSession$executeForLastInsertedRowId(...) ▼ SQLiteConnection$executeForLastInsertedRowId(...) ▼ SQLiteConnection$nativeExecuteForLastInsertedRowId(...) ▼ base/core/jni/android_database_SQLiteConnection.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static jlong nativeExecuteForLastInsertedRowId (JNIEnv* env, jclass clazz, jlong connectionPtr, jlong statementPtr) { SQLiteConnection* connection = reinterpret_cast <SQLiteConnection*>(connectionPtr); sqlite3_stmt* statement = reinterpret_cast <sqlite3_stmt*>(statementPtr); int err = executeNonQuery(env, connection, statement); return err == SQLITE_DONE && sqlite3_changes(connection->db) > 0 ? sqlite3_last_insert_rowid(connection->db) : -1 ; } static int executeNonQuery (JNIEnv* env, SQLiteConnection* connection, sqlite3_stmt* statement) { int err = sqlite3_step(statement); if (err == SQLITE_ROW) { throw_sqlite3_exception(env, "Queries can be performed using SQLiteDatabase query or rawQuery methods only." ); } else if (err != SQLITE_DONE) { throw_sqlite3_exception(env, connection->db); } return err; }
最终,会调用到 sqlite3_step
方法,该方法的源码如下所示,最终会执行 xProfile
这个回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int sqlite3_step (sqlite3_stmt*) { if ( rc != SQLITE_ROW && db->xProfile && !db->init.busy && p->zSql ){ sqlite3_int64 iNow; sqlite3OsCurrentTimeInt64(db->pVfs, &iNow); db->xProfile(db->pProfileArg, p->zSql, (iNow - p->startTime)*1000000 ); } }
Android 中如何调用的 1 2 3 4 SQLiteConnection$open(...) ▼ SQLiteConnection$open()
Open
方法中会调用 nativeOpen
方法,这个方法的实现在 android_database_SQLiteConnection.cpp
文件中。该方法最终会调用 sqlite3_profile
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private static native long nativeOpen (String path, int openFlags, String label, boolean enableTrace, boolean enableProfile, int lookasideSlotSize, int lookasideSlotCount) ;static jlong nativeOpen (JNIEnv* env, jclass clazz, jstring pathStr, jint openFlags, jstring labelStr, jboolean enableTrace, jboolean enableProfile, jint lookasideSz, jint lookasideCnt) { if (enableProfile) { sqlite3_profile(db, &sqliteProfileCallback, connection); } return reinterpret_cast<jlong>(connection); }
要想 sqlite3_profile
得到执行,需要 enableProfile = true
。Java 层 传递过来的 enableProfile
值是:NoPreloadHolder.DEBUG_SQL_TIME
,这个字段来自于 SQLiteDebug
类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public final class SQLiteDebug { public static final class NoPreloadHolder { public static final boolean DEBUG_SQL_LOG = Log.isLoggable("SQLiteLog" , Log.VERBOSE); public static final boolean DEBUG_SQL_STATEMENTS = Log.isLoggable("SQLiteStatements" , Log.VERBOSE); public static final boolean DEBUG_SQL_TIME = Log.isLoggable("SQLiteTime" , Log.VERBOSE); public static final boolean DEBUG_LOG_SLOW_QUERIES = Build.IS_DEBUGGABLE; private static final String SLOW_QUERY_THRESHOLD_PROP = "db.log.slow_query_threshold" ; private static final String SLOW_QUERY_THRESHOLD_UID_PROP = SLOW_QUERY_THRESHOLD_PROP + "." + Process.myUid(); public static final boolean DEBUG_LOG_DETAILED = Build.IS_DEBUGGABLE && SystemProperties.getBoolean("db.log.detailed" , false ); } }
总结一下,要想 Hook 拿到执行的 SQL 语句,需要:
NoPreloadHolder.DEBUG_SQL_TIME = true
替换 sqlite3_profile 的 sqliteProfileCallback
SQLiteDebug 在 Android targetsdk >= 28后,属于Hide类型,反射无法访问。 解决办法:使用 FreeReflection 突破 Android P 的封锁。
兼容问题 DEBUG_SQL_TIME 所在类的全类名在 Android 10 前后不同:
Android 10 前:android.database.sqlite.SQLiteDebug$NoPreloadHolder
Android 10 后:android.database.sqlite.SQLiteDebug
查找代码版本变更小技巧:https://cs.android.com/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Test { public static void hookEnableProfile () { try { Class<?> clsSQLiteDebug = Class.forName("android.database.sqlite.SQLiteDebug$NoPreloadHolder" ); Field fieldDST = clsSQLiteDebug.getDeclaredField("DEBUG_SQL_TIME" ); fieldDST.setAccessible(true ); fieldDST.setBoolean(clsSQLiteDebug, true ); fieldDST.setAccessible(false ); } catch (Exception e1) { try { Class<?> clsSQLiteDebug = Class.forName("android.database.sqlite.SQLiteDebug" ); Field fieldDST = clsSQLiteDebug.getDeclaredField("DEBUG_SQL_TIME" ); fieldDST.setAccessible(true ); fieldDST.setBoolean(clsSQLiteDebug, true ); fieldDST.setAccessible(false ); } catch (Exception e2) { } } } }
Hook sqlite3_profile 函数指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void (*xProfile)(void *data, const char *sql, sqlite3_uint64 tm);void sqliteProfileCallback (void *data, const char *sql, sqlite3_uint64 tm) { } xProfile = sqliteProfileCallback; (*xProfile)(....)
xHook 方式
xHook:https://github.com/iqiyi/xHook/blob/master/README.zh-CN.md android_runtime:framework/base/core/jni/目录下有 AnroidRuntime 的源码,编译后生成 libandroid_runtime.so
1 2 3 4 5 6 7 8 9 10 11 12 13 static void * (*original_sqlite3_profile) (sqlite3* db, void (*xProfile)(void *, const char *, sqlite_uint64), void * p);static void SQLiteLintSqlite3ProfileCallback (void *data, const char *sql, sqlite_uint64 tm) { } void * hooked_sqlite3_profile (sqlite3* db, void (*xProfile)(void *, const char *, sqlite_uint64), void * p) { return original_sqlite3_profile(db, SQLiteLintSqlite3ProfileCallback, p); } xhook_register(".*/libandroid_runtime\\.so$" , "sqlite3_profile" , (void *)hooked_sqlite3_profile, (void **)&original_sqlite3_profile); xhook_enable_sigsegv_protection(1 ); xhook_refresh(0 );
byteHook 方式
byteHook 快速开始:https://github.com/bytedance/bhook/blob/main/doc/quickstart.zh-CN.md
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static void SQLiteLintSqlite3ProfileCallback (void *data, const char *sql, sqlite_uint64 tm) { LOGE("xulei" , "SQL = %s" , sql); } void * hooked_sqlite3_profile (sqlite3 *db, void (*xProfile)(void *, const char *, sqlite_uint64), void *p) { BYTEHOOK_STACK_SCOPE(); return BYTEHOOK_CALL_PREV(hooked_sqlite3_profile, db, SQLiteLintSqlite3ProfileCallback, p); } extern "C" JNIEXPORT void JNICALL Java_com_bytedance_calidge_Test_hookSqliteNative (JNIEnv *env, jclass clazz) { sSqliteProfileHookStub = bytehook_hook_single( "libandroid_runtime.so" , nullptr , "sqlite3_profile" , reinterpret_cast <void *>(hooked_sqlite3_profile), nullptr , nullptr ); }
Xposed 系列
Epic 就是 ART 上的 Dexposed