物联网中的 ARM 漏洞利用
發文動機
幾周前我參加某個會議的時候,有個“物聯網上的 ARM 漏洞利用課程”的議題我覺得很多干貨,我也決定自己寫一篇,給去不了現場的同學發些福利。我打算分為三個部分來寫。
當然我的文章沒辦法和現場的 course 相比,我還是想為大家做一些微小的工作。
這三個部分是:
-
第一部分:逆向 ARM 應用
-
第二部分:編寫 ARM shellcode
-
第三部分:ARM 漏洞利用
一、逆向ARM應用
-
環境:
樹莓派3——我選了這個又便宜又好配置的環境,Android 也是個不錯的選擇。
-
硬件:
具體型號:>樹莓派3、型號B、ARM-Cortex-A53架構
-
軟件
這是些軟件信息,接下來三部分都會用到。
操作系統的安裝請看:https://www.raspberrypi.org/documentation/installation/installing-images/linux.md
配置遠程 ssh 請看:https://www.raspberrypi.org/documentation/remote-access/ssh/
? 編譯器
我們用到的所有 C、C++、匯編代碼都會用樹莓派自帶的 GCC 編譯器。
版本如下:
還有一點是 GCC 的匯編指令和其他的編譯器不同,最好先看一下這些指令:http://www.ic.unicamp.br/~celio/mc404-2014/docs/gnu-arm-directives.pdf
? 源碼
本部分我用到的源碼都放在這里了:https://github.com/invictus1306/ARM-episodes/tree/master/Episode1
??編譯選項
這一節我會介紹三個選項,并附帶例子。
這是用到的源碼:
??調試符號?
-g 選項會在編譯時向可執行文件中加入調試信息(符號表)。
編譯帶 -g 和不帶 -g 選項的兩個 ELF 文件,比較大小:
第二個文件更大,意味著它被加入了其他的信息。用 readelf 命令的 -S 選項(查看節頭)查看其調試信息:
這些調試信息以 GCC 默認的 DWARF 格式存儲。用 objdump 查看:
去除符號表和重定位信息
選項為 -s。
可以看到 .symtab 有很多本地符號,這些符號運行時并不需要,可以把它們去掉。
用過 -s 選項后,函數名之類的信息已經去掉了,某個逆向小子的生活又艱難了一步。
在之后的第三部分我會介紹其他更復雜的編譯選項。
ARM Hello World
我們以兩種方式開始這個 hello world 的研究:
1. 使用樹莓派系統調用
2. 使用 libc 函數
? 使用樹莓派的系統調用
匯編并鏈接此程序:
注意:如果用 gcc,
會得到錯誤:
這是因為源程序里沒有 main 函數,在另一種實現里我們會看到如何使用 gcc 編譯。
執行:
接下來使用 gdb:
可以看到 .text 段中放著我們的代碼。0x10078 處的指令代表將 0x10090 指向的值載入 r1。
? 使用 libc 函數
這次我們會使用 printf 函數,這里我們將程序中的 .global _start 改為 ?.global main。
編譯器需要我們指定 global main, .func main, main: 等符號告訴 libc main 函數在哪。
匯編器和鏈接器只是 GCC 的一小部分,下面我們會用到 GCC 其他的特性來編譯。
可以看到在進程的地址空間里加載了 libc 共享庫(libc-2.19.so)。
0x10428 處調用了 printf 函數,0x10428 是個 PLT 入口,指向 GOT 中 printf 函數在運行時的真實地址。
GCC 編譯時,libc 的函數并沒有被編譯到可執行文件中,而是通過動態鏈接到 libc 使之可用。用 ldd 命令查看程序引用的動態庫。
可用看到 libc 是程序所需要的。多次查看如果 libc 的地址不同,是因為打開了 ASLR。用 IDA 打開:
0x10428 處調用 printf,雙擊并沒有進入 libc。
而是到了 PLT 段,0x102D0 處通過 LDR PC, […] 修改 PC 跳轉到了其他地址。
到達 GOT 段,這里存著外部符號的地址。
下面用 gdb 調試,斷點下在 0x10428 處。
stepi 繼續運行。
走幾步到達 ld 庫中的 dl_runtime_resolve 函數。
ld 是動態鏈接器,這里建立起了 libc 的外部引用環境。
更多細節參考?http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/。
逆向工程介紹
這部分對于要分析的程序我不會提供源碼。
??逆向算法
第一個例子是輸入一個字符串,經過算法處理后會輸出另一個字符串,我需要使輸出字符串為“Hello”。
下面是源碼(我只會提供這個例子的):
編譯:
調試理解算法:
0x10454 代表 r0=*(pc+92)。查看 pc+92 的內容:
是數據段的地址,看一下內容:
0x20668 是 printf 函數的參數。
運行到 0x10464,r0 是格式的地址,r1 是輸入字符串的地址。
從源碼中可以看到輸入字符串的長度為5:
輸入 “ABCDE”:
0x10468 和 0x1046c 處的指令將輸出字符串地址賦給 r5,輸入字符串地址賦給 r1。運行至 0x10470:
注釋如下:
運行到 0x10480 處查看 r0, r2, r3 的值:
即 *r5 = r2 xor r3。
可以用偽代碼表示:byte1strOut = byte1strInput xor byte2strInput。 比如我們想要生成“Hello”,需要使 r0 為 0x48(H)。
接著看 0x10480,
注意看注釋:
執行到 0x1048c,看一下 r0、r3、r4 的值:
即 *(r5+1) = r4 xor r3。偽代碼表示:byte2strOut = byte2strInput xor byte3strInput。
意味著 *(r5+2) = r2 + 0x5。偽代碼表示:byte3outStr = byte1strInput + 0x5。第 4 個字節:
就是 *(r5+3) = r3 xor r4。偽代碼:byte4strOut = byte2strInput xor byte4strInput。第 5 個字節:
就是 *(r5+4) = r4 xor r2。偽代碼:byte5strOut = byte4strInput xor byte5strInput。
整個算法合在一起:
byte1strOut = byte1strInput xor byte2strInput
byte2strOut = byte2strInput xor byte3strInput
byte3strOut = byte2strInput + 0x5
byte4strOut = byte2strInput xor byte4strInput
byte5strOut = byte4strInput xor byte5strInput
將輸出字符替換:
‘H’ = 0x48 = byte1strInput xor byte2strInput
‘e’ = 0x65 = byte2strInput xor byte3strInput
‘l’ = 0x6c = byte1strInput + 0x5
‘l’ = 0x6c = byte2strInput xor byte4strInput
‘o’ = 0x6f = byte4strInput xor byte5strInput
推出應輸入字符:
byte1strInput = 0x6c – 0x5 = 0x67 (g)
byte2strInput = 0x48 xor 0x67 = 0x2f (/)
byte3strInput = 0x2f xor 0x65 = 0x4a (J)
byte4strInput = 0x2f xor 0x6c = 0x43 (C)
byte5strInput = 0x43 xor 0x6f = 0x2c (,)
輸入試試:
? 逆向一個簡單的加載器
這個加載器的作用是把指令加載到內存里,當你打印消息時執行。我們這次要打印“WIN”。程序在這里。
用 IDA 打開:
在 _start 這兒可以看到 0x10090 處有系統調用,調用號是 0xc0(mmap)。
看下面我的注釋分析:
mmap 后有一塊新內存(0x30000)。
0x10098 處的指令 .text:00010098 LDR R1, =code 把某個變量的地址存入 r1,看一下:
這些看起來不像 arm 代碼,接著看 0x100A4:
.text:000100A4 LDR R2, [R1,R4]?把 r1+r4 地址額值存入 r2,r1 是 code 變量,r4 表示索引,第一次值為 0。
.text:000100A8 EOR R2, R2, R6?r2 與 r6 異或,r6 的值是 0x123456,第一次 r2 的值是 0x56。異或的值存在 r2,在下條指令?
.text:000100AC STR R2, [R0,R4]?中被寫入 mmap 分配的地址 0x30000 處,注意 r0 是 mmap 的返回值。
循環的作用是解密 code 的所有字節,在 0x100BC 處下斷點查看 0x30000 的值。
這些就是 ARM 指令了,也可以用 idc 腳本解密:
現在來分析解密的指令:
執行過 0x30004 到 0x30014 的 5 條指令后,棧指針向低地址處移動了 8 ,r4 是棧指針,r2 的值是 0x3e,r3 的值是 0x2,r5 的值是 0x96。
接下來兩條指令(0x30018 和 0x3001c)r2 與 r5 異或的結果 0xa8 存入 r1,這個值寫入了棧頂,棧指針向高地址移動了 1 。此時:
0x30020 處 r2 自減 0x1e,得到:
0x30024 處是一個循環:
每次循環都將 r2 和 r5 異或,結果存入棧頂,sp + 1。0x30030 處可以看到 r3 是循環索引,每次減1,初始值為 2,所以共循環兩次。
循環結束時運行到 0x30038時, 0x7efff7b0 值為:
還有兩個字節在棧頂存著,此時棧指針為:
0x3003c 處的兩條指令將剩下的兩個字節存入棧頂:
執行完 0x30040 后0x7efff7b0 值為:
往下看就是 write 的系統調用了:
write 過后:
我們想要的是“WIN”,這時候就要修改 xor 的 key,這樣存入棧頂的才會是正確的 0x57 0x49 0x4e。
來看 0x30018 處的異或操作 0x30018: eor r1, r2, r5,r2 每次都變,所以 r5 是異或的 key,我們需要修改 r5 使得 r1 = r2 xor r5 = 0x57。
r2 的值是 0x3e,則 r5 應該是 0x69。
異或的 key 沒有變過,這就直接繼續執行就可以了。
? 基本的反調試技術
這是本章最后一個程序了,這次需要理解算法并繞過一些基本的反調試,使程序輸出“Good”。在這里下載。
試著用調試器運行:
即使再用 strace/ltrace 命令也是同樣的輸出。
IDA 打開:
我們從 ldr r2, =aAd 開始分析。
aAd 是個變量:
把 Array 轉為 data 更好理解:
0x10988 處的數組用 var_c 表示,還有另一個變量 var_10,aAd + 4 的值如下:
即 var_10 變量存著 0x1098C 處的數組。
看接下來的指令做了什么:
總結來說就是有兩個數組:
4個元素的 var_c: ?0x7, 0x2f, 0x2f, 0x24; 3個元素的 var_10:0x22, 0x41, 0x44。
下面有個 flag 變量,我們來看 main 函數中關于它的流程圖:
flag為1,紅色執行,不為1綠色執行。
flag為1,r3 為 0 隨后與 3 比較。
flag不為1,r3 為 0 隨后 與 2 比較。
flag 為 1,我們來到 loc_107F8,最關鍵的是這句 ADD R3, R3, #0x40,r3 的值是 r3 = *(var_C+var_8),var_C 和 var_8 分別是:
var_C = 4個元素的數組
var_8 = 0 (索引,第一次的值)
相加之后 r3 的值為 r3 = 0x7 + 0x40 = 0x47。
可以用個簡單的 idc 腳本計算:
結果是 Good:
再來看flag不為1時的 loc_10864,這里的循環計算的是3個元素的數組,關鍵的是 ADD R3, R3, #0x20。
像之前一樣,idc 腳本
結果是 Bad:
為了使程序輸出“Good”,需要找到 flag 賦值的地方,而且剛剛并沒有發現檢測調試器的地方, “You want debug me?” 也沒有出現過。
查看 flag 的交叉引用:
發現有個 ptrace_capt 的函數,在 main 函數前執行。可以在 .ctors (或者 .init_array) 段中發現其提供了一些列函數用來在程序開始/結束前執行。
來看 ptrace_capt 函數:
這里有個檢查:
用調試器可以輕易繞過,先來看 loc_10690:
大致如下:
1. 只讀模式打開 password.raw;
2. 計算大小
3. 驗證大小是否小于 6
如果小于等于6,來到 loc_10700:
往下看發現這是個循環:
調用 fgetc 函數:
如果 r3 等于 0,則來到 loc_10750:
var_18 是讀取的字符,var_8 是循環索引,sub0 的調用則為 sub0(var_18, var_8, &var_1C);。
看 sub0 函數:
C 代碼:
sub0 返回時繼續執行,var_1C 保存返回值:
這段用偽碼表示即:
如果 r3 不等于 0,則來到:
C 代碼表示:
終于找到了 flag 賦值的地方,而我們需要其值為 1。
新建 password.raw 文件,寫入:
我用 vim 的設置刪掉了換行:
運行:
現在用 gdb 運行去掉反調試:# gdb ./3b
在 0x10678 處下斷,修改 r3 的值:
繼續往下分析,我現在要使 var_C=0x997,flag 才能賦值為 1。現在文件里的內容是:
要修改第五個字節使得 var_C=0x997,就要知道第4次循環時 var_C 的值。
在 0x10774 下斷點,
此時循環索引為 3(第4次),var_C 的值為 0x724,現在要改掉第 5 個字節的值,我用了 Python 來計算:
運行:
得到了第五個字節的值,修改它:
記得刪掉換行 :set noendofline binary。
運行:
“Good” 就被打印了。
本文由看雪翻譯小組 kiyaa 編譯,來源quequero@andrea sindoni
轉載請注明來自看雪社區
總結
以上是生活随笔為你收集整理的物联网中的 ARM 漏洞利用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GAN for NLP (论文笔记及解读
- 下一篇: Using Markov Chains