一、目标
李老板: 奋飞呀,上次分析的那个App http://91fans.com.cn/post/bankdataone/ 光能Debug还不够呀, 网页中的js也用不了Frida,我还想 Hook它的函数 ,咋搞呀? 再有App可以RPC去执行签名,这个js我如何去利用呀?总不能代码都改成js去做请求吧?
奋飞:老板呀,你一下提这么多要求,不是明摆着要我们加班吗?这次加班费可得加倍。
二、步骤
最简单易行的js Hook - console.log
我们的目的是Hook这个 encryptSm4ECB 函数,然后打印出它的入参和返回值。
在合适的位置下断点(一般是函数入口和出口)。然后在断点上点右键 -> 修改断点,然后在弹出的窗口里面输入要打印的变量。
TIP: 实际上这个功能是条件断点,可以在符合条件的时候触发断点,但是恰好可以用于打印变量值。修改成功之后断点图标会变颜色。
跑一下,我们想要的入参和结果都打印出来了。
TamperMonkey 注入
TamperMonkey 俗称油猴,你都可以理解他就是浏览器届的Frida,不过在这个样本里面我没有找到如何Hook 这个 encryptSm4ECB, 但使用它来Hook全局函数是可以成功的。有用油猴 Hook成功这个 encryptSm4ECB 的兄弟可以给我留言交流下。
Fiddler 插件注入
Fiddler抓包的同时是可以用插件来注入js代码的,这个看上去比较复杂,我也木有搞
Chrome启用本地替换
要是可以直接在这个 ArticleDetail.js 上去修改,增加打印变量的代码,岂不快哉。
Chrome其实提供了这个功能,算是文件级别的Hook,就是执行到 ArticleDetail.js 这个请求的时候,不向服务器发请求了,而是直接使用你本地替换的js。这样你就想怎么改就怎么改了。
在 源代码页 选择 替换,然后 勾选 启用本地替换,这时候浏览器会提示你给权限,然后选择一个本地的目录来存放要替换的js。
回到 网络 页,选择你想替换的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保护只有一条路可以走了,那就是混淆。下次找到合适的样本我们再一起分析下。
廉颇老矣,尚一饭斗米,肉十斤,生命不止,coding不息。
TIP: 本文的目的只有一个就是学习更多的逆向技巧和思路,如果有人利用本文技术去进行非法商业获取利益带来的法律责任都是操作者自己承担,和本文以及作者没关系,本文涉及到的代码项目可以去 奋飞的朋友们 知识星球自取,欢迎加入知识星球一起学习探讨技术。有问题可以加我wx: fenfei331 讨论下。
关注微信公众号: 奋飞安全,最新技术干货实时推送