本文只是粗略的介绍 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)的导出表。以修改导入表为例:

  1. 找到被hook函数在调用方导入表中的位置。
  2. 修改该导入表项的值。同时备份原值。
  3. 使用方可以根据需要,通过上一步中备份的原值调用原函数。

难点

对于某个目标函数,如何 hook 所有对它的调用。

优点

稳定性好,hook 操作的执行速度快。可用于线上。

缺点

  1. 只能 hook 通过 PLT 执行的函数调用。
  2. 只能以函数为单位执行 hook,不能以指令为单位。

典型场景

APM 类型的 app 监控。

典型实现

  1. bytehook:https://code.byted.org/mobile_perf_infra/bytehook
  2. xhook:https://github.com/iqiyi/xHook
  3. facebook plthook:https://github.com/facebookincubator/profilo/tree/master/deps/plthooks

Inline hook

原理

可以 hook 函数起始位置,也可以 hook 函数中的某条指令。以 hook 函数起始位置为例:

  1. 找到被 hook 函数的指令起始点(函数地址)。
  2. 创建 trampoline,备份函数开头的 N 条指令。因为备份后 PC 地址改变了,所以所有涉及到 pc 值计算的指令都需要修复(修改指令的部分数据,或者用其他指令序列来达到和原指令相同的效果)。最后在 trampoline 末尾添加“跳回到原函数剩余部分的跳转指令”。此时 trampoline 就可以用作调用原函数的入口。
  3. 将被 hook 函数的开头 N 条指令,修改为“跳转到新函数的指令”。(thumb2典型值 8/10/12 字节,arm32 典型值 8 字节,arm64 典型值 24 字节)
  4. 使用方可以根据需要,使用 trampoline 的地址来调用原函数。
    (如果需要 hook 函数中的某条指令,还需要做寄存器备份和恢复的工作)

难点

  1. 对“被 hook 函数的开头 N 条指令”的替换操作,可能不是原子的。ptrace/suspend 在线上容易导致 anr。
  2. 指令修复的实现和测试,比较繁琐和容易出错。

优点

不受 PLT 的限制,可以 hook 动态库内部函数的项目调用,可以 hook 绝大多数的指令位置。

缺点

  1. “稳定性”和“hook 操作的执行性能”很难同时保证。
  2. 太短的函数不能 hook。

典型场景

  1. 外挂,vip破解。
  2. 动态分析调试。
  3. 修复特定机型/os 版本的问题。
  4. 实现 java hook/热修复等。

典型实现

  1. ele7enxxh t32/a32 inlinehook:https://github.com/ele7enxxh/Android-Inline-Hook
  2. sandhook:https://github.com/ganyao114/SandHook
  3. GToad t32/a32 inlinehook:https://github.com/GToad/Android_Inline_Hook
  4. GToad a64 inlinehook:https://github.com/GToad/Android_Inline_Hook_ARM64
  5. 字节 shadowhook:https://github.com/bytedance/android-inline-hook

PLT hook 实现比较

  1. bytehook:https://code.byted.org/mobile_perf_infra/bytehook
  2. xhook:https://github.com/iqiyi/xHook
  3. 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
//base/core/jni/android_database_SQLiteConnection.cpp - 涉及源码参考
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*) {
//...

/*
* Invoke the profile callback if there is one
*/
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 {
/**
* Controls the printing of informational SQL log messages.
*
* Enable using "adb shell setprop log.tag.SQLiteLog VERBOSE".
*/
public static final boolean DEBUG_SQL_LOG =
Log.isLoggable("SQLiteLog", Log.VERBOSE);


/**
* Controls the printing of SQL statements as they are executed.
*
* Enable using "adb shell setprop log.tag.SQLiteStatements VERBOSE".
*/
public static final boolean DEBUG_SQL_STATEMENTS =
Log.isLoggable("SQLiteStatements", Log.VERBOSE);


/**
* Controls the printing of wall-clock time taken to execute SQL statements
* as they are executed.
*
* Enable using "adb shell setprop log.tag.SQLiteTime VERBOSE".
*/
public static final boolean DEBUG_SQL_TIME =
Log.isLoggable("SQLiteTime", Log.VERBOSE);



/**
* True to enable database performance testing instrumentation.
*/
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();


/**
* Whether to add detailed information to slow query log.
*/
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) {
//nop
}
}
}
}

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) {
// do something
}

// 函数指针赋值
xProfile = sqliteProfileCallback;

// 调用函数指针
(*xProfile)(....)
// C++ 也允许这样写: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) {
//do something
}

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) {
//do something
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