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代碼。
注:本文並非認為其它產品技術實現方案不行,只是在不同場景下不同的用戶問題更適合使用某種方案,並沒有絕對完美的方案。該方案雖然解決了己有產品的許多問題,但其同樣也不是最完美的方案。

歡迎關注公眾號

 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM