【tee小白的第一篇隨筆】keystone程式碼略讀

語言: CN / TW / HK

武大信安在讀,最近在自學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應該擁有所有許可權。

參考:http://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機制的工作原理:

參考: https://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實現,設計原則:

(參考: https://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

未經允許,禁止轉載 !