这几天一直在处理APP,遇到了libsgmain.so, 问了大佬和找资料后,基于Unidbg的demo跑通了,于是做个简单记录。

  1. 代码里找不到核心加密接口和类,Frida里也hook不到(如ISecureSignatureComponent接口的实现类,JNICLibrary类),是因为这些代码是动态注册的。如何解决看参考资料1
  2. libsgmain.so是假so, 本质上是jar, 在jadx里打开就能看到核心加密类,真正的so文件也在这假so里,形如libsgmainso-*.so
  3. doCommandNative是真正加密的方法,这个方法也用于初始化,至于初始化多少次,看so文件难度了, 我要处理的so相对简单,只有一次初始化,缺啥补啥完事。可以看参考资料2。
  4. Unidbg真是大杀器,so加密小白福音。

参考资料

  1. 奋飞佬的某A系电商App x-sign签名分析
  2. 意识存在感的阿里系某电影票务APP加密参数还原-Unidbg篇

联系作者

remove debugger

有些时候想移除debugger

1
2
3
4
5
6
7
8
_Function = Function;
Function.prototype.constructor = function(){
if (arguments[0].indexOf('debugger') != -1){
//debugger;
return _Function('');
}
return _Function(arguments[0]);
};

hook cookie

我们需要快速定位cookie在哪里生成的,需要hook cookie

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
26
27
28
29
var cookie_cache = document.cookie;
Object.defineProperty(document, 'cookie', {
get: function() {
debugger;
console.log('Getting cookie');
return cookie_cache;
},
set: function(val) {
debugger;
console.log('Setting cookie', val);
var cookie = val.split(";")[0];
var ncookie = cookie.split("=");
var flag = false;
var cache = cookie_cache.split("; ");
cache = cache.map(function(a){
if (a.split("=")[0] === ncookie[0]){
flag = true;
return cookie;
}
return a;
})
cookie_cache = cache.join("; ");
if (!flag){
cookie_cache += cookie + "; ";
}
this._value = val;
return cookie_cache;
},
});

hook eval

有时候我们需要快速定位vm.js在哪里生成的,需要hook eval

1
2
3
4
5
6
7
8
9
window.__eval = window.eval
var myeval = function(src) {
debugger ;
console.log('=======eval====')
return window.__eval(src)
}
Object.defineProperty(window.prototype, 'eval', {
value: myeval
})

hook random

有时候我们需要固定随机参数,需要hook random

1
2
3
4
old_random = Math.random
window.Math.random = Math.random = function () {
return 0.3
};

hook regex

有时候我们需要知道代码如何进行格式化检测,需要hook 正则

1
2
3
4
5
6
7
8
9
10
11
 RegExp.prototype.my_test = RegExp.prototype.test

let my_regex = function(arguments) {
debugger ;
console.log(this, arguments)
return this.my_test(arguments)
}

Object.defineProperty(RegExp.prototype, 'test', {
value: my_regex
})

联系作者

Node服务设置–max-old-space-size参数

可以先问问GPT这个参数是做什么的

–max-old-space-size 是 Node.js 运行时的一个参数,它用来设置 V8 引擎的老生代内存限制。
V8 的内存空间主要分为新生代空间(Young Generation)和老生代空间(Old Generation),这两种空间的大小决定了应用程序可以使用的最大内存。新生代空间通常较小,并且垃圾回收频率较高;而老生代空间较大,并且垃圾回收频率较低。
当你设置了 –max-old-space-size 的值,你就在 V8 引擎运行时告诉它,它在老生代空间可以使用的最大内存量。如果你的应用程序尝试超过这个限制,V8 引擎会抛出一个 FATAL ERROR: Reached heap limit Allocation failed 的错误,表示内存已用尽,无法分配新的内存。
请注意,–max-old-space-size 只是 V8 引擎可以使用的总内存的一个部分,还有其他部分是由 V8 内部使用的,例如它的执行代码和堆栈空间等。所以,如果你的系统内存较少,你可能会需要减小 –max-old-space-size 的值,以确保其他重要的系统进程也能正常运行。

在PM2中我们就可以如此启动服务 pm2 start server.js -i 2 –node-args=”–max-old-space-size=800” –no-daemon

–max-old-space-size 这个参数比 –max-memory-restart 好用多了,它会尝试进行内存回收,能回收了,就不重启进程,不能回收就才会重启进程,这样可以减少 502 服务器错误。

有了这个参数,也不需要再使用强制内存回收插件了,因为本身自带内存回收。唯一的问题是,PM2守护的进程内存都是从0开始,然后进程一起到达设置的老生代内存大小,此时CPU占比会很高,可能会影响服务执行。

联系作者

未雨绸缪,虽然有了补环境通用方案,但为了应对可能更高的QPS需求,一直在想办法储备更快的cookies生成方案。简单计算下,假设需要400 QPS,而一般的补环境耗时250ms, 那么一个CPU一秒可以处理4个请求,400 QPS就需要100核CPU, 这对资源的消耗就很可观了。如果有个方案能50ms生成一个cookies, 那么就只需要20核CPU就能满足需求。

寻寻觅觅,在如画佬的星球里找到一个补环境方案,环境代码极少。示例代码是在Python中使用execjs来执行生成cookies, 改成Node服务后执行速度非常理想,能做到80ms生成一次。

美中不足的是需要在Node中使用eval执行js代码,会导致内存泄漏,这种内存泄漏使用强制内存回收也回收不了,最后程序会奔溃,用PM2限制内存使用和自动重启可以解决。但不可避免会出现502服务器错误,这在我们公司的生成环境中就不合适,因为运维一旦发现502超过一定数量就得让解决,得寻求其它方案。

要想又快又稳,想来只有算法方案了。

联系作者

之前在群里就看到有道友在讨论这个问题,但因为没有遇到过这个问题,就没细看,这次自己也遇到了,就认真看了下。

在寻找高效的某数 cookies 生成方案时,看到如画佬星球的补环境方案后,深有感触。最近两年,虽然把补环境框架搞的越来越完善,但同时执行速度也越来越慢,想想似乎有点背离补环境的初衷。上一次yidun的加密生成方案尤其明显,补环境方案把整个CPU都打满了,后面搞了个补环境和扣代码结合的方案后跑的就稳稳的,两者之间性能整整差了10倍。

于是就尝试这种最开始学习的补环境方案,缺啥补啥,争取补的环境每一行代码都是有用的,然后就遇到这个 ActiveXObject 问题了。在 proxy 输出日志中,看到有 ActiveXObject 调用日志,于是在浏览器里看,window.ActiveXObject 就是 undefined, 在Node里,window=global 后,window.ActiveXObject 也是 undefined,于是就没管 ActiveXObject 。但生成的 cookies 就是过不去,只好和如画佬的代码做对比,发现就是因为他的代码有 window.ActiveXObject=undefined,所以能过。

于是去了解 ActiveXObject,发现它是IE才有的对象,在IE中,’ActiveXObject’ in window 是true, 而在 Chrome 中 ‘ActiveXObject’ in window 是 false。如果在检测代码中有 ‘ActiveXObject’ in window 这种检测,那么补环境代码中有没有 window.ActiveXObject=undefined 就会影响检测代码执行路径了。在Node中加上 window.ActiveXObject=undefined 后,’ActiveXObject’ in window 也就成了 true 了,检测代码就会认为是 IE浏览器,就会按照IE浏览器来检测。手头上方便调试的只有Chome浏览器,就再继续探究后面如何检测的。神奇的是,我是在Mac上用Chrome调试的代码,最后还得伪装成IE才能通过检测,就离谱。

回过头来看代理日志,有针对in操作符的代理has=> [window] 有无属性名 => [ActiveXObject], 结果 => [false] 这行,就是 代理has输出日志,说明检测代码里有 ‘ActiveXObject’ in window 的检测,只是没注意到。

联系作者

本来是想问ChatGPT在Node.js里如何读取Redis的,问着问着,就拿如何解决document.all问题去问ChatGPT, 顺便测试下其它几个厂商给出的回答,其它几个都是挺常规的回答,只有Copilot给出的回答出乎意料,因为它的回答是基于我的当补环境遇到document.all文章给的。

Copilot 给的回答是

在解决 jsdom 被针对的逆向过程中,遇到了 document.all 的问题。尽管 document.all 的类型是 undefined,但却能够获取值,这让人感到非常困惑。不过,我们有一些解决办法:

  1. 不补环境:有时候网站的校验可能不严格,直接绕过补环境步骤也许就能成功。
  2. 扣算法:这是一种绕过各种检测的方法,直接将值写死。不过,这需要一定的技巧。
  3. 使用 V8 中修改后的不可检测对象:这是风佬提出的方法,可以完美解决问题。1
  4. 请大佬们出山,共同探讨解决方案。
    总之,解决这个问题的途径有很多,我们可以根据具体情况选择合适的方法。

我的文章里说到:

目前看来,解决这个问题的办法有如下几种

  1. 直接不补了,有可能网站校验不严格,直接可以过
  2. 扣算法,这样绕过各种检测,直接把值写死,就是扣算法难搞。
  3. 用风佬修改后的Node
  4. V8里的不可检测对象
  5. 请大佬出山帮你解决

惊不惊喜,意不意外。阿龙的学习笔记作为个人学习笔记从13年就开始写,有一年还因为域名续费失败的原因,导致域名被抢注而换到现在的域名,但还是坚持独立博客写下来,不管有没有人看。最近一份工作的主要工作内容是Web逆向,写了很多补环境相关的技术文章后收到一些道友的关注,并加联系方式讨论相关问题,还收到一位不知名字道友的20块钱巨赏, 甚是鼓舞。今天看到自己的文章被AI引用,甚是开心。

还是那句话,兼济天下则达,独善其身则穷。

联系作者

很多道友使用jsdom来补环境,但没有设置好userAgent(以下简称ua), 如果生成的token中有使用到ua, 反爬人员就可以通过分析token发现是由jsdom生成的,于是针对jsdom的特征做检测。所以使用jsdom时,设置ua很重要。

下面我们来看看怎么设置ua, 网上常见的设置ua代码如下

1
2
3
4
5
6
7
8
9
10
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
userAgent: userAgent,
});
window = dom.window
console.log(window.navigator.userAgent)

输出结果为 Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/24.0.0, 有jsdom特征,与设置的ua不一致。

尝试添加如下代码

1
2
window.navigator.userAgent = userAgent
console.log(window.navigator.userAgent)

输出结果依然为 Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/24.0.0

继续尝试增加如下代码

1
2
3
4
navigator = {
userAgent: userAgent
}
console.log(window.navigator.userAgent)

输出结果依然为 Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/24.0.0

查看官方文档,使用ResourceLoader可以修改ua, 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
const jsdom = require("jsdom");  // 引入 jsdom
const { JSDOM } = jsdom; // 引出 JSDOM 类, 等同于 JSDOM = jsdom.JSDOM
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
const resourceLoader = new jsdom.ResourceLoader({
userAgent: userAgent
});
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
resources: resourceLoader,
});
window = dom.window
console.log(window.navigator.userAgent)

输出结果为 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, 已经没有jsdom的特征,目标达成。

联系作者

在使用Express搭建Node服务时,会遇到内存持续增长的情况,增长到一定程度后,进程就会被PM2强制重启。而如果进程正好重启时,接收了请求,这个请求就会没有响应,测试就会返回一个502给请求方。这种情况502情况一旦多起来,就会有告警,服务的可用性也就没法做到99.99%。

此时去排查为啥内存会持续增长,看看是不是哪里存在内存泄漏。排查下来后发现,并没有存在内存泄漏的情况,单纯是因为内存的释放速度赶不上内存的占用速度,如果并发不是很高的情况下,内存一直可以稳定释放,而一旦并发高了,内存就来不及释放,就会持续增长。此时只好强制开启内存释放,调用V8中的强制回收内存函数,实现内存的强制回收,使内存一直稳定在健康的状态。

具体如何操作,参考开始佬的编译nodejs c插件-纯笔记一文

当然,鱼和熊掌不可兼得,使用强制内存回收是个很耗时的操作,对请求响应时间会有影响,例如请求20次之后强制内存回收,平均请求时间会慢20%。

联系作者

今年如约开启了海外反爬之旅,对常见的四个海外反爬中inCaplusa,Akamai, CloudFlare(一般称为5秒盾), reCaptcha粗略评估后,觉得inCaplusa最为简单,决定先挑软柿子捏,于是先拿inCaplusa试试水。

打开网站,会先返回一段会执行eval的js, 用于生成___utmvc这个cookie, 这段js是个ob混淆,此时祭出蔡老板的ob解混淆工具搞一搞就好了,不难。很多网站没有这个cookie也没关系,可以先不看。

之后是get请求一份网站对应的版本文件,然后会post请求一次版本文件相同的url, 其中返回的token就是reese84。需要说明的是,即便inCaplusa觉得有异常,也会返回这个token, 也就是说token不一定有效,得拿去请求了才知道对不对。一般拿去请求时,状态码不是403就代表reese84是有效的。

拿到版本文件,先进行AST还原,然后开始补环境。不得不说,这个环境校验是真的细,常规的如Canvas, WebGL, font, window[“Function”][“prototype”][“toString”]‘toString’, window[“Function”][“prototype”][“call”]‘toString’等检测。还有第一次见的类似window[“Object”]“getOwnPropertyNames”的检测, 这个得能做到只返回[‘length’, ‘name’]。还有iframe里的contentWindow检测,这里就不一一列举了。

好在代码结构很清晰,可以一步一步调试,补到一半补累了,于是改成扣代码了。花了一天时间扣了下代码能通过之后,信心大增。拿着这个网站的代码去测试其它网站,发现过不去,于是继续补环境,没有全部补完就能过了,估计是校验不严格。测试了几个其它的网站,都能通过,美滋滋了。

目前只是测试了reese84有效性,后续风控之类的还没涉及到。总的来说,如果不是校验不严格,补环境挺不容易的,太多细节了,用来完善补环境框架再好不过了。

联系作者

上周写完使用Canvas指纹插件被检测到后,Nanda佬正好看到,提了一句关注下HTMLCanvasElement.prototype.toDataURL.prototype,这个值可能发生变化。

于是对比原始浏览器和重定义toDataURL函数后的 toDataURL ,果然不一样。原先浏览器的 HTMLCanvasElement.prototype.toDataURL.prototype 的值是 undefined , 重定义toDataURL函数后,HTMLCanvasElement.prototype.toDataURL.prototype 的值就变成了函数。加上HTMLCanvasElement.prototype.toDataURL.prototype = undefined 后,网站上就返回了边界数据,也不返回假数据了。

继续测试了几个 toString 后有 native code 字样的函数,如 toString, atob, setTimeout 等等,ChatGPT 的解释是有 native code 意味着该函数的具体实现是由浏览器或 JavaScript 引擎提供的,而不是由 JavaScript 本身的代码编写的,它们的 prototype 都是 undefined 。

这就值得引起注意了,在补环境的时候,对native函数都要引起重视,重定义 native 函数后,要保证它们的 prototype 还是 undefined。打开自己写的补环境框架,测试了几个 native 函数,prototype 都是函数,处理的真垃圾。打开开始佬开源的 qxvm, 测试了几个 native 函数, prototype 都是 undefined,处理的真不错。

联系作者