去哪儿
App:去哪儿客户端
versionCode="172"
versionName="8.8.5"
去哪儿是采用基于React Native技术的QRN开发,使用QRN Ext做路由转发,QHotDogNetWork做网络请求。
先分析自定义App
如果没有React Native的开发基础,需要搭建一个React Native开发环境,反编译hello world来分析React Native App的特征。关于React Native开发环境的搭建可以参考这里。运气不好可能会遇到比较多的问题,多百度总是可以解决的。
成功运行App以后,打包App,分别用jadx和apktool反编译自己打包的apk,在assets目录下,可以看到index.android.bundle文件,App的入口是MainActivity,其实现是空,继承自Facebook包里的一个类。结合前面的开发环境搭建可以知道,我们实际上写的是JS,综合来看只有index.android.bundle这个文件比较可疑。打开发现是压缩了的js,没关系,美化一下,还是可以阅读的,稍微看一下,可以发现前面部分是React Native的基础框架,后面部分,跟之前的App.js的代码相似度非常高,基本可以确定这个就是真正的逻辑代码了。
原App.js
const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
android:
'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});
type Props = {};
export default class App extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome to React Native!</Text>
<Text style={styles.instructions}>To get started, edit App.js</Text>
<Text style={styles.instructions}>{instructions}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
获取的代码:
v = y.Platform.select({
ios: "Press Cmd+R to reload,\nCmd+D or shake for dev menu",
android: "Double tap R on your keyboard to reload,\nShake or press menu button for dev menu"
}),
p = (function(t) {
function n() {
return (0, o.
default)(this, n), (0, u.
default)(this, (0, c.
default)(n).apply(this, arguments))
}
return (0, s.
default)(n, t), (0, l.
default)(n, [{
key: "render",
value: function() {
return f.
default.createElement(y.View, {
style: h.container
}, f.
default.createElement(y.Text, {
style: h.welcome
}, "Welcome to React Native!"), f.
default.createElement(y.Text, {
style: h.instructions
}, "To get started, edit App.js"), f.
default.createElement(y.Text, {
style: h.instructions
}, v))
}
}]), n
})(f.Component);
e.
default = p;
var h = y.StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF'
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5
}
})
}, 435, [436, 1, 103, 104, 114, 124, 140, 156, 2]);
关于React Native的参考:
ReactNative Android源码分析
Native与Javascript通信原理(一)
Native与Javascript通信原理(二)
Native与Javascript通信原理(三)
SoLoader加载动态链接库
再看去哪儿
分析去哪儿这种App,虽然知道抓包可能抓不出什么,但是还是可以看看,有什么有用的信息没。这里没有用Charles和fiddler之类的,直接用的Packet Capture,果然request body和response body都是加密的,这里有一个特征,request body的开头都是a1708,稍微记一下。把去哪儿拖到jadx看看,发现基本没有Activity,但是找到了com.mqunar.network.NetRequestManager,写个xposed,hook一下request方法,发现请求还真的是走的这里,跟抓包的结果一摸一样。
在lib文件夹下面,找到libq_lib_rnqp.so,这个是去哪儿的静态资源离线方案,把这个so解压,里面的assets文件夹,全是qp和qpmd5文件,随机打开一个QP文件,可以发现前面是一个类似json字符串的东西,里面:
{
"files": [{
"url": "https://rn.qunar.com/packages/f_flight_search_rn_android/index.bundle",
"md5": "lO+fevWF8DT4S4lV1FE/vj4Rpwv3z0I9sM9HN93ZLwwEaw5pE01vW/a94Q==",
"sl": "0,477450"
}, {
"url": "https://rn.qunar.com/packages/f_flight_search_rn_android/index.bundle.meta",
"md5": "BZRwzup822tj5sA0O/IterLMCgmJmgIVQeSGI2oSGcKdkSNxFF4pxAWf6g==",
"sl": "477450,21"
}, {
"url": "https://s.qunarzz.com/flight_search_rn/fonts/0.0.16/f_flight_search_rn_font.ttf",
"md5": "PyIDorrKuPUsyRoSnjCkqYWGgUIJ8b8evQ7nOB5rxWtlTAJ7bnkua/n32g==",
"sl": "477471,10320"
}, {
"url": "https://s.qunarzz.com/route_service_rn/bgBody.png",
"md5": "AhojMss6hxmBo39WS3ikUxI7gKykolUSSzT9DBGn5vRxbU+gNRH2bM9FqQ==",
"sl": "487791,736"
}, {
"url": "https://s.qunarzz.com/flight_search_rn/img/FOTAPopLogo.png",
"md5": "AWq74ZbLcpb19+FUteBPPuJ2X2PC4GFFzwZtii+p+SguUpP+Im0nviULsg==",
"sl": "488527,3512"
}, {
"url": "https://s.qunarzz.com/flight_search_rn/img/FOTAYixuan.png",
"md5": "wkRBXC16U7lu1/pgEMQ1inI5H3ZZyGkO/6XME61LRVdCdrfifwiaekSQ/A==",
"sl": "492039,342"
}, {
"url": "https://s.qunarzz.com/flight_search_rn/img/atom_flight_pay_booking.png",
"md5": "Qp/7XxZ424PMSpQd+n4R5tDcyLbtIj2zASr1neJdb7pehBIobsXYTGQdNw==",
"sl": "492381,994"
}, {
"url": "https://s.qunarzz.com/flight_search_rn/img/atom_flight_down_arrow.png",
"md5": "Vpx0GnB7CXrcJ03rpiUfz5T9lNWbPANmYhZTSosK9xsnSxQQ5QIKSy8GTQ==",
"sl": "493375,262913"
}, {
"url": "https://s.qunarzz.com/flight_search_rn/img/cry_camel.png",
"md5": "ne3OZ535RNS34nI9LDf/C1gMTRQtaldSrrHjDQ+pEaOlFZE3qQ2ezJxT9g==",
"sl": "756288,4551"
}, {
"url": "https://s.qunarzz.com/flight_search_rn/img/qunar_icon.jpg",
"md5": "lW0X/N3EP3Y7t4IXvfq2DdTV5s/vPVx9ftAfn2P9IUK/FODBYY6Wo/vR8Q==",
"sl": "760839,2602"
}],
"timestamp": 1536903580677,
"hybridid": "f_flight_search_rn_android",
"version": "64",
"iOS_vid": "vid_80019999",
"android_vid": "vid_60001194,com.mqunar.react_41",
"pid": "10010",
"rnpackage": true,
"platform": "android"
}
这里就是去哪儿的资源文件了,不用猜也知道了,https://rn.qunar.com/packages/f_flight_search_rn_android/index.bundle 贴到浏览器,下载打开,可以发现,这个就是真正的js代码了。但是它跟我们自己导出的js不一样,去哪儿对这个优化了,所有模块共用同一套RN框架的js代码,所以每个模块的就只有真正的逻辑代码了。这个QHotDogNetWork请求的使用说明:
import { QHotDogNetWork } from 'qunar-react-native';
//如果需要使用APP中配置的 hotdog 地址,则 requestParam 中不设置 url 属性
var requestParam = {
serviceType:'', //网络请求type,serviceType和url不能同时为空
url:'', //网络请求url(默认为APP中设置的hotdog 地址),serviceType和url不能同时为空
param:{}, //网络请求参数
useCache:true, //是否可以使用cache, true或者false
cacheKey:'', //cacheKey,如果useCache为true则cacheKey不能为空
timeout: 300 //如果 300ms 内没有返回,则会触发 failCallback 回调
successCallback:(response)=>{}, //网络请求成功的回调
cacheCallback:(response)=>{}, //网络请求从cache返回的回调
failCallback:()=>{}, //网络请求失败的回调
}
//发起网络请求,返回该网络请求的requestID
var requestID = QHotDogNetWork.postRequest(requestParam);
//取消网络请求
QHotDogNetWork.cancelNetWorkTask(requestID);
跟这里是不是一样?
https://imgchr.com/i/iYF84H
后记
去哪儿的代码逻辑找到了,但是加密的问题并没有解决,因为它不会把加密放在js里面呐!libgoblin_3_1_7.so有个字符串是a1708,这个你还记得吗?哥布林这个so里面有xposed和Cydia Substrate的检测,还有ollvm混淆,加密的函数是int __fastcall sub_123D0(unsigned int a1, int a2, int a3, int a4, _DWORD a5, int a6)。
由于上传图片太麻烦了,就没什么图,都在这里了:链接:https://pan.baidu.com/s/1manoCV4e3By7Hf5-_hgb5A
提取码:8olp