某视频解析分析-AST反混淆与nodejs调用
本帖最后由 漁滒 于 2021-5-31 22:05 编辑@(颜亦解析分析)
##一、抓包并进行简单的静态分析
打开Fiddler,然后打开目标网址https://jsap.attakids.com/?url=https://v.qq.com/x/cover/mzc0020002ka95z/k0036081njj.html
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531211445654.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
这里可以看到请求了https://jsap.attakids.com/Api.php这个接口,其中还需要10个请求的参数。
接着继续看看主页的内容
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531211937913.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
这里可以看到,除了Sign和Token两个参数是实际有加密的,其他的参数都是直接在源代码中给出,使用正则匹配等方式提取即可。
下面还有一段ob混淆的js代码,将其先进行反混淆(关于ast的详细内容可以查看之前的帖子,这里就不做过长的解析。地址:[《JavaScript AST其实很简单》](https://www.52pojie.cn/thread-1332822-1-1.html))
反混淆的js过程,下面只展示部分内容
```javascript
function ajax_api() {
$["cookie"]("uuid", Vkey + '-' + Key + '-' + Sign + '-' + Token);
AccessToken = Vkey + '-' + Key + '-' + Sign + '-' + Token;
if (isios) {
ios = '1';
} else {
ios = '0';
}
if (isiPad) {
wap = '1';
} else {
wap = '0';
}
$["ajax"]({
'type': "post",
'url': Api + "/Api.php",
'dataType': "json",
'headers': {
'Token': Vkey,
'Access-Token': AccessToken,
'Version': Version
},
'data': {
'url': Vurl,
'wap': wap,
'ios': ios,
'host': Host,
'key': Key,
'sign': Sign,
'token': Token,
'type': Type,
'referer': Ref,
'time': Time
},
'success': function (_0x3040ec) {
if (_0x3040ec["code"] == "200") {
var _0x4acba7 = decode_url(_0x3040ec["url"], $["md5"](Host + Token));
_0x3040ec["url"] = decodeURIComponent(_0x4acba7);
}
}
});
}
```
此时,请求的逻辑就非常清晰了,但是还需要加密参数Sign和Token的算法。
## 二、网页断点动态分析
如果有一些实际的参数,会更好的分析。打开f12,然后重新加载网页
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531212942369.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
直接出现反调试,无限debugger。从函数调用堆栈可以看出,是从jquery.md5.js中出来的。接着查看一下这个js
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531213211415.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
可以看到大数组,又是一段ob混淆,继续进行反混淆,然后使用浏览器的Overrides功能,加载本地的js文件。(详细的Overrides功能教程可以参考鹅大的文章,地址:[基于Chrome Overrides和Initiator进行js分析](https://blog.weimo.info/archives/598/))
设置好以后再次刷新,此时就不会出现无限debugger,可以自己设置断点
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531214118894.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
这里一共套了三层函数,分别为
| 函数名
|--|
|$.md5|
|lc|
|encode_url|
$.md5函数是标准的md5函数,自己随便试一下就可以得知,接着跟进去lc函数
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531214526651.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
lc函数里面又套了4层函数,_0x2f6ff0是加盐,在传入的字符串前加上固定的"17325841932717338791732584194271733878",然后另外三个函数共同组成一个标准的md5。
前面两个函数都比较简单,接着看看encode_url
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531214906695.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531215307335.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
encode_url函数的函数实体就是_0x3c4b2e函数,里面主要是md5,字符串切分拼接,异或等运算。我这里就不做python的还原了,直接扣js出来用。那么三个函数搞定了,请求也应该顺理成章的搞定了。
## 三.扣取代码,使用python实现自动化请求
将_0x3c4b2e函数扣出来,然后使用node运行,提示缺什么函数没有定义,就继续扣出来。这里没有涉及环境检测等问题,所以一直复制粘贴到不报错就可以了
最后增加一段自定义的代码来实现加密还是解密
```javascript
var do_type = process.argv;
if (do_type === 'ENCODE'){
console.log(_0x3c4b2e(process.argv, "ENCODE", process.argv, 0));
}else {
console.log(_0x3c4b2e(process.argv, "DECODE", process.argv));
}
```
为什么还有解密呢?再回到第一次解混淆的js。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531215747171.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center)
当请求成功时,返回的url是加密的,需要解密后才是真实地址。下面是完整python代码
```python
import requests
import os
import re
import hashlib
from urllib import parse
def main():
url = 'https://jsap.attakids.com/?url=https://v.qq.com/x/cover/mzc0020002ka95z/k0036081njj.html'
response = requests.get(url)
salt = '17325841932717338791732584194271733878'
Domain = re.findall('(?<=var Domain = ").+?(?=")', response.text)
Vurl = re.findall('(?<=var Vurl = ").+?(?=")', response.text)
Vkey = re.findall('(?<=var Vkey = ").+?(?=")', response.text)
Key = re.findall('(?<=var Key = ").+?(?=")', response.text)
Version = re.findall('(?<=var Version = ").+?(?=")', response.text)
Time = re.findall('(?<=var Time = )\d+', response.text)
sign_v2 = re.findall('var Sign = encode_url.+', response.text)
sign_v2 = re.findall("(?<=').{32}(?=')", sign_v2)
Token_v2 = re.findall('var Token = encode_url.+', response.text)
Token_v2 = re.findall("(?<=').{32}(?=')", Token_v2)
url = response.url
Host = parse.urlparse(url).netloc
Sign = Host+Time+Vurl+Key
Sign = salt + hashlib.md5(Sign.encode()).hexdigest()
Sign = hashlib.md5(Sign.encode()).hexdigest()
nodejs = os.popen('node attakids ENCODE ' + Sign + ' ' + sign_v2)
Sign = nodejs.read().replace('\n', '').replace('+', '-').replace('/', '_').replace('=', '.')
nodejs.close()
Token = Domain+Time+Vurl+Sign
Token = salt + hashlib.md5(Token.encode()).hexdigest()
Token = hashlib.md5(Token.encode()).hexdigest()
nodejs = os.popen('node attakids ENCODE ' + Token + ' ' + Time + Token_v2)
Token = nodejs.read().replace('\n', '').replace('+', '-').replace('/', '_').replace('=', '.')
nodejs.close()
headers = {
'Token': Vkey,
'access-token': Vkey + '-' + Key + '-' + Sign + '-' + Token,
'Version': Version,
'cookie': 'uuid=' + Vkey + '-' + Key + '-' + Sign + '-' + Token,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
}
data = {
'url': Vurl,
'wap': 0,
'ios': 0,
'host': Host,
'key': Key,
'sign': Sign,
'token': Token,
'type': '',
'referer': '',
'time': Time
}
url = 'https://jsap.attakids.com/Api.php'
response = requests.post(url, headers=headers, data=data)
if response.status_code == 200:
response = response.json()
print(response)
title = response['title']
print(title)
enc_url = response['url'].replace('-', '+').replace('_', '/').replace('.', '=')
nodejs = os.popen('node attakids DECODE ' + enc_url + ' ' + hashlib.md5((Host + Token).encode()).hexdigest())
dec_url = nodejs.read().replace('\n', '')
nodejs.close()
dec_url = 'https://'+Host+parse.unquote(dec_url)
print(dec_url)
if response['type'] == 'hls':
response = requests.get(dec_url)
print(response.text)
else:
print(response.status_code)
print(response.content)
if __name__ == '__main__':
main()
```
js代码就不放出来了,有兴趣的可以自己尝试扣取js测试
最后结果图
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210531220508436.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pqcTU5Mjc2NzgwOQ==,size_16,color_FFFFFF,t_70#pic_center) 漁滒 发表于 2021-6-4 15:00
工具需要自己用babel模块来写
//babel库及文件模块导入
const fs = require('fs');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const types = require("@babel/types");
const generator = require("@babel/generator").default;
/**********************************************************
命令行输入,获取混淆前的js源代码及解混淆后生成的新的js代码。
eg: node decode_obfuscator.js encode.js decode_result.js
encode.js 混淆前js源代码的路径
decode_result.js 生成新js代码的路径
默认 混淆前js源代码的路径为 ./encode.js
默认 生成新js代码的路径为 ./decode_result.js
***********************************************************/
let encode_file = "./source.js",
decode_file = "./decode_result1.js";
if (process.argv.length > 2) {
encode_file = process.argv;
}
if (process.argv.length > 3) {
decode_file = process.argv;
}
let jscode = fs.readFileSync(encode_file, { encoding: "utf-8" });
let ast = parser.parse(jscode);
/***********************************************************
NumericLiteral ---> Literal
StringLiteral---> Literal
用于处理已十六进制显示的字符串或者数值
***********************************************************/
const delete_extra = {
"NumericLiteral|StringLiteral": function(path) {
delete path.node.extra;
},
}
traverse(ast, delete_extra);
/********************************************************
BinaryExpression --> Literal,object对象还原预处理
UnaryExpression--> Literal,object对象还原预处理
********************************************************/
const combin_BinaryExpression = {
"BinaryExpression|UnaryExpression|ConditionalExpression" (path) {
if (path.type == "UnaryExpression" && path.node.operator == "-") {
return;
}
const { confident, value } = path.evaluate();
value != "Infinity" && confident && path.replaceInline(types.valueToNode(value));
},
}
traverse(ast, combin_BinaryExpression);
/********************************************************************
替换函数调用处的字符串 即 CallExpression ----> StringLiteral
obfuscator 混淆过的js代码特征很明显 大数组 + 移位函数 + 解密函数,
然后在其他地方多次调用该解密函数
下面的插件将调用处的CallExpression直接计算出来,然后再替换值。
*********************************************************************/
const decode_str = {
VariableDeclarator(path) {
let { id, init } = path.node;
if (!types.isArrayExpression(init) || init.elements.length == 0) return;
let code = path.toString();
let second_sibling = path.parentPath.getNextSibling(); //获取移位函数节点
let third_sibling = second_sibling.getNextSibling(); //获取解密函数节点
//******************************************************特征判断开始
if (!second_sibling.isExpressionStatement() ||
!third_sibling.isVariableDeclaration()) {
return;
}
let expression = second_sibling.get('expression');
if (!expression.isCallExpression()) return;
let { callee, arguments } = expression.node;
if (!types.isFunctionExpression(callee) || arguments.length < 2 ||
!types.isIdentifier(arguments, { name: id.name }) ||
!types.isNumericLiteral(arguments)) {
return;
}
let declarations = third_sibling.node.declarations;
if (declarations.length < 1 || !types.isFunctionExpression(declarations.init)) {
return;
}
//******************************************************特征判断结束
let end = third_sibling.node.end; //防止遍历函数体里的调用
let func_name = third_sibling.node.declarations.id.name; //解密函数的函数名,用于遍历其作用域
let source_code = second_sibling.toString();
if (source_code.indexOf('removeCookie') !== -1) { //如果含有检测格式化的代码,处理移位函数及解密函数
let second_arg_node = callee.params;
let body = callee.body.body;
let call_fun = body.declarations.id;
body.pop();
body.push(types.ExpressionStatement(types.UpdateExpression("++", second_arg_node)));
body.push(types.ExpressionStatement(types.CallExpression(call_fun, )));
third_sibling.traverse({
AssignmentExpression(_path) {
let left = _path.get('left');
let left_code = left.toString();
let right = _path.get('right');
let right_code = right.toString();
if (right_code.indexOf(func_name) === -1 || right_code.indexOf(left_code) === -1) {
return;
}
const if_parent_path = _path.findParent(p => { return p.isIfStatement(); });
if_parent_path && if_parent_path.replaceWith(_path.node);
},
})
}
code += ';!' + second_sibling.toString() + third_sibling.toString();
//eval到本地环境
eval(code);
const binding = third_sibling.scope.getBinding(func_name);
if (!binding || binding.constantViolations.length > 0) {
return;
}
let can_removed = true;
for (const refer_path of binding.referencePaths) {
if (refer_path.node.start < end) {
continue;
}
let call_path = refer_path.findParent(p => { return p.isCallExpression(); });
try {
let value = eval(call_path.toString());
console.log(call_path.toString(), "-->", value);
call_path.replaceWith(types.valueToNode(value))
} catch (e) { can_removed = false; }
}
if (can_removed) {
path.remove();
second_sibling.remove();
third_sibling.remove();
}
},
}
traverse(ast, decode_str);
traverse(ast, combin_BinaryExpression);
//SequenceExpression ---> ExpressionStatement,object对象还原预处理
const decode_comma = {
ExpressionStatement(path) {
//****************************************特征判断开始
let prev_sibling = path.getPrevSibling();
if (!prev_sibling.isVariableDeclaration()) return;
let { declarations } = prev_sibling.node;
if (declarations.length < 1) return;
let { id, init } = declarations;
if (!types.isObjectExpression(init)) return;
let { expression } = path.node;
if (!types.isSequenceExpression(expression)) return;
//****************************************特征判断结束
let body = [];
expression.expressions.forEach(express => {
body.push(types.ExpressionStatement(express));
})
path.replaceInline(body);
},
}
traverse(ast, decode_comma);
/*******************************************************
还原object,key多为字符串,value为字符串和函数
*******************************************************/
const decode_object = {
VariableDeclarator(path) {
const { id, init } = path.node;
if (!types.isObjectExpression(init)) return;
let name = id.name;
let properties = init.properties;
let all_next_siblings = path.parentPath.getAllNextSiblings();
for (let next_sibling of all_next_siblings) {
if (!next_sibling.isExpressionStatement()) break;
let expression = next_sibling.get('expression');
if (!expression.isAssignmentExpression()) break;
let { operator, left, right } = expression.node;
if (operator != '=' || !types.isMemberExpression(left) ||
!types.isIdentifier(left.object, { name: name }) || !types.isStringLiteral(left.property)) {
break;
}
properties.push(types.ObjectProperty(left.property, right));
next_sibling.remove();
}
if (properties.length == 0) {
return;
}
let scope = path.scope;
let next_sibling = path.parentPath.getNextSibling();
if (next_sibling.isVariableDeclaration()) {
let declarations = next_sibling.node.declarations;
if (declarations.length > 0 && types.isIdentifier(declarations.init, { name: name })) {
scope.rename(declarations.id.name, name);
next_sibling.remove();
}
}
for (const property of properties) { //预判是否为 obfuscator 混淆的object
let key = property.key.value;
let value = property.value;
if (!types.isStringLiteral(value) && !types.isFunctionExpression(value)) {
return;
}
}
for (const property of properties) {
let key = property.key.value;
let value = property.value;
if (types.isLiteral(value)) {
scope.traverse(scope.block, {
MemberExpression(_path) {
let _node = _path.node;
if (!types.isIdentifier(_node.object, { name: name })) return;
if (!types.isLiteral(_node.property, { value: key })) return;
_path.replaceWith(value);
},
})
} else if (types.isFunctionExpression(value)) {
let ret_state = value.body.body;
if (!types.isReturnStatement(ret_state)) continue;
scope.traverse(scope.block, {
CallExpression: function(_path) {
let { callee, arguments } = _path.node;
if (!types.isMemberExpression(callee)) return;
if (!types.isIdentifier(callee.object, { name: name })) return;
if (!types.isLiteral(callee.property, { value: key })) return;
let replace_node = null;
if (types.isCallExpression(ret_state.argument) && arguments.length > 0) {
replace_node = types.CallExpression(arguments, arguments.slice(1));
} else if (types.isBinaryExpression(ret_state.argument) && arguments.length === 2) {
replace_node = types.BinaryExpression(ret_state.argument.operator, arguments, arguments);
} else if (types.isLogicalExpression(ret_state.argument) && arguments.length === 2) {
replace_node = types.LogicalExpression(ret_state.argument.operator, arguments, arguments);
}
replace_node && _path.replaceWith(replace_node);
}
})
}
}
path.remove();
},
}
traverse(ast, decode_object);
/*******************************************************
去控制流
*******************************************************/
const decode_while = {
WhileStatement(path) {
const { test, body } = path.node;
if (!types.isLiteral(test, { value: true }) || body.body.length === 0 || !types.isSwitchStatement(body.body)) return;
let switch_state = body.body;
let { discriminant, cases } = switch_state;
if (!types.isMemberExpression(discriminant) || !types.isUpdateExpression(discriminant.property)) return;
let arr_name = discriminant.object.name;
let arr = [];
let all_pre_siblings = path.getAllPrevSiblings();
all_pre_siblings.forEach(pre_path => {
const { declarations } = pre_path.node;
let { id, init } = declarations;
if (arr_name == id.name) {
//arr = init.callee.object.value.split("|");
}
pre_path.remove();
})
let ret_body = [];
arr.forEach(index => {
let case_body = cases.consequent;
if (types.isContinueStatement(case_body)) {
case_body.pop();
}
ret_body = ret_body.concat(case_body);
})
path.replaceInline(ret_body);
},
}
traverse(ast, decode_while);
/***************************************************
处理IfStatement,规范If表达式
删除条件已知的语句
***************************************************/
traverse(ast, combin_BinaryExpression);
const decode_if = {
IfStatement(path) {
let { test, consequent, alternate } = path.node;
if (!types.isBlockStatement(consequent)) {
path.node.consequent = types.BlockStatement();
}
if (alternate !== null && !types.isBlockStatement(alternate)) {
path.node.alternate = types.BlockStatement();
}
if (!types.isLiteral(test)) return;
let value = test.value;
consequent = path.node.consequent;
alternate = path.node.alternate;
if (value) {
path.replaceInline(consequent.body);
} else {
alternate === null ? path.remove() : path.replaceInline(alternate.body);
}
},
EmptyStatement(path) {
path.remove();
},
}
traverse(ast, decode_if);
/************************************
处理完毕,生成新代码
*************************************/
let { code } = generator(ast);
fs.writeFile(decode_file, code, (err) => {}); 学习了,最近也在忙于这个破解,楼主有兴趣可以破解这个,http://kj.gouhys.cn/appzy/09031916/?url=https://v.qq.com/x/cover/mzc00200wi4iqo4/v0036evukju.html;
我找到了核心断点: hpostpm,(https://api.jiubojx.com/vip/?url=https://v.qq.com/x/cover/mzc00200wi4iqo4/v0036evukju.html)扣代码出来鬼鬼调试一运行就卡死,求指导
wannabe 发表于 2021-5-31 22:22
学习了,最近也在忙于这个破解,楼主有兴趣可以破解这个,http://kj.gouhys.cn/appzy/09031916/?url=https: ...
下次有空看一下 漁滒 发表于 2021-5-31 22:25
下次有空看一下
嗯嗯,断点好找,但是抠出来之后js,鬼鬼调试js 就卡死! 大佬牛逼 写的不错,大部分都看懂了,有点营养呀!~~~谢谢了 学习一下。
大体思路,知道是怎么回事。
但是,细节上有些还不明白啊。
继续学习啊,自己给自己加油。
小白一个,看来自己还需要努力啊 兄弟好样的,前一段时间想要拿下他,奈何败在了混淆JS上,你这简直是雪中送炭 优秀,多发帖多学习{:301_993:}