京东到家APP签名分析
0x01、 目标需求:
.a) 分析京东到家APP的接口参数以及相关的签名获取的方式
0x02、分析背景:
.a) 京东到家APP
.b) APP版本7.9.0(目前最新)
.c) 软件无壳
.d) 软件通讯过程中部分接口采取了签名验证的方式,返回数据无加密
.e) APP反Xposed Hook,本文使用Frida
0x03、分析流程:
.a) 通过数据包抓取或敏感函数hook方式获得接口功能
.b) 通过函数内部参数的组装继续分析参数的来源以及加密的流程
0x04、获取店铺详细信息接口:
.a) 截图如下:
0x001. 抓包分析。


0x005. 通过抓包发现图1中的signKey和signKeyV1共两个签名,接下来我们分析一下他的这个签名的出处

0x006. 把这个so拖到IDE中查看,如下图,so中的gk方法对应java中的k方法,gk2对应java中的k2函数,signKey调用的是so中的gk signKeyV1调用的是so中的k2函数,发现调用了java层的函数jd.utils.StatisticsReportUtil.getSign()返回值为String类型

0x007. 查看java 层获取签名的函数,截图代码如下:

a). 查看java 层获取签名的函数,截图代码如下:
public static String getSign() {
try {
return StringToolBox.getHexString(((X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(JDApplication.getInstance().getPackageManager().getPackageInfo(JDApplication.getInstance().getPackageName(), 64).signatures[0].toByteArray()))).getPublicKey().getEncoded());
} catch (Exception e) {
ThrowableExtension.printStackTrace(e);
return null;
}
}
b). 上面代码主要功能是获取一个64位的秘钥来对数据进行加密,秘钥的来源为包签名再使用公钥证书进行价签:
c). 既然知道这个so文件要调用这个类下的这个方法,那我们就简单的处理一下他的类文件和so文件直接复制拷贝到我们自己的APP里做验证(既然跟包签名有关系,这个so调用我们的APP的方法肯定跟它自己的APP返回的不一致,所以会导致签名加密错误,那我们先hook他的getSign()返回值是啥,然后自己的方法内直接返回这个值即可)



//主动调用getSign()函数获得签名数据:30819f300d06092a864886f70d010101050003818d00308189028181008c470af7c751ee12edbae8dd9e7c98fa60d3c631efa0f7172ed36c86bb85c8288391e718c05fdbef008d61f2e8fce4ef4457a69ae5a2fa53ead0c806c18f8b475847c07bf4451d82845efc30d5fc4aa2500f4bc84234a36749e83a9361c9ec89771a762e3d791eebf3154c2e95d06df95be68b4a4dcff33ef1ba5d6d90758b6d0203010001
public static String getSign() {
return "30819f300d06092a864886f70d010101050003818d00308189028181008c470af7c751ee12edbae8dd9e7c98fa60d3c631efa0f7172ed36c86bb85c8288391e718c05fdbef008d61f2e8fce4ef4457a69ae5a2fa53ead0c806c18f8b475847c07bf4451d82845efc30d5fc4aa2500f4bc84234a36749e83a9361c9ec89771a762e3d791eebf3154c2e95d06df95be68b4a4dcff33ef1ba5d6d90758b6d0203010001";
}
0x008. 验证两端APP加密相同数据结果是否一致(APP内的加签盐值是空格N多个):





a). 验证结果:
//Hook原版APP 传参为'123123123123123123123123123123'+' ';
//=============================================================
//k方法返回值为:b4dce9f2007697bdb605d657d86f6cf4
//K2方法返回值为:691fed2927a8d16b9f4ed3593bb1ab25f0c2764a8f4144f2d0790fac93464471
//=============================================================
//测试APP打印结果
//I/ContentValues: K方法返回值为:b4dce9f2007697bdb605d657d86f6cf4
//I/JNI: ----------------xx begin------------------
// 36
// 31
// 63
// 64
// 35
// 30
// 61
// 30
// 63
// 66
// 33
// 38
// 34
// 35
// 35
// 38
// 61
// 66
// 64
// 62
// 37
// 31
// 34
// 31
// 30
// 63
// 36
// 35
// 65
// 30
// 36
// 35
// 0
// -----------------xx end--------------------
// pass3
//I/ContentValues: K2方法返回值为:691fed2927a8d16b9f4ed3593bb1ab25f0c2764a8f4144f2d0790fac93464471
b). 经过验证签名一致,验证成功(感兴趣的小伙伴可以尝试还原so的算法),接下来就是组装参数请求服务器api:


c).下图中的剔除参数直接复制源码到我们自己的APP然后调用即可:


d).使用python组装请求的地址传给安卓算签名并返回最终请求地址:


e).python请求代码如下:
import requests,json, base64
requests.packages.urllib3.disable_warnings()
body = '{"storeId":"%s","longitude":116.48148,"latitude":39.996506,"ref":"channel","ctp":"storeinfo","pageSource":"store"}' % ('10053586')
data = {
'url':'https://gw-o2o.jddj.com/client?appVersion=7.9.0'
'&lat_pos=39.996506'
'&platVersion=10'
'&channel=wandoujia'
'&screen=1080*2110'
'&poi=望京SOHO'
'&body=%s'
'&deviceId=ff64a44a8f00a447406a9cacba2dba2d'
'&platCode=android'
'&functionId=store/storeDetailV220'
'&lng_pos=116.48148'
'&networkType=WIFI'
'&brand=Redmi'
'&androidId=a5ca07dc90224ee0'
'&lat=39.996506'
'&traceId=ff64a44a8f00a447406a9cacba2dba2d15965998612261596622332455'
'&lng=116.48148'
'&appName=Paidaojia'
'&deviceToken=ff64a44a8f00a447406a9cacba2dba2d'
'&partner=wandoujia'
'&imei=a5ca07dc90224ee0'
'&deviceModel=M2003J15SC'
'&city_id=1' % body
}
headers = {
'Content-Type': 'application/text;charset=utf-8'
}
t = json.dumps(data, ensure_ascii=False)
t = base64.b64encode(t.encode("utf-8"))
# 这里请求的地址其实是我写的APP,开放了HTTP服务
res = requests.post(url="http://47.93.128.122:82/?type=getUrl", headers=headers, data=t)
url = res.text
headers = {
'x-mlaas-at': 'wl=0',
'accept-encoding': 'gzip',
'user-agent': 'okhttp/3.11.0'
}
res = requests.get(url=url,headers=headers, verify=False)
print(res.text)
f).Android部分代码如下:
package jd.net;
import android.util.Log;
import java.util.HashMap;
import java.util.Map;
import static android.content.ContentValues.TAG;
public class z {
public static String host = "";
public static final String KEY_NEW_SIGN = "signKeyV1";
private static final String PLACE_HOLDER = " ";
String k1;
public static native void a(Object obj);
public static native int flushlog();
public static native int initlog(String str, String str2, int i);
public static native String jdk();
public static native String k(byte[] bArr);
public static native String k2(byte[] bArr);
public static native String readlog(String str);
public static native int wlog(int i, String str, long j);
static {
System.loadLibrary("jdpdj");
}
//计算签名
public static void b(String body,Map<String,String> params) {
params.put("signKey", k((body + PLACE_HOLDER).getBytes()));
params.put("t", System.currentTimeMillis() + "");
params.put("subVersion", "7.9.0.5");
params.put(KEY_NEW_SIGN, k2((ASCIISortUtil.formatQueryParaMap(params, false) + PLACE_HOLDER).getBytes()));
}
public static String c() {
return k2("{} ".getBytes());
}
//获取最终url入口函数,调用此函数直接返回最终请求的url结果
public static String returnResult(String url){
host = url.substring(0,url.indexOf("?")+1);
url = url.substring(url.indexOf("?")+1);
Map<String,String> m = stringToMap(url);
String body = m.get("body");
b(body,m);
return mapToString(m);
}
//String转Map
public static Map<String,String> stringToMap(String str){
Map<String,String> map = new HashMap<String,String>();
String[] strings = str.split("&");
for (String s : strings){
map.put(s.split("=")[0],s.split("=")[1]);
}
return map;
}
//map转String
public static String mapToString(Map<String,String> map){
StringBuffer sb = new StringBuffer();
for (String key : map.keySet()){
sb.append(key+"="+map.get(key)+"&");
}
return host+sb.toString().substring(0,sb.toString().length()-1);
}
}

友情提示:本文只为技术分享交流,请勿非法用途.产生一切法律问题与本人无关
服务端代码GIT地址 https://gitee.com/mr_qjj/JingdongDaoJia.git
在浏览的同时希望给予作者打赏,来支持作者的服务器维护费用.一分也是爱~