hacking MicroMsg mini program

  5 mins to read  

List [CTL]

    前段时间迷上了一个小游戏神手,对锻炼左右脑很有好处o_o。好久没玩,下午突然想到便尝试了对其的破解(之前也写过用ADB来hack跳一跳的,不过那个算纯黑盒)

    微信小程序在传统意义上属于前端应用,所以本质就是不安全的。仅仅想依靠前端的混淆来隐藏源码和接口,依靠签名来保证接口不被盗用用,依靠SSL certificate来保证不被mitm是不靠谱的,仅仅能够增加攻击成本而已


    简单介绍一下,微信小程序使用前端三件套进行开发(虽然换了个名字)

    首先使用Fiddler+Android模拟器代理抓包,本以为需要Xposed绕过SSL pinning,但发现小程序的开发者SSL证书没有被app本身pinning,所以这一步省掉也只需要导入Fiddler cert为根证书了

    登录过程的数据包是这样的(没图床,脑补一张fiddler数据包图),可以看到通信的域名为max.wanzhushipin.cn

    接着试着玩一局,因为我不清楚每个参数的含义,所以可以多玩几局来推断。设置几个模拟按键来尝试

    抓了两个report数据包

    GET https://mas.wanzhushipin.cn/uo/1.1.5/uo/report?continue=0&help=0&id=0&level=0&mark=43.75875000000009&shares=0&tick=1570094323&uid=xxxxx&key=4c15bad54ad61f12d6b19318502714ab HTTP/1.1
    
    
    GET https://mas.wanzhushipin.cn/uo/1.1.5/uo/report?continue=0&help=0&id=0&level=0&mark=147.72954000000078&shares=0&tick=1570094346&uid=xxxxx&key=5e2cea478b859137d507b3c1b691f926 HTTP/1.1
    

    响应是

    HTTP/1.1 200 OK
    Date: Thu, 03 Oct 2019 09:19:08 GMT
    Content-Type: text/html
    Connection: keep-alive
    Server: nginx
    Vary: Accept-Encoding
    Access-Control-Allow-Origin: *
    Vary: Accept-Encoding
    Content-Length: 72
    
    {"id":3668836013,"challengeid":0,"cards":"8","rank":0,"code":0}
    

    裸猜一下,mark是分数 * 10,tick是timestamp,key是signature,uid我打码了

    那么重放一下试试

    import requests
    
    url = 'https://mas.wanzhushipin.cn/uo/1.1.5/uo/report?continue=0&help=0&id=0&level=0&mark=1177.4020999998797&shares=0&tick=1570089005&uid=xxxxx&key=67427f970e98c42130eb64e1009064b4'
    
    
    h = {
            'charset': 'utf-8',
            'Accept-Encoding': 'gzip',
            'referer': 'https://servicewechat.com/wx3c889b4f402e924e/110/page-frame.html',
            'content-type': 'application/x-www-form-urlencoded',
            'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; google Pixel 2 Build/LMY47I; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.136 Mobile Safari/537.36 MicroMessenger/7.0.6.1460(0x27000634) Process/appbrand2 NetType/WIFI Language/zh_CN',
            'Host': 'mas.wanzhushipin.cn',
            'Connection': 'Keep-Alive',
    }
    
    r = requests.get(url, headers=h)
    print(r.text)
    

    没问题的,说明后端没有对时间戳进行校验。接着尝试修改一下分数或其他参数,发现后端返回非法调用

    {"error":"\u975e\u6cd5\u8c03\u7528","code":-1}
    

    说明后端将整个query参数进行签名,因为具体参数拼接方式和是否加了salt未知,所以仅仅依靠fuzz很难通过签名校验了


    于是尝试下载小程序的源码,使用模拟器的文件管理器就可以了(root都免了)

    路径是/data/data/com.tencent.mm/MicroMsg/[32bytes hash]/appbrand/pkg

    小程序的打包后缀名是.wxapkg

    down下来使用https://github.com/qwerty472123/wxappUnpacker解包

    npm i装完依赖后,node wuWxapkg.js xx.wxapkg


    拿到源码发现是用Webpack打包过的,应该不会有人会去硬读3W行代码。所以更好的办法装一个微信开发者工具来debug,但是抵制小程序从开发者做起,就不往磁盘里塞垃圾了

    grep -rn "1.1.5/uo/report"瞅一眼

    PS C:\Users\40691\Desktop\wx\ss> grep -rn "1.1.5/uo/report"
    code.js:17937:        var s = this.mURL + "1.1.5/uo/report?" + util.getUrlParams(i, "1.0.3");
    

    定位到getUrlParams…….算了,这并不是一篇教程,所以具体怎么回溯就不细写了,太麻烦了,过程其实挺简单的

    拼出签名脚本,整个逻辑也就是根据版本号选择salt,再将salt和query params拼接生成签名,将签名当作key参数

    var md5 = require('./md5.js')
    
    let t = {}
    
    t.mKeys = {
            "1.0.1": "fatality",
            "1.0.3": "vicky2009",
            "1.0.5": "kivi2017",
            "1.0.7": "kivi2018",
            "1.0.9": "fczlm3",
            "1.1.0": "vicky2017"
    };
    
    var p = [];
    p.mark = 11077.4020999998797
    p.uid = xxxxxx
    p.id = 0
    p.level = 0
    p.shares = 0
    p.continue = 0
    p.help = 0
    
    getUrlParams = (e, i) => {
            void 0 === i && (i = "1.0.1");
            var s = !1, n = new Array();
            for (var a in e) if ("string" == typeof e[a] || "number" == typeof e[a]) {
                "tick" == a && (s = !0);
                var r = a.toLocaleLowerCase();
                e[r] = e[a], n.push(r);
            }
            if (0 == s) {
                var o = new Date();
                e.tick = Math.floor(o.getTime() / 1e3), n.push("tick");
            }
            n.sort(function(t, e) {
                return t > e ? 1 : -1;
            });
            for (var h = "", l = 0; l < n.length; l++) h = h + n[l] + "=" + e[n[l]] + "&";
            var u = "";
            return u = md5(null != t.mKeys[i] ? h + "key=" + t.mKeys[i] : h + "key=fatality"), 
            h + "key=" + u;
        }
    
    console.log(getUrlParams(p, "1.0.3"))
    

    md5.js是util库,从src里copy出来的。然后把mark改为要刷的分 * 10,把生成的query params粘贴到上面的重放脚本里就行了

    为了方便,合并一下

    import requests
    import time
    from hashlib import md5
    
    mark = 20120.872159698898
    tick = int(time.time())
    
    args = ('continue=0&help=0&id=0&level=0&'
            'mark={}&shares=0&'
            'tick={}&uid=xxxxx&key=').format(mark, tick)
    args = args + md5(args.encode()+b'vicky2009').hexdigest()
    
    url = 'https://mas.wanzhushipin.cn/uo/1.1.5/uo/report?' + args
    
    h = {
            'charset': 'utf-8',
            'Accept-Encoding': 'gzip',
            'referer': 'https://servicewechat.com/wx3c889b4f402e924e/110/page-frame.html',
            'content-type': 'application/x-www-form-urlencoded',
            'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; google Pixel 2 Build/LMY47I; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.136 Mobile Safari/537.36 MicroMessenger/7.0.6.1460(0x27000634) Process/appbrand2 NetType/WIFI Language/zh_CN',
            'Host': 'mas.wanzhushipin.cn',
            'Connection': 'Keep-Alive',
    }
    
    r = requests.get(url, headers=h)
    print(r.text)
    

    然后就可以任意刷分了。当然,刷分并不是目的,hacking的过程才是最有趣的