【技术】【小程序】Apinat 工具原理解析

前言

最近需要研究小程序 API 与调用到的 Android API 映射的问题,因此研究文章CCS20 Haoran LuApinat 工具原理,并在微信 7.0.20 版本上实现。

Apinat

目标

在小程序中调用小程序 API,给出为实现该小程序 API,Java 层实现所用到的 Android API。作者开发本工具的目的是为了找到没有被微信小程序权限保护(scope.xxx)但是底层实现的 Android API被 AOSP dangerous permission 保护的那些小程序 API,这些小程序 API 被称为 Encapsulated API。详见前文paper summary CCS20

实现

背景知识

Dispatch function(wx api)线程不直接调用 Android API,而是触发新的线程来调用 Android API,这么实现可能是出于不阻塞 dispatch 线程的考虑。
这就导致 dispatch 在 Thread1,实际执行的 Android APIs 在 Thread2。

问题

因此这就需要解决一个问题,就是处理多线程环境,将相关 threads 联系起来。

解决思路

通常 dispatch thread 会使用 Handler 机制(微信是这样,其他小程序平台如支付宝可能用的其他机制)来触发多线程。因此只需要基于 Handler 机制的推送及处理消息的原理就可以将线程联系起来。

  • Triggering Thread:有 handler 实例,通过调用 handler.post(runnable) 来向消息队列提交一个 runnable
  • Handler thread:将 runnable 从队列拿出,并执行
  • 如果 handler thread 中处理的 runnable 与 triggering thread 添加到队列的 runnable 相同,则可以将两个线程连接。

实现

本实现基于前面提到的 Apinat 提供的参考代码有修改,因为作者给出的代码没有指明在何版本微信上运行,因此笔者在参考工具框架基础上,结合微信 7.0.20 版本的小程序 API 调用的 Handler 实现而修改开发了适用 7.0.20 版本微信的 Apinat 工具,后续 8.x.x 版本应该也可用,只要 Handler 实现原理不变就适用。

  1. 工具代码中 hook 的 apk 版本未给出,因此需要根据实际测试的微信 apk 版本修改。具体修改项:Xp_demo.java 中的 “com.tencent.mm.plugin.appbrand.jsapi.d” 类中被 hook 的是 “y” 方法,改方法是逆向对应的 7.0.20 中小程序 API invokeHandler 处理后必调方法。(原本这里是“n”,对应的是作者分析的未知版本的微信 apk)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    XposedHelpers.findAndHookMethod(
    //7.0.20 d 类的 y 方法
    "com.tencent.mm.plugin.appbrand.jsapi.d", lpparam.classLoader, "y", String.class, String.class, int.class,// 被Hook函数的名称
    new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param)
    throws Throwable {
    String tag = "[Dump Stack]";
    String msg = "=====miniAppApi" + String.valueOf(param.args[0]) + "*******";
    printLog(tag,msg);

    }
    });
  2. hook 到小程序 thread 的 handler post
    原代码是 hook android.os.Handler.getPostMessage,可能是微信 apk 版本不同,7.0.20 中是通过 handler.post(runnable),因此 hook 作者给出的 getPostMessage 拿不到任何调用信息,hook handler.post 方法有信息,并且检查调用栈可以找到上述 jsapi.d.a 方法。通过 hook 并进行调用栈筛选可以拿到 __实现小程序 API 的 runnable 实例__。

  3. hook handler 的消息处理函数
    作者 hook 的是 handleCallback,笔者认为应该是基于 handler.dispatchMessage 中下列代码的考虑

    1
    2
    3
    4
    // dispatchMessage 内部
    if(message.callback){
    handleCallback(message);
    }

    因此作者的 hook 代码为

    1
    2
    3
    // 作者 hook 并做了 message.what != 0 的过滤
    // hook android.os.Handler.handleCallback
    if(message.what == 0) return;

    但是这里 hook 不到任何东西,因为这里想 hook 的消息处理逻辑跟前面的 getPostMessage 是对应的,因为在那种情况下 message.what 是对应的处理线程 id,但是如果是用 handler.post 的话会直接回调运行 runnable.run 代码,因此 message.what 不是 threadId,而是 0(调度线程id),所以即使 hook 到也会被第二行 if(message.what == 0) 判断过滤掉,所以笔者修改 hook 函数为为 android.os.Handler.dispatchMessage,修改筛选条件为

    1
    2
    3
    4
    // hook android.os.Handler.dispatchMessage
    if(message.callback == null){
    return
    }
  4. hook Android API
    这个就不多说了,正常 hook 系统 API 并打印

  5. Log 处理
    Hook 到的以上信息都打印在 log 中,还需要后续追加对 log 处理,但是逻辑很简单,就是提取上述四个信息,构建(小程序 API thread —- runnable 实例 name —- 处理含 runnable 实例的 Handler thread —- 该 Handler Thread 的 Android API 调用),即可得到 __小程序 API —- Android API Mapping__。

完整代码

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package com.example.a91377.xpdemo;

import android.app.Application;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.net.wifi.WifiManager;
import android.os.Message;
import android.telephony.TelephonyManager;
import android.util.Log;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

import static de.robv.android.xposed.XposedHelpers.findClass;

public class Xp_Demo implements IXposedHookLoadPackage { //1.实现接口

@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { //1.实现接口方法
//com.example.a91377.myapplication;
//com.tencent.mm
if (lpparam.packageName.equals("com.tencent.mm")) //进入其他应用的进程 -参数:包名
{
try{
XposedHelpers.findAndHookMethod(Application.class,
"attach",
Context.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Context context = (Context)param.args[0];
ClassLoader classLoader = context.getClassLoader();
HookLocation(classLoader);
}
});
}catch (Throwable e){
XposedBridge.log(e);
}



}
}

private static void HookLocation(ClassLoader classLoader) throws ClassNotFoundException{
Class<?> runnable = findClass("java.lang.Runnable", classLoader);
// hook android.os.Handler.post
XposedHelpers.findAndHookMethod(
"android.os.Handler", classLoader, "post", runnable,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param)
throws Throwable {
boolean flag2 = false;
long threadId = 00000;
for (Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()) {
Thread thread = (Thread) stackTrace.getKey();
StackTraceElement[] stack = (StackTraceElement[]) stackTrace.getValue();

// 进行过滤
if (!thread.equals(Thread.currentThread())) {
continue;
}

for (StackTraceElement stackTraceElement : stack) {
if (stackTraceElement.getClassName().equals("com.tencent.mm.plugin.appbrand.jsapi.d") && stackTraceElement.getMethodName().equals("a")) {
flag2 = true;
break;
}
}
if (!flag2) {
break;
}
threadId = thread.getId();
Log.i("hookgetPostMessage", String.valueOf(param.args[0]) + "********" + thread.getName() + "-------" + thread.getId());

}
if (flag2) {
Message m = (Message) param.getResult();
m.what = (int) threadId;
param.setResult(m);

}
}
});

// hook android.os.Handler.dispatchMessage
Class<?> message = findClass("android.os.Message", classLoader);
XposedHelpers.findAndHookMethod(
"android.os.Handler", classLoader, "dispatchMessage", message,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param)
throws Throwable {
Message m = (Message) param.args[0];
Runnable r = m.getCallback();
int relatedThreadId = m.what;
if(r == null){
return;
}
String tag = "=========handleCallback";
String msg = "runnable: " + String.valueOf(r) + "********" + "relatedThreadid: " + String.valueOf(relatedThreadId) + "---" + "currentId: ";
printLog(tag,msg);

}
});

// hook invokeHandler 打印出线程ID和第一个参数, 即小程序的api
XposedHelpers.findAndHookMethod(
//7.0.20 d 类的 y 方法
"com.tencent.mm.plugin.appbrand.jsapi.d", classLoader, "y", String.class, String.class, int.class, // 被Hook函数的名称
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param)
throws Throwable {
String tag = "[Dump Stack]";
String msg = "=====miniAppApi" + String.valueOf(param.args[0]) + "*******";
printLog(tag,msg);

}
});


//hook dangerous Android APi
String methodList[][] = com.example.a91377.xpdemo.Method.methodList;
for (int i = 0; i < methodList.length; i++) {
Class<?> clazz = findClass(methodList[i][0], classLoader);
for (Method method : clazz.getDeclaredMethods()) {
final String methodName = method.getName();
if (!methodName.equals(methodList[i][1])) {
continue;
}
if (!Modifier.isAbstract(method.getModifiers())) {

XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
String tag = "[hookWifi]";
String msg = "****" + methodName + "threadId";
printLog(tag,msg);

}
});
}
}
}
}

public static void printLog(String tag, String msg) {
for (Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()) {
Thread thread = (Thread) stackTrace.getKey();
StackTraceElement[] stack = (StackTraceElement[]) stackTrace.getValue();
// 进行过滤
if (!thread.equals(Thread.currentThread())) {
continue;
}
String threadName = thread.getName();
long threadId = thread.getId();
Log.d(tag, msg + threadId);

}
}

// IXposedHookLoadPackage.java
// 1. handleLoadPackage, 这个方法用于在加载应用程序包的时候执行用户操作
// 2. final LoadPackageParam lpparam 这个参数包含了加载应用程序的基本信息


// XposedHelpers.java
//findAndHookMethod 是一个辅助方法,可以静态导入使用
//参数: 1. 需要hook住的类名 2. 需要hook住的方法名 3.回调函数,参数集包含了(1. hook的目标方法的参数, 2 回调方法)


//XposedBridge.java
// 1. 无参方法:log 该方法可以将log信息以及Throwable 抛出的异常信息输出到标准的logcat以及/data/Xposed/debug.log这个文件中
// 2. 无参方法 hookAllMethods/hookAllConstructors 该方法可以用来hook住某个类中所有方法或者构造函数
}