补 JNI 环境的数据 补jni内容分成两部分,数据和代码。
数据主要指各种字段和属性的获取
1 2 3 4 5 6 7 8 9 case "com/meituan/android/common/mtguard/NBridge$SIUACollector->uiAutomatorClickCount()I" :{ return 0 ; } case "android/view/Display->getHeight()I" :{ return 2160 ; } case "android/view/Display->getWidth()I" :{ return 1080 ; }
代码主要指各种方法的调用
1 2 3 4 case "java/lang/String->compareToIgnoreCase(Ljava/lang/String;)I" :{ String str = vaList.getObjectArg(0 ).getValue().toString(); return dvmObject.getValue().toString().compareToIgnoreCase(str); }
第一个方法 通过分析字段/方法的语义和上下文返回合理值。
比如isVpn
函数,通过面向 Google 学习,我们意识到它是检测Vpn
的函数,以及返回什么值对应什么语义。
1 2 3 4 case "com/meituan/android/common/mtguard/NBridge$SIUACollector->isVPN()Ljava/lang/String;" :{ return new StringObject (vm, "0" ); }
第二个方法
ADB
命令比如上篇获取屏幕的宽高,通过wm size
命令获取。
1 2 C:\Users\13352>adb shell wm size Physical size: 1080x2160
第三个办法 自写 demo app 做验证
上篇加速度传感器名字的获取就是通过这个办法
1 2 3 4 5 6 7 8 9 case "android/hardware/Sensor->getName()Ljava/lang/String;" :{ int type = (int ) dvmObject.getValue(); System.out.println("Sensor getName:" +type); if (type == 1 ){ return new StringObject (vm, "ICM20690" ); }else { throw new UnsupportedOperationException (signature); } }
JNI 调用很复杂,但复写成 Java 代码逻辑往往很简单。
1 2 3 SensorManager manager= (SensorManager)getSystemService(SENSOR_SERVICE); Sensor accelSensor = manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);Log.e("lilac sensor Name" ,accelSensor.getName());
第四个办法 Frida Hook 和 Call
Unidbg 补的执行流在真机上也理应执行过,那么 Frida Spwan 模式 Hook 应该能拦截到这种调用并做出打印。比如上篇uiAutomatorClickCount
的处理就依赖于 Hook。
1 2 3 4 5 6 7 8 9 @Override public int callIntMethodV (BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature){ case "com/meituan/android/common/mtguard/NBridge$SIUACollector->uiAutomatorClickCount()I" :{ return 0 ; } } return super .callIntMethodV(vm, dvmObject, signature, vaList); }
Hook 代码通过Jadx
一键生成,然后裹上Java.perform
。
1 2 3 4 5 6 7 8 9 Java.perform(function() { let SIUACollector = Java.use("com.meituan.android.common.mtguard.NBridge$SIUACollector" ); SIUACollector["uiAutomatorClickCount" ].implementation = function () { console.log('uiAutomatorClickCount is called' ); let ret = this .uiAutomatorClickCount(); console.log('uiAutomatorClickCount ret value is ' + ret); return ret; }; })
如果 Hook 拦截不到或有其他考虑,那么直接 Call 函数也挺好。
第五个办法是使用 trace
r0tracer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function main () { Java.perform(function () { console.Purple("r0tracer begin ... !" ) traceClass("javax.crypto.Cipher" ) }) }
它支持三种模式
一是追踪单个类里的所有域值、参数、调用栈和返回值
二是批量追踪包含某个关键字的所有类,并且可以设置白名单
三是在二的基础上,枚举类加载器,努力找寻某个类
我们可以使用它追踪SIUACollector
类里的数据情况。
1 2 traceClass("com.meituan.android.common.mtguard.NBridge$SIUACollector" )
在补 JNI 时,我们并不预先确定样本会调用哪个或哪些类,但大体上可以分为两类,
1 是样本自定义的类库,比如com.meituan.android.common.mtguard.NBridge$SIUACollector
2 是 Android FrameWork 类库,比如android.hardware.Sensor
。
其他:
jnitrace
jTrace
补 JNI 环境的代码 直接复用 JDK 类库 对于 JDK 中的类库,可以直接复现其方法逻辑,保持代码简洁并符合预期。
比如对 SimpleDateFormat 的操作
1 2 3 4 5 6 case "java/text/SimpleDateFormat-><init>(Ljava/lang/String;Ljava/util/Locale;)V" :{ String pattern = vaList.getObjectArg(0 ).getValue().toString(); Locale locale = (Locale) vaList.getObjectArg(1 ).getValue(); simpleDateFormat = new SimpleDateFormat (pattern, locale); return ; }
比如对 StringBuilder 的操作
1 2 3 4 case "java/lang/StringBuilder->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" :{ String str = vaList.getObjectArg(0 ).getValue().toString(); return ProxyDvmObject.createObject(vm, ((StringBuilder) dvmObject.getValue()).append(str)); }
比如对 String 的操作
1 2 3 case "java/lang/String->equalsIgnoreCase(Ljava/lang/String;)Z" :{ return dvmObject.getValue().toString().equalsIgnoreCase(vaList.getObjectArg(0 ).getValue().toString()); }
比如 okhttp3,可以 maven 引入然后正常使用
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 case "okhttp3/Headers->name(I)Ljava/lang/String;" :{ Headers headers = (Headers) dvmObject.getValue(); return new StringObject (vm, headers.name(vaList.getIntArg(0 ))); } case "okhttp3/Headers->value(I)Ljava/lang/String;" :{ Headers headers = (Headers) dvmObject.getValue(); return new StringObject (vm, headers.value(vaList.getIntArg(0 ))); } case "okio/Buffer->clone()Lokio/Buffer;" :{ Buffer buffer = (Buffer) dvmObject.getValue(); return vm.resolveClass("okio/Buffer" ).newObject(buffer.clone()); } case "okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;" : { Request request = (Request) dvmObject.getValue(); return vm.resolveClass("okhttp3/Request$Builder" ).newObject(request.newBuilder()); } case "okhttp3/Request$Builder->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder;" : { Request.Builder builder = (Request.Builder) dvmObject.getValue(); builder.header(vaList.getObjectArg(0 ).getValue().toString(), vaList.getObjectArg(1 ).getValue().toString()); return dvmObject; } case "okhttp3/Request$Builder->build()Lokhttp3/Request;" : { Request.Builder builder = (Request.Builder) dvmObject.getValue(); request = builder.build(); return vm.resolveClass("okhttp3/Request" ).newObject(request); }
复用样本自定义类库 通过反编译工具(如 JADX、GDA、JEB)获取样本自定义类库的逻辑,并直接嵌入到 Unidbg 环境中。有些 Android FrameWork 层的类库也同样可以拷贝过来
TextUtils
中的方法
1 2 3 4 case "android/text/TextUtils->isEmpty(Ljava/lang/CharSequence;)Z" : { String str = vaList.getObjectArg(0 ).getValue().toString(); return str = = null || str.length() == 0 ; }
比如 Android 的 Base64 工具类,可以直接抠出来。
1 2 3 4 5 case "android/util/Base64->decode(Ljava/lang/String;I)[B" :{ String arg = vaList.getObjectArg(0 ).getValue().toString(); int flag = vaList.getIntArg(1 ); return new ByteArray (vm, Base64.decode(arg, flag)); }
高频 Framework 类的自定义实现 对于 JDK 中没有直接实现,但 Android Framework 使用频率较高的类,可以通过自定义逻辑实现基本功能。
SharedPreferences
自定义实现。我们最好写一个小的 demo,实现 Android 中对 SP 的创建、打开、读取、修改、保存这些基本功能。
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 public class SharedPreferences { private JSONObject jsonObject; private final File mFile; SharedPreferences(File file) { mFile = file; loadFromDisk(); } private void loadFromDisk () { if (mFile.exists() && mFile.canRead()) { try (BufferedInputStream str = new BufferedInputStream (new FileInputStream (mFile), 16 * 1024 )) { readSP(str); } catch (Exception e) { System.out.println("Error loading SharedPreferences: " + e); } } } public String getString (String key, String defValue) { String value = jsonObject.getString(key); return value != null ? value : defValue; } public void putString (String key, String value) { jsonObject.put(key, value); } private void readSP (InputStream in) { try (InputStreamReader input = new InputStreamReader (in)) { char [] buf = new char [16 * 1024 ]; int len = input.read(buf); jsonObject = JSON.parseObject(new String (buf, 0 , len)); } catch (IOException e) { e.printStackTrace(); } } }
以及根据样本所操作的属性和方法,构建对应的类去描述它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class AccessibilityServiceInfo { public String[] packageNames; public String name; public CharSequence label; public String packageName; public AccessibilityServiceInfo (String packageName, String name, CharSequence label, String[] packageNames) { this .packageName = packageName; this .name = name; this .label = label; this .packageNames = packageNames; } }
降级方法调用 对于难以处理的方法,可以通过简单数据返回或占位的方式处理,降低复杂性。
简单返回占位对象
1 2 3 4 5 6 7 8 9 10 11 12 case "android/bluetooth/BluetoothAdapter->getDefaultAdapter()Landroid/bluetooth/BluetoothAdapter;" :{ return dvmClass.newObject(signature); } case "android/telephony/SubscriptionManager->from(Landroid/content/Context;)Landroid/telephony/SubscriptionManager;" :{ return dvmClass.newObject(signature); } case "android/net/Uri->parse(Ljava/lang/String;)Landroid/net/Uri;" :{ String key = vaList.getObjectArg(0 ).getValue().toString(); if (key.equals("content://telephony/siminfo" )){ return dvmClass.newObject(key); } }
返回空数据避免阻塞
1 2 3 case "android/content/pm/PackageManager->getInstalledPackages(I)Ljava/util/List;" : { return null ; }
通过这几种方法,基本可以覆盖大部分补环境的需求:
优先复用现有类库 ,减少不必要的实现工作量。
样本自定义类库和 Framework 类库 ,通过反编译工具获取逻辑,并尽量还原代码。
高频 Framework 类的自定义实现 ,确保功能可用且灵活扩展。
降级处理 适用于高风险或非关键逻辑,确保程序可继续运行。
案例:百度网盘链接:https://pan.baidu.com/s/1G0U8frKK3dotY2cryztFjQ 提取码:6666