最近需要回來熟悉一下 ghidra,而且還有 wannaCry 的債要還…先用這個比較簡單的
ref:
https://www.youtube.com/watch?v=4urMITJKQQs
我覺得寫得不錯,流程很簡單,就是抓舊版的韌體,透過 GHIDRA 從中找出 AES KEY 之後拿來解密新版本的韌體
首先抓 moxa v1.11 的韌體,這邊抓 v1.11的韌體根據是他的 release note:
裡面提到了可以直升 v2.0 以上的加密韌體,代表裡面要有 key 才能解
根據影片可以去
下載韌體,但是現在網站上已經沒有 1.11 韌體的下載點了
透過 archive.org 有找到舊版的韌體
好玩的地方來了,我在 archive.org 上抓的 1.11 版韌體有加密(或是遺失 bytes),在官方 link 上抓得沒有(官方 link 還活著,只是從官網上移除入口而已)
上面是中間的小插曲,然後也同步把 2.2(加密) 版本抓下來了
照著影片方法取個別檔案的 entropy
這邊快速科普一下 entropy
1 | 在資訊理論中,entropy(熵) 描述的是資料的「不確定性」或「隨機性」。在 binwalk 的分析中: |
1 | 熵值範圍 意義 |
binwalk 的 Entropy x/y 軸分別代表 offset 與 entropy,但為什麼 Y 軸的 entropy 最大值不是 8 呢?
在 binwalk 的 source code 可以找到答案 (最後 return 的時候 / 8)
source: https://github.com/foreni-packages/binwalk/blob/master/src/binwalk/modules/entropy.py
所以在 binwalk 來說:
Y 值 | 實際 entropy | 意義範例 |
---|---|---|
1.0 | 8.0 bits | 非常隨機(加密或壓縮資料) |
0.75 | 6.0 bits | 高 entropy(壓縮內容可能性高) |
0.5 | 4.0 bits | 中等 entropy(普通程式碼或資料) |
0.25 | 2.0 bits | 很低 entropy(可能是填充區) |
0.0 | 0.0 bits | 完全重複(0x00、0xFF 填充) |
但這是舊版 (python) 的 binwalk 才是這樣呈現
新版 (Rust) 的最大值就是 8
ref: https://github.com/ReFirmLabs/binwalk/blob/master/src/entropy.rs
總之使用 binwalk -E
解完韌體後,會出現 squashfs-root
的資料夾,我們需要在裡面找韌體升級的相關檔案
但在這之前記得先 sudo chmod -R +rx squashfs-root
不然會有權限問題
然後嘗試 grep find upgrade 相關字串的檔案,因為我們的目標是升級韌體的那隻檔案
可以看到在 squashfs-root-0
有找到相關檔案
現在嘗試用 ghidra 解他看看,在 analysis 選項部份可以打勾
透過選擇工具列上的 Window->Functions
呼叫出 Function List 清單,搜尋關鍵字 (原本要搜尋 decrypt,但搜到 dec 的時候就有結果了)
然後選擇工具列上的 Window->Functions Graph
可以用圖的方式呈現 control flow
然後我們再往下追 ecb128Decrypt
的關聯性,可以看到有一個 AES_set_decrypt_key
AES_set_decrypt_key
裡面雖然沒東西(應該是解析失敗),但沒關係,我們追到這邊就取得 function 的 return type 跟 parameter type
所以我們回去 ecb128Decrypt
修改相關的 value type
首先把 AES_set_decrypt_key(auStack_30,0x80,&AStack_124);
改 AES_set_decrypt_key(userKey,128,&aes_key);
(中間的十六進位轉十進位透過滑鼠右鍵就能了)
往下看到 13 14 行
可以看到他把計算後的結果又過一個 uchar *
的轉換,然後在 function parameter 的型別卻是 void,這邊我們把它改的好看一下,直接 retype parameter 變成 uchar *
(滑鼠右鍵 -> Retype Variable)
AES_ecb_encrypt 的 function spec 在下面
https://github.com/openssl/openssl/blob/master/crypto/aes/aes_ecb.c
整體改完的樣子長這樣:
接著到 fw_decrypt
也就是 call ecb128Decrypt
的 function
我的習慣是先照著已知型別的變數先改 type,也就是上一個我們看完的 function 參數的型別
首先 param_1
retype 成 uchar *
,然後 uVar2
改 int,這樣他就會顯示 -1,-2 正常的呈現
然後 pbVar3
應該就是明文密碼了
最後的 return 很奇怪return CONCAT44(param_1,uVar2)
這邊可以細解一下
1 | 根據 AAPCS (ARM Architecture Procedure Call Standard): |
總之就是看 r0 (r0 相當於 eax,用來儲存參數用的)
而 uVar
放的在 r0
然後最後明明是 ldmia
的指令,為什麼 c 語言會有 return
這邊說一下 ldmia
是幹嘛的
以這為例子LDMIA R1!, {R0, R2, R3}
假設
R1 = 0x1000,R0、R2、R3 = 0
且記憶體內容
1 | [0x1000] = 0xAAAA |
執行後:
1 | R0 = 0xAAAA |
讓我們來看看最後 return 指向的 ARM
ldmia sp!,{param_2,passwd,r3,r4,r5,r6,r7,r8,r10,pc}
可以看到他有動到最後的 pc
register 所以會有跳轉的行為
再加上 uVar2
在 r0,所以可以得出他僅 return uVar2
的值,uVar2
為 int,因此改 function return type 為 int
最後的最後 就是解密啦
透過這段可以知道密碼被存在 passwd.3309
裡面
但要取多長呢?
在 ecb128Decrypt
可以知道 key 的長度是 0x10
因此就已 0x10 為長度取 passwd.3309
的 byte
複製出來後順著 c code 的呈現寫一個 python 取出 key
1 | passwd = [0x95, 0xb3, 0x15, 0x32, 0xe4, 0xe4, 0x43, 0x6b, 0x90, 0xbe, 0x1b, 0x31, 0xa7, 0x8b, 0x2d, 0x05] |
因為我們要餵進去 openssl 解密,所以給16進位key
然後不要 0x
所以我選擇用這種方式
1 | astar@blog:/tmp$ python3 passwd.py | tr -d '0x' |
還有一個點,細看 decrypt 的過程,透過 decrypt_size-0x28
可以得知,要解密的開頭是往後數第 40 個 bytes 開始解密
這部分可以用 dd 解決
1 | $ dd if=./moxa-nport-w2150a-w2250a-series-firmware-v2.2.rom of=moxa-nport-w2150a-w2250a-series-firmware-v2.2.rom.offset bs=1 skip=40 |
然後再用 openssl 解密
openssl aes-128-ecb -d -K "32383837436f6e6e373536340000" -in ./moxa-nport-w2150a-w2250a-series-firmware-v2.2.rom.offset -out decrypt_firmware