前言

前两天看到大佬搞了一个wasm套vmp壳,极大提升了还原纯算的难度。

但是wasm不能直接从浏览器取环境,必须通过初始化时传入的js方法取浏览器环境,因此把导入对象中的函数抠出来就可以黑盒调用,这可能是谷歌基于浏览器安全以及wasm的多平台生态特性做出的取舍。因此自己写了一套简单的js转汇编代码,源码见文章底部。

在线体验地址(可检测是否使用dp打开页面,签名算法使用了asm vmp):

DrissionPage Check

效果

输入:

var request = {
    data: "a=a&b=b&c=c&d=d",
    url: "/api/v2/xxxx/xxxx"
}
var ua = navigator.userAgent;
var string = request.data + request.url + ua;
function myhash(string, goon) {
    var salt = "asm_v0.0.1_未混淆版本";
    var inputString = string + salt;
    var hash = 0;
    var hashString = '';
    for (var i = 0; i < inputString.length; i++) {
        hash = (hash << 5) - hash + inputString.charCodeAt(i);
        hash = hash & hash;
    }
    hashString = hash.toString(16).split('-').join('');
    if (!goon) {
        for (;hashString.length < 56;) {
            hashString += myhash(hashString, true);
        }
        if (hashString.length > 56) {
            hashString = hashString.substring(0, 56);
        }
    }
    hashString = hashString.split('').reverse().join('');
    return hashString;
}
var sign = myhash(string, '');
console.log(sign);

调用asmCompile后输出:

VAR request
PUSH a=a&b=b&c=c&d=d
PUSH /api/v2/xxxx/xxxx
OBJECT data,url
STORE request
VAR ua
LOAD navigator
LOAD_MEMBER userAgent
STORE ua
VAR string
LOAD request
LOAD_MEMBER data
LOAD request
LOAD_MEMBER url
ADD 
LOAD ua
ADD 
STORE string
FUNC myhash,string,goon
VAR salt
PUSH asm_v0.0.1_未混淆版本
STORE salt
VAR inputString
LOAD string
LOAD salt
ADD 
STORE inputString
VAR hash
PUSH vm0xi0
STORE hash
VAR hashString
PUSH 
STORE hashString
VAR i
PUSH vm0xi0
STORE i
LOAD i
LOAD inputString
LOAD_MEMBER length
LT 
JMP_IF_FALSE vm0xi12
LOAD hash
PUSH vm0xi5
SHL 
LOAD hash
SUB 
LOAD i
LOAD inputString
CALL charCodeAt
ADD 
STORE hash
LOAD hash
LOAD hash
ANDB 
STORE hash
LOAD i
INC i
JMP vm0xi-16
PUSH 
PUSH -
PUSH vm0xi10
LOAD hash
CALL toString
CALL split
CALL join
STORE hashString
LOAD goon
UNARY !
JMP_IF_FALSE vm0xi17
LOAD hashString
LOAD_MEMBER length
PUSH vm0xi38
LT 
JMP_IF_FALSE vm0xi8
LOAD hashString
LOAD hashString
PUSH true
CALL myhash
ADD 
STORE hashString
JMP vm0xi-c
LOAD hashString
LOAD_MEMBER length
PUSH vm0xi38
GT 
JMP_IF_FALSE vm0xi6
PUSH vm0xi0
PUSH vm0xi38
LOAD hashString
CALL substring
STORE hashString
PUSH 
PUSH 
LOAD hashString
CALL split
CALL reverse
CALL join
STORE hashString
LOAD hashString
RETURN 
ENDFUNC myhash
VAR sign
LOAD string
PUSH 
CALL myhash
STORE sign
LOAD sign
LOAD console
CALL log

将伪asm代码传入asmRun运行获得结果:

实现

其实就是一个栈式虚拟机,只不过是以汇编的形式表达及运行。

与wasm和一般的vmp相比有以下优缺点:

优点

  1. 迁移方便。自己自定义其它语言的指令运行代码后,就可以把相应语言的加密代码直接转汇编后直接丢到js中用。

  2. 前置混淆方便。编译代码基于AST抽象语法树,可以直接使用js写好的ob混淆、webpack、控制流甚至vmp插件先将源码进行混淆,再丢进来转为汇编。

  3. 可扩展性好。基于原生js,对浏览器的环境检测不会有wasm必的局限性。

缺点

  1. 慢。vmp的通病。

  2. 代码可读性高。不前置混淆的话,汇编可读性高于一般的vmp,容易被逆向。

  3. 调试困难。同一般vmp一样,编译过程/运行过程产生的某些"特性bug"难以调试。

自定义

修改asmCompile中的hunxiao方法,在此方法中实现自己的js混淆逻辑(ob、控制流、再套vmp等),就可以输出套了vmp的伪汇编,绝对提升还原难度。

这里只对对象存取、函数定义及调用、if/for循环等进行了编译,很多运算符也写了同类型的一两个实现示例,其它的运算符以及语法实现根据程序抛出的异常自己加就好。

源码

js2asm源代码(感觉没什么讲解的必要,对vmp熟悉的很容易就看懂了):

// 原文地址: http://yazong.pro/archives/1717385747347
var generator = require("@babel/generator").default;
var parser = require("@babel/parser");
var traverse = require("@babel/traverse").default;
var t = require("@babel/types");

// 中间语言虚拟机ast版
function asmCompile(jscode) {
    // 将AST中的嵌套语句拆分, 也可在此加ob混淆, vmp混淆等
    function hunxiao(ast) {
        var objEzPlugin = {
            MemberExpression(path) {
                // 把a.b变成a["b"]
                var node = path.node;
                var obj = node.object;
                var property = node.property;
                if (property.type == 'Identifier') {
                    path.replaceWith(t.memberExpression(obj, t.stringLiteral(property.name), true));
                }
            },
        }
        traverse(ast, objEzPlugin);
        // console.log(generator(ast).code);
    }
    function get_op_list(node, il_zhiling, labels) {
        if (!node){
            return il_zhiling;
        }
        if (!il_zhiling) {
            il_zhiling = [];
            labels = [];
        }
        switch (node.type) {
            case 'File':
                get_op_list(node.program, il_zhiling, labels);
                break;
            case 'Program':
                node.body.forEach(statement => get_op_list(statement, il_zhiling, labels));
                break;
            case 'VariableDeclaration':
                node.declarations.forEach(declarator => get_op_list(declarator, il_zhiling, labels));
                break;
            case 'VariableDeclarator':
                il_zhiling.push({ op: 'VAR', args: [node.id.name] });
                get_op_list(node.init, il_zhiling, labels);
                il_zhiling.push({ op: 'STORE', args: [node.id.name] });
                break;
            case 'StringLiteral':
                il_zhiling.push({ op: 'PUSH', args: [node.value] });
                break;
            case 'NumericLiteral':
                il_zhiling.push({ op: 'PUSH', args: [node.value] });
                break;
            case 'Identifier':
                il_zhiling.push({ op: 'LOAD', args: [node.name] });
                break;
            case 'BinaryExpression':
                get_op_list(node.left, il_zhiling, labels);
                get_op_list(node.right, il_zhiling, labels);
                switch (node.operator) {
                    case '+': il_zhiling.push({ op: 'ADD', args: [] }); break;
                    case '-': il_zhiling.push({ op: 'SUB', args: [] }); break;
                    case '*': il_zhiling.push({ op: 'MUL', args: [] }); break;
                    case '/': il_zhiling.push({ op: 'DIV', args: [] }); break;
                    case '>': il_zhiling.push({ op: 'GT', args: [] }); break;
                    case '<': il_zhiling.push({ op: 'LT', args: [] }); break;
                    case '&': il_zhiling.push({ op: 'ANDB', args: [] }); break;
                    case '<<': il_zhiling.push({ op: 'SHL', args: [] }); break;
                    case '==': il_zhiling.push({ op: 'EQ', args: [] }); break;
                    case '!=': il_zhiling.push({ op: 'NEQ', args: [] }); break;
                    case '%': il_zhiling.push({ op: 'MOD', args: [] }); break;
                    default: throw new Error('Unsupported operator: ' + node.operator);
                }
                break;
            case 'ExpressionStatement':
                get_op_list(node.expression, il_zhiling, labels);
                break;
            case 'CallExpression':
                node.arguments.forEach(arg => get_op_list(arg, il_zhiling, labels));
                get_op_list(node.callee, il_zhiling, labels);
                il_zhiling.push({ op: 'CALL', args: [node.arguments.length] });
                break;
            case 'MemberExpression':
                get_op_list(node.object, il_zhiling, labels);
                get_op_list(node.property, il_zhiling, labels);
                il_zhiling.push({ op: 'LOAD_MEMBER', args: [] });
                break;
            case 'AssignmentExpression':
                switch (node.operator) {
                    case '=': 
                        if (node.left.type === 'MemberExpression') {
                            get_op_list(node.left.object, il_zhiling, labels);
                            get_op_list(node.right, il_zhiling, labels);
                            il_zhiling.push({ op: 'STORE_MEMBER', args: [node.left.property.value] });
                        } else {
                            get_op_list(node.right, il_zhiling, labels);
                            il_zhiling.push({ op: 'STORE', args: [node.left.name] });
                        }
                        break;
                    case '+=':
                        if (node.left.type === 'MemberExpression') {
                            throw new Error('+= not supported for member expressions');
                        } else {
                            il_zhiling.push({ op: 'LOAD', args: [node.left.name] });
                            get_op_list(node.right, il_zhiling, labels);
                            il_zhiling.push({ op: 'ADD', args: [] });
                            il_zhiling.push({ op: 'STORE', args: [node.left.name] });
                        }
                        break;
                    default:
                        throw new Error('Unsupported operator: ' + node.operator);
                }
                break;
            case 'ObjectExpression':
                node.properties.forEach(property => get_op_list(property.value, il_zhiling, labels));
                il_zhiling.push({ op: 'OBJECT', args: [node.properties.map(prop => prop.key.name)] });
                break;
            case 'Property':
                get_op_list(node.value, il_zhiling, labels);
                break;
            case 'FunctionDeclaration':
            case 'FunctionExpression':
                var funcName = node.id ? node.id.name : null;
                var params = node.params.map(param => param.name);
                il_zhiling.push({ op: 'FUNC', args: [funcName, params] });
                get_op_list(node.body, il_zhiling, labels);
                il_zhiling.push({ op: 'ENDFUNC', args: [funcName] });
                break;
            case 'BlockStatement':
                node.body.forEach(statement => get_op_list(statement, il_zhiling, labels));
                break;
            case 'IfStatement':
                get_op_list(node.test, il_zhiling, labels);
                var jmpIfFalseIndex = il_zhiling.length;
                il_zhiling.push({ op: 'JMP_IF_FALSE', args: [0] });  // Placeholder
                var consequentStart = il_zhiling.length;
                get_op_list(node.consequent, il_zhiling, labels);
                var consequentEnd = il_zhiling.length;
                if (node.alternate) {
                    var jmpIndex = il_zhiling.length;
                    il_zhiling.push({ op: 'JMP', args: [0] });  // Placeholder
                    var alternateStart = il_zhiling.length;
                    get_op_list(node.alternate, il_zhiling, labels);
                    var alternateEnd = il_zhiling.length;
                    il_zhiling[jmpIfFalseIndex].args[0] = consequentEnd - jmpIfFalseIndex;
                    il_zhiling[jmpIndex].args[0] = alternateEnd - jmpIndex;
                } else {
                    il_zhiling[jmpIfFalseIndex].args[0] = consequentEnd - jmpIfFalseIndex;
                }
                break;
            case 'ForStatement':
                get_op_list(node.init, il_zhiling, labels);
                var testLabel = il_zhiling.length;
                get_op_list(node.test, il_zhiling, labels);
                var jmpIfFalseForIndex = il_zhiling.length;
                il_zhiling.push({ op: 'JMP_IF_FALSE', args: [0] });  // Placeholder
                var bodyStartLabel = il_zhiling.length;
                get_op_list(node.body, il_zhiling, labels);
                get_op_list(node.update, il_zhiling, labels);
                il_zhiling.push({ op: 'JMP', args: [testLabel - il_zhiling.length - 1] });
                var bodyEndLabel = il_zhiling.length;
                il_zhiling[jmpIfFalseForIndex].args[0] = bodyEndLabel - jmpIfFalseForIndex;
                break;
            case 'WhileStatement':
                var whivarestLabel = il_zhiling.length;
                get_op_list(node.test, il_zhiling, labels);
                var jmpIfFalseWhileIndex = il_zhiling.length;
                il_zhiling.push({ op: 'JMP_IF_FALSE', args: [0] });  // Placeholder
                var whileBodyStartLabel = il_zhiling.length;
                get_op_list(node.body, il_zhiling, labels);
                il_zhiling.push({ op: 'JMP', args: [whivarestLabel - il_zhiling.length - 1] });
                var whileBodyEndLabel = il_zhiling.length;
                il_zhiling[jmpIfFalseWhileIndex].args[0] = whileBodyEndLabel - jmpIfFalseWhileIndex;
                break;
            case 'UpdateExpression':
                get_op_list(node.argument, il_zhiling, labels);
                il_zhiling.push({ op: node.operator === '++' ? 'INC' : 'DEC', args: [node.argument.name] });
                break;
            case 'UnaryExpression':
                get_op_list(node.argument, il_zhiling, labels);
                il_zhiling.push({ op: 'UNARY', args: [node.operator] });
                break;
            case 'BooleanLiteral':
                il_zhiling.push({ op: 'PUSH', args: [node.value] });
                break;
            case 'ReturnStatement':
                get_op_list(node.argument, il_zhiling, labels);
                il_zhiling.push({ op: 'RETURN', args: [] });
                break;
            case 'ArrayExpression':
                node.elements.forEach(element => get_op_list(element, il_zhiling, labels));
                il_zhiling.push({ op: 'ARRAY', args: [node.elements.length] });
                break;
            case 'ThisExpression':
                il_zhiling.push({ op: 'PUSHTHIS', args: [] });
                break;
            case 'EmptyStatement':
                break;
            case 'DebuggerStatement':
                il_zhiling.push({ op: 'DBG', args: [] });
                break;
            default:
                throw new Error(`Unsupported node type: ${node.type}`);
        }
        return il_zhiling;
    }
    function formatInstructions(il_zhiling_list) {
        return il_zhiling_list.map(instr => {
            function switchToString(arg) {
                if (Array.isArray(arg)) {
                    return arg.map(switchToString);
                } else if (typeof arg === 'number') {
                    return `vm0xi${arg.toString(16)}`;
                } else if (typeof arg === 'boolean'){
                    if (arg) return 'vmbl-1';
                    else return 'vmbl-0';
                } else {
                    return arg;
                }
            }
            args = instr.args.map(switchToString);
            args = args.join(',');
            return `${instr.op} ${args}`;
        }).join('\n');
    }
    ast_tree = parser.parse(jscode);
    hunxiao(ast_tree); // 打开就调用此方法对js混淆,再转化为汇编
    il_zhiling_list = get_op_list(ast_tree);
    return formatInstructions(il_zhiling_list);
}

function asmRun(il_langauge) {
    function parseInstructions(instructionString) {
        var lines = instructionString.split('\n');
        var instructions = lines.map(line => {
            var [op, ...args] = line.split(' ');
            switch (op) {
                case 'PUSH':
                    args = [args.join(' ')];
                    break;
                case 'OBJECT':
                    args = [args[0].split(',')]
                    break;
                case 'FUNC':
                    args = args[0].split(',');
                    if (args[1] !== ''){
                        args[1] = args.slice(1);
                        args.splice(2);
                    } else {
                        args[1] = [];
                    }
            }
            function switchToNumber(arg) {
                if (Array.isArray(arg)) {
                    return arg.map(switchToNumber);
                } else if (typeof arg === 'string' && arg.startsWith('vm0xi')) {
                    return parseInt(arg.slice(5), 16);
                } else if (typeof arg === 'string' && arg.startsWith('vmbl')) {
                    if (arg === 'vmbl-1') return true;
                    else if (arg === 'vmbl-0') return false;
                } else {
                    return arg;
                }
            }
            args = args.map(switchToNumber);
            return { op, args };
        });
        return instructions;
    }
    function run_zhiling(il_zhiling, functions = {}, localVars = globalThis, context = globalThis) {
        var stack = [];
        var ip = 0;
        while (ip < il_zhiling.length) {
            var instr = il_zhiling[ip];
            // console.log(JSON.stringify(instr))
            switch (instr.op) {
                case 'VAR':
                    localVars[instr.args[0]] = localVars[instr.args[0]] || undefined;
                    break;
                case 'PUSH':
                    stack.push(instr.args[0]);
                    break;
                case 'STORE':
                    localVars[instr.args[0]] = stack.pop();
                    break;
                case 'LOAD':
                    stack.push(localVars[instr.args[0]]);
                    break;
                case 'ADD':
                    right = stack.pop();
                    left = stack.pop();
                    stack.push(left + right);
                    break;
                case 'SUB':
                    var subtrahend = stack.pop();
                    var minuend = stack.pop();
                    stack.push(minuend - subtrahend);
                    break;
                case 'CALL':
                    var func = stack.pop();
                    if (typeof func === 'function') {
                        var args = [];
                        for (var i = 0; i < instr.args[0]; i++) {
                            args.push(stack.pop());
                        }
                        args.reverse();
                        stack.push(func.apply(func.thisContext, args));
                    } else if (func && func.body && func.params) {
                        var args = [];
                        var newLocalVars = {};
                        for (var i = 0; i < instr.args[0]; i++) {
                            args.push(stack.pop());
                        }
                        var record_args = [];
                        for (var i=0; i<args.length; i++) {
                            record_args.push(args[i]);
                        }
                        record_args.reverse();
                        Object.getOwnPropertyNames(localVars).forEach(varName => {
                            newLocalVars[varName] = localVars[varName];
                        });
                        for (var i = 0; i < func.params.length; i++) {
                            newLocalVars[func.params[i]] = args.pop();
                        }
                        newLocalVars.arguments = record_args;
                        stack.push(run_zhiling(func.body, functions, newLocalVars, func.thisContext));
                    } else {
                        throw new Error(`Unsupported function: ${func}`);
                    }
                    break;
                case 'FUNC':
                    !function(){
                        var params = instr.args[1];
                        var funcName = instr.args[0];
                        functions[instr.args[0]] = { params: params, body: [] };
                        while (il_zhiling[++ip].op !== 'ENDFUNC' || il_zhiling[ip].args[0] !== instr.args[0]) {
                            functions[instr.args[0]].body.push(il_zhiling[ip]);
                        }
                        var newFunc = function(...args) {
                            var newLocalVars = {};
                            Object.getOwnPropertyNames(localVars).forEach(varName => {
                                newLocalVars[varName] = localVars[varName];
                            });
                            for (var i = 0; i < params.length; i++) {
                                newLocalVars[params[i]] = args[i];
                            }
                            var newFunctions = {};
                            for (var old_stack_func in functions) {
                                if (old_stack_func == 'getDate'){
                                    // debugger
                                }
                                newFunctions[old_stack_func] = functions[old_stack_func];
                            }
                            newLocalVars.arguments = args; // 设置 arguments 对象
                            return run_zhiling(functions[funcName].body, newFunctions, newLocalVars, this);
                        };
                        newFunc.aaa = funcName;
                        localVars[funcName] = newFunc;
                    }()
                    break;
                case 'OBJECT':
                    var obj = {};
                    arg_len = instr.args[0].length;
                    for (var i = 0; i < arg_len; i++) {
                        obj[instr.args[0].pop()] = stack.pop();
                    }
                    delete obj[''];
                    stack.push(obj);
                    break;
                case 'JMP':
                    ip += instr.args[0];
                    break;
                case 'JMP_IF_FALSE':
                    if (!stack.pop()) {
                        ip += instr.args[0] - 1;
                    }
                    break;
                case 'INC':
                    localVars[instr.args[0]]++;
                    break;
                case 'UNARY':
                    var arg = stack.pop();
                    switch (instr.args[0]) {
                        case '-':
                            stack.push(-arg);
                            break;
                        case '+':
                            stack.push(+arg);
                            break;
                        case '!':
                            stack.push(!arg);
                            break;
                        case '~':
                            stack.push(~arg);
                            break;
                        case 'typeof':
                            stack.push(typeof arg);
                            break;
                        default:
                            throw new Error(`Unsupported unary operator: ${instr.args[0]}`);
                    }
                    break;
                case 'LOAD_MEMBER':
                    var member = stack.pop();
                    var objRef = stack.pop();
                    if (Array.isArray(objRef) && localVars[member] !== undefined && typeof localVars[member] == 'number') {
                        stack.push(objRef[localVars[member]]);
                        objRef[localVars[member]].thisContext = objRef;
                    } else {
                        stack.push(objRef[member]);
                        objRef[member].thisContext = objRef;
                    }
                    break;
                case 'LT':
                    var b = stack.pop();
                    var a = stack.pop();
                    stack.push(a < b);
                    break;
                case 'SHL':
                    var shiftAmount = stack.pop();
                    var value = stack.pop();
                    stack.push(value << shiftAmount);
                    break;
                case 'ANDB':
                    var andbRight = stack.pop();
                    var andbLeft = stack.pop();
                    stack.push(andbLeft & andbRight);
                    break;
                case 'RETURN':
                    return stack.pop();
                case 'GT':
                    var gtRight = stack.pop();
                    var gtLeft = stack.pop();
                    stack.push(gtLeft > gtRight);
                    break;
                case 'ENDFUNC':
                    break;
                case 'EQ':
                    var eqRight = stack.pop();
                    var eqLeft = stack.pop();
                    stack.push(eqLeft == eqRight);
                    break;
                case 'NEQ':
                    var neqRight = stack.pop();
                    var neqLeft = stack.pop();
                    stack.push(neqLeft !== neqRight);
                    break;
                case 'ARRAY':
                    arg_len = instr.args[0];
                    array = [];
                    for (var i = 0; i < arg_len; i++) {
                        array.push(stack.pop());
                    }
                    array.reverse();
                    stack.push(array);
                    break;
                case 'PUSHTHIS':
                    stack.push(context);
                    break;
                case 'DBG':
                    debugger;
                    break;
                case '':
                    return;
                case 'MUL':
                    var mulRight = stack.pop();
                    var mulLeft = stack.pop();
                    stack.push(mulLeft * mulRight);
                    break;
                case 'STORE_MEMBER':
                    var value = stack.pop();
                    var objRef = stack.pop();
                    var member = instr.args[0];
                    objRef[member] = value;
                    break;
                case 'MOD':
                    var modRight = stack.pop();
                    var modLeft = stack.pop();
                    stack.push(modLeft % modRight);
                    break;
                default:
                    throw new Error(`Unsupported operation: ${instr.op}`);
            }
            ip++;
        }
        return stack.pop();
    }
    var op_list = parseInstructions(il_langauge);
    return run_zhiling(op_list);
}

exports.asmCompile = asmCompile;
exports.asmRun = asmRun;

使用

const { asmCompile, asmRun } = require('./js2asm');

jscodeTest = `
var request = {
    data: "a=a&b=b&c=c&d=d",
    url: "/api/v2/xxxx/xxxx"
}
var ua = navigator.userAgent;
var string = request.data + request.url + ua;
function myhash(string, goon) {
    var salt = "asm+ollvm+v0.0.1";
    var inputString = string + salt;
    var hash = 0;
    var hashString = '';
    for (var i = 0; i < inputString.length; i++) {
        hash = (hash << 5) - hash + inputString.charCodeAt(i);
        hash = hash & hash;
    }
    hashString = hash.toString(16).split('-').join('');
    if (!goon) {
        for (;hashString.length < 56;) {
            hashString += myhash(hashString, true);
        }
        if (hashString.length > 56) {
            hashString = hashString.substring(0, 56);
        }
    }
    hashString = hashString.split('').reverse().join('');
    return hashString;
}
var sign = myhash(string, '');
console.log(sign);
`

asmCode = asmCompile(jscodeTest);
console.log('编译后结果:');
console.log(asmCode);
console.log('------------------------------------');
navigator = {userAgent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"};
console.log('虚拟机运行结果:');
asmRun(asmCode);
console.log('------------------------------------');
console.log('控制台运行结果:');
eval(jscodeTest);