shopee app算法分析第一篇
分析完整算法大概要好几篇,耐心等待下

抓的搜索接口,get请求,核心的有3个4字节键和x-sap-ri,如果是post,就是4个4字节键和值,多出来的一个和post的表单有关

后续内容都是post的,post会了,get肯定也会了,4个4字节3个短的,一个长的.这5个参数来着同一个so,libshpssdk.so,后面说怎么定位到的.
4字节的key和value也是一直变化的,最好摸清楚生成机制,随机可能引起风控.(有9套算法)
1 2 3 4 5 6 7 8 9 10 11 |
Java.perform( function (){
// HashMap.put var hashMap = Java.use( "java.util.HashMap" ); hashMap.put.implementation = function (a, b) {
if (a!= null && a.equals( "x-sap-ri" )){
console.log(Java.use( "android.util.Log" ).getStackTraceString(Java.use( "java.lang.Throwable" ).$ new ())) console.log( "hashMap.put: " , a, b); } return this .put(a, b); } }) |
太长了,放前面的结果
1 2 3 4 5 6 7 8 9 10 11 |
java.lang.Throwable at java.util.HashMap.put(Native Method) at org.json.JSONObject.put(JSONObject.java: 276 ) at org.json.JSONTokener.readObject(JSONTokener.java: 394 ) at org.json.JSONTokener.nextValue(JSONTokener.java: 104 ) at org.json.JSONObject.<init>(JSONObject.java: 168 ) at org.json.JSONObject.<init>(JSONObject.java: 185 ) at com.shopee.shpssdk.SHPSSDK.uvwvvwvvw(Unknown Source: 81 ) at com.shopee.shpssdk.SHPSSDK.requestDefense(Unknown Source: 59 ) hashMap.put: x - sap - ri 443e41678c54f44f62b70d1901d115bc703ebab92a5b4251176d |
在com.shopee.shpssdk.SHPSSDK.uvwvvwvvw方法里,这个app有很多站点的,初步看了只是包名不一样,函数,so偏移都是一样的,包名是com.shopee.xx xx是域名后面的,我使用的样本是ph,3.37.31
1 2 3 4 5 6 7 8 |
马来西亚:https: / / shopee.my 新加坡:https: / / shopee.sg 泰国:https: / / shopee.th 印度尼西亚:https: / / shopee. id 越南:https: / / shopee.vn 菲律宾:https: / / shopee.ph 台湾:https: / / shopee.tw 大陆:https: / / shopee.cn |

这个位置值已经生成了,往上找调用,就一个,结果来自wvvvuwwu.vuwuuwvw(str.getBytes(), bArr),hook发现,第一个参数url,第二个表单(get就是null)

点过去来到native注册的地方

hook下libart
1 2 3 4 5 |
[RegisterNatives] method_count: 0x4 name: com.shopee.shpssdk.wvvvuwwu vuwuuwvv sig: (I)V module_name: libshpssdk.so offset: 0x1bef84 name: com.shopee.shpssdk.wvvvuwwu vuwuuwvu sig: (Lcom / shopee / shpssdk / wvvvuvvv;)I module_name: libshpssdk.so offset: 0x1bf478 name: com.shopee.shpssdk.wvvvuwwu vuwuuwvw sig: ([B[B)Ljava / lang / String; module_name: libshpssdk.so offset: 0x995dc name: com.shopee.shpssdk.wvvvuwwu vuwuuwvvw sig: ([B)V module_name: libshpssdk.so offset: 0x9932c |
so名字libshpssdk,偏移0x995dc,到lib目录下找发现一个so都没有,hook下dlopen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var dlopen = Module.findExportByName( null , "dlopen" ); //6.0 var android_dlopen_ext = Module.findExportByName( null , "android_dlopen_ext" ); //高版本8.1以上 Interceptor.attach(dlopen, {
onEnter: function (args) {
var path_ptr = args[0]; var path = ptr(path_ptr).readCString(); console.log( "[dlopen:]" , path); }, onLeave: function (retval) {
// Thread.sleep(3); } }); Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var path_ptr = args[0]; var path = ptr(path_ptr).readCString(); console.log( "[dlopen_ext:]" , path); }, onLeave: function (retval) {
// Thread.sleep(3); } }); |
1 |
/ data / app / ~~KVEKJlJur2r6197VML5LRw = = / com.shopee.sg - ZyVbLWincySSEAiqQSuPLQ = = / split_config.arm64_v8a.apk! / lib / arm64 - v8a / libshpssdk.so |
可以看到so是压缩了下,拖出来即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Java.perform( function (){
console.log( '======================' ) let wvvvuwwu = Java.use( "com.shopee.shpssdk.wvvvuwwu" ); wvvvuwwu[ "vuwuuwvw" ].implementation = function (bArr, bArr2) {
var ByteString = Java.use( "com.android.okhttp.okio.ByteString" ); if (bArr!== null ){
try {
console.log( '111111111' ) console.log(`wvvvuwwu.vuwuuwvw is called: bArr=${ByteString.of(bArr).utf8()}, bArr2=${bArr2}`); } catch (e) {
} } // console.log(`wvvvuwwu.vuwuuwvw is called: bArr=${bArr}, bArr2=${bArr2}`); let result = this [ "vuwuuwvw" ](bArr, bArr2); console.log(`wvvvuwwu.vuwuuwvw result=${result}`); return result; }; }) |
注意脚本写法(排除空,异常字段),很多时候排除掉手机,app,frida版本这些的问题, 为什么明明确定是这个位置hook脚本就是没输出,考虑下是不是脚本有问题,有时候有问题是不会输出东西的,也不报错,然后你以为没走这,就是因为你对参数进行了错误操作,比如类型没判断正确,空没有排除掉导致脚本什么都不打印.好几个人问我,这里统一回复下.
都是基本写法,不熟悉多google,自己琢磨才能记得住.
1 2 3 4 5 6 7 8 9 10 11 12 |
function call(){
Java.perform( function (){
let wvvvuwwu = Java.use( "com.shopee.shpssdk.wvvvuwwu" ); var str = 'https://mall.shopee.ph/api/v4/pages/bottom_tab_bar' var StringClass = Java.use( 'java.lang.String' ); var byteArray = StringClass.$ new (str).getBytes(); var str2 = '{"img_size":"3.0x","latitude":"","location":"[]","longitude":"","new_arrival_reddot_last_dismissed_ts":0,"feed_reddots":[{"timestamp":0,"noti_code":28}],"client_feature_meta":{"is_live_and_video_merged_tab_supported":false},"video_reddot_last_dismissed_ts":0,"view_count":[{"count":1,"source":0,"tab_name":"Live"}]}' var byteArray2 = StringClass.$ new (str2).getBytes(); var res = wvvvuwwu[ "vuwuuwvw" ](byteArray,byteArray2) console.log( 'res==>' ,res) }) } |
连续调用会有一个4字节key不变,1A9EC9B9,这个只和url有关,后续写还原,这个键对应的值解密出来是16字节随机+9套算法中的哪3套,第二篇写算法还原
1 2 3 4 5 6 7 8 9 |
url = '/api/v4/pages/bottom_tab_bar' def getKey1(url): tmp = 0 for i in bytearray(url.encode()): tmp = tmp * 0x334b & 0xffffffff tmp = (tmp + i) & 0xffffffff key = tmp& 0x7fffffff return f "{key:04X}" print ( 'getKey1' ,getKey1(url)) # 1A9EC9B9 还原这个只需要url |
native call的作用是方便确定哪些是so的初始化函数(不管有没有),可以节省后续很多时间.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
function stringToHexArray(str) {
var hexArray = []; for ( var i = 0; i < str.len
|