天天看點

嘗試對 jsjiami 加密結果手工解密

看了下 jsjiami,簡單的一個 <code>console.log("james")</code>,加密出來的結果居然有 3k,說明這個加密轉了不知道多少彎在裡面。如果要把真正一段業務代碼拿來手工解密,應該會挺累的,但是本文不研究工作量的問題,隻是嘗試一下手工解密,向各位讀者介紹一下分析方法和工具應用。

同一句話在 jsjiami 裡可能會加密出不同的結果,我相信這個工具上加入了随機因素。但是為了節約篇幅,這裡就不貼我用于試驗的加密結果了。分析過程中會貼一些代碼段。

毋庸置疑,要想人工識别,首先需要斷句。幸好目前美化(格式化)js 的工具還是不少,随便找兩個試下,看哪個效果好。我這裡是用的浏覽器插件 fehelper。

然後注意到,所有變量都改了名字,數字加字母的,怎麼讀都難受。是以需要使用“重命名”重構工具來改名。這事讓 vscode 幹毫無壓力。

這一句聲明了兩個變量,一個顯然是 jsjiami 的版本版本;另一個是一個數組,除版本資訊外,内容猜測是 base64,上網用 base64 解碼試了一下,解出來亂碼,是以先放着,後面再來看是啥。

為了便于識别,可以 rename 重構一下,順便按規範拆分聲明:

這個 iife 的三個形參,順便改個名字:<code>p1</code>、<code>p2</code>、<code>p3</code>。iife 裡定義了一個局部函數,給它更名為 <code>localfunc1</code>。這個函數定義完之後直接調用,查了一下,沒有遞歸,是以相當于又是一個 iife。同樣,它的 5 個參數給改個沒啥意義,但是好識别的名字,結果:

注意到,外層 iife 的 <code>p1</code> 就是上面改名為 <code>constarray</code> 的那個數組,反正都是作用域内,幹脆一不做二不休,給它換掉:

将 <code>p1</code> 更名為 <code>constarray</code>,跟外面的數組同名

同時删除外層 iife 的第一個形參和實參

既然已經知道 <code>constarray</code> 是個數組,作用在上面的所有屬性都應該跟數組相關。就這幾行 代碼,觀察一下不難發現:

<code>lp5</code> 隻參與了一個表達式,結果是 <code>"pop"</code>

<code>var _0x1e174c = "shift", _0x5428fe = "push"</code> 兩個變量隻是當常量使用的,把 <code>var</code> 改成 <code>const</code> 可以讓編輯器幫忙檢查是否有寫操作 —— 當然結果是沒有。

不過很遺憾,vscode 沒提供内聯 (inline) 重構工具,是以隻能手工操作,把這兩個變量直接替換成常量。以 <code>_0x1e174c = "shift"</code> 為例,先把 <code>"shift"</code>(含引号)複制到剪貼闆中,然後在 <code>_0x1e174c</code> 使用若幹次 &lt;kbd&gt;ctrl+d&lt;/kbd&gt; 把所有 <code>_0x1e174c</code> 都選中,再 &lt;kbd&gt;ctrl+v&lt;/kbd&gt; 即可。如法炮制處理掉 <code>_0x5428fe = "push"</code>。然後删除兩個聲明。

嘗試對 jsjiami 加密結果手工解密

不過 <code>constarray["shift"]()</code> 這種寫法看起來很不習慣,最好能改成 <code>constarray.shift()</code> —— 這就需要借助一下 eslint 了。将目前目錄初始化為 npm module 項目,安裝并初始化 eslint,然後在配置裡添加一條規則:

這時候 vscode 會提示

将滑鼠移過去,使用快捷修複自動把所有 <code>[]</code> 調用改為 <code>.</code> 調用。

嘗試對 jsjiami 加密結果手工解密

接下就很有意思了,看 <code>localfunc1(++p2, p3)</code> 調用,隻傳入了兩個參數,是以除了剛才去掉的 <code>lp5</code> 之外,形參 <code>lp3</code>、<code>lp4</code> 并沒有起到參數的作用,而是當作局部變量來用的。這裡可以把它們從參數清單中删除,使用 <code>let</code> 定義為局部變量 —— 當然,這一步做不做無所謂。

而 <code>p2</code> 和 <code>p3</code> 的值是外部 iife 傳入的:

乍一看像變量,仔細一看都是 <code>0x</code> 字首,明明就是整數。而且 <code>p3</code> 就是比 <code>p2</code> 後面多綴兩個 <code>0</code>。

再看 <code>localfunc1</code> 内部第一句話就是 <code>lp2 = lp2 &amp;gt;&amp;gt; 0x8</code>(記住 <code>lp2</code> 是傳入的 <code>p3</code>),這不就是把 <code>0x1c700</code> 後面兩個 <code>0</code> 給去掉變成 <code>0x1c7</code> 嗎 —— 現在 <code>lp2</code> 和 <code>p2</code> 的值一樣了。而 <code>lp1</code> 是傳入的 <code>++p2</code>,是以在現在 <code>lp1 === lp2 + 1</code>。

這樣就滿足了 <code>if</code> 條件 <code>(lp2 &amp;lt; lp1)</code>,這個 <code>if</code> 語句沒用了,可以直接解掉。

接下來是一個神奇的循環,<code>while (--lp1) { }</code>,中間沒有 <code>break</code>,也就是說,需要循環 <code>0x1c7 + 1</code> 次,也就是 <code>456</code> 次。基本上可以猜測這個循環幹的就是沒用的事情,浪費 cpu 而已。

來分析一下是不是:

既然剛才已經說了 <code>lp3</code> 和 <code>lp4</code> 就是局部變量,不妨再改個名,分别改為 <code>local1</code> 和 <code>local2</code>,好識别。現在的 <code>while</code> 循環是這樣:

剛才還分析了 <code>lp1 === lp2 + 1</code>,是以 <code>while (--lp1)</code> 第一次執行的時候,<code>lp1</code> 和 <code>lp2</code> 就相等了,進入 <code>if (lp2 === lp1)</code> 分支;此後,都不會再進入這個分支,因為 <code>lp1</code> 一直在減小。

那麼第一次循環執行的内容可以寫成:

此後,這個循環中再沒有對 <code>lp2</code> 和 <code>local1</code> 賦過值。而此時 <code>constarray</code> 的值是

後面的 <code>local1.replace(...)</code> 這句話可以直接拿到控制台去跑一下,結果讓人哭笑不得,就是 <code>"jsjiami.com.v6"</code>。從這個結果來看,<code>else if (...)</code> 條件除第一次不執行,之後都是 <code>true</code>,也就是說,總是執行,那不就和 <code>else</code> 一樣了嘛。

好嘛,除去第一次循環,這個循環變成了:

沒别的,就是轉圈,一共轉了 <code>455 - 1 = 454</code> 次!次數如果算不清楚,寫一個循環跑一下就知道了:

<code>local2</code> 之後再沒使用,是以 <code>while</code> 中的兩句話可以合并成一句:

這和 <code>while</code> 循環之後那一句完全一樣。是以這句話執行的次數一共是 <code>454 + 1</code>,也就是 <code>455</code> 次。由于 <code>constarray</code> 現在有兩個元素,而 <code>455</code> 是奇數,是以跑完之後 <code>constarray</code> 是這樣:

至此,第一小段代碼分析完成,除了改變 <code>constarray</code> 沒幹任何有意義的事情。

至于這段代碼裡的兩句 <code>return</code>,沒半點用,因為外層 iife 的傳回值直接被丢棄了。是以傳回語句裡的位運算,都懶得去算了。

整個這一段代碼最終變成一句話:

而且猜測 <code>constarray</code> 其實沒啥用

分析了半天,基本上沒啥有用的代碼。而且基本上可以斷定,後面的幾十行代碼也隻是在浪費 cpu。

因為我們知道原代碼是 <code>console.log("james")</code>。是以為了加快分析速度,就不再一行行往下讀了,直接從後往前看。一眼就看到了

反推,<code>_0x2a10("0", "]o48")</code> 的結果就是 <code>"log"</code>,而 <code>_0x2a10("1", "wcmn")</code> 的結果就是 <code>"james"</code>。

猜測,<code>_0x2a10</code> 就是個拼字元串的函數,而第 1 個參數,就是個标記,作分支用。

既然都已經知道 <code>_0x2a10</code> 是拼字元串的了,那改名叫 <code>getstring</code> 吧。第一個參數是标記,改名為 <code>flag</code>,第二個參數多半是計算用的初始值,就叫 <code>initvalue</code> 好了。

其中第一句:<code>flag = ~~"0x".concat(flag);</code>。這句就是把 <code>flag</code> 按 16 進制轉換成數值類型的值而已。根據實際的調用參數,去控制台跑一下 <code>~~"0x1"</code> 和 <code>~~"0x2"</code> 就知道了,還可以試驗一下 <code>~~"0xa"</code>。

接下來的 <code>var _0x1fb2c5 = constarray[flag];</code> 也就好了解了,而且到這裡總算明白了,原來 <code>constarray</code> 是用來提供拼接字元串的部分因素的。既然如此,給它更名為 <code>factor</code>。

如果不管這個長長的 <code>if</code> 語句内部那些複雜的邏輯,精簡下來就是:

也就是在第一次運作 <code>getstring</code> 的時候對它進行初始化。

其中 <code>.ioaiiu</code> 隻有兩處引用,一處判斷,一處指派 —— 明顯是個初始化标記,可以改名為 <code>initialized</code>。隻不過這時候 rename 重構工具似乎不能用,手工更名吧。

<code>if</code> 分支内第一段代碼又是個 iife,單獨拷貝出來放到一個獨立的 <code>js</code> 檔案中,vscode 并沒有提示找不到變量之類的事情。是以這段代碼是可以獨立運作的。

第一句很明顯是在找 <code>global</code> 對象,相當于 <code>var _0xea3c63 = globalthis</code>。

第二句先忽略,第三句明顯是看 <code>globalthis</code> 上有沒有 <code>atob()</code>,如果沒有就給它一個。既然 <code>atob()</code>在多數環境下都存在,那就不用糾結其内容了。

那麼,這段 iife 就是保證 <code>atob()</code> 可用,可以直接删掉不看。

接下來又定義了一個函數,去掉内容,長這樣:

通過後面的調用來用,應該是個比較有用的函數。為了友善識别,把兩個參數分别更名為 <code>first</code> 和 <code>second</code>。

我們也把它摘出來拷貝到一個獨立的 <code>.js</code> 檔案中,發現也沒有缺失變量,說明可以單獨拿出來分析,就是個工具函數。

這個函數一來定義了 5 個變量,先不管,用到的時候再去找。

下面的代碼是:

這段代碼不用仔細看,大概知道是把一個 base64 轉成 <code>%xx</code> 的形式,而這個形式的字元串用 <code>decodeuricompoment()</code> 可以再轉成字元串(繞好大一圈)。

回想一下 <code>constarray</code> 的元素,确實長得像 base64,是以這裡應該是處理那些元素了。

接下來的代碼就是通過一大堆的數學計算,從 <code>initvalue</code> 和 <code>constarray[i]</code> 把我們需要的字元串恢複出來。算法肯定是加密工具自己設計的,懶得去分析了。計算都不難,就是燒腦,需要仔細,一點不能出差錯。

是的,結束了,戛然而止。

寫這篇文章的目的并不是要把代碼完全解出來,隻是證明其可能性,同時介紹分析方法和工具應用。第 2 部分寫完就該結束的,因為後面也沒有用到什麼新的方法。

總的來說,jsjiami 向原始代碼中添加了非常多無用而燒腦的程式來提高解碼的難度,這麼簡單的一句話都解了這麼久,生産代碼就更不用說了。代價也是有的 —— 真燒 cpu。

好吧,我又幹了一件無聊的事情!

繼續閱讀