pxx app 算法逆向与设备指纹信息提取分析
这里只是做个记录
pdd anti-token参数网上unidbg补环境有很多,这里不再过多赘述
package com.pdd;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneEncoded;
import keystone.KeystoneMode;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;
public class demo extends AbstractJni implements IOResolver {
public static AndroidEmulator emulator;
public static Memory memory;
public static VM vm;
public static Module module;
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
System.out.println("[IO] open => " + pathname + " flags=" + oflags); //监视文件访问
return null;
}
public demo(){
emulator = AndroidEmulatorBuilder
.for64Bit()
.setProcessName("com.xunmeng.pinduoduo")
.addBackendFactory(new Unicorn2Factory(false))
.build();
emulator.getSyscallHandler().addIOResolver(this);
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("D:\\NewWorld\\android_tool\\unidbg\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\apk\\pinduoduo-7-80-0.apk"));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary(new File("D:\\NewWorld\\android_tool\\unidbg\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\pdd\\libpdd_secure64.so"), true);
module = dm.getModule();
System.out.printf("so文件基地址==>>>>>>>> %s base = 0x%x%n", module.name, module.base);
dm.callJNI_OnLoad(emulator);
//emulator.traceWrite(0x4009ee74,0x4009ee74+4);
hook_memcpy();
hook_system_property_get();
hook_breakpoint1();
emulator.attach().addBreakPoint(module.base + 0x1A1D0);
//emulator.attach().addBreakPoint(module.base+0x11664);
}
public static void main(String[] args) {
demo pdd_demo = new demo();
pdd_demo.call_info2();
}
@Override
public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "com/tencent/mars/xlog/PLog->i(Ljava/lang/String;Ljava/lang/String;)V":
System.out.println("[+]PLog.i arg0-->>" + vaList.getObjectArg(0));
System.out.println("[+]PLog.i arg1-->>" + vaList.getObjectArg(1));
return;
}
super.callStaticVoidMethodV(vm, dvmClass, signature, vaList);
}
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "com/xunmeng/pinduoduo/secure/EU->gad()Ljava/lang/String;":
return new StringObject(vm,"98c3863151319dc6");
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
@Override
public boolean callStaticBooleanMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "android/os/Debug->isDebuggerConnected()Z":
return false;
}
return super.callStaticBooleanMethod(vm, dvmClass, signature, varArg);
}
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "java/lang/Throwable-><init>()V":
return vm.resolveClass("java/lang/Throwable").newObject(new Throwable());
case "java/io/ByteArrayOutputStream-><init>()V":
ByteArrayOutputStream OutputMemoryBuffer = new ByteArrayOutputStream();
return vm.resolveClass("java/io/ByteArrayOutputStream").newObject(OutputMemoryBuffer);
case "java/util/zip/GZIPOutputStream-><init>(Ljava/io/OutputStream;)V":
DvmObject obj = varArg.getObjectArg(0);
OutputStream outputStream = (OutputStream) obj.getValue();
try {
return dvmClass.newObject(new GZIPOutputStream(outputStream));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
return super.newObject(vm, dvmClass, signature, varArg);
}
@Override
public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "android/content/Context->checkSelfPermission(Ljava/lang/String;)I":
System.out.println("[+]Context.checkSelfPermission arg0-->>" + varArg.getObjectArg(0));
return 1;
case "android/telephony/TelephonyManager->getSimState()I":
return 5;
case "android/telephony/TelephonyManager->getNetworkType()I":
return 2;
case "android/telephony/TelephonyManager->getDataState()I":
return 0;
case "android/telephony/TelephonyManager->getDataActivity()I":
return 0;
}
return super.callIntMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "android/content/Context->getSystemService(Ljava/lang/String;)Ljava/lang/Object;":
String serviceName = varArg.getObjectArg(0).getValue().toString();
System.out.println("getSystemService->" + serviceName);
switch (serviceName){
case "phone":{
return vm.resolveClass("android/telephony/TelephonyManager").newObject(null);
}
}
case "android/telephony/TelephonyManager->getSimOperatorName()Ljava/lang/String;":
return new StringObject(vm, "中国移动");
case "android/telephony/TelephonyManager->getSimCountryIso()Ljava/lang/String;":
return new StringObject(vm, "cn");
case "android/telephony/TelephonyManager->getNetworkOperator()Ljava/lang/String;":
return new StringObject(vm, "46000");
case "android/telephony/TelephonyManager->getNetworkOperatorName()Ljava/lang/String;":
return new StringObject(vm, "中国移动");
case "android/telephony/TelephonyManager->getNetworkCountryIso()Ljava/lang/String;":
return new StringObject(vm, "cn");
case "java/lang/Throwable->getStackTrace()[Ljava/lang/StackTraceElement;":
return new ArrayObject(
vm.resolveClass("java/lang/StackTraceElement").newObject(null),
vm.resolveClass("java/lang/StackTraceElement").newObject(null),
vm.resolveClass("java/lang/StackTraceElement").newObject(null)
);
case "java/lang/StackTraceElement->getClassName()Ljava/lang/String;":
return new StringObject(vm, "java.lang.Object");
case "java/io/ByteArrayOutputStream->toByteArray()[B":
ByteArrayOutputStream ByteArrayArg = (ByteArrayOutputStream) dvmObject.getValue();
byte[] data = ByteArrayArg.toByteArray();
// Inspector.inspect(data, "java/io/ByteArrayOutputStream->toByteArray()");
return new ByteArray(vm, data);
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature){
case "java/lang/String->replaceAll(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":
// 获取原始字符串
String original = (String) dvmObject.getValue();
// 获取参数
DvmObject<?> regexObj = vaList.getObjectArg(0);
DvmObject<?> replacementObj = vaList.getObjectArg(1);
if (regexObj == null || replacementObj == null) {
return new StringObject(vm, original);
}
String regex = (String) regexObj.getValue();
String replacement = (String) replacementObj.getValue();
// 执行替换
String result = original.replaceAll(regex, replacement);
// 返回结果
return new StringObject(vm, result);
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
@Override
public void callVoidMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "java/util/zip/GZIPOutputStream->write([B)V":
OutputStream outputStream = (OutputStream) dvmObject.getValue();
ByteArray array = varArg.getObjectArg(0);
Inspector.inspect(array.getValue(), "java/util/zip/GZIPOutputStream->write outputStream"); //unidbg提供的十六进制输出
try {
outputStream.write(array.getValue());
} catch (IOException e) {
throw new IllegalStateException(e);
}
return;
case "java/util/zip/GZIPOutputStream->finish()V":
GZIPOutputStream gzipOutputStream = (GZIPOutputStream) dvmObject.getValue();
try {
gzipOutputStream.finish();
} catch (IOException e) {
throw new IllegalStateException(e);
}
return;
case "java/util/zip/GZIPOutputStream->close()V":
GZIPOutputStream gzipOutputStream2 = (GZIPOutputStream) dvmObject.getValue();
try {
gzipOutputStream2.finish();
} catch (IOException e) {
throw new IllegalStateException(e);
}
return;
}
super.callVoidMethod(vm, dvmObject, signature, varArg);
}
public void hook_memcpy(){
emulator.attach().addBreakPoint(module.findSymbolByName("memcpy").getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
int len = context.getIntArg(2);
UnidbgPointer pointer1 = context.getPointerArg(0);
UnidbgPointer pointer2 = context.getPointerArg(1);
Inspector.inspect(pointer2.getByteArray(0, len), "src " + Long.toHexString(pointer1.peer) + " memcpy " + Long.toHexString(pointer2.peer));
return true;
}
});
}
public void hook_system_property_get() {
emulator.attach().addBreakPoint(module.findSymbolByName("__system_property_get").getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext ctx = emulator.getContext();
UnidbgPointer namePtr = ctx.getPointerArg(0);
UnidbgPointer valuePtr = ctx.getPointerArg(1);
String key = namePtr.getString(0);
System.out.println("[__system_property_get] key = " + key);
return true;
}
});
}
protected void printJavaObject(long jobjectOffset, String label) {
DvmObject<?> javaObject = vm.getObject((int) jobjectOffset);
if (javaObject != null) {
System.out.println("[" + label + "] " + javaObject.getValue());
} else {
System.out.println("[" + label + "] null");
}
}
public void hook_breakpoint1(){
emulator.attach().addBreakPoint(
module.base + 0x11378,
new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
UnidbgPointer pointer1 = context.getPointerArg(1);
int pointer2 = context.getIntArg(2);
UnidbgPointer pointer3 = context.getPointerArg(3);
Inspector.inspect(pointer1.getByteArray(0,300), "0x11378_arg1");
System.out.println("0x11378_arg2===>>>"+pointer2);
Inspector.inspect(pointer3.getByteArray(0,100),"0x11378_arg3");
emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer pointer0 = context.getPointerArg(0); //x0此时是返回值
printJavaObject(pointer0.peer, "0x11378返回值");
return true;
}
});
return true; // 继续执行
}
}
);
}
private void call_info2(){
DvmClass deviceNativeClass = vm.resolveClass("com/xunmeng/pinduoduo/secure/DeviceNative");
// 2. 构造一个假的 Context(很多 native 只检查非 null / 调少量方法)
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
DvmObject<?> result = deviceNativeClass.callStaticJniMethodObject(
emulator,
"info2(Landroid/content/Context;J)Ljava/lang/String;",
context,
1764743489547L
);
System.out.println("主动调用结果result====>>>>>>"+result.toString());
}
}
算法加密逻辑就是Gzip加密数据+AES(cbc)+base64

通过这个JNI函数的偏移可以确定一下函数位置

函数是

unidbg下断点调试看看,可以发现第二个参数s是设备信息

交叉引用看一下位置,发现只有一处调用在device_info2函数内

有ollvm混淆但不妨碍分析,结合ai问一下即可定位到关键代码段

这里有几个关键方法可以点进去看一下
如sub_1A1D0

看到有相关的设备指纹获取api,qword_9A120是运行时动态解密的数据,下断点看看

可以发现此时就是该字段就是即将要获取的指纹信息
其实在unidbg中hook system_property_get也会获取到数据,但这里是想追一下代码的执行过程
public void hook_system_property_get() {
emulator.attach().addBreakPoint(module.findSymbolByName("__system_property_get").getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext ctx = emulator.getContext();
UnidbgPointer namePtr = ctx.getPointerArg(0);
UnidbgPointer valuePtr = ctx.getPointerArg(1);
String key = namePtr.getString(0);
System.out.println("[__system_property_get] key = " + key);
return true;
}
});
}
>"ANDROID.PERMISSION.READ-PHONE-STATE" [__SYSTEM_PROPERTY_GET] KEY - RO.SERIALNO JNIENV-SNEWSTRINAUTE("015763636321672505') WAS CALLED FROM RXGOX6003A80OFLIBNDD-SECUNE-SOLOX3A809 JNIENV--NENSLOBALREF("015753636321C72505') WAS CALLED FROM RXODX4004E668(LIBDDDD-SECURE,SOLOX4E6689 JHIENV--GETSTRINQUTFLENATH("0157636321C72505") WAS CALLED FROM RXBAXBOX60O1LIBDDDDDA-SECURS,SOLEX14ED JNIENV->GATSTRINAUTFCHARS("01503573321672505年) WAS CALLED FROM RXAOXHOX4001503OLLIBDD-SECUNS,SOLOX15 JNIENY-RELEASESTRINGUTFCHANS('015763210DDDD FROS") WAS CALLED FRON RXOXAOO15864LLIOPDDDDDDDDDX15X150B [__SYSTEM_PROPERTY_GET] KEY - RO.PRODUCT.BRAND [__SYSTEM_PROPERTY_GET] KEY 三 R RO.PRODUCT.DEVICE [__SYSTEM_PROPERTY_GET] KEY 三 RO.PRODUCT.MODEL [__SYSTEM_PROPERTY_GET] KEY RO.P O.PRODUCT.MANUFACTURER [_SYSTEM_PROPERTY_GET] KEY - RO.PRODUCT.BOARD [__SYSTEM_PROPERTY_GET] RO.BUILD.DISPLAY.ID KEY R [__SYSTEM_PROPERTY_GET] KEY - R RO.BUILD.ID [__SYSTEM_PROPERTY_GET] KEY 三 R RO.BUILD.VERSION.INCREMENTAL [__SYSTEM_PROPERTY_GET] KEY - RO.BUILD.TYPE [__SYSTEM_PROPERTY_GET] KEY RO.BUILD.TAGS [__SYSTEM_PROPERTY_GET] KEY 三 RO.BUILD.VERSION.RELEASE [__SYSTEM_PROPERTY_GET] KEY 三 R RO.BUILD.VERSION.SDK [__SYSTEM_PROPERTY_GET] KEY - RO.BUILD.DATE.UTC [TOL ONEN :> /SVSTEM/BUILD.DRON FLAGS:O -->

接着看sub_1B6DC

看一下这个file路径是什么

** /system/build.prop 是系统在编译ROM时生成的,可用于判断是否为模拟器**
这里没有使用open等函数进行获取数据,而是使用了stat函数,是为了避免被hook
stat()用来将参数file_name 所指的文件状态, 复制到参数buf 所指的结构中
struct stat {
dev_t st_dev; //device 文件的设备编号
ino_t st_ino; //inode 文件的i-node
mode_t st_mode; //protection 文件的类型和存取的权限
nlink_t st_nlink; //number of hard links 连到该文件的硬连接数目, 刚建立的文件值为1.
uid_t st_uid; //user ID of owner 文件所有者的用户识别码
gid_t st_gid; //group ID of owner 文件所有者的组识别码
dev_t st_rdev; //device type 若此文件为装置设备文件, 则为其设备编号
off_t st_size; //total size, in bytes 文件大小, 以字节计算
unsigned long st_blksize; //blocksize for filesystem I/O 文件系统的I/O 缓冲区大小.
u nsigned long st_blocks; //number of blocks allocated 占用文件区块的个数, 每一区块大小为512 个字节.
time_t st_atime; //time of lastaccess 文件最近一次被存取或被执行的时间, 一般只有在用mknod、 utime、read、write 与tructate 时改变.
time_t st_mtime; //time of last modification 文件最后一次被修改的时间, 一般只有在用mknod、 utime 和write 时才会改变
time_t st_ctime; //time of last change i-node 最近一次被更改的时间, 此参数会在文件所有者、组、 权限被更改时更新
};
所以此刻v3 = buf[11]; 获取的数据就是time_t st_mtime
sub_1B960函数ollvm混淆比较严重,这里结合unidbg进行分析
在该函数下断点如何blr,运行到函数结尾,可发现运行大量JNI函数,大部分都是调用java层的api获取设备sim卡信息。
"ANDROID.DERMISSION.READ_PHONE_STATEW JNIENV->CALLINTHOD[ANDROID-CONTENTENTENTEXTPASFBAO77, CHRCONTSOLIPENISSION["ANAROID,PEMISSIONE STATE ) -> OX1) WAS CALLED FROM RXCOX40018AB4[LIBDD_SECURE.SO]OX18AB4 JUTENV- FINDCLASS(ONAROLD/TELEPHONY/TOLEPHONYTANAGER) WAS CALLED FRON FXDPXAO3ELTIBPA. 30JBX3AB35 [20:A7:38 697]SRC 40417008 MEMCPY 68417013, MD5-8BB6C17838643F9691CCBAADE6C51789, HEX-05 SIZE:1 0000:05 -->
sub_1D450函数

依旧是字符串解密后调用
继续下断点看一下传入了什么数据

这是即将要调用的方法签名
看一下java层的实现


通过JNI回调java层获取设备ID
sub_20814函数

根据三个函数的返回值不同进行调用
进去sub_1E8C0函数看看

还是一样

回调java层android/os/Debug.isDebuggerConnected()的方法,判断是否被调试
sub_1EF50函数


其实在unidbg中这个函数干了什么也看的七七八八了
首先获取进程pid,获取status文件TracerPid值检查是否被反调试
sub_1F3E0函数

这个函数同时也是调用了大量JNI函数,回调Java调用栈,判断是否被unidbg等模拟器模拟
正常 Android Java 调用 native 的栈
com.xunmeng.pinduoduo.secure.EU.gad
com.xunmeng.pinduoduo.secure.EU.xxx
com.xunmeng.pinduoduo.xxx.xxx
android.app.ActivityThread
而我unidbg调用栈却是
java.lang.Object
java.lang.Object
java.lang.Object

浙公网安备 33010602011771号