1.首先charles抓包发现每个请求Url后都接了一个sign的参数且每次都不一样。也没有其他的一些别的特别参数,那么关键问题就是分析sign参数的生成了
2.jadx反编译,寻找sign的生成的位置
直接搜索sign参数匹配的出来的结果太多了,一时间不好区分哪个是真的。于是使用getSign为前缀搜索
运气不错搜索到一个native方法 前缀也是getSign的方法名,打开这个类。找下引用位置进一步确认下是否为计算sign的类
最终在BaseApplication里面找到这个类引用,可以确定这个就是计算sign的类
3.开始分析sign方法的入参
frida hook这个方法得到结果如下:
function main(){
Java.perform(function(){
var BitmapkitUtils = Java.use("com.jingdong.common.utils.BitmapkitUtils")
BitmapkitUtils.getSignFromJni.overload('android.content.Context', 'java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function( context, str, str2, str3, str4, str5){
console.log("签名入参为: ","str =>",str,"str2=>",str2,"str3=>",str3,"str4=>",str4,"str5=>",str5)
var result = this.getSignFromJni(context,str,str2,str3,str4,str5)
console.log('签名sign为 ',result)
return result
}
})
}
根据hook结果稍加分析大概知道各个参数代表的意义:
context:上下文对象
str:url路径
str2:请求body信息
str3:55a9c688729bb118 为KEY 16位长度,且每次请求都固定
str4:android 为平台
str5:版本号
查看源码大概得出str3 的为installationId生成规则是UUID 随机16位长度,在app第一次安装的时候创建保存在缓存中.
4.使用unidbg进行黑盒调用
1.第一步当然是补环境了
需要补充返回一个application对象,偷个懒想从AbstractJni 这里copy一份
重新运行一下又报错了,这个错是找不到对应的methodId、于是想着把Application换成Activity也可以,毕竟Activity也是可以获取Application对象的
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
if ("com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;".equals(signature)) {
//返回appliation
return vm.resolveClass("android/app/Activity",
vm.resolveClass("android/content/ContextWrapper",
vm.resolveClass("android/content/Context"))).newObject(null);
}
return super.getStaticObjectField(vm, dvmClass, signature);
}
重新运行就没问题了,接下来继续报错补环境,返回apk的路径。很简单
@Override
public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
//sourceDir 代表当前apk目录
if("android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;".equals(signature)){
StringObject stringObject = new StringObject(vm, APK_PATH);
return stringObject;
}
return super.getObjectField(vm, dvmObject, signature);
}
图上这个返回刚开始不知道传具体什么参数,于是打算看看源码。结果没有反编译出来具体的函数实现。于是用frida hook了这个方法拿到图下的返回
第一个参数为:app安装目录 第二个参数为META-INF 目录 第三个是RSA
既然这样就直接返回RSA公钥了
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
if ("com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B".equals(signature)) {
byte[] unzip = vm.unzip("META-INF/xxxx.RSA");
System.out.println("unzip " + new String(unzip));
return new ByteArray(vm, unzip);
}
return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}
继续补充环境
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
if ("sun/security/pkcs/PKCS7-><init>([B)V".equals(signature)) {
DvmObject<?> objectArg = varArg.getObjectArg(0);
try {
PKCS7 pkcs7 = new PKCS7((byte[]) objectArg.getValue());
return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(pkcs7);
} catch (ParsingException e) {
e.printStackTrace();
}
}
return super.newObject(vm, dvmClass, signature, varArg);
}
继续补充环境,需补充如下
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
if ("sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;".equals(signature)) {
PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();
X509Certificate[] certificates = pkcs7.getCertificates();
DvmObject<?> object = ProxyDvmObject.createObject(vm, certificates);
return object;
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
继续补充环境,从反编译的源码中copy 一份objectToBytes方法即可然后构造ByteArray返回即可
if("com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B".equals(signature)){
DvmObject<?> objectArg = varArg.getObjectArg(0);
byte[] bytes = objectToBytes(objectArg.getValue());
return new ByteArray(vm,bytes);
}
5.开始黑盒调用计算sign的方法
List<Object> params = new ArrayList<>();
params.add(dalvikVM.getJNIEnv());
params.add(0);
DvmClass context = dalvikVM.resolveClass("android/content/Context");
params.add(dalvikVM.addLocalObject(context.newObject(null)));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "personinfoBusiness")));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "{\"callCJH\":\"1\",\"callNPS\":\"1\",\"closeJX\":\"0\",\"headTaskRefresh\":\"1\",\"locationArea\":\"0_0_0_0\",\"menuStaticSource\":\"0\",\"menuTimeStamp\":\"1631586010000\"}")));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, INSTALL_ID)));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, PLAT_FROM)));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, VERSION)));
Number numbers = moduleModule.callFunction(androidEmulator, 0x028B4+1, params.toArray())[0];
运行报错,还缺少环境
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if("java/lang/StringBuffer-><init>()V".equals(signature)){
StringBuffer stringBuffer = new StringBuffer();
return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}
重新继续补充环境 这部分环境补充比较简单 直接上最后运行结果
黑盒调用没问题了,接下来开始分析sign具体是怎么生成的。全部代码如下:
package jd;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.apk.Apk;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.ParsingException;
import java.io.*;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
public class JD extends AbstractJni {
private static final String SO_PATH = "";
private static final String APK_PATH = "";
private static final String INSTALL_ID = "55a9c688729bb118";
private static final String PLAT_FROM = "android";
private static final String VERSION = "10.4.6";
private AndroidEmulator androidEmulator;
public static void main(String[] args) {
Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG);
JD jd = new JD();
jd.start();
}
public void start() {
androidEmulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.jingdong.android")
.build();
Memory androidEmulatorMemory = androidEmulator.getMemory();
androidEmulatorMemory.setLibraryResolver(new AndroidResolver(23));
VM dalvikVM = androidEmulator.createDalvikVM(new File(APK_PATH));
DalvikModule module = dalvikVM.loadLibrary(new File(SO_PATH), false);
dalvikVM.setJni(this);
Module moduleModule = module.getModule();
dalvikVM.callJNI_OnLoad(androidEmulator, moduleModule);
List<Object> params = new ArrayList<>();
params.add(dalvikVM.getJNIEnv());
params.add(0);
DvmClass context = dalvikVM.resolveClass("android/content/Context");
params.add(dalvikVM.addLocalObject(context.newObject(null)));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "personinfoBusiness")));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "{\"callCJH\":\"1\",\"callNPS\":\"1\",\"closeJX\":\"0\",\"headTaskRefresh\":\"1\",\"locationArea\":\"0_0_0_0\",\"menuStaticSource\":\"0\",\"menuTimeStamp\":\"1631586010000\"}")));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, INSTALL_ID)));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, PLAT_FROM)));
params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, VERSION)));
Number numbers = moduleModule.callFunction(androidEmulator, 0x028B4 + 1, params.toArray())[0];
DvmObject<?> object = dalvikVM.getObject(numbers.intValue());
System.out.println("加密结果为:" + object.getValue());
}
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
if ("com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;".equals(signature)) {
//返回appliation
return vm.resolveClass("android/app/Activity",
vm.resolveClass("android/content/ContextWrapper",
vm.resolveClass("android/content/Context"))).newObject(null);
}
return super.getStaticObjectField(vm, dvmClass, signature);
}
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
if ("com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B".equals(signature)) {
byte[] unzip = vm.unzip("META-INF/xx.RSA");
System.out.println("unzip " + new String(unzip));
return new ByteArray(vm, unzip);
} else if ("com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B".equals(signature)) {
DvmObject<?> objectArg = varArg.getObjectArg(0);
byte[] bytes = objectToBytes(objectArg.getValue());
return new ByteArray(vm, bytes);
}
return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}
public static byte[] objectToBytes(Object obj) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
byte[] byteArray = byteArrayOutputStream.toByteArray();
objectOutputStream.close();
byteArrayOutputStream.close();
return byteArray;
} catch (IOException e) {
return null;
}
}
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
if ("sun/security/pkcs/PKCS7-><init>([B)V".equals(signature)) {
ByteArray byteArray = varArg.getObjectArg(0);
try {
PKCS7 pkcs7 = new PKCS7(byteArray.getValue());
return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(pkcs7);
} catch (ParsingException e) {
e.printStackTrace();
}
}
return super.newObject(vm, dvmClass, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
if ("sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;".equals(signature)) {
PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();
X509Certificate[] certificates = pkcs7.getCertificates();
DvmObject<?> object = ProxyDvmObject.createObject(vm, certificates);
return object;
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
//sourceDir 代表当前apk目录
if ("android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;".equals(signature)) {
StringObject stringObject = new StringObject(vm, APK_PATH);
return stringObject;
}
return super.getObjectField(vm, dvmObject, signature);
}
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if ("java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;".equals(signature)) {
StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();
DvmObject<?> objectArg = vaList.getObjectArg(0);
stringBuffer.append(objectArg.getValue().toString());
return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);
} else if ("java/lang/Integer->toString()Ljava/lang/String;".equals(signature)) {
Integer integer = (Integer) dvmObject.getValue();
return new StringObject(vm, integer.toString());
}else if("java/lang/StringBuffer->toString()Ljava/lang/String;".equals(signature)){
StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();
return new StringObject(vm, stringBuffer.toString());
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if ("java/lang/StringBuffer-><init>()V".equals(signature)) {
StringBuffer stringBuffer = new StringBuffer();
return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);
} else if ("java/lang/Integer-><init>(I)V".equals(signature)) {
int intArg = vaList.getIntArg(0);
Integer integer = Integer.valueOf(intArg);
return vm.resolveClass("java/lang/Integer").newObject(integer);
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}
}
6.ida+unidbg分析sign的具体如何生成的
ida导入so 包 export导出中找到sign生成方法 截图如下:
F5生成伪代码
根据方法的具体逻辑修改了下方法名称 方便直观查看。初步观察是讲传入参数通过key=value 的方式拼接起来
通过unidbg的执行日志中也可以发现这点
接下来开始从函数返回的地方开始往上分析,单独调用执行返回如下:
st=1648374072802&sign=8102aefd41782d405174725cd433c5a0&sv=111
从上可知要分析的为st 、sign、sv这个三个值
1.st生成分析:生成一个时间戳 长度为13位的 用于计算sign
2.sign的生成:
分析伪代码找到sign的生成方法为sub_126AC,该方法上面的伪代码都是一些拼接字符串的操作,就是把入参拼接起来。
接下来开始unidbg hook该方法
然后开启无尽的S 单步执行往下走当程序走到switch判断时 发现bl指令跳转到sub_10de4函数 多次在这个位置debugger 到 发现 到 sv =111 走的是 case 2 还遇到过sv = 110 走的是case 1 、这部分我们只分析为2的情况也就是走sub_10DE4,至于为啥呢?就是因为这后面走的加密算法看起来简单些 像md5
查看下函数sub_10de4入参,hook该函数
打印mr1,为一串固定的字符串
mr2 为0x1 固定值
mr3 打印如下,为前面拼接的参数串 长度为0x106
接着分析sub_10de4 这个函数,点击进入sub_12ECC函数 ,这里为啥不分析sub_12FF0 呢 因为具体看了下没啥特别的。
hook sub_12ECC函数分析下入参
参数1:打印看看 ,看不太出来是什么 但是是64位 看后面的伪代码这个值主要是 跟 md5魔数有关系
参数2: 固定字符串 80306f4370b39fd5630ad0529f77adb6
参数3: 0x1
参数4: 为明文的参数拼接串
functionId=personinfoBusiness&body={“callCJH”:“1”,“callNPS”:“1”,“closeJX”:“0”,“headTaskRefresh”:“1”,“locationArea”:“0_0_0_0”,“menuStaticSource”:“0”,“menuTimeStamp”:“1631586010000”}&uuid=55a9c688729bb118&client=android&clientVersion=10.4.6&st=1648455172760&sv=111
参数5:为长度 0x106
开始分析代码
从上面分析 v27 应该就是 md5 魔数 数组咯 大致为
v27[1]= 0x68449237;
v27[2]= 0x7FCC3DA5;
v27[3] = 0x88D90FBB;
v27[4] = 0x5AE99AEE;
继续往下分析
这个区域是具体的实现的位置,flag 是之前传入的0x1 所以走的是if 判断 。这个do-while 大致是 从v27中取具体魔数 s 往下走 v18 = 0x37 猜测应该是不断从v27中取值 还原算法时v27 就是这个下面这个数组
{0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0x0F, 0xD9, 0x88, 0xEE, 0x9A,
0xE9, 0x5A};
继续S 往下走 0x38 就是传入固定字符串的第一个字符 80306f4370b39fd5630ad0529f77adb6
r0 = 0x66 是从拼接的字符串中取出的第一个
r0=0x66 r1=0x0 r2=0x37 r3=0x1 r4=0x38
R0 = R0 ^ R2 = 0x51
R0 = R0 ^ R4 = 0x69
按照汇编 分析猜测 后面就是 将前面异或的结果 加上R2 0x37 然后在 R2 = R2 ^ R0
*(v5 - 1) = md5_init_key_index ^ *(_BYTE *)(key + v17);// EOR.W R2, R2, R1 STRB.W R2, [R10,#-1]
根据ida 上伪代码 结和汇编 大致可知 这部分是每执行一次do while 就修改一次v5的索引对应的值 一直循环到循环到v5数据长度,所以结合伪代码 用C还原 sign 的算法 如下
void encryption(char *data) {
uint32_t v1, v2, v3 = 0;
unsigned char init_key[60] = {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0x0F, 0xD9, 0x88, 0xEE, 0x9A,
0xE9, 0x5A};
char *sign = "80306f4370b39fd5630ad0529f77adb6";
int data_length = strlen(data);
unsigned char md5_init_key = 0;
unsigned char sign_key = 0;
do {
v1 = v3 & 0xF;
v2 = v3 & 7;
v3 = v3 + 1;
sign_key = sign[v2];
md5_init_key = init_key[v1];
md5_init_key = md5_init_key ^ ((md5_init_key ^ *data ^sign_key) + md5_init_key);
*data = md5_init_key ^ sign_key;
data++;
} while (v3 != data_length);
}
用python 来调用C代码 跟unidbg 黑盒调用结果比对如下
总结: 某东的sign有三个算法 这边只还原了其中比较简单的 sv =111 的这种 。还原这部分算法也花的时间挺多的,结合unidbg 来分析算法 真的方便。
合作联系加Q 1452142503