三層switch轉一層switch的處理方法

package_main發表於2024-08-17

阿里三層switch轉一層switch的處理方法

如下所示的混淆程式碼,在淘系140、227等滑塊的程式碼經過一些轉化後得到。

這些轉化包括:

//去自執行避免變數汙染
traverse(ast, { UnaryExpression: renameScopIdn })
//去自執行
traverse(ast, { UnaryExpression: movezishixing })
//三目運算轉if-else
traverse(ast, { ConditionalExpression: condition2ifelse })


//例如a&&console.log(b)  轉if-no-else
traverse(ast, { LogicalExpression: Logic2if })

//將9==Ai,轉成Ai==9,放在左邊
traverse(ast, { BinaryExpression: putIdenLeft })

// Ai > 9  Ai < 10 轉 Ai == x
traverse(ast, { BinaryExpression: ifLeGe2Eq })


//從switchCase入口是很有用的
//if巢狀轉switch
//if巢狀能轉switch的究極原因在於,它是透過一些混淆邏輯得到的程式碼,肉眼分析就會發現,每個程式碼塊總是對應了條件變數等於某個值時,才會進入
traverse(ast, { SwitchCase: if2switch })

透過上面的轉化得到了三層巢狀的switch,這篇我們重點看看這個轉化:

 try {
    for (var li = 16996; void 0 !== li;) {
      var Ci = 31 & li,
        fi = li >> 5,
        mi = 31 & fi,
        bi = fi >> 5,
        Ai = 31 & bi;
      switch (Ci) {
        case 0:
          switch (mi) {
            case 0:
              switch (Ai) {
                case 12:
                  N = Se[vo], Q = N[Z](), li = Q ? 11460 : 1475;
                  break;
                case 5:
                  li = 8804;
                  break;
                case 2:
                  W = $ % 128, ie = [], M = W + 128, _ = $ - W, W = _ / 128, _ = 127 & W, ie.push(M, _), se = ie, li = 16390;
                  break;
                case 0:
                  Dn.push(0), li = 11522;
                  break;
                case 1:
                  L = mo, li = 24641;
                  break;
                case 3:
                  Oe = K, li = 20257;
                  break;
                case 4:
                  _ = 0 !== se.length, I = je, li = _ ? 22694 : 16963;
                  break;
                ……
                ……
                ……
                ……
                
                

透過上面可以看出,在滿足

var Ci = 31 & li,
fi = li >> 5,
mi = 31 & fi,
bi = fi >> 5,
Ai = 31 & bi

並且 Ci===0 && mi ===0 && Ai ===0

的情況下進入第一個程式碼塊,眼看好像很多變數,實際上都是由li運算得來,因此就是求滿足這些約束的條件下的li的值。

然後最終轉化為:

switch(li){
    case x:
    …………
    …………
}

那麼怎麼求解嘞?

嘿嘿,下面提供兩種思路:

1.窮舉

透過for迴圈,窮舉0到999999(一個較大的數)的範圍內,看看是否有滿足上面條件的li的值

程式碼示例:

function iter() {

  here: for (let li = 0; li < 999999; li++) {
    var Ci = 31 & li,
      fi = li >> 5,
      mi = 31 & fi,
      bi = fi >> 5,
      Ai = 31 & bi;
    for (let con1 = 0; con1 < 26; con1++) {
      for (let con2 = 0; con2 < 26; con2++) {
        for (let con3 = 0; con3 < 26; con3++) {
          if (Ci === con1 && mi === con2 && Ai === con3) {
            //記錄下來li和對應程式碼塊的對映關係,後續重建switch即可
            console.log(con1, con2, con3, "<==>", li)
            // if (li === 16996) break here
          }


        }

      }

    }



  }
}
iter()

輸出:

……
21 18 16 <==> 16981
22 18 16 <==> 16982
23 18 16 <==> 16983
24 18 16 <==> 16984
25 18 16 <==> 16985
0 19 16 <==> 16992
1 19 16 <==> 16993
2 19 16 <==> 16994
3 19 16 <==> 16995
4 19 16 <==> 16996
……

可以看到,還是挺快的就得出結果了。

值得注意的是,你會發現為什麼我只遍歷0到26的範圍,這個的話取決於程式三層switch的case的範圍有多大,當然你也可以寫ast程式去搜集這三個的變化範圍


2.約束求解

作者推薦使用這種方式,想到這個是因為作者的畢業設計使用的就是這個技術,如今沒想到在js逆向上還能排上用場

我們直接使用z3-solver進行約束求解得到一個滿足條件的解即可,窮舉些許顯得low

如下我們直接使用Z3對收集到的情況進行求解:

import json
from z3 import *
import sys


li = BitVec('li', 16)
s = Solver()  # 建立約束求解器
Ci = li & 31
fi = li >> 5
mi = 31 & fi
bi = fi >> 5
Ai = 31 & bi


def get_ans(n1, n2, n3):
    s.add(Ci == n1)  # 新增約束條件
    s.add(mi == n2)  # 新增約束條件
    s.add(Ai == n3)  # 新增約束條件
    if s.check() == sat:  # 檢測是否有解
        # result = s.model()
        resst = s.model().eval(li).as_string()  # 若有解則得出解,注意這裡的解是等式
        s.reset()
        return resst
    else:
        print('no result')  # 無解
for n1 in range(26):
    for n2 in range(26):
        for n3 in range(26):
            ans = get_ans(n1, n2, n3)
            print(n1,n2,n3,"<==>",ans)

總結

兩種方式的話,速度我沒有進行比較,甚至目前cpu情況下,窮舉更快一些,為什麼提出第二個方法呢?

實際上我在設想,我們沒必要寫前面所說那些複雜的ast還原外掛,寫了費勁巴拉除錯了很久,才得到,三層switch巢狀,最後來轉一層,如果我們能透過程式分析的策略往深度分支進行探索,一路上不斷收集約束集合,直到最深的程式碼塊(沒有子分支),此時將收集到的約束進行求解,即可一步到位直接得到li和最終執行程式碼塊的關係,直接就從混淆的程式碼得到了一層switch

上述方法的難點在於約束收集,縱觀基於js寫的符號執行引擎,ExpoSE算一個,但是文件稀少,安裝編譯都成問題,其次這些符號執行引擎都是動態符號執行,需要程式能夠執行的情況下設計實現的,但是我們的被混淆的js程式碼一般都是不能直接執行的,雖然我們可以透過一些操作,比如將最主要的需要還原的程式碼摳出來,然後將不能執行的程式碼塊暫時替換成能夠執行的程式碼,但是對映關係需要留存好,後續還原用。但是這些為了能夠使用動態符號執行思路的前期操作,比起直接寫ast外掛的方式,工作量也不一定小,所以解混淆嘛!本著簡單直接的方式,所以還是目前建議使用ast外掛的方式,進行,因為動態符號執行還涉及到程式碼插裝等,寫論文的水平了,已經是……

記得加入我們的學習群,更多知識盡在我的知識星球:

我的星球https://t.zsxq.com/125umU2l8

qq群 961566389獲取更多資訊

相關文章