专栏首页逆向与安全H5应用加固防破解-js虚拟机保护方案浅谈

H5应用加固防破解-js虚拟机保护方案浅谈

目录: 一、为什么要对JS代码进行保护? 二、js代码保护前世今生 三、js虚拟保护方案与技术原理 四、总结

一、为什么要对JS代码进行保护?

1.1、H5应用场景

由于H5的跨平台优势,大大提高了用户体验,无需下载,只需要点开就可以观看,不需要下载后占据内存,简化流程。在传播的效率上也得到了很大的提升,可以通过微信平台的朋友圈、微信群、公众号等渠道得到了最大化的曝光,收获爆火的热度和话题度。但是在一定程度上,H5要实现以上各种应用功能,其实是JavaScript赋予了它更强大的能力。

1.2、目前存在的风险

JavaScript(简称“JS”)是一种具有函数优先的轻量级,解释型或即时编译型的高级编程语言。虽然它是作为开发Web页面的脚本语言而出名的,但是它也被用到了很多非浏览器环境中,JavaScript基于原型编程、多范式的动态脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。 如今各种h5应用场景越来越多也变得越来越复杂,满足了用户的各种需求,但是也存在一些安全方面的问题,比如:电商、金融、小游戏、小程序、招聘网站、旅游都存在登录、注册、支付、交易、信息展示等功能,如果这些业务所依赖的js代码被人轻易的破解、哪么应用将面临的安全问题有,商品信息被恶意爬取、价格、评分、评论信息盗取、原创内容盗取、羊毛党实现商品自动秒杀、批量注册小号领取优惠券,黄牛恶意占座,广告点击欺诈等等。 由于js脚本语言特性,代码是公开透明,由于这个原因,至使JS代码是不安全的,任何人都可以直接获取源代码阅读、分析、复制、盗用,甚至篡改。如果不想让js代码被恶意篡改攻击,保障业务的安全。那么使用JS保护是必需的。

1.3、如何对JS代码进行保护?

目前主流的方式是使JS代码不可读,增加攻击者理解代码功能复杂度,代码变得难也阅读之后,攻击者往往会进行动态跟踪调试,通过逆向还原出原始代码,或分析出程序功能。因此,使JS代码不可分析,增加反调式从防破解角度来看效果不佳。 随着浏览器技术的发展,动态调试器的功能越来越完善,把代码变得难也阅读这些保护方法很难起到很好的保护效果。 故该方案将js源码编译生成自定义指令集并通过实现一个虚拟机来模拟解释执行自定义指令的方法防止被破解、篡改,该方案由于是将js源码转换成自定义的任意指令,将原本比较容易理解的代码转换成只能自己虚拟机才能读懂与执行的指令,这样黑客无法通过直接反编译该指令分析或修改代码逻辑,攻击者需了解自定义指令细节,才能分析程序逻辑。避免了黑客直接通过获取前端js代码就能破解、篡改代码逻辑,从而极大提升了js代码的安全防破解能力。

二、js代码保护前世今生

2.1、现有产品介绍
360加固保H5加固

通过对JavaScript代码进行混淆加密、压缩等方式,保证H5的核心代码无法被破解和查看,从而降低非法篡改、恶意利用的风险。

爱加密H5移动应用安全加固

控制流平坦化、垃圾指令注入、常量字符串加密、常数加密、二元表达式加密、代码压缩、函数变量名混淆、禁止控制台输出

顶像H5代码混淆

H5代码除了混淆加密之外,顶象还独家提供H5接口Native化的保护,因H5代码即使混淆之后仍有可能存在被破解的情况,所以对H5中重要的接口提供保护之外,还将原本JS或H5代码Native为C/C++代码,极大增大破解的难度。

jscrambler

jscrambler是国外一家JavaScript保护领域老牌的安全公司,产品主要功能:具有多态性混淆、代码锁定和自我防御功能的Web和混合应用程序的弹性JavaScript保护。受保护的代码极难进行反向工程,并防止调试/篡改尝试。

jshaman

萨满科技是国内最专注于Web前端安全的JS代码保护团队。 Jshaman是专业的JS代码在线加密平台。 JShaman的含意是什么? JShaman = JS + Shaman,即:JS萨满。 在传统的世界观,或游戏概念中。萨满巫师具有治愈、辅助、守护的含意。 那么我们想赋予“JS萨满”的寓意是:治愈JS代码公开透明的缺陷、辅助JS开发、守护JS产品。 产品主要功能: 使JS代码不可读 因为JS代码是公开透明的,所以其安全问题的根本出自于可读性,使代码不可读成为了JS保护的首要需求。JS混淆,可以实现JS代码不可读化。 使JS代码不可分析 除了可读性之外,被跟踪分析是JS面临的另一个重大安全问题,一份不可读的代码仍有被攻击者动态跟踪调试以分析出技术原理的可能性。 使JS代码每次被调用(引用),代码自身可自动发生变异,变化为与之前完全不同的代码(功能完全不变,只是代码形式变异),以此杜绝代码被动态分析调试! 平展控制流 通过打乱函数执行流程,随机插入无用代码、函数等方式,改变原有的程序执行流程,进而达到防止代码被静态分析的目的。 明文字符加密 包括对变量名进行不易读重命名、对字符进行阵列化加密等,使代码中容易被攻击者参考的明文内容变的不可见,使代码分析更难以进行。

Js2x

JavaScript多态保护引擎、压缩代码、自我保护、平展控制流、多态、JS虚拟机字节码技术。

SecurityWorker

SecurityWorker提供完全隐匿且兼容ECMAScript 5.1的类WebWorker的安全可信环境,帮助保护你的核心Javascript代码不被破解。 SecurityWorker不同于普通的Javascript代码混淆,我们使用 独立Javascript VM + 二进制混淆opcode核心执行的方式防止您的代码被开发者工具调试、代码反向。

2.2、主流保护方案
js压缩:

将脚本进行编码,插入无用代码,干扰显示大量换行、注释、字符串等,运行时解码再eval执行。

运行环境监测:

代码自检验:计算某片断代码hash,运行时比较hash值是否相同来检测代码是否被篡改。 调试器检测:检测是否有调试特征、控制台是否打开、检测debugger指令是否执行。

字符串混淆

去除尽可能多的有意义信息,删除注释、空格、换行、冗余符号,变量重命名,变成a、b、c、1、2、3等,属性重命名,变成 a.a、a.b() ,将无用代码移除。 样例如下:

(function(a, b, c, d, e, f, g, ……) {
if (a[b] === c) {
d[e][f](g, ……)
……
}
流程混淆

对代码流程进行混淆,因为在代码开发的过程中,为了使代码逻辑清晰,便于维护和扩展,会把代码编写的逻辑非常清晰。这样攻击者也比较容易分析,一段代码从输入,经过各种if/else分支,顺序执行之后得到不同的结果,流程混淆是将这些执行流程和判定流程进行混淆,让攻击者没那么容易摸清楚代码的执行逻辑。 简单举例说明下,有如下没有混淆的代码:

(function () {
    console.log(1);
    console.log(2);
    console.log(3);
    console.log(4);
    console.log(5);
})();

流程如图1所示:

              图1

混淆过后代码如下:

(function () {
    var flow = '3|4|0|1|2'.split('|'), index = 0;
    while (!![]) {
        switch (flow[index++]) {
        case '0':
            console.log(3);
            continue;
        case '1':
            console.log(4);
            continue;
        case '2':
            console.log(5);
            continue;
        case '3':
            console.log(1);
            continue;
        case '4':
            console.log(2);
            continue;
        }
        break;
    }
}());

混淆过后的流程如图2所示:

              图2

从上图上看代码变了,所有的代码都挤到了一层当中,这样做的好处在于在让攻击者无法直观,或通过静态分析的方法判断哪些代码先执行哪些后执行,必需要通过动态运行才能记录执行顺序,增加了分析的负担。

三、js虚拟保护方案与技术原理

3.1、jsvmp总体架构

整体架构流程是服务器端通过对JavaScript代码词法分析 -> 语法分析 -> 语法树->生成AST->生成私有指令->生成对应私有解释器,将私有指令加密与私有解释器发送给浏览器,就开始一边解释,一边执行,如图3所示:

              图3

3.2、指令生成原理
指令生成流程

分为如下几个步骤: 第一步:读取分析整个源代码 第二步:编译器扫描函数声明和变量声明,做语法分析,有错则报语法错误 处理函数声明:如果出现函数命名冲突,会进行覆盖 处理变量声明:如果出现变量命名冲突,会忽略 第三步:将扫描到的函数和变量保存到一个对象中。 第四步:根据对象生成字段码。 整体流程如图4所示

              图4

什么是编译器

简单来说,编译器的功 能就是当一段代码经过编译器的词法分析、语法分析等阶段之后,会生成一个树状结构的“抽象语法树(AST)”,该语法树的每一个节点都对应着代码当中不同含义的片段。 示例代码:

function func(a) {
    var c = 10;
    var s = 22;
    var m = c + s;
    return b => a * b + m;
}
var ret = func(2);
print(ret(4));

上面代码通过编译后生成语法树如下:

[
  FunctionDeclaration {
    name: 'func',
    params: [ 'a' ],
    body: [
      VariableDeclaration {
        name: 'c',
        value: IntegerLiteral { value: 10 }
      },
      VariableDeclaration {
        name: 's',
        value: IntegerLiteral { value: 22 }
      },
      VariableDeclaration {
        name: 'm',
        value: BinaryExpression {
          lhs: IdentifierExpression { name: 'c' },
          op: '+',
          rhs: IdentifierExpression { name: 's' }
        }
      },
      ReturnStatement {
        value: FunctionDeclaration {
          name: null,
          params: [ 'b' ],
          body: [
            ReturnStatement {
              value: BinaryExpression {
                lhs: BinaryExpression {
                  lhs: IdentifierExpression { name: 'a' },
                  op: '*',
                  rhs: IdentifierExpression { name: 'b' }
                },
                op: '+',
                rhs: IdentifierExpression { name: 'm' }
              }
            }
          ],
          asExpression: true
        }
      }
    ],
    asExpression: false
  },
  VariableDeclaration {
    name: 'ret',
    value: CallExpression {
      expr: IdentifierExpression { name: 'func' },
      args: [ IntegerLiteral { value: 2 } ]
    }
  },
  ExpressionStatement {
    expr: CallExpression {
      expr: IdentifierExpression { name: 'print' },
      args: [
        CallExpression {
          expr: IdentifierExpression { name: 'ret' },
          args: [ IntegerLiteral { value: 4 } ]
        }
      ]
    }
  }
]
字节码bycode生成

解析语法树生成对应的自定义指令过程如下:

 compile(ctx) {
    const functionLabel = ctx.bc.newLabel();
    ctx.bc.write(OpCodes.OP_NEWFUNCTION);
    functionLabel.address();
    const innerCtx = ctx.with({
      scope: new Scope(ctx.scope),
      fn: {
        arity: this.params.length,
        bindings: [],
      },
    });
    const innerScope = innerCtx.scope;
    const skip = ctx.bc.newLabel();
    if (this.name !== null) {
      (this.asExpression ? innerCtx : ctx).scope.declareVariable(this.name);
    }
    for (const param of this.params) {
      innerScope.declareParameter(param);
    }
    ctx.bc.write(OpCodes.OP_JMP);
    skip.address();
    functionLabel.label();
    for (const statement of this.body) {
      statement.compile(innerCtx);
    }
    const { instructions } = ctx.bc;
    if (instructions[instructions.length - 3] !== OpCodes.OP_RET) {
      new ReturnStatement(new IntegerLiteral(0)).compile(innerCtx);
    }
    skip.label();
    for (const variable of innerCtx.fn.bindings) {
      IdentifierExpression.compileAccess(ctx, {
        ...variable,
        scopeNum: variable.scopeNum - 1,
      });
      ctx.write([
        OpCodes.OP_BINDVAR,
      ]);
    }
  }
}

将bycode用一种中间肋记符(汇编语法)表示如下:

0: OP_NEWFUNCTION 6
3: OP_JMP 58
6: OP_CONST 10
9: OP_CONST 22
12: OP_LOAD0 0
15: OP_LOAD1 0
18: OP_ADD
19: OP_NEWFUNCTION 25
22: OP_JMP 45
25: OP_ENCFUNCTION 0
28: OP_LOADBOUND 0
31: OP_LOADARG0 0
34: OP_MUL
35: OP_ENCFUNCTION 0
38: OP_LOADBOUND 1
41: OP_ADD
42: OP_RET 1
45: OP_LOADARG0 0
48: OP_BINDVAR
49: OP_LOAD 2 0
54: OP_BINDVAR
55: OP_RET 1
58: OP_CONST 2
61: OP_LOAD 2 0
66: OP_CALL
67: OP_CONST 4
70: OP_LOAD 3 0
75: OP_CALL
76: OP_LOAD0 0
79: OP_CALL
80: OP_POP
81: OP_HALT

最后再将汇编转换成最终的bycode码如下:

[
  42,  0,  6, 36,  0, 58,  2,  0, 10,  2,  0, 22,
   8,  0,  0,  9,  0,  0, 20, 42,  0, 25, 36,  0,
  45, 44,  0,  0, 46,  0,  0, 11,  0,  0, 24, 44,
   0,  0, 46,  0,  1, 20, 47,  0,  1, 11,  0,  0,
  45,  7,  0,  2,  0,  0, 45, 47,  0,  1,  2,  0,
   2,  7,  0,  2,  0,  0, 43,  2,  0,  4,  7,  0,
   3,  0,  0, 43,  8,  0,  0, 43,  1,  0
]

上面的示例js代码就被编译生成一堆看起来无意义的数字,这样攻击者基本不可能从这些数字中推断出程序的逻辑。只有解释器能明白它代表的意义。

3.3、解释器原理
什么是解释器

如同翻译人员不仅能看懂一门外语,也能对其艺术加工后把它翻译成母语一样,人们把能够将代码转化成AST的工具叫做“编译器”,而把能够将AST翻译成目标语言并运行的工具叫做“解释器”。 在编译原理的课程中,我们思考过这么一个问题:如何让计算机运行算数表达式1+2+3: 当机器执行的时候,它会将表达式翻译成这样的机器码:

1 PUSH 1
2 PUSH 2
3 ADD
4 PUSH 3
5 ADD

而需要运行这段机器码的程序,就是解释器。

解释器运行过程

解释器的运行过程跟计算器差不多。解释器也是一个段代码,你输入一个“表达式”,它内部进行计算后就输出一个 “值”,像这样: 比如,你输入表达式 '(+ 1 2) ,它就输出值,整数3。表达式是一种“表象”或者“符号”,而值却更加接近“本质”或者“意义”。我们“解释”了符号,得到它的意义,这解释器的运行过程。流程如图5所示:

              图5

其实电脑CPU就是一个解释器,它专门解释执行机器语言。当我们点击程序图标打开对应的程序时,CPU就开始解释程序中的代码。 解释js编译生成的bycode字解释器代码如下:

op = read();
    switch (op) {
      case OpCodes.OP_PUSH:
        return push();
      case OpCodes.OP_POP:
        pop();
        break;
      case OpCodes.OP_CONST:
        push(value.makeInteger(read16()));
        break;
      case OpCodes.OP_ADD:
        add();
        break;
      case OpCodes.OP_SUB:
        sub();
        break;
      case OpCodes.OP_CONSTTRUE:
        push(value.makeBoolean(true));
        break;
      case OpCodes.OP_CONSTFALSE:
        push(value.makeBoolean(false));
        break;
      case OpCodes.OP_LOAD:
        push(stack[localOffset(read16(), read16())]);
        break;
      case OpCodes.OP_LOAD0:
        push(stack[localOffset(0, read16())]);
        break;

整个解释过程也是用js代码完成,只是真正的js代码被转换成了bycode码,无法直接看出它的逻辑,这样就很难直接分析它的意义与篡改代码逻辑。

四、总结

4.1、性能

由于性能原因,只能尽可能保护核心算法模块,因为当用户对应用体积敏感或是要求极高的执行效率时,这样做最终代码尽可能小及提高执行效率,这样在极端条件下也不影响用户体验。

4.2、安全

整个过程中,由于是将js明文代码生成的opcode设计是私有不公开的,所以已经不存在明文的Javascript代码了,因此安全性得到了极大的提升。 与此同时,加上精简了opcode的设计,一个数字对应原来的一句js明文,使得生成的opcode体积小于原有的Javascript代码。 注:本文并非认为其它产品技术实现方案不行,只是在不同场景下不同的用户问题更适合使用某种方案,并没有绝对完美的方案。该方案虽然解决了己有产品的许多问题,但其同样也不是最完美的方案。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Js 逆向进阶 | 浅谈 Js 代码保护

    原文:https://blog.csdn.net/feibabeibei_beibei/article/details/98232069

    咸鱼学Python
  • 以变制变 - 前端动态化代码保护方案探索

    本文分享了腾讯防水墙团队关于机器对抗的动态化思路,希望能抛砖引玉,给现在正在做人机对抗的团队一些启发。

    腾讯防水墙
  • 解构IoT安全隐患,探寻安全防护部署新思路

    随着IoT的发展,催生了大量新产品、新服务、新模式,并逐步改变了传统产业模式,引发了产业、经济和社会发展新浪潮。但与此同时,数以亿计设备的接入带来安全攻击也在不...

    几维安全
  • 反爬虫常见策略总结

    反爬虫,即应对爬虫进行反制的统称,主要区分“正常用户”与“机器人”的一种策略统称。

    PayneWu
  • 说说前端未来几年的发展方向

    做 web 前端的同学都知道,和原生的 App 相比,性能一直一个致命的痛点,如果要追求性能,肯定得用原生 App。那么在性能上,未来几年可能是一个方向。

    桃翁
  • 写给女朋友的中级前端面试秘籍(含详细答案,15k级别)

    WeakMap只能以复杂数据类型作为key,并且key值是弱引用,对于垃圾回收更加友好。

    ssh_晨曦时梦见兮
  • WEB应用常见15种安全漏洞一览

    SQL 注入就是通过给 web 应用接口传入一些特殊字符,达到欺骗服务器执行恶意的 SQL 命令。

    Fundebug
  • H5 前端性能测试实践

    H5 页面发版灵活,轻量,又具有跨平台的特性,在业务上有很多应用场景。但是同时对比 App,H5 的性能表现总是要逊色一筹,比如页面打开往往会出现白屏,滑动列表...

    有赞coder
  • 企业安全建设之浅谈办公网安全

    ? 01 前言 在大多数互联网公司,安全建设的主要精力都投入在业务网安全上,办公网往往成为短板。为避免教科书式的理论说教,本文以攻防的角度,以中型互联网公司为...

    FB客服
  • 一文了解安卓APP逆向分析与保护机制

    3月17日,安卓巴士全球开发者论坛在重庆举办,网易资深安全工程师钟亚平出席交流活动,并做《安卓APP逆向与保护》的演讲。在分享中,他介绍了 Android Ap...

    FB客服
  • 网页视频加密成熟方案简介【H5/M3U8/Hls】

    视频网站运营者都很关注自己网站视频的版权,怎样防止下载,防止传播,防止翻录等功能,今天给大家介绍一个成熟方案:

    点量小崔
  • 顶象全场景IoT安全方案解决物联网两大难题

    12月22日,第一届“物联网安全沙龙”在杭州召开。来自浙江省物联网协会、中国移动、顶象技术、莹石网络、鸿泉数字等机构和企业的专家、学者等近百人就物联网安全现状及...

    企鹅号小编
  • 顶象全场景IoT安全解决方案亮相 “首届物联网安全沙龙”

    12月22日,第一届“物联网安全沙龙”在杭州召开。来自浙江省物联网协会、中国移动、顶象技术、莹石网络、鸿泉数字等机构和企业的专家、学者等近百人就物联网安全现状及...

    企鹅号小编
  • 详解微信原生小程序架构及同构方案

    最近实习中参与了H5项目向小程序迁移的工作,在微信官方文档和一些帖子上学习了小程序运行机制和底层原理,以及与Web页面的区别,在此基础上又看了一些关于小程序同构...

    极乐君
  • 大型企业的研发部门 是如何保护代码安全的?

    研发部门,是企业的核心部门,企业的大部分知识产权,都是跟研发部门相关的,比如发明专利、实用新型专利、软件著作权、作品著作权、集成电路布图设计等,都是研发部门的智...

    企业文件数据交换
  • 关于火绒的12个技术问题

    近日有卡饭网友向火绒提出12个问题,从产品性能到核心技术。这些问题非常棒,无论提问者是网友还是友商,火绒团队都非常愿意一起探讨。我们尝试一一作答如下。

    用户6477171
  • BrowserWAF:免费、开源的前端WAF

    BrowserWAF,一款由ShareWAF推出的免费、开源的前端WAF,也可称为浏览器WAF。

    FB客服
  • 打通虚拟与现实,LBS游戏解决方案上线

    就在今天,广大游戏玩家的盛宴,一年一度的ChinaJoy刚刚在上海落下了帷幕。在本届ChinaJoy的展台上,众多AR类游戏又吸引了众多游戏迷和观众的眼球。

    腾讯位置服务
  • 从WannaCry血案谈数据中心信息安全

    接下来,病毒事件引起了公安部门的重视: 尔后,全球范多地沦陷: 与此同时,以安全著称的苹果手机和电脑,紧急推出系统修复补丁: 这次WannaCry病毒,受感染的...

    魏新宇

扫码关注云+社区

领取腾讯云代金券