Skip to content

Latest commit

 

History

History
142 lines (95 loc) · 14.6 KB

01.md

File metadata and controls

142 lines (95 loc) · 14.6 KB

运行第一个SGX程序

本系列文章并不是客观的“如何写SGX程序”教程,而是一份非常主观的SGX导读,夹杂了本人对当下的SGX实现的解读。目前linux sgx套件的版本是2.2.

本系列文章中本人想引导读者对SGX的能力有一个更透彻的认识——其实是对“可信+保密”这个概念在实现成程序的时候究竟是什么样子展开讨论。其中不乏反问”这究竟是应该被信任的吗“之类的提问。在我过去的SGX推广经验中,看到的大多数问题都源自于对”可信+保密“理解不透彻。毕竟写一个传统应用和一个可信计算基是截然不同的两码事,没有经验也实属正常。本人在研究SGX的过程中也在不断的反思这两个问题。

这一节先从SGX运行环境的结构开始。

Intel SGX 程序通常分成两部分:untrusted和trusted。trusted包括了用户自己写的SGX enclave。用户自己的untrusted app需要和Intel提供的untrusted runtime等运行时库配合使用。

此外,为了支持Intel SGX程序的执行,还需要的东西包括:一个untrusted部分的守护进程aesmd,提供"Application Enclave Service Manager";几个必备的官方Enclave:Launch Enclave (le), Quoting Enclave (qe), Platform Service Enclave (pse), Provisioning Enclave (pve), Provisioning Certification Enclave (pce), Reference Launch Enclave (ref_le, 2.1.3新增, 具有提供Flexible launch control的能力);和一些配套的untrusted AE接口。

Application Enclave Service Manager (aesm), 是Intel SGX的系统组件,主要提供了 SGX Enclave 启动支持,密钥配置,远程认证等服务。

用一个在rust-sgx-sdk里提供的图来说明整体结构吧: sgx and mesalock linux

左图和右图分别是两种在docker里运行SGX程序的结构。图中展示的信息包括:

  1. SGX的内核驱动暴露一个/dev/isgx的misc device给用户态。用户态的AESM守护进程和uae service等经由这个misc device为用户程序的untrusted部分提供create/destroy enclave等的能力。
  2. AESM的守护进程aesmd会打开一个domain socket: aesm.socket给用户态程序的urts(untrusted runtime service)库和uae_service库提供支持。AESM对于用户来说“应该是个黑盒子”。用户只需要通过urts和uae_service提供的接口来实现业务逻辑就可以了。

其他的后面再说。

那么从之上的讨论中可以看出:想在真实硬件环境中运行一个SGX程序的前提包括平台提供isgx设备和全套的aesm服务和urts/uae_service的动态库。这里先抛开基于Intel ME提供的PSE不谈,后面再说。

为了方便开发人员,Intel提供了一套软件模拟环境。这套模拟环境可以通过在编译时指定SGX_MODE=SW打开。启用SW环境后,编译时会链接所有库的模拟执行版本,例如libsgx_trts_sim.a, libsgx_tservice_sim.a等。对应的untrusted部分也有模拟环境:libsgx_uae_service_sim.so, libsgx_urts_sim.so. 所以,在没有SGX硬件的支持下,也是可以通过软件模拟的方式来玩SGX的(但是做不了remote attestation,并且没有PSE)。

可以参考 SGX-Hardware 来确认自己的硬件是否支持SGX。如果CPU+主板都支持的,还需要在BIOS里将SGX模式设置为(Enable)。如果设置为了Software Control,那么在写代码时候还要多写两行,不推荐。如果SGX设置里能看到OWNEREPOCH的设置的话,那恭喜你,你的主板对SGX的支持比较完整。

用纯软件模式+docker模拟SGX环境实验

这甚至可以在macOS下完成,只要安装了docker-for-mac。无论在什么平台上,只要有了docker,都可以跑。为了方便,直接用我项目里提供的docker做实验。

$ docker pull teaclave/teaclave-build-ubuntu-1804-sgx-2.9.1:latest
$ git clone https://github.com/apache/incubator-teaclave-sgx-sdk.git
$ docker run --rm -v /your/path/to/incubator-teaclave-sgx-sdk:/root/sgx -ti teaclave/teaclave-build-ubuntu-1804-sgx-2.9.1:latest
root@docker:~# rustup toolchain list
nightly-2018-04-12-x86_64-unknown-linux-gnu (default)
root@docker:~# cd /root/sgx/samplecode/hello-rust
root@docker:~/sgx/samplecode/hello-rust# SGX_MODE=SW make
root@docker:~/sgx/samplecode/hello-rust# cd bin
root@docker:~/sgx/samplecode/hello-rust/bin# ./app
[+] Init Enclave Successful 30696131264514!
This is a normal world string passed into Enclave!
This is a in-Enclave Rust string!
[+] say_something success...

用硬件模式+docker真实执行

这里采取结构图左侧的方法。要求:CPU有SGX功能+主板开启了SGX支持(SGX为Enabled)。建议使用Ubuntu Desktop 16.04 x64。

首先是安装驱动。在左图中AESM是跑在docker内的所以不需要Host OS安装Intel SGX PSW。安装完后确保/dev/isgx存在即可。

$ docker pull baiduxlab/sgx-rust
$ git clone https://github.com/baidu/rust-sgx-sdk.git
$ docker run --rm -v /your/path/to/rust-sgx:/root/sgx --device /dev/isgx -ti baiduxlab/sgx-rust
root@docker:~# rustup toolchain list
nightly-2018-04-12-x86_64-unknown-linux-gnu (default)
root@docker:~# /opt/intel/sgxpsw/aesm/aesm_service
aesm_service[17]: The server sock is 0x560f782bf960
aesm_service[17]: [ADMIN]Platform Services initializing
aesm_service[17]: [ADMIN]Platform Services initialization failed due to DAL error
aesm_service[17]: [ADMIN]White list update request successful for Version: 36
root@docker:~# cd /root/sgx/samplecode/hello-rust
root@docker:~/sgx/samplecode/hello-rust# make
root@docker:~/sgx/samplecode/hello-rust# cd bin
root@docker:~/sgx/samplecode/hello-rust/bin# ./app
[+] Home dir is /root
[-] Open token file /root/enclave.token error! Will create one.
[+] Saved updated launch token!
[+] Init Enclave Successful 2!
This is a normal world string passed into Enclave!
This is a in-Enclave Rust string!
[+] say_something success...

结构图右侧的运行方法我们以后再谈。

配置本机SGX环境+真实执行

这里依旧不讨论PSE功能的支持。

首先还是照例安装驱动,确认/dev/isgx存在后安装PSW。确认aesmd开启后安装Intel SGX SDK。这里建议安装到/opt。安装完SDK后会提示执行一次source ...,就执行一下。

然后进入SDK的安装目录,再进入SampleCode/SampleEnclave目录,直接make./app即可。

剖析hello-rust

hello-rust是第一个全Rust的Intel SGX程序。这里的“全Rust”指的是untrusted和trusted部分均用Rust编写。

首先看app目录下的untrusted部分。其Cargo.toml非常简单,只包含两个依赖项:sgx_typessgx_urts。顾名思义,sgx_types就是数据结构和函数接口定义;sgx_urts是SGX的untrusted runtime service的Rust接口。对于一个untrusted端app来说,不考虑PSE、uprotected_fs等的情况下,这两个依赖项就足够创建、关闭enclave了。

再看app/build.rs。这个文件会将编译选项输出给rustc。可以看到其主要做的事情是:(1)帮助rustc找到SGX SDK的位置,(2)帮助rustc找到Enclave EDL所生成的Enclave_u的位置,(3)帮助rustc link到正确的urts库(HW or SW)。

这里引入了一个EDL的概念。EDL是Intel给定的一种文件格式,非常近似于C的header file写法。它规定了Enclave的边界ECALL/OCALL的定义。Intel提供了一个由Ocaml编写的EDL编译器edger8r,用于将每一个输入的EDL转化为(1)Enclave_u.c+Enclave_u.h + (2) Enclave_t.c + Enclave_t.h。这四个文件中包含了ECALL/OCALL中所引入的必要的“额外proxy functionmarshaling数据结构”用于完成ECALL/OCALL操作(Intel对于某些CVE的patch包括了在这些stub中插入lfence/sfence)。当我们在app中调用导出的ECALL函数时,控制流会转到这个自动生成的stub中,完成必要的操作,再把控制权交给sgx_ecall函数,sgx_ecall负责完成状态切换到SGX模式,进入Enclave。进入Enclave之后会先来到Enclave_t.c中实现的bridge function的另一端,进行另一些参数包装之后最后才会执行到Enclave函数的实现。

以上这些过程虽然是程序员“不可见”的,但是他们是理解SGX的关键之一。Intel甚至在其主页上公开了两份文档[1][2]来“教导”开发人员如何正确的做Bound Check以防止产生spectre风险。然而鲜有开发者会来读这两份冗长且无聊的文档——“知道太多的细节对于我们来说是负担”这句话根深蒂固的隐藏在开发人员的心里但是,这对构造一个Intel SGX程序来说是非常致命的。我将这两份文档潦草的翻译成了中文[3][4],希望对于看到本文的开发者有所帮助。

简单读过hello-rust/enclave/Enclave.edl之后,并不难理解在本例中唯一一个用户定义的ECALL函数是say_something,其参数包括一个uint8_t *的指针和一个长度len。在EDL里利用size=len来告诉SGX要把多少个字节的参数拷贝到Enclave里。达到的效果是:在Enclave里的say_something看来,这个指针是指向Enclave内存的,而不是指向untrusted部分的内存。所以,ECall会把这样的参数连续的按字节拷贝到Enclave的加密内存中,因此需要个长度。指针型参数只有一个例外:危险的[usercheck]标签修饰的参数,这个以后再说。

hello-rust/app/src/main.rs的128-145行,是一个完整的调用ECALL的例子。其中可以看到,给ECALL传递参数的时候是包括两个“隐参数”的,即enclave id和"返回值所存的地址&ret。并且这个ECALL会具有另外一个返回值result。这里result代表的含义是:ECALL执行是否成功。ret代表的含义是:ECALL函数的返回值。读者现在可以区分这两个值的区别吗?哪个值是Intel SGX定义的返回值,哪个是say_something的返回值呢?

main.rs的135行展示了如何将一个Rust string转化成一个char *类型的C string并传给Enclave。

再来看Enclave部分。Enclave部分的Cargo.toml声明了这是一个staticlib,也就是说,Enclave部分最后会被编译成一个.a文件。这个.a文件会和Intel提供的一系列sgx_tstdc.a等库文件链接在一起形成enclave.so,再经由sgx_sign工具配合Enclave.config.xml配置文件、签名私钥一起做签名和属性刻画,最后生成enclave.signed.so,才是Enclave的完全体。在dependency一栏,写了两个依赖项sgx_typessgx_tstd。前者不用细说,后者就是我们移植的标准库,包含了诸多基本功能的实现。

enclave/src/lib.rs即是Enclave的源代码了。有C/C++基础的读者读起来应该没有什么困难。唯一值得注意的是71行:

 71     // Ocall to normal world for output
 72     println!("{}", &hello_string);

这里要print出去。众所周知,print东西到屏幕上是需要做syscall的,而SGX内不可以做syscall,所以这里一定要做OCALL。但是在该例子EDL里没有直接出现OCALL打印函数。这是因为我们在实现println!的过程中加入了内置的OCALL,并定义了对应的EDL,并且在Enclave.edl中做了import。

 32 enclave {
 33     from "sgx_tstd.edl" import *;
 34     from "sgx_stdio.edl" import *;
 35     from "sgx_backtrace.edl" import *;
 36     from "sgx_tstdc.edl" import *;

如果有libc基础的同学一定知道printf实现的复杂性。从printf到最后write调用中间的过程是非常复杂的,在Enclave里也是如此。我们一定要在这个超长的调用过程中的某一点做切割,将控制流从Enclave内转到Enclave外,在untrusted部分完成write。那在哪里切割比较好呢?这就是所谓的partition了。

Partition, partition, partition

在SGX编程中,partition意味着划分可信/不可信的边界。在Enclave内的代码一般被认为是被信任的代码,在Enclave外的代码一般被认为是不可以被信任的代码。这里一个良好的设计原则是:无论Enclave外的代码如何执行,都不会泄露Enclave内的数据,破坏Enclave内的执行逻辑。因此partition是一个“业务逻辑”层面的事。对于一个业务来说,partition可以有无数种方式,但不恰当的partition会导致SGX程序“似乎被保护了”但是并不可信

让我们拿Scone-python做例子。Scone(和Graphene SGX)是一类试图将”任意程序“跑在SGX内的解决方案。他们甚至可以直接跑python解释器。看上去很美。但是在SGX的威胁模型下,只有Enclave内是可信的,这导致,在Enclave内执行的python解释器在执行到import numpy的时候,会从磁盘中读取numpy的python文件并进行解释,而读进来的文件是不可信的!这时相当于:在可信环境中执行了不可信的代码,然而——用户依然认为它是可信的!这是一个典型的错误可信计算基的实例。攻击者可以简单的替换磁盘中的numpy,下次用户再次import numpy的时候,就会使用植入了恶意逻辑(比如发送secret)的numpy在Enclave内执行,导致秘密泄露。

回到我们的例子中。我们的prinln!是在哪里切割的呢?这参考了Rust自身的抽象设计。感兴趣的同学可以查看sgx_tstd的源代码。相信你一定会有所发现。

Enclave代码lib.rs的最后就是返回一个SGX_SUCCESS了,很像python是不是?

有点累了,今天先说到这。希望各位能读一下[3][4],增加对SGX安全的理解。要重申的是,编写一个Enclave和编写普通app完全不同,需要考虑所有直接输入和隐式输入是否可信,而不是简单的——使用它们。