【tee小白的第一篇隨筆】keystone程式碼略讀
武大信安在讀,最近在自學Risc-v架構的可信執行環境。
(實驗報告多半是為了交差。臨時起意寫寫部落格,分享一些自己讀程式碼的心得理解。)
本篇內容由隊和我友總結而成,如有錯誤歡迎指正交流。
keystone是risc-v架構的開源tee。
利用risc-v的pmp來隔離頁表,進一步縮小了可信基。
runtime和sm的解耦也很有意思:
可以近似理解為:
將安全功能集中在sm中,作為保安。
runtime則提供edge call等各種與安全關係不大的服務,可以理解為保姆。
目錄
一、Keystone框架及enclave運行過程:
其他感覺比較重要的函數
二、runtime和sdk的工作原理:
調用、響應路線
runtime中其他零零碎碎的東西
開始正文:
一、Keystone框架及enclave運行過程
keystone的框架圖如上所示
所以host端(圖中的untrusted)需要調用keystone相關服務的時候,需要從U模式OS層,再從OS到SM層,然後SM調用opensbi介面完成針對暫存器的修改等操作。
在程式碼結構上就是
sdk -> linux_kernel_driver -> sm -> opensbi
以host的enclave.run操作為例,從頂層往底層查看調用過程:
首先在host端調用enclave.run方法:
這個函數最終的效果應該是將程式執行流從host端轉向eapp,並且保存暫存器組,修改暫存器組到eapp對應的值
host.cpp:
這個函數最終調用了pDevce的run方法:
裡面利用了Ioctl函數,和linux驅動層進行通訊,將操作碼request傳到驅動層。
自此程式流進行到了OS驅動層。
OS驅動層在啟動之前進行init初始化:
之後用ioctl進行的通訊,會轉到註冊的keystone_ioctl函數內部:
經過switch對cmd的分類,進入這個分支:
這個函數內部,完成了對參數的保存和檢查,之後進入sbi_sm_run_enclave函數內
這個函數最終調用sbi_ecall:
sbi_ecall應該是用下述結構體傳遞,因為extid具體數據一致,應該是根據這一項進行識別
(註:猜測這個sbi_ecall函數應該是一個OpenSBI庫函數,第一個參數代表的是底層實際程式碼ecall異常調用的a7的值,在riscv裡面,約定用a7傳遞異常類型,之後sm通過這個a7去分配異常處理函數。)
對應的註冊函數如下:
在sm初始化的時候完成
之後sbi_ecall會進入到sm層:
這個sbi_sm_run_enclave:
- run_enclave中,完成以下操作:
1.1修改暫存器組的值,對應需要run的那個enclave,並且把當前的暫存器組的值保存起來
1.2翻轉pmp的許可權。
個人理解:對於host端來說高許可權的pmp條目,對於eapp端來說就應該是低許可權度。
例如eapp應該擁有對於自己的enclave的所有許可權,但是os對其應該沒有許可權。
而host端而言,os應該擁有所有許可權。
參考://docs.keystone-enclave.org/en/latest/Security-Monitor/index.html#pmp-internals
1.3保存一些資訊,用於之後的一些操作,例如檢查之類的。比如保存當前的hart(硬體執行緒)對應的eid,以及是否在enclave中,用於之後的操作。
2.sbi_trap_exit:
這函數調用了opensbi的介面,功能是執行中斷,並且重新載入暫存器組regs。
因為在之前的函數中修改了暫存器組regs,配套到了eapp,所以執行完這個之後,執行流就到了eapp當中。
自此enclave.run()操作結束。
其他的enclave操作,比如enclave.init()等調用環節類似。功能上有一些異同。
其他感覺比較重要的函數:
/sm/pmp.c
參數:1.region_idx為之前通過需要配置的enclave的記憶體大小,提前存儲下來的數據。
先mark一下Pmp機制的工作原理:
參考://zhuanlan.zhihu.com/p/139695407
pmp機制通過Pmp地址暫存器和Pmp配置暫存器共同配置。
PMP配置暫存器一方面決定了這個PMP條目下的許可權,是否可讀,可寫,可執行,一方面決定了地址暫存器決定地址的方式。共有TOR,NA4,NAPOT,3種不同方式。
具體形式如下圖所示
所以根據這兩個暫存器可以共同決定一個PMP條目決定的地址空間和所具有的許可權。
pmp_set_keystone()函數實現了兩個事情:
1.根據傳入的region_idx對應的 pmp_region對應結構體的資訊,計算需要寫入PMP條目的PMP配置暫存器和PMP地址暫存器的值。
2.判斷是否需要多個PMP條目來共同寫這一個地址空間。
之後利用PMP_SET宏調用來寫暫存器,這個PMP_SET宏內部展開之後是OPENSBI的介面和RISCV的內聯彙編,用於寫RISCV的狀態暫存器。。
二、runtime和sdk的工作原理
edge_common封裝了邊緣調用的格式:
每次調用都用一個結構體,規定size來來限制訪問許可權。edge data和ret data都一樣,是 指針 + size的形式。
參數用偏移量來尋找。
返回的數據單獨定義一個結構體。
edge_call.c封裝了syscall的io格式、邊緣檢查。每次edge call都需要檢查指針有效性,在共享記憶體區中找到對應的結構體,來完成edge call setup call。
runtime中的syscall 依託上述edge_call實現,設計原則:
(參考://rmheng.github.io/2021/01/29/keystone-2020/)
runtime可以理解為負責為eapp提供與安全無關的服務的一個代理。因為只是將調用請求進行檢查、封裝,再交給sbi用ecall彙編處理,所以說是代理。
(handler syscall 用了pk的介面。不在keystone的範疇。)
syscall依靠edge_call實現:
dispatch edgecall ocall
dispatch edgecall syscall
分別完成ocall和syscall的調用,共同的大致流程:
在shared mem的位置個edge call結構。在shared mem中取地址,找到結構體的指針。賦值call id,拷貝call data等記憶體。
邊緣檢查的過程在產生指針時進行。
調用、響應路線:
調用時:
eapp發起syscall或ocall。
syscall:
被io_wrap封裝成如下系統調用:
eg:
代理過程就是:在io_wrap中用dispatch_edgecall_syscall函數進行派遣。
dispatch_edgecall_syscall具體工作:
set up call,把共享記憶體區的一個指針變成一個安全可用的edge call結構體,賦值其中數據。
派遣結果ret就是eapp想要知道的系統調用的返回值。
ocall在handle_syscall中經過pk被派遣出去:
(handle_syscall這個函數在pk里被調用,pk暫時還沒研究)
dispatch_edgecall_ocall比syscall多一個拷貝用戶記憶體的過程。
底層通過操作csr暫存器來實現,還沒看。
響應時:
syscall:
由sdk完成對調用的響應。
每個enclave創建時都要把incoming dispatch註冊到oFuncDispatch,意思就是,為即將到來的edge call留一個指針,到時候遇到邊緣調用或者中斷,就通過這個函數來響應。這裡的函數,參數都是指針,所以響應時要通過call id來獲悉自己要做什麼事情。
syscall dispatch.c文件中的incoming call dispatch進行檢查:
檢查call id。判斷是syscall 還是ocall還是無效。有效的call id需要在edge call table中註冊。這裡的buffer是edge call結構體的指針,可以理解為一個函數指針。
enclave中的registerOcallDispatch:
把一個函數指針賦值給oFuncDispatch,後續在run的時候,會調用oFuncDispatch,這個函數的參數是個指針。
host處,進行賦值,將edge call綁定到一個指針上:
把上面講過的incoming_call_dispatch賦值給oFuncDispatch,該函數會解析指針處的記憶體,獲得call id判斷是syscall、ocall還是badcall,並進行相應處理。
enclave::run的定義:
enclave的運行過程。error是個枚舉結構。代表run的不同結果。
運行交給pDevice後,host掛起。檢測到edge call host或者發生中斷後進入while循環。接著進入if語句,判斷該調用是否安全。
enclave中的run函數,調用了oFuncDispatch,這東西就是剛才的edge call的指針,運行了這個edge call,就完成對call的響應:
edge call會自己把返回值封裝為結構體,寫進共享記憶體區。不用在這裡return。
處理結束後,通過resume函數,將控制權還給enclave。eapp繼續運行。
ocall要註冊:
把函數指針寫到edge call table里
響應流程還是經過incoming_call_dispatch:
判斷為用戶註冊過的edge call之後就用edge call table表裡的函數指針運行,buffer同樣是共享記憶體區的指針,指向了edge call。
edge call會自己把返回值封裝為結構體,寫進共享記憶體區。不用在這裡return。
runtime中其他零零碎碎的東西:
interrupt:
支援時鐘中斷:
linux_wrap封裝了支援的linux系統調用:
這些函數會輸出syscall的結果,例如:
sbi.c
sbi.h
封裝了最底層的sbi操作,通過ecall修改csr完成各種異常處理。
page_swap.c:
封裝了調頁操作:
如果定義了頁表加密,就會用aes256加密頁表。
如果定義了頁表哈希,就會用merkle樹檢驗頁表是否被非法改動過。用到的哈希演算法是sha256
兩種密碼學演算法都在runtime文件夾中有c語言實現。
mm.c
mm.h
是記憶體管理
內容很多,但還是能看個大概的。具體用到了在細說吧。
vm.c
vm.h
實現虛擬地址和物理地址的轉換
paging.c
paging.h
段頁式管理的實現,看起來還更吃力一些,因為有時候看不懂函數名稱。= =
freemem.c
freemem.h
free記憶體的函數,spa是simple page allocator 。沒仔細看,但是程式碼可讀性比較高,和之前看過的free實現比較類似。
再就沒什麼主要的文件了,runtime的大致結構就是這些。
感謝閱讀  ̄▽ ̄ 歡迎交流!
第一次寫部落格,寫的跟實驗報告似的,比較簡陋,見諒哈
2021-05-17
未經允許,禁止轉載 !