写点什么

不能 Hook 的人生不值得 jsHook 和模拟执行

作者:奋飞安全
  • 2022 年 2 月 28 日
  • 本文字数:4583 字

    阅读完需:约 15 分钟

一、目标

李老板: 奋飞呀,上次分析的那个 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 函数貌似也就是个错误提示,干掉它。


然后把依赖的 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_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 不息。

发布于: 2022 年 02 月 28 日阅读数: 45
用户头像

奋飞安全

关注

独立安全研究员。 公众号: 奋飞安全 2022.02.21 加入

少习文,经史子集略有涉猎;北上学艺,A-Z语言敢说略懂;初入江湖,混入外企,乐不思蜀;后为人父,发愤图强,略有建树;而今重新出发,万事随缘。

评论

发布
暂无评论
不能Hook的人生不值得  jsHook和模拟执行_安全_奋飞安全_InfoQ写作平台