李老板: 奋飞呀,上次分析的那个 App http://91fans.com.cn/post/bankdataone/ 光能 Debug 还不够呀, 网页中的 js 也用不了 Frida,我还想 Hook 它的函数 ,咋搞呀? 再有 App 可以 RPC 去执行签名,这个 js 我如何去利用呀?总不能代码都改成 js 去做请求吧?
奋飞:老板呀,你一下提这么多要求,不是明摆着要我们加班吗?这次加班费可得加倍。
我们的目的是 Hook 这个 encryptSm4ECB 函数,然后打印出它的入参和返回值。
在合适的位置下断点(一般是函数入口和出口)。然后在断点上点右键 -> 修改断点,然后在弹出的窗口里面输入要打印的变量。
TIP: 实际上这个功能是条件断点,可以在符合条件的时候触发断点,但是恰好可以用于打印变量值。修改成功之后断点图标会变颜色。
跑一下,我们想要的入参和结果都打印出来了。
TamperMonkey 俗称油猴,你都可以理解他就是浏览器届的 Frida,不过在这个样本里面我没有找到如何 Hook 这个 encryptSm4ECB, 但使用它来 Hook 全局函数是可以成功的。有用油猴 Hook 成功这个 encryptSm4ECB 的兄弟可以给我留言交流下。
Fiddler 抓包的同时是可以用插件来注入 js 代码的,这个看上去比较复杂,我也木有搞
要是可以直接在这个 ArticleDetail.js 上去修改,增加打印变量的代码,岂不快哉。
Chrome 其实提供了这个功能,算是文件级别的 Hook,就是执行到 ArticleDetail.js 这个请求的时候,不向服务器发请求了,而是直接使用你本地替换的 js。这样你就想怎么改就怎么改了。
在 源代码页 选择 替换,然后 勾选 启用本地替换,这时候浏览器会提示你给权限,然后选择一个本地的目录来存放要替换的 js。
回到 网络 页,选择你想替换的 js,点右键 -> 保存并覆盖。
再回到 源代码 页,找到这个 js 文件,实际它已经存到我们开始指定的目录下了。
这时候找到指定的函数位置写 hook 代码就可以了。
TIP: xxx.js 这种链接替换没问题,hook 代码也能激活。 ArticleDetail.js?v=ab4f0b37a4a90050d429 这种模式的 js 没有替换成功。原因未知,有成功的兄弟也留言交流下。
子曾经曰过:逆向是杂学,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);
复制代码
TIP: 这里其实埋了一个坑,c 的值和最后的时间戳 timestamp 是有关系的,要对应上。
在继续往下搞
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 函数貌似也就是个错误提示,干掉它。
然后把依赖的 vt 、 bt 等等函数都复制进来,貌似就能跑起来了,还有一个报错就是这个返回值。
由于我们直接返回了 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
江湖上有很多 Python 写的 JavaScript 执行引擎。
据说年老失修,最新的版本是 2010 年的,大佬们不推荐使用。
但是实际上 2013 年它还更新了一般,廉颇老矣,尚能饭否?我觉得就冲 V8 这个名字,就值得试试。
https://github.com/PiotrDabkowski/Js2Py
同样嫌它年纪大了,实际上人家 5 个月前有更新,不能小看大龄程序员的潜力。
https://pypi.org/project/PyExecJS/
一个最开始诞生于 Ruby 中的库,后来被移植到了 Python 上。
比较活跃,最新的更新是 2018 年,江湖上有很多它的使用例子。很多人建议使用
https://github.com/sqreen/PyMiniRacer
作者号称这是一个继任 PyExecJS 的库,比较新,这玩意看缘分,飞哥第一次就搜到了它,所以今天就用它了。
https://github.com/pyppeteer/pyppeteer
这个也可以试试,其实很多被人嫌弃年纪大的库,都还在努力更新呢。
Selenium 可以驱使浏览器,那么执行个 js 就不在话下了,这个做最后的杀手锏用。
先来个 Hello World
from py_mini_racer import py_mini_racerjsSource = '''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 保护只有一条路可以走了,那就是混淆。下次找到合适的样本我们再一起分析下。
廉颇老矣,尚一饭斗米,肉十斤,生命不止,coding 不息。
领取专属 10元无门槛券
私享最新 技术干货