知乎APP协议分析--签名分析


0x01、 目标需求:

  .a) 知乎APP的接口参数以及相关的签名获取的方式

  .b) 通过关键字来搜索我们想要的数据

0x02、分析背景:

  .a) 知乎APP

  .b) APP版本5.17.1

  .c) 软件无壳

  .d) 软件通讯过程中采取了签名验证的方式,返回数据无加密

  .e) 搜索接口使用了H2(HTTP2)

0x03、分析流程:

  .a) 通过数据包抓取或敏感函数hook方式获得接口功能

  .b) 通过函数内部参数的组装继续分析参数的来源以及加密的流程

0x04、抓取数据包:

  .a) 截图如下:

    0x001. 抓包分析,首次打开软件会有一个设备注册的接口,上传的设备信息不多,可以构造,主要是x-req-signature 这个签名会校验数据的正确性

设备注册

    0x002. post数据,接下来先来处理他的这个签名

post数据

  .b) 签名分析:

    0x001. 老规矩 先打开jadx-gui搜索一波关键词(x-req-signature)

搜索关键词



    0x002. 找到加密的地方,跟进去

加密签名的地方



    0x003. 加密的地方是在so里面做的在libencryp.so里面,我们可以先hook一下这个函数得到参数值看看都是什么.

跟踪函数



    0x004. 使用fridaHook这个函数看看参数值

fridaHook



    0x006. 使用ida打开so简单的看一下

ida打开静态分析



ida静态分析



    0x007. 由于时间的原因,就不分析他的拼接方式了,我这里有个更快捷的方法,就是把他的so提出来写在自己的APP里面然后调用他的so的函数就可以了,如果不出意外是可以直接用的,如果依赖其他的so就都要加载进来,然后验证包签名也是不行的,都要处理掉,这种可以修改他的逻辑位置的arm汇编代码,就可以了

编写安卓服务端



    0x008. 验证签名是否一致,安卓端采用post方式接收参数

验证参数正确性



    0x009. 参数没问题了,写代码调用服务注册udid,先上截图,在上代码

测试代码

package com.zhihu.crawl.device.register;

import com.alibaba.fastjson.JSONObject;
import com.zhihu.crawl.utils.HttpTools;
import com.zhihu.crawl.utils.HttpsTools;

import java.net.URLEncoder;
import java.util.Map;
import java.util.TreeMap;

/**
 * @project: zhihuDemo
 * @package: com.zhihu.crawl.device.register
 * @Description: 注册设备, 获取设备ID
 * @Since: 1.8.0_111
 * @Auther MyPC, QuJianJun
 * @Email: 8577352@qq.com
 * @Date: 2021-01-09 01:36:21
 */

public class RegisterDevice {

    static String ENCRYPT_API_URL = "http://127.0.0.1:7788/?type=zhihu";
    static String ZHIHU_DEVICE_REGISTER_URL = "https://appcloud.zhihu.com/v1/device";


    public static RegisterDevice get(){
        return new RegisterDevice();
    }
    /**
     * 注册设备,获取DID
     * @return
     */
    public static String getUdid(Map<String,String> map){
        String time = String.valueOf(System.currentTimeMillis() / 1000);
        ENCRYPT_API_URL += String.format("&t=%s",time);
        Map<String,String> headerMap = new TreeMap<>();
        headerMap.put("Content-Type","application/json;charset=UTF-8");
        String res = HttpTools.sendPost(ENCRYPT_API_URL, get().mapToParam(map), headerMap);
        int code = JSONObject.parseObject(res).getInteger("code");
        if (code == 200){
            //获取签名成功,
            String sign = JSONObject.parseObject(res).getString("data");
//            System.out.println("[*] sign: " + sign);
            //请求 知乎服务器获取设备ID  udid
            headerMap.remove("Content-Type");
            headerMap.put("user-agent","Mozilla/5.0 (Linux; Android 10; M2003J15SC Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.101 Mobile Safari/537.36");
            headerMap.put("x-req-signature",sign);
            headerMap.put("content-type","application/x-www-form-urlencoded");
            headerMap.put("accept-encoding","gzip");
            headerMap.put("x-sign-version","2");
            headerMap.put("x-req-ts",time);
            headerMap.put("x-app-id","1355");
            res = HttpsTools.Getcode(ZHIHU_DEVICE_REGISTER_URL,"POST","UTF-8",get().mapToParam(map),headerMap);
            String udid = JSONObject.parseObject(res).getString("udid");
            return udid;
        }else{
            return "-1";
        }
    }

    /**
     * map转url参数
     * @param map
     * @return
     */
    public String mapToParam(Map<String,String> map){
        StringBuilder sb = new StringBuilder();
        for(Map.Entry entry: map.entrySet()){
            String key = entry.getKey().toString();
            //特殊原因 Url编码后 空格会变成加号服务器接收到后就丢失了所以这里编码后替换成%20,
            sb.append(key + "=" + URLEncoder.encode(map.get(key)).replace("+","%20") + "&");
        }
        return sb.substring(0,sb.toString().length()-1);
    }
}


    0x010. udid已经可以成功从服务器获取到,接下来看一下搜索接口,搜索请求中的t参数是搜索类型(视频 t=zvideo ,综合搜索 t=general, 搜索用户 t=people, 搜索话题 t=topic, 电子书 t=publication, 其他类型自行抓包)

搜索接口



    0x011. 把curl命令行复制出来,丢到pyCharm里面的http文件粘贴执行一下,经过测试没被注释掉的参数 都是不可以丢失的

curl测试参数



    0x012.老规矩直接找头部的key然后看看搜索的到不(这次好像失败了,搜索大法失效了.),所有的字段我都搜索的但是没找到。

搜索关键词



    0x013.冥想5分钟(我的第一想法是在so内做的,我hook了网络请求函数打印堆栈信息,但是一点结果没有),那是不是有可能在网页里做的操作呢?抱着试试看的想法,重新找一下请求的数据包,还真发现了



    0x014. 看到这个关键字应该是搜索的索引页,把html代码复制出来,在桌面新建一个html页面然后浏览器打开,看控制台,他都做了些啥...

准备开始调试js



    0x015. 还是老样子,准备发起请求前给断点,然后一步一步调试...(我们现在已有的参数 udid, app-version, api-version, x-hd),所以我们只需要把x-zse-86的值给调出来就可以了.

所需参数



准备断点调试



    0x016. 打好断点后,刷新网页.一直往下跟,直到出现我们需要的参数

断下来之后,一路跟下去



    0x017. 为了快一点,我这里直接找这个字符串,在他加载的js里面挨个搜索,我在inxta.a*********文件里找到了这个参数来源,然后断点直接断在了参数生成之前,刷新网页重新加载到断点处(要记得把右边浏览器之前勾选的地方给去掉)

我这里直接搜索的关键字



    0x018. 找到加密前的拼接参数



    0x019. 拼接的参数(4_2.0+/udid+5.17.1+8e9b8c23bb7fe86fed551c8322fda1ae+ALDQSZbyeBJLBZch-SAhAMuR-UHW60jSkC4=) 我们之前做了一个截图,用python发送http请求的时候数据的格式应该是这样的(x-zse-83+要请求地址的url部分不包含域名+x-hd+udid)这个格式我们先记下来,继续跟

拼接参数



    0x020. 它这里还没走完,还得继续往下跟,因为已知x-zse-86的值格式不是这样子的,再继续跟

验证一次加密



    0x021. 得到最终返回值,我们直接把它的js代码算法复制出来.写在一个html里面做验证.控制台直接调用b函数传参

最终返回值

        var A = "2.0"
          , __g = {};
        function s() {}
        function i(t) {
            this.t = (2048 & t) >> 11,
            this.s = (1536 & t) >> 9,
            this.i = 511 & t,
            this.h = 511 & t
        }
        function h(t) {
            this.s = (3072 & t) >> 10,
            this.h = 1023 & t
        }
        function a(t) {
            this.a = (3072 & t) >> 10,
            this.c = (768 & t) >> 8,
            this.n = (192 & t) >> 6,
            this.t = 63 & t
        }
        function c(t) {
            this.s = t >> 10 & 3,
            this.i = 1023 & t
        }
        function n() {}
        function e(t) {
            this.a = (3072 & t) >> 10,
            this.c = (768 & t) >> 8,
            this.n = (192 & t) >> 6,
            this.t = 63 & t
        }
        function o(t) {
            this.h = (4095 & t) >> 2,
            this.t = 3 & t
        }
        function r(t) {
            this.s = t >> 10 & 3,
            this.i = t >> 2 & 255,
            this.t = 3 & t
        }
        s.prototype.e = function(t) {
            t.o = !1
        }
        ,
        i.prototype.e = function(t) {
            switch (this.t) {
            case 0:
                t.r[this.s] = this.i;
                break;
            case 1:
                t.r[this.s] = t.k[this.h]
            }
        }
        ,
        h.prototype.e = function(t) {
            t.k[this.h] = t.r[this.s]
        }
        ,
        a.prototype.e = function(e) {
            switch (this.t) {
            case 0:
                e.r[this.a] = e.r[this.c] + e.r[this.n];
                break;
            case 1:
                e.r[this.a] = e.r[this.c] - e.r[this.n];
                break;
            case 2:
                e.r[this.a] = e.r[this.c] * e.r[this.n];
                break;
            case 3:
                e.r[this.a] = e.r[this.c] / e.r[this.n];
                break;
            case 4:
                e.r[this.a] = e.r[this.c] % e.r[this.n];
                break;
            case 5:
                e.r[this.a] = e.r[this.c] == e.r[this.n];
                break;
            case 6:
                e.r[this.a] = e.r[this.c] >= e.r[this.n];
                break;
            case 7:
                e.r[this.a] = e.r[this.c] || e.r[this.n];
                break;
            case 8:
                e.r[this.a] = e.r[this.c] && e.r[this.n];
                break;
            case 9:
                e.r[this.a] = e.r[this.c] !== e.r[this.n];
                break;
            case 10:
                e.r[this.a] = t(e.r[this.c]);
                break;
            case 11:
                e.r[this.a] = e.r[this.c]in e.r[this.n];
                break;
            case 12:
                e.r[this.a] = e.r[this.c] > e.r[this.n];
                break;
            case 13:
                e.r[this.a] = -e.r[this.c];
                break;
            case 14:
                e.r[this.a] = e.r[this.c] < e.r[this.n];
                break;
            case 15:
                e.r[this.a] = e.r[this.c] & e.r[this.n];
                break;
            case 16:
                e.r[this.a] = e.r[this.c] ^ e.r[this.n];
                break;
            case 17:
                e.r[this.a] = e.r[this.c] << e.r[this.n];
                break;
            case 18:
                e.r[this.a] = e.r[this.c] >>> e.r[this.n];
                break;
            case 19:
                e.r[this.a] = e.r[this.c] | e.r[this.n];
                break;
            case 20:
                e.r[this.a] = !e.r[this.c]
            }
        }
        ,
        c.prototype.e = function(t) {
            t.Q.push(t.C),
            t.B.push(t.k),
            t.C = t.r[this.s],
            t.k = [];
            for (var e = 0; e < this.i; e++)
                t.k.unshift(t.f.pop());
            t.g.push(t.f),
            t.f = []
        }
        ,
        n.prototype.e = function(t) {
            t.C = t.Q.pop(),
            t.k = t.B.pop(),
            t.f = t.g.pop()
        }
        ,
        e.prototype.e = function(t) {
            switch (this.t) {
            case 0:
                t.u = t.r[this.a] >= t.r[this.c];
                break;
            case 1:
                t.u = t.r[this.a] <= t.r[this.c];
                break;
            case 2:
                t.u = t.r[this.a] > t.r[this.c];
                break;
            case 3:
                t.u = t.r[this.a] < t.r[this.c];
                break;
            case 4:
                t.u = t.r[this.a] == t.r[this.c];
                break;
            case 5:
                t.u = t.r[this.a] != t.r[this.c];
                break;
            case 6:
                t.u = t.r[this.a];
                break;
            case 7:
                t.u = !t.r[this.a]
            }
        }
        ,
        o.prototype.e = function(t) {
            switch (this.t) {
            case 0:
                t.C = this.h;
                break;
            case 1:
                t.u && (t.C = this.h);
                break;
            case 2:
                t.u || (t.C = this.h);
                break;
            case 3:
                t.C = this.h,
                t.w = null
            }
            t.u = !1
        }
        ,
        r.prototype.e = function(t) {
            switch (this.t) {
            case 0:
                for (var e = [], n = 0; n < this.i; n++)
                    e.unshift(t.f.pop());
                t.r[3] = t.r[this.s](e[0], e[1]);
                break;
            case 1:
                for (var r = t.f.pop(), o = [], i = 0; i < this.i; i++)
                    o.unshift(t.f.pop());
                t.r[3] = t.r[this.s][r](o[0], o[1]);
                break;
            case 2:
                for (var a = [], c = 0; c < this.i; c++)
                    a.unshift(t.f.pop());
                t.r[3] = new t.r[this.s](a[0],a[1])
            }
        }
        ;
        var k = function(t) {
            for (var e = 66, n = [], r = 0; r < t.length; r++) {
                var o = 24 ^ t.charCodeAt(r) ^ e;
                n.push(String.fromCharCode(o)),
                e = o
            }
            return n.join("")
        };
        function Q(t) {
            this.t = (4095 & t) >> 10,
            this.s = (1023 & t) >> 8,
            this.i = 1023 & t,
            this.h = 63 & t
        }
        function C(t) {
            this.t = (4095 & t) >> 10,
            this.a = (1023 & t) >> 8,
            this.c = (255 & t) >> 6
        }
        function B(t) {
            this.s = (3072 & t) >> 10,
            this.h = 1023 & t
        }
        function f(t) {
            this.h = 4095 & t
        }
        function g(t) {
            this.s = (3072 & t) >> 10
        }
        function u(t) {
            this.h = 4095 & t
        }
        function w(t) {
            this.t = (3840 & t) >> 8,
            this.s = (192 & t) >> 6,
            this.i = 63 & t
        }
        function G() {
            this.r = [0, 0, 0, 0],
            this.C = 0,
            this.Q = [],
            this.k = [],
            this.B = [],
            this.f = [],
            this.g = [],
            this.u = !1,
            this.G = [],
            this.b = [],
            this.o = !1,
            this.w = null,
            this.U = null,
            this.F = [],
            this.R = 0,
            this.J = {
                0: s,
                1: i,
                2: h,
                3: a,
                4: c,
                5: n,
                6: e,
                7: o,
                8: r,
                9: Q,
                10: C,
                11: B,
                12: f,
                13: g,
                14: u,
                15: w
            }
        }
        Q.prototype.e = function(t) {
            switch (this.t) {
            case 0:
                t.f.push(t.r[this.s]);
                break;
            case 1:
                t.f.push(this.i);
                break;
            case 2:
                t.f.push(t.k[this.h]);
                break;
            case 3:
                t.f.push(k(t.b[this.h]))
            }
        }
        ,
        C.prototype.e = function(A) {
            switch (this.t) {
            case 0:
                var t = A.f.pop();
                A.r[this.a] = A.r[this.c][t];
                break;
            case 1:
                var s = A.f.pop()
                  , i = A.f.pop();
                A.r[this.c][s] = i;
                break;
            case 2:
                var h = A.f.pop();
                A.r[this.a] = eval(h)
            }
        }
        ,
        B.prototype.e = function(t) {
            t.r[this.s] = k(t.b[this.h])
        }
        ,
        f.prototype.e = function(t) {
            t.w = this.h
        }
        ,
        g.prototype.e = function(t) {
            throw t.r[this.s]
        }
        ,
        u.prototype.e = function(t) {
            var e = this
              , n = [0];
            t.k.forEach(function(t) {
                n.push(t)
            });
            var r = function(r) {
                var o = new G;
                return o.k = n,
                o.k[0] = r,
                o.v(t.G, e.h, t.b, t.F),
                o.r[3]
            };
            r.toString = function() {
                return "() { [native code] }"
            }
            ,
            t.r[3] = r
        }
        ,
        w.prototype.e = function(t) {
            switch (this.t) {
            case 0:
                for (var e = {}, n = 0; n < this.i; n++) {
                    var r = t.f.pop();
                    e[t.f.pop()] = r
                }
                t.r[this.s] = e;
                break;
            case 1:
                for (var o = [], i = 0; i < this.i; i++)
                    o.unshift(t.f.pop());
                t.r[this.s] = o
            }
        }
        ,
        G.prototype.D = function(t) {
            for (var e = atob(t), n = e.charCodeAt(0) << 8 | e.charCodeAt(1), r = [], o = 2; o < n + 2; o += 2)
                r.push(e.charCodeAt(o) << 8 | e.charCodeAt(o + 1));
            this.G = r;
            for (var i = [], a = n + 2; a < e.length; ) {
                var c = e.charCodeAt(a) << 8 | e.charCodeAt(a + 1)
                  , u = e.slice(a + 2, a + 2 + c);
                i.push(u),
                a += c + 2
            }
            this.b = i
        }
        ,
        G.prototype.v = function(t, e, n) {
            for (e = e || 0,
            n = n || [],
            this.C = e,
            "string" == typeof t ? this.D(t) : (this.G = t,
            this.b = n),
            this.o = !0,
            this.R = Date.now(); this.o; ) {
                var r = this.G[this.C++];
                if ("number" != typeof r)
                    break;
                var o = Date.now();
                if (500 < o - this.R)
                    return;
                this.R = o;
                try {
                    this.e(r)
                } catch (t) {
                    this.U = t,
                    this.w && (this.C = this.w)
                }
            }
        }
        ,
        G.prototype.e = function(t) {
            var e = (61440 & t) >> 12;
            new this.J[e](t).e(this)
        }
        ,
        "undefined" != typeof window && (new G).v("AxjgB5MAnACoAJwBpAAAABAAIAKcAqgAMAq0AzRJZAZwUpwCqACQACACGAKcBKAAIAOcBagAIAQYAjAUGgKcBqFAuAc5hTSHZAZwqrAIGgA0QJEAJAAYAzAUGgOcCaFANRQ0R2QGcOKwChoANECRACQAsAuQABgDnAmgAJwMgAGcDYwFEAAzBmAGcSqwDhoANECRACQAGAKcD6AAGgKcEKFANEcYApwRoAAxB2AGcXKwEhoANECRACQAGAKcE6AAGgKcFKFANEdkBnGqsBUaADRAkQAkABgCnBagAGAGcdKwFxoANECRACQAGAKcGKAAYAZx+rAZGgA0QJEAJAAYA5waoABgBnIisBsaADRAkQAkABgCnBygABoCnB2hQDRHZAZyWrAeGgA0QJEAJAAYBJwfoAAwFGAGcoawIBoANECRACQAGAOQALAJkAAYBJwfgAlsBnK+sCEaADRAkQAkABgDkACwGpAAGAScH4AJbAZy9rAiGgA0QJEAJACwI5AAGAScH6AAkACcJKgAnCWgAJwmoACcJ4AFnA2MBRAAMw5gBnNasCgaADRAkQAkABgBEio0R5EAJAGwKSAFGACcKqAAEgM0RCQGGAYSATRFZAZzshgAtCs0QCQAGAYSAjRFZAZz1hgAtCw0QCQAEAAgB7AtIAgYAJwqoAASATRBJAkYCRIANEZkBnYqEAgaBxQBOYAoBxQEOYQ0giQKGAmQABgAnC6ABRgBGgo0UhD/MQ8zECALEAgaBxQBOYAoBxQEOYQ0gpEAJAoYARoKNFIQ/zEPkAAgChgLGgkUATmBkgAaAJwuhAUaCjdQFAg5kTSTJAsQCBoHFAE5gCgHFAQ5hDSCkQAkChgBGgo0UhD/MQ+QACAKGAsaCRQCOYGSABoAnC6EBRoKN1AUEDmRNJMkCxgFGgsUPzmPkgAaCJwvhAU0wCQFGAUaCxQGOZISPzZPkQAaCJwvhAU0wCQFGAUaCxQMOZISPzZPkQAaCJwvhAU0wCQFGAUaCxQSOZISPzZPkQAaCJwvhAU0wCQFGAkSAzRBJAlz/B4FUAAAAwUYIAAIBSITFQkTERwABi0GHxITAAAJLwMSGRsXHxMZAAk0Fw8HFh4NAwUABhU1EBceDwAENBcUEAAGNBkTGRcBAAFKAAkvHg4PKz4aEwIAAUsACDIVHB0QEQ4YAAsuAzs7AAoPKToKDgAHMx8SGQUvMQABSAALORoVGCQgERcCAxoACAU3ABEXAgMaAAsFGDcAERcCAxoUCgABSQAGOA8LGBsPAAYYLwsYGw8AAU4ABD8QHAUAAU8ABSkbCQ4BAAFMAAktCh8eDgMHCw8AAU0ADT4TGjQsGQMaFA0FHhkAFz4TGjQsGQMaFA0FHhk1NBkCHgUbGBEPAAFCABg9GgkjIAEmOgUHDQ8eFSU5DggJAwEcAwUAAUMAAUAAAUEADQEtFw0FBwtdWxQTGSAACBwrAxUPBR4ZAAkqGgUDAwMVEQ0ACC4DJD8eAx8RAAQ5GhUYAAFGAAAABjYRExELBAACWhgAAVoAQAg/PTw0NxcQPCQ5C3JZEBs9fkcnDRcUAXZia0Q4EhQgXHojMBY3MWVCNT0uDhMXcGQ7AUFPHigkQUwQFkhaAkEACjkTEQspNBMZPC0ABjkTEQsrLQ==");
        var b = function(t) {
            return __g._encrypt(encodeURIComponent(t))
        };


    0x022. 验证结果如下图(是验证成功的,所以只要把JavaScript代码用java或者python直接调用就行了),值得注意的是js内有window对象的调用,所以在用java或者python调用的时候要支持window等浏览器内置对象,java默认的js执行是不行的,无法调用起来,所以我这里采用的是 htmlunit-driver 这个三方包,maven的pom配置文件新增以下jar包配置即可.

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>htmlunit-driver</artifactId>
            <version>2.43.1</version>
        </dependency>

验证结果



    0x023. 下面是java代码请求我自己写的html,然后html引入这个js,通过三方的jar包进行访问并且传参,获取html页面的body内容即可

        HtmlUnitDriver webDriver = new HtmlUnitDriver();
        webDriver.setJavascriptEnabled(true);
        String url = this.getClass().getResource("/encrypt.html").toString()+"?str="+strMd5;
        webDriver.get(url);
        WebElement webElement = webDriver.findElement(By.xpath("/html/body"));
        String sig = webElement.getText();
        System.out.println("x_zse_86 签名为:" + "2.0_"+sig);
        webDriver.close();
        return "2.0_"+sig;


    0x024. 在js的尾部增加如下代码,window.onload() 方法用于在网页加载完毕后立刻执行的操作,即当 HTML 文档加载完毕后,立刻执行某个方法,通过js获取参数,然后传入b函数,得到返回值 赋值给body,给了之后,java代码通过xpath取body的值,这样就可以拿到返回值了

function getQueryVariable(variable)
{
    var query = window.location.search.substring(1);
    var vars = query.split("&");
    for (var i=0;i<vars.length;i++) {
        var pair = vars[i].split("=");
        if(pair[0] == variable){return pair[1];}
    }
    return(false);
}

window.onload = function (){
const param = getQueryVariable("str");
const str = b(param)
const body = document.getElementById("body");
body.innerText = str;
}


    0x025. 生成udid和x-zse-86参数使用python发送curl请求测试是否可以成功.(获得返回数据后,可以请求video_url地址,正则匹配页面内的mp4下载地址,即可下载视频.)

验证最终请求



    0x026. 测试代码并解析返回数据.

接口协议



返回json格式化数据



遍历结果

    0x027. 如果需要写好的js文件,请付费下载,付费后可以提供完整项目代码

价格: 300.00 元
VIP会员免费终身会员免费
温馨提示:登录付款后可永久阅读隐藏内容。 付费可读



友情提示:本文只为技术分享交流,请勿非法用途.产生一切法律问题与本人无关



在浏览的同时希望给予作者打赏,来支持作者的服务器维护费用.一分也是爱~