案例地址: aHR0cHM6Ly9kZW1vcy5nZWV0ZXN0LmNvbS9mdWxscGFnZS5odG1s
1. 分析整个请求流程
先刷新一下页面,看一下整体流程,如图总共有4个关键请求。

1. register-fullpage
这个请求用于获取gt
和challenge
参数,后面会用到。
这个请求用于获取gt
和challenge
参数,后面会用到
2. gettype.php
看请求的名字是获取验证码的类型,请求时需要带着第一步的gt
参数。
看请求的名字是获取验证码的类型,请求时需要带着第一步的gt
参数。
3. get.php
这个请求就有说法了,喜闻乐见的w值就在其请求参数内,除了w
值外还有gt
和challenge
参数。其中gt
和challenge
参数是第一步register-fullpage
请求中得到的,关键参数就是w
值。
这里如果通过全局搜索w
字符串并没有什么有效的位置,而且这个请求不是xhr请求,没办法通过xhr断点断住,这里用initiator的进行追踪:

最终可以定位到fullpage.9.1.9-cyhomb.js
文件,看它样子就知道八九不离十就是它了。

可看到这是经过混淆的js文件,接下来为了方便分析代码,我们可以通过ast
来进行适当的还原混淆的代码,在正式开始反混淆之前可以先用v_jstool
工具进行一次普通解混淆,这一步会对代码中的unicode
字符串进行处理,以及一些基础的反混淆操作。
2. 使用ast进行反混淆
这里是babel
相关的库来进行解混淆,配合[https://astexplorer.net/](https://astexplorer.net/)
网站食用。
2.1 介绍一下babel
Babel 是一个 JavaScript 编译器,Babel以及一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js)
- 源码转换(codemods)
这是babel
文档中对于它的介绍,我们主要用到的是它的源码转换
功能。举个栗子,通过babel
我们可以将以下代码进行简化方便阅读。
1 2 3 4 5
| var a,b,c = 1,2,3;
var a = 1; var b = 2; var c = 3;
|
1 2 3 4 5 6 7 8 9 10 11 12
| function add(x, y){ var cc = ff.d; var dd = cc[0]; return x + y; }
function add(x,y){ return x + y; }
|
当然要完成这些操作,首先是要学习如何使用babel
,个人认为上手一种新的技术最好的方法就是边看文档边实践,这篇文章也是我在看完babel
的文档后实践的。附一下babel
的文档:
https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md
2.2 使用astexplorer辅助编写ast脚本
这其实是一个网站,是一个左右布局的页面,左边写入js代码,右边会给它的抽象语法树。在编写解混淆代码时可以使用它进行辅助。

2.3 分析混淆后的代码
通过vscode
打开后,将所有代码快折叠,可以看到开头有一个AFiup函数,然后在它内部又定义了几个函数,在最后有一个很长的自执行函数。

浏览一下最后的自执行函数,不难发现里面有很多调用AFiup.$_CP
函数的代码块,放到浏览器执行一下发现这个函数在传入一个数字后返回一个字符串。
好,那第一步就是还原对AFiup.$_CP
函数的调用。

2.4 还原AFiup.$_CP
函数调用
这里需要说一下,为什么可以对AFiup.$_CP
的调用进行还原,可以看下它的特征:
- 代码中对其进行调用的参数都是字面量,像
659
、89
这种,不涉及到其他的变量或者是函数之类的。 $_CP
函数在传入参数不变的情况下,无论调用次数或执行环境如何变化,函数始终返回相同的计算结果,也就是说它是一个纯函数
或者叫确定性函数
。当然这需要在浏览器和nodejs中进行验证,才能得到的结论,这里就略过了。
既然它是一个纯函数,我们就可以直接把它在反混淆脚本中定义好,然后在使用babel遍历语法树,在调用的地方直接用结果将调用节点替换掉。比如说下面代码中的$_BIJHw(89) ``$_BIJHw(34)
、
1 2 3 4 5 6 7 8 9 10 11
| var $_BIJHw = AFiup.$_CP; var $_BIJGv = ["$_BJAAQ"].concat($_BIJHw); var $_BIJID = $_BIJGv[1]; $_BIJGv.shift(); var $_BIJJe = $_BIJGv[0];
var _;
var e = Object[$_BIJHw(89)]; var c = e[$_BIJHw(34)]; var t;
|
思路如下:
1. 首先定位到类似var $_BIJHw = AFiup.$_CP;
语句
1. 查找$_BIJHw
变量所有的引用位置,找到其中类似$_BIJHw(xxx)
的位置,其中xxx
意思是纯数字
1. 拿到xxx
值,本地调用后替换节点.
完整的visitor
代码如下:
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 30 31 32 33 34 35
| const replaceCallAFiup$_CP = { VariableDeclaration(path) { if ( types.isVariableDeclaration(path.node) && path.node.declarations.length === 1 && types.isVariableDeclarator(path.node.declarations[0]) && types.isIdentifier(path.node.declarations[0].id) && types.isMemberExpression(path.node.declarations[0].init) && types.isIdentifier(path.node.declarations[0].init.object) && types.isIdentifier(path.node.declarations[0].init.property) && path.node.declarations[0].init.property.name === "$_CP" ) { var varName = path.node.declarations[0].id.name; let binding = path.scope.getBinding(varName); binding.referencePaths.forEach(refPath => { if ( types.isCallExpression(refPath.parent) && refPath.parent.arguments.length === 1 && types.isNumericLiteral(refPath.parent.arguments[0]) ) { let callArgumentInt = refPath.parent.arguments[0].value; console.log(`${refPath.parentPath.toString()} --> ${AFiup.$_CP(callArgumentInt)}`) refPath.parentPath.replaceWith(types.valueToNode(AFiup.$_CP(callArgumentInt))) } }); } path.scope.crawl();
}, };
|
执行以下可以看到替换掉了很多的节点:

然后接着分析代码,还有一部分AFiup.$_CP函数的调用,是比较绕的,先是赋值给变量$_CHIHc,接着将其concat到$_CHIGy然后变量$_CHIJT又从$_CHIGy中取出来。最后在通过调用$_CHIIJ的方式来调用AFiup.$_CP函数。思考一下,这里如果将var $_CHIJT = $_CHIGy[0];
替换成var $
_CHIJT = AFiup.$_
_CP
就可以直接复用上一步写的visitor
了。那思路就有了:
- 依次匹配语句1、2、3、4、5
- 将语句5进行替换,格式:
var xxx = AFiup.$_CP
1 2 3 4 5 6
| var $_CHIHc = AFiup.$_CP; // 语句1 var $_CHIGy = ["$_CHJAQ"].concat($_CHIHc); // 语句2 var $_CHIIJ = $_CHIGy[1]; // 语句3 $_CHIGy.shift(); // 语句4 var $_CHIJT = $_CHIGy[0]; // 语句5 e[$_CHIIJ(1220)]();
|
完整的visitor
代码如下:
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 30 31 32 33 34 35 36 37 38 39 40
| const replaceCallAFiup$_CP = { VariableDeclaration(path) { if ( types.isVariableDeclaration(path.node) && path.node.declarations.length === 1 && types.isVariableDeclarator(path.node.declarations[0]) && types.isIdentifier(path.node.declarations[0].id) && types.isMemberExpression(path.node.declarations[0].init) && types.isIdentifier(path.node.declarations[0].init.object) && types.isLiteral(path.node.declarations[0].init.property) && path.node.declarations[0].init.property.value === 1 ) { const memberExpression = types.memberExpression( types.identifier("AFiup"), // 对象:AFiup types.identifier("$_CP"), // 属性:$_CP false // 非计算属性(即用点符号访问,而非方括号) );
// 创建变量声明符 $_FCIU = AFiup.$_CP const declarator = types.variableDeclarator( types.identifier( path.node.declarations[0].id.name ), // 变量名 memberExpression // 初始值 );
// 创建完整的变量声明节点 var $_FCIU = AFiup.$_CP const newNode = types.variableDeclaration( "var", // kind: var [declarator] // declarations 数组 ); const beforeCode = path.toString(); path.replaceWith(newNode); console.log(`${beforeCode} -> ${path.toString()}`); }
// 定位到类似的语句var _ccc = AFiup.$_CP; ... };
|
2.5 移除无用代码
很简单的操作不多说了
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 30 31 32 33 34 35 36 37 38 39 40 41
| const pluginRemoveUnuseVarDeclaration = { VariableDeclaration(path) { const declarations = path.node.declarations;
// 收集所有变量的引用信息 const bindings = new Map(); declarations.forEach((decl) => { const binding = path.scope.getBinding(decl.id.name); if (binding) { bindings.set(decl.id.name, binding.referencePaths.length); } });
// 过滤出未使用的变量声明 const unusedDeclarations = declarations.filter((decl) => { return bindings.get(decl.id.name) === 0; });
// 如果所有声明都未使用,删除整个声明语句 if (unusedDeclarations.length === declarations.length) { console.log( `移除整个未使用的变量声明语句,包含 ${declarations .map((d) => d.id.name) .join(", ")}` ); path.remove(); } // 否则只删除未使用的声明 else if (unusedDeclarations.length > 0) { console.log( `移除部分未使用的变量: ${unusedDeclarations .map((d) => d.id.name) .join(", ")}` ); path.node.declarations = declarations.filter( (decl) => !unusedDeclarations.includes(decl) ); } path.scope.crawl(); }, };
|
3. 分析反混淆后的代码
替换掉AFiup.$_CP
函数调用后代码基本上就可读了,后面就可以用反混淆的文件替换掉浏览器上的进行分析了。
