一、目标

一、目标

李老板: 奋飞呀,上次分析的那个App http://91fans.com.cn/post/bankdataone/ 光能Debug还不够呀, 网页中的js也用不了Frida,我还想 Hook它的函数 ,咋搞呀? 再有App可以RPC去执行签名,这个js我如何去利用呀?总不能代码都改成js去做请求吧?

奋飞:老板呀,你一下提这么多要求,不是明摆着要我们加班吗?这次加班费可得加倍。

二、步骤

最简单易行的js Hook - console.log

main
1:main

我们的目的是Hook这个 encryptSm4ECB 函数,然后打印出它的入参和返回值。

在合适的位置下断点(一般是函数入口和出口)。然后在断点上点右键 → 修改断点,然后在弹出的窗口里面输入要打印的变量。

rc1
1:rc1

跑一下,我们想要的入参和结果都打印出来了。

TamperMonkey 注入

TamperMonkey 俗称油猴,你都可以理解他就是浏览器届的Frida,不过在这个样本里面我没有找到如何Hook 这个 encryptSm4ECB, 但使用它来Hook全局函数是可以成功的。有用油猴 Hook成功这个 encryptSm4ECB 的兄弟可以给我留言交流下。

Fiddler 插件注入

Fiddler抓包的同时是可以用插件来注入js代码的,这个看上去比较复杂,我也木有搞

Chrome启用本地替换

要是可以直接在这个 ArticleDetail.js 上去修改,增加打印变量的代码,岂不快哉。

Chrome其实提供了这个功能,算是文件级别的Hook,就是执行到 ArticleDetail.js 这个请求的时候,不向服务器发请求了,而是直接使用你本地替换的js。这样你就想怎么改就怎么改了。

replace1
1:replace1

源代码页 选择 替换,然后 勾选 启用本地替换,这时候浏览器会提示你给权限,然后选择一个本地的目录来存放要替换的js。

replace2
1:replace2

回到 网络 页,选择你想替换的js,点右键 → 保存并覆盖

再回到 源代码 页,找到这个js文件,实际它已经存到我们开始指定的目录下了。

这时候找到指定的函数位置写hook代码就可以了。

模拟执行第一步 先用 Nodejs 跑通

子曾经曰过:逆向是杂学,A-Z语言都要略懂点。js本来是跑在服务器端的,Nodejs一出,谁与争锋。

问下度娘和谷哥,把VSCode + NodeJs 搭配好,Hello World跑通,开干。

ArticleDetail.js 这个样本的代码还是很厚道的,基本木有混淆,一览无遗。

跑通代码的八字真言是 循序渐进,分而治之

一段一段代码,一个一个函数去跑通,你别一上来就把整段代码都复制上去,然后看着一堆报错就放弃治疗。

encryptSm4ECB: function(t) {
        var e = s("string" == typeof t ? t : JSON.stringify(t))
  ...
}

先执行这个e的值, e 调用了s这个函数,参数是t,但是判断了t是不是字符串,我们之前Hook的时候直接打印的就是 console.log(JSON.stringify(t));

所以这里的代码在 Nodejs里面可以写成:

var n = "dro";
var o = [20320, 25105, 20182, 30340, 22320, 30334, 21315, 19975, 20986, 20837, 19978, 19979, 21069, 21518, 25307, 38134, 22269, 26085, 26376, 23545, 38169, 22909, 22351];

function s(t) {
    var e, i, n = new Array;
    e = t.length;
    for (var r = 0; r < e; r++)
        (i = t.charCodeAt(r)) >= 65536 && i <= 1114111 ? (n.push(i >> 18 & 7 | 240),
            n.push(i >> 12 & 63 | 128),
            n.push(i >> 6 & 63 | 128),
            n.push(63 & i | 128)) : i >= 2048 && i <= 65535 ? (n.push(i >> 12 & 15 | 224),
                n.push(i >> 6 & 63 | 128),
                n.push(63 & i | 128)) : i >= 128 && i <= 2047 ? (n.push(i >> 6 & 31 | 192),
                    n.push(63 & i | 128)) : n.push(255 & i);
    return n
}

var t = '{"parentId":"f6be7358-f906-4087-b387-69cc17a9ebf8","parentType":"ARTICLE","pageIndex":1,"time":"2022-02-23T10:05:34.760","pageSize":5}';
var e = s(t);
console.log(e);

这里n、t、e的值都可以通过之前的hook方案打印出来。比对一下,e的值是ok的,说明s函数是可用的。

var encryptSm4ECB = function (t) {
    var e = s(t)
    , i = (new Date).getTime()
    , r = (i + "").split("")
    , o = [r[5], r[10]].join("")
    , c = s("CFKt03X9Ufk" + n + o);

这个c的值就有点复杂了,不过我们Hook的时候可以把n和o的值打印出来,那实际上调试的时候可以把c先写死,等价于

var cStr = 'CFKt03X9Ufkdro88';
var c = s(cStr);

在继续往下搞

var CMBSM4EncryptWithECB = function (t, e) {
        // if (!e || !t)
        //    return y.failed(c);
    // if ("object" != s(e) || "object" != s(t))
    //    return y.failed(F);

    // if (e.length <= 0)
    //    return y.failed(h);

    // if (16 != t.length)
    //    return y.failed(f);
    var i = encodeWithPKCS5(e, 16)
        , n = encryptWithECB(i, t);
    return n;

    // , r = new C;
    // return r.set("result", n),
    // y.success(r)
}

y这个类貌似就是为了输出错误提示,干脆不要它了。

返回值r就是把n封装了一下,感觉不够优雅,我们直接返回n吧。

var encryptWithECB = function (t, e) {
    // l(void 0 !== t && t.length % 16 == 0, "illegal plaintext:the length of plaintext must be the multiple of 16."),
    // l(void 0 !== e && 16 === e.length, "illegal key:the length of sm4Key must be 16 bytes.");
    for (var i = vt(e), n = t.length, r = new Array(n), a = 0; a < n;)
        bt(t, a, r, a, i, 0),
            a += 16;
    return r
}

这个l函数貌似也就是个错误提示,干掉它。

然后把依赖的 vtbt 等等函数都复制进来,貌似就能跑起来了,还有一个报错就是这个返回值。

由于我们直接返回了n所以要改改

var encryptSm4ECB = function (t) {
    var e = s(t)
    , i = (new Date).getTime()
    , r = (i + "").split("")
    , o = [r[5], r[10]].join("")
    , c = s("CFKt03X9Ufk" + n + o);

    // var cStr = 'CFKt03X9Ufkdro88';
    // var c = s(cStr);

    try {
        var l = CMBSM4EncryptWithECB(c, e);

        for (var u = "", h = 0; h < l.length; h++)
            u += String.fromCharCode(l[h]);

        console.log(i);
        return base64encode(u);
        /*
        return {
                                              data: window.btoa(u),
            timestamp: i
        }
        // */

    } catch (d) { }
    return t instanceof Object ? null : ""
}

这里被这个window.btoa给坑了,问了一下谷哥,哥说这是浏览器提供的Base64转码。NodeJs也提供一个Base64函数,但是转出来不一样……

幸好谷哥还是靠谱的,找了个js写的Base64

var base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
    base64DecodeChars = new Array((-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), (-1), 62, (-1), (-1), (-1), 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, (-1), (-1), (-1), (-1), (-1), (-1), (-1), 0, 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, (-1), (-1), (-1), (-1), (-1), (-1), 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, (-1), (-1), (-1), (-1), (-1));
var base64encode = function (e) {
    var r, a, c, h, o, t;
    for (c = e.length, a = 0, r = ''; a < c;) {
        if (h = 255 & e.charCodeAt(a++), a == c) {
            r += base64EncodeChars.charAt(h >> 2),
                r += base64EncodeChars.charAt((3 & h) << 4),
                r += '==';
            break
        }
        if (o = e.charCodeAt(a++), a == c) {
            r += base64EncodeChars.charAt(h >> 2),
                r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
                r += base64EncodeChars.charAt((15 & o) << 2),
                r += '=';
            break
        }
        t = e.charCodeAt(a++),
            r += base64EncodeChars.charAt(h >> 2),
            r += base64EncodeChars.charAt((3 & h) << 4 | (240 & o) >> 4),
            r += base64EncodeChars.charAt((15 & o) << 2 | (192 & t) >> 6),
            r += base64EncodeChars.charAt(63 & t)
    }
    return r
}

比对了一下,一级棒,和Chrome Hook出来的结果一致。

那如何利用这个结果呢?可以用NodeJs启动一个web服务器,然后rpc来执行。

下面我们再介绍一个优雅的方法,直接用python来执行js

Js模拟库介绍

江湖上有很多Python写的JavaScript执行引擎。

PyV8

https://pypi.org/project/PyV8

据说年老失修,最新的版本是2010年的,大佬们不推荐使用。

但是实际上2013年它还更新了一般,廉颇老矣,尚能饭否?我觉得就冲V8这个名字,就值得试试。

Js2Py

https://github.com/PiotrDabkowski/Js2Py

同样嫌它年纪大了,实际上人家5个月前有更新,不能小看大龄程序员的潜力。

PyExecJS

https://pypi.org/project/PyExecJS/

一个最开始诞生于 Ruby 中的库,后来被移植到了 Python 上。

比较活跃,最新的更新是2018年,江湖上有很多它的使用例子。很多人建议使用

PyminiRacer

https://github.com/sqreen/PyMiniRacer

作者号称这是一个继任 PyExecJS 的库,比较新,这玩意看缘分,飞哥第一次就搜到了它,所以今天就用它了。

Pyppeteer

https://github.com/pyppeteer/pyppeteer

这个也可以试试,其实很多被人嫌弃年纪大的库,都还在努力更新呢。

Selenium

https://www.selenium.dev/

  • 一个 web 自动化测试框架,可以驱动各种浏览器进行模拟人工操作
  • 用于渲染页面以方便提取数据或过验证码
  • 也可以直接驱动浏览器执行 JS

Selenium可以驱使浏览器,那么执行个js就不在话下了,这个做最后的杀手锏用。

PyminiRacer模拟执行encryptSm4ECB

先来个Hello World

from py_mini_racer import py_mini_racer
jsSource = '''
var ffdemo = function(str){
        return str;
}

'''
ctx = py_mini_racer.MiniRacer()
ctx.eval(jsSource)
print(ctx.call("ffdemo", "Hello World"))

是的,就是这么帅,3行代码搞定。

依葫芦画瓢,把刚才NodeJs跑通的代码复制进去,执行 print(ctx.call("encryptSm4ECB", strFF))

结果就出来了。

三、总结

NodeJs去执行的之后,不要一开始就把整页代码都拷贝上去,要分而治之,一个一个函数跑通。

JavaScript保护只有一条路可以走了,那就是混淆。下次找到合适的样本我们再一起分析下。

ffshow
1:ffshow

廉颇老矣,尚一饭斗米,肉十斤,生命不止,coding不息。

100

关注微信公众号,最新技术干货实时推送

100