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跟端口号就要变了。