不能Hook的人生不值得 jsHook和模拟执行
一、目标
李老板: 奋飞呀,上次分析的那个App http://91fans.com.cn/post/bankdataone/ 光能Debug还不够呀, 网页中的js也用不了Frida,我还想 Hook它的函数 ,咋搞呀? 再有App可以RPC去执行签名,这个js我如何去利用呀?总不能代码都改成js去做请求吧?
奋飞:老板呀,你一下提这么多要求,不是明摆着要我们加班吗?这次加班费可得加倍。
二、步骤
最简单易行的js Hook - console.log
main.png我们的目的是Hook这个 encryptSm4ECB 函数,然后打印出它的入参和返回值。
在合适的位置下断点(一般是函数入口和出口)。然后在断点上点右键 -> 修改断点,然后在弹出的窗口里面输入要打印的变量。
TIP: 实际上这个功能是条件断点,可以在符合条件的时候触发断点,但是恰好可以用于打印变量值。修改成功之后断点图标会变颜色。
rc1.png跑一下,我们想要的入参和结果都打印出来了。
TamperMonkey 注入
TamperMonkey 俗称油猴,你都可以理解他就是浏览器届的Frida,不过在这个样本里面我没有找到如何Hook 这个 encryptSm4ECB, 但使用它来Hook全局函数是可以成功的。有用油猴 Hook成功这个 encryptSm4ECB 的兄弟可以给我留言交流下。
Fiddler 插件注入
Fiddler抓包的同时是可以用插件来注入js代码的,这个看上去比较复杂,我也木有搞
Chrome启用本地替换
要是可以直接在这个 ArticleDetail.js 上去修改,增加打印变量的代码,岂不快哉。
Chrome其实提供了这个功能,算是文件级别的Hook,就是执行到 ArticleDetail.js 这个请求的时候,不向服务器发请求了,而是直接使用你本地替换的js。这样你就想怎么改就怎么改了。
replace1.png在 源代码页 选择 替换,然后 勾选 启用本地替换,这时候浏览器会提示你给权限,然后选择一个本地的目录来存放要替换的js。
replace2.png回到 网络 页,选择你想替换的js,点右键 -> 保存并覆盖。
再回到 源代码 页,找到这个js文件,实际它已经存到我们开始指定的目录下了。
这时候找到指定的函数位置写hook代码就可以了。
TIP: xxx.js 这种链接替换没问题,hook代码也能激活。 ArticleDetail.js?v=ab4f0b37a4a90050d429 这种模式的js没有替换成功。原因未知,有成功的兄弟也留言交流下。
模拟执行第一步 先用 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);
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
Js模拟库介绍
江湖上有很多Python写的JavaScript执行引擎。
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
- 一个 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.jpeg廉颇老矣,尚一饭斗米,肉十斤,生命不止,coding不息。