京东到家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共两个签名,接下来我们分析一下他的这个签名的出处

图中得知这两个签名通过so做的



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

IDE查看So



    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()返回值是啥,然后自己的方法内直接返回这个值即可)

复制文件和so库



获取原版APP签名



模拟原版APP返回数据

//主动调用getSign()函数获得签名数据:30819f300d06092a864886f70d010101050003818d00308189028181008c470af7c751ee12edbae8dd9e7c98fa60d3c631efa0f7172ed36c86bb85c8288391e718c05fdbef008d61f2e8fce4ef4457a69ae5a2fa53ead0c806c18f8b475847c07bf4451d82845efc30d5fc4aa2500f4bc84234a36749e83a9361c9ec89771a762e3d791eebf3154c2e95d06df95be68b4a4dcff33ef1ba5d6d90758b6d0203010001

    public static String getSign() {
        return "30819f300d06092a864886f70d010101050003818d00308189028181008c470af7c751ee12edbae8dd9e7c98fa60d3c631efa0f7172ed36c86bb85c8288391e718c05fdbef008d61f2e8fce4ef4457a69ae5a2fa53ead0c806c18f8b475847c07bf4451d82845efc30d5fc4aa2500f4bc84234a36749e83a9361c9ec89771a762e3d791eebf3154c2e95d06df95be68b4a4dcff33ef1ba5d6d90758b6d0203010001";
    }


    0x008. 验证两端APP加密相同数据结果是否一致(APP内的加签盐值是空格N多个):

签名



原版APP加签结果



调用测试方法



自己编写APP调用



测试APP打印结果



      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
在浏览的同时希望给予作者打赏,来支持作者的服务器维护费用.一分也是爱~