阿里三層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
獲取更多資訊