【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:

  1. 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

未經允許,禁止轉載 !