Bootstrap

APP逆向 day19v品会逆向 part1

一.前言

今天我们任务是破解v品会的搜索接口,这里给出地址

https://mapi.appvipshop.com/vips-mobile/rest/shopping/search/product/list/v1

我们打开charles抓包分析一下(这次抓包分析比较复杂,但是我讲不了那么细,只能把关键点和大家讲一下,最后再实操一下)

二.抓包分析

这个接口就是我们要的搜索数据,这个接口需要破的参数有

        authorization
        api_key
        mars_cid
        session_id
        skey
        did:接口generate_token返回的  (这个也是需要大家逆向才发现,因为怎么都找不到)

        这个did就是 https://mapi.appvipshop.com/vips-mobile/rest/device/generate_token​​​​​​

        返回的

看到这里就是了,需要破的参数有 

        api_key     见过,固定的
        edata  很多东西,需要破,需要 vcsptoken
        skey    见过,固定的

而vcsptoken是 https://vcsp-api.vip.com/token/getTokenByFP

返回的,这个先做了的话肯定能够发现,这里为了教程更加顺利,所以和大家用上帝视角讲

这里可以看到返回了 vcsptoken,当然在这个请求之前还有一个请求不知道大家关注了没有

https://mp.appvipshop.com/apns/device_reg

这个就是设备注册接口 app第一次打开(数据清空再打开)

因为很多的app都采取这个逻辑(将关键key注册,接下来的请求才有用),但是这个app并没有,为了学习我也和大家说一下

需要破的参数

        device_token
        skey
        authorization  api_sign

三.总结流程

1 注册设备接口: https://mp.appvipshop.com/apns/device_reg
    -app第一次打开(数据清空再打开)
    -向后端发送请求,注册app
    -需要破解的:
        device_token
        skey
        api_sign 又叫 authorization (api_sign在这个里面) 
        
        
2 getTokenByFP
    -地址:
        https://vcsp-api.vip.com/token/getTokenByFP
    -返回:(是生成edata的关键)
        vcsptoken
        
    -需要破的:
           vcspKey
        vcspauthorization
3 generate_token
    -地址:
    https://mapi.appvipshop.com/vips-mobile/rest/device/generate_token
    -需要破:
        api_key    23e7f28019e8407b98b84cd05b5aef2c # 见过,固定的
        edata  # 很多东西,需要破,需要 vcsptoken
        skey    6692c461c3810ab150c9a980d0c275ec # 见过,固定的
    -返回 did
    
    
4 真正搜索接口
    -地址
    https://mapi.appvipshop.com/vips-mobile/rest/shopping/search/product/list/v1
    -需要破解
        authorization
        api_key
        mars_cid
        session_id
        skey
        did:接口generate_token返回的
        

今天我们就破解第一个,破解第一个,后面的参数基本都有了 

四.破解注册接口 device_reg

4.1 破解 device_token

我们先进行反编译,搜索device_token发现很多位置

最终其实都指向同一个位置,那我们先看第一个 点进来发现

 这个代码中这个decide_token就是这个c.Q().m(),我们再点进去

发现返回 this.f73030x,我们往上找找赋值的位置

 发现在这里,就是u0传进来的参数,那我们现在找找哪里调用了,也可以打印一下u0的调用栈信息,这里就带大家找一下

发现查找用例有很多,那我们点开第一个看看,第一个比较像

这个就是了,那我们再点进去

 发现这就是生成的逻辑了,那我们接下来就来分析一下这个代码

这个不就是典型的去内存拿,去xml拿,都没有的话就生成一个uuid赋值进来,我们再看device_token的样子,不就是uuid么,至此device_token已破解

4.2 破解skey

我们搜素skey,发现有很多

那我们换个关键词搜索,"skey,

发现就只搜到一个常量,这个在开发中是经常使用的,对常量进行赋值,然后再对其进行字符串操作,我们查找用例

发现有很多,而我们要找这种put的,那我们就点第一个

再往里面点

再点进去

 再点进去

发现到这里就点不进去了

我们来分析一下上面的写法

clazz = KeyInfo.class; # 找到类
object = KeyInfo.class.newInstance(); # 得到类的对象
method = clazz.getMethod("getInfo", Context.class, String.class) # 找到对象中的方法,传入方法名,参数
method.invoke(object, context, str);# 找到方法后真正调用对象的方法,传入参数,str就是 'skey' 字符串
我们就知道:KeyInfo 类中有个方法叫  getInfo  ,需要两个参数:context, str
这就是java的反射:根据字符串找到方法执行

换成正常写法就是

KeyInfo info=new KeyInfo()
String res=info.getInfo(context,str)   # 这里生成了skey
return res 

那我们就是要找到keyinfo里面的getinfo方法

那我们双击keyinfo进去,找到里面的getinfo方法 

 发现这里真正的加密位置是在so层,我们可以不着急抓,我们可以直接hook一下getNavInfo,看看返回值是啥,但是我们hook的时候发现做了frida的反调试,那我们就可以打印so文件然后删除停止的那个,这个之前的识货案例和大家说过,这里再说一下。

import frida
import sys

rdev = frida.get_remote_device()
pid = rdev.spawn(["com.achievo.vipshop"]) #放入包名
session = rdev.attach(pid)

scr = """
Java.perform(function () {

    var dlopen = Module.findExportByName(null, "dlopen");
    var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");

    Interceptor.attach(dlopen, {
        onEnter: function (args) {
            var path_ptr = args[0];
            var path = ptr(path_ptr).readCString();
            console.log("[dlopen:]", path);
        },
        onLeave: function (retval) {

        }
    });

    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) {

        }
    });


});
"""
script = session.create_script(scr)
def on_message(message, data):
    print(message, data)
script.on("message", on_message)
script.load()
rdev.resume(pid)
sys.stdin.read()

那所以我们就要删除最后一个,直接执行

rm /data/app/~~pntfvIcIy6UFKGdBLQeafg==/com.achievo.vipshop-aBzjOrAAfLy3AAae-9sDvQ==/lib/arm/libmsaoaidsec.so

删除成功,清除内存后执行hook代码,这里也给出

import frida
import sys

rdev = frida.get_remote_device()
pid = rdev.spawn(["com.achievo.vipshop"])
session = rdev.attach(pid)

scr = """
Java.perform(function () {
    var KeyInfo = Java.use("com.vip.vcsp.KeyInfo");

    KeyInfo.getNavInfo.implementation = function (ctx, str) {
        console.log("-----------------");
        console.log("参数==>", str);
        var res = this.getNavInfo(ctx, str);
        console.log("返回的值==>", res);
        return res;
    }
});
"""


script = session.create_script(scr)


def on_message(message, data):
    print(message, data)


script.on("message", on_message)
script.load()
rdev.resume(pid)
sys.stdin.read()

多次hook发现值都一样 ,我们把这些值都记一下,因为后续请求用得上,也就是明天会和大家讲

-----------------
参数==> app_name
返回的值==> shop_android
-----------------
参数==> vcsp_key
返回的值==> 4d9e524ad536c03ff203787cf0dfcd29
-----------------
参数==> api_key
返回的值==> 23e7f28019e8407b98b84cd05b5aef2c
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec

 至此,skey破解完成

4.3 破解api_sign

我们直接搜索api_sign

第一个不就是是了嘛,那我们点进去

发现这里,不就是b.n里面生成的,我们再点进去 

再点进去

我们接着点进去

 content肯定不是空,我们点击下面的

 我们又点进去

接着点进去

到这里我们不就发现反射了,但是我们不知道是找到了哪个类中的fs方法,那答案只有一个,再initInstance中找到类的,那我们点进去

发现还是KeyInfo中的gs方法

发现又是在so层加密的

可以发现so文件名字是libkeyinfo.so

但我们先不着急去找so,可以先看看gsNav的参数和返回值,这里写一个hook

# 清除一下数据
import frida
import sys

rdev = frida.get_remote_device()

session = rdev.attach("唯品会")

scr = """
Java.perform(function () {
    var KeyInfo = Java.use("com.vip.vcsp.KeyInfo");
    var TreeMap = Java.use('java.util.TreeMap');
    
    
    KeyInfo.gsNav.implementation = function (ctx, map,str,z10) {
        console.log("-----------------gsNav-----------------");
        console.log("参数==>", Java.cast(map,TreeMap).toString());
        console.log("参数==>", str);
        console.log("参数==>", z10);
        var res = this.gsNav(ctx, map,str,z10);
        console.log("返回的值==>", res);
        return res;
    }
});
"""
script = session.create_script(scr)


def on_message(message, data):
    print(message, data)


script.on("message", on_message)
script.load()
sys.stdin.read()

 那我们清除数据,hook一下

我们通过抓包到的值搜索一下,发现参数是下面这些

这不就是请求参数嘛,对这些进行加密生成api_sign

那我们现在就去so文件找找,直接把 libkeyinfo.so拖入idea

搜索发现,这是静态注册

 双击进去

再次双击

再进去 

发现到了加密位置

我们导入jni.h,再转成env的环境

发现v59不就是加密值嘛,再点进去 

 看不太懂,但是看到了关键词sha1 而结果也很像sha1的,那我们就返回上一层,看看逻辑

可以发现这个v79就是传进来的字符串,v58就是字符串的长度,那我们可以hook一下 这个方法,看看参数值是啥,但是!

我们每次hook都要清除数据,这个so文件加载的时候进行hook,如果加载完了,hook不到,如果还没加载,那就报错,所以我们得用延迟hook,这个是关键,这里给出代码

function do_hook() {
	setTimeout(function(){
        var addr = Module.findExportByName("libkeyinfo.so", "getByteHash");  //后期改这里
        console.log(addr); //0xb696387d


        Interceptor.attach(addr, {
            onEnter: function (args) {
                this.x1 = args[2];
            },
            onLeave: function (retval) {
                console.log("--------------------")
                console.log(Memory.readCString(this.x1));
                console.log(Memory.readCString(retval));
            }
        })
     },10);

}

function load_so_and_hook() {
    var dlopen = Module.findExportByName(null, "dlopen");
    var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");

    Interceptor.attach(dlopen, {
        onEnter: function (args) {
            var path_ptr = args[0];
            var path = ptr(path_ptr).readCString();
            // console.log("[dlopen:]", path);
            this.path = path;
        }, onLeave: function (retval) {
            if (this.path.indexOf("libkeyinfo.so") !== -1) { //还有这里
                console.log("[dlopen:]", this.path);
                do_hook();

            }
        }
    });

    Interceptor.attach(android_dlopen_ext, {
        onEnter: function (args) {
            var path_ptr = args[0];
            var path = ptr(path_ptr).readCString();

            this.path = path;
        }, onLeave: function (retval) {
            if (this.path.indexOf("libkeyinfo.so") !== -1) { //还有这里
                console.log("\nandroid_dlopen_ext加载:", this.path);
                do_hook();

            }
        }
    });
}

load_so_and_hook();

//frida -U -f com.achievo.vipshop -l 20.延迟hook_so.js

得到的结果我们去抓包找个key搜索

发现前面拼接了 d1ac91bda7397796a3b28a65bc319181822ca051 

得到的结果再拼接 d1ac91bda7397796a3b28a65bc319181822ca051

再进行sha1

第一步:
aee4c425dbb2288b80c71347cc37d04b+请求参数--》使用sha1签名--》得到签名结果
第二步:
aee4c425dbb2288b80c71347cc37d04b+签名结果--》使用sha1签名--》得到签名结果--》才是真正的api_sign

验证一样,这里给出代码

import hashlib

data_string ='app_name=achievo_ad&app_version=7.83.3&channel=oziq7dxw:::&device=Pixel 3&device_token=a0307f53-623c-312a-be12-a1e51a797d2d&manufacturer=Google&os_version=30&regPlat=0&regid=null&rom=Dalvik/2.1.0 (Linux; U; Android 11; Pixel 3 Build/RQ3A.211001.001)&skey=6692c461c3810ab150c9a980d0c275ec&status=1&vipruid=&warehouse=VIP_HZ'


# sha1加密
hash_object = hashlib.sha1()
hash_object.update(b'aee4c425dbb2288b80c71347cc37d04b')
hash_object.update(data_string.encode('utf-8'))
arg7 = hash_object.hexdigest()
print(arg7) #

x = "aee4c425dbb2288b80c71347cc37d04b"+arg7
# sha1加密
hash_object = hashlib.sha1()
hash_object.update(x.encode('utf-8'))
arg7 = hash_object.hexdigest()
print(arg7)


#hook得到的结果:d1ac91bda7397796a3b28a65bc319181822ca051

至此 api_sign破解完成 

五.总结 

今天的重点就是延迟hook,还有个新的知识点是java的映射

补充

有需要源码的看我主页签名名字私信我,有求必应

;