破解一号店的心得

1、豌豆荚下载最新版一号店,版本7.0.3,(下载老版本貌似会强制升级)。

2、通过fiddler抓包,这里我抓了一个根据关键字搜索产品的包。

       请求头跟请求体大致如上,通过发送http请求发现,返回的json在几分钟内会失效,这时候想到里面有跟时间相关的加密参数来对请求进行校验。在改变参数的情况下,找到了影响参数。发现url的后缀有一个sign,这时候参数找到了,再通过jadx-gui这个软件来看源码想要找到这个参数是怎么进行加密的。因为影响结果的参数跟sign有关,所以搜索跟sign有关的代码,并通过vs code来调试看整个过程有没有走这个方法。

       通过frida来hook,这个在前面的博客里有写到,这里就不再说了。下面是hook的代码

function hook1(){
    /**
     * .overload()
        .overload('[B')
        .overload('[B', '[B')
        .overload('java.util.Map', 'java.lang.String', 'java.lang.String')
     */
    var sign = Java.use('com.jingdong.jdsdk.network.toolbox.GatewaySignatureHelper');//类名
    sign.signature.implementation= function(a,b,c){
        console.log('**************start*******************')
        console.log('加密前:'+a)
        console.log('加密前b:'+b)
        console.log('加密前c:'+c)
        var res = this.signature(a,b,c)
        console.log('加密后:'+res);
        console.log('----------------end----------------')
        console.log('                    ')
        console.log('                    ')
        return res;
    }
}


function main(){
    Java.perform(function(){
        hook1();
    })
}


setImmediate(main);

       这里有几个要注意的点,一个类里面会有很多个相同名字的方法,但是我们hook的时候就需要hook具体的某一个方法就可以,所以如果有两个以上的同名方法,需要overload一下,参考注释里的代码;还有就是源码中该方法传了几个参数,那这里hook的话就需要传几个参数。

       在源码中找想要的方法也是需要技巧的,一般来说,如果我们直接搜sign那么可能会出现几千条跟这个有关的代码,所以我们可以加上一个双引号,这样就会大大的筛选了结果,方便我们去hook。通过筛选结果可以看到就三十来条,然后点进去,找到是哪个类的哪个方法,将其拷到上面的hook代码中,通过手机搜索关键字看控制台有无结果输出。(命令frida -U com.thestore.main-l Hook.js)

        然后就是很难受的发现这里面的所有方法都不走....后来通过&sign来搜索,终于找到了那个加密方法,发现有三个参数影响加密结果,进一步解析发现secretKey是个定值,所有判定只有url跟body进行加密。

       通过hook确定了在通过关键字搜索产品的时候,确实走了这个方法进行了加密,但是需要进一步确认,我最终拿到返回的数据,是不是跟我抓包时候的请求结构一样,是不是可以通过发请求拿到真数据,是不是跟我在APP搜索返回展示的数据一样。所以这时候需要远程调用一下,然后本地写个测试类看下控制台输出的结果是不是我想要的。python脚本如下(脚本名rpc2.py)

from flask import Flask
from flask import request
import frida
import hashlib
import requests
import time
import json
import chardet

app = Flask(__name__)

def on_message(message, data):
    if message['type'] == 'send':
        print(message)
    else:
        print(message)

script = None

def begin():
    global script
    process = frida.get_remote_device().attach('com.thestore.main')
    # process = frida.get_device_manager().get_device("127.0.0.1:21503").attach('com.thestore.main')
    with open("rpc2.js",'r',encoding='utf-8') as js:
        jscode=js.read()
    script = process.create_script(jscode)
    script.on('message', on_message)
    script.load()#加载脚本完毕
    print('1. 加载脚本完毕,成功获取script对象.....')
    app.run(debug=True, port=8004)# 启动服务


@app.route('/sign', methods=['GET'])
def waimai_function():
    p1 = request.args.get("p1")#根据加密方法来确定传几个参
    p2 = request.args.get("p2")
    print("p1是"+p1)
    print("p2是"+p2)
    res = script.exports.wirelesscode(p1,p2)#在这里传值,剩下就是开服务的问题了
    # res = '{wirelesscode:"'+res+'"}'
    print(res)
    return res


if __name__ == "__main__":
    begin()

rpc2.js代码如下

rpc.exports = {
    wirelesscode: function (p1,p2) {
        var result = ''
        Java.perform(function () {
            var sign = Java.use('com.jingdong.jdsdk.network.toolbox.GatewaySignatureHelper');//类名
            console.log('来了老弟---'+p1+'-------'+p2);
          
            console.log('传进来的参数:'+p1);
            console.log('传进来的参数:'+p2);
           
            var c = 'f9b37e4b28e84c169f9d503baaa23b6c';//定值
            result = sign.signature(p1,p2,c);
            console.log(result);
        });
        return result;
    }
};

      本地测试类代码如下

public class TestKeyword {
    public static void main(String[] args) throws Exception {
    	
    	String keyword  = "洁面乳";
    	int page = 1;
    	
    	String p2 = URLEncoder.encode("{"addrFilter":"1","apolloId":"b3fbf56db4484f42bb8464b94374d5a0","
    			+ ""apolloSecret":"af200ce75e1a4e188eca5fa90907f4a7","articleEssay":"1","
    			+ ""deviceidTail":"","insertArticle":"1","insertScene":"1","
    			+ ""insertedCount":"0","isCorrect":"1","keyword":""+keyword+"","
    			+ ""latitude":"0.0","longitude":"0.0","newMiddleTag":"1","
    			+ ""newVersion":"3","oneBoxMod":"1","orignalSearch":"1","
    			+ ""orignalSelect":"1","page":""+page+"","pageEntrance":"1","
    			+ ""pagesize":"10","pvid":"","sdkClient":"plugin_android","
    			+ ""sdkName":"search","sdkVersion":"1.0.0","searchVersionCode":"0","
    			+ ""showShopTab":"yes","showStoreTab":"1","stock":"1"}","utf-8");
    	
    	String res = "http://localhost:8004/sign?p1="+DecipherUtil.key(keyword, page)+"&p2="+p2;
    	String url = HttpBase.get(res, "utf-8").getResult();
    	String body = "body="+p2+"&";
    	
    	Map<String,String> header = new HashMap();
		header.put("Charset", "UTF-8");
		header.put("Connection", "keep-alive");
		header.put("Cache-Control", "no-cache");
		header.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
		header.put("Content-Length", "927");
		header.put("Host", "api.m.jd.com");
		header.put("User-Agent", "okhttp/3.12.1");
		
		try {
			String result = PostUtil.post(url, header, body);
			System.out.println(result);
			
		} catch (ConnectException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    	
    }
    
}

  util类代码如下

public class DecipherUtil {
    public static  String key(String keyword,int page) throws Exception {
        String time = System.currentTimeMillis()+"";
        
        String p1 = URLEncoder.encode("http://api.m.jd.com/api?appid=member_yhd&functionId=yhd_nsearch"
                + "&t="+time+"&clientVersion=7.0.3&build=703&client=yhd_android&d_brand=Coolpad"
                + "&d_model=N3C&osVersion=7.1.1&screen=1344*720&partner=jingdong"
                + "&lang=zh_CN&uuid=352118197740800-00281a339ed5&area=2_2825_0_0"
                + "&networkType=wifi&wifiBssid=unknown","utf-8");
        String p2 = URLEncoder.encode("{"addrFilter":"1","apolloId":"b3fbf56db4484f42bb8464b94374d5a0","
                + ""apolloSecret":"af200ce75e1a4e188eca5fa90907f4a7","articleEssay":"1","
                + ""deviceidTail":"","insertArticle":"1","insertScene":"1","
                + ""insertedCount":"0","isCorrect":"1","keyword":""+keyword+"","
                + ""latitude":"0.0","longitude":"0.0","newMiddleTag":"1","
                + ""newVersion":"3","oneBoxMod":"1","orignalSearch":"1","
                + ""orignalSelect":"1","page":""+page+"","pageEntrance":"1","
                + ""pagesize":"10","pvid":"","sdkClient":"plugin_android","
                + ""sdkName":"search","sdkVersion":"1.0.0","searchVersionCode":"0","
                + ""showShopTab":"yes","showStoreTab":"1","stock":"1"}","utf-8");
        return p1;
        
    }
    
}

       其实通过hook那个方法就可以知道,参与加密的无非是搜索的关键字,时间最多加上一个翻页参数,主要就是看它加密前是怎么拼接的。一般看到body里面有很多个%,就会很自然的想到编码解码。以前碰到的那些编码解码也就是只有汉字进行编码,但一号店这个是全部都参与了编码。

整个过程中要注意的几个点:      

      1.在整个hook过程以及本地测试的时候,真机或者模拟器都必须连接电脑,且需要打开一号店APP,如果hook过程中报有关frida的错,可能是未给最高权限,也可能是没有转发,这时候把这俩步骤重新弄一遍再启动frida即可。

      2.上面的python脚本,如果有依赖未下载的话,启动也是会报错的,导入依赖的命令pip install 包名。

      3.有时候frida启动了,一号店这个APP也打开了,但是执行脚本的时候会报错,显示说frida.InvalidArgumentError: device not found,就是我们process=frida.get_devices_manager()这行代码写错了,换成上面那行就行了,这个也不是不能用这个方法来写,主要是有时候会出现第一次连接的时候连接不上,之后第二次,第三次就可以了。保险起见用上面那行代码。

     4. 还会出现服务启动了,python脚本也可以执行,但是会出现APP闪退,再次打开就打开不了了。这时候需要将手机或者模拟器重启,然后要重新转发,启动frida,再打开一号店APP,再启脚本。

     5.String res = "http://localhost:8004/sign?p1="+DecipherUtil.key(keyword, page)+"&p2="+p2;这一行代码就是起到在本地开个端口调用那个加密方法的作用,端口号无所谓,只要不占用其他的进程就好。如果我们不是在本地调用,而是丢到服务器上的话,那就要在服务器上弄个手机模拟器,然后启动frida,执行python脚本进行远程调用,步骤同上,这样的话我们的ip跟端口号就要变了。

原文地址:https://www.cnblogs.com/shitechnology/p/13633453.html