§.01 简介
Intel SGX 是一个把应用与 OS 完全隔离的可信执行环境,应用无法直接访问 OS 提供的资源。我们采用的 Teaclave-SGX-SDK 只提供了 no_std 环境,导致 crates 生态下大量的库都无法被使用。我们通过添加 libc 函数模拟 linux 平台特性,实现依赖 std 的 Rust 生态库无需修改即可在SGX环境使用。为了保证尽可能小的安全边界,我们对每个增补的 libc 函数做了权限控制。同时引入了二进制分析,确保程序不会出现 SGX 非法指令。§.02 背景
Phala Network 的隐私云计算服务基于 teaclave-sgx-sdk 开发,由于 Intel CPU 的 SGX 执行环境相当于裸机无系统,自然地基于 teaclave-sgx-sdk 开发的 rust 程序也只能用 no_std 开发。
teaclave-sgx-sdk:https://github.com/apache/incubator-teaclave-sgx-sdk
但当项目复杂后,我们还是希望能够充分利用 Rust 的 crate 生态,这个生态里大部分 crate 是依赖 rust std 的。我们 no_std 环境想要使用这些 std 的 crate 的话那就得做移植了。
如果单纯地将 std 的 crate 移植到 no_std 环境,那每一个crate都会有比较大的工作量。teaclave-sgx-sdk 为了方便移植,给我们准备了一个 sgx_tstd(一个sgx环境的std仿制品)。sgx_tstd 保留了 rust std 中的大部分功能,因此,一般简单的 crate 移植到 sgx_tstd 仅需要改动数行代码,比如在 crate 根部添加extern sgx_tstd as std,以及添加一些 use std::prelude::v1::*;。这样移植 rust crate 生态就方便了许多,teaclave-sgx-sdk 团队甚至将一些常用的 crate 都移植好了放到 github.com/mesalock-linux 中。
mesalock-linux:https://github.com/mesalock-linux/§.03 sgx_tstd 的问题
移植一个 crate 看上去工作量并不大,但我们很多时候引入一个 crate 并不是单纯的一个 crate,他背后的依赖树连根拔起可能有数十个 crate。原本正常 Rust 生态使用一个第三方 crate 只需要在 Cargo.toml 中添加一行代码,而现在变成要去移植一大车 crate 到 sgx 环境。
这种开发模式已经事实上导致了 Rust 生态被分叉成了 crates.io 和 mesalock-linux 两个互不兼容世界。这种分裂甚至让一些 no_std 的 crate 也受影响,比如混用一些依赖 log 或 serde 的 no_std crate 就不能正常编译,不得不修改他们使用 log-sgx 和 serde-sgx。如果哪天有人再为 arm/AMD的TEE 做一个类似的 rust sdk,难道我们要将 crates.io 继续分叉下去?
这种分叉行为同时会导致被移植的生态可能代码更新不及时,一些针对 crates.io,github.com 的安全扫描公共设施可能也会漏掉 mesalock-linux 生态中的隐患,从而影响下游开发或带来安全威胁。§.04 让 SGX 支持 Rust 原生 std
teaclave-sgx-sdk 开发应用目前标准做法是开启 #![no_std]并编译 target 到 x86_64-unknown-linux-gnu。
既然已经 target 到了 x86_64-unknown-linux-gnu,那么我们如果不开启 no_std 编译会有什么问题呢?
简单尝试会得到类似如下链接错误:
Rust 的 std 会依赖 libc 来和 OS 交互,intel sgx-sdk 里面有一个不完全实现的 sgx libc。但 Rust 需要的和系统交互这部分 libc函数往往是SGX不信任的,所以 sgx libc 没有直接提供,而是大部分实现在 ocall 模块下以 rust ABI 函数的方式提供对等功能,以此提醒开发者这是不受信任的操作。
因此,我们想提供一个转接层,把这些缺失 libc 函数都补齐,并代理到 sgx-sdk 的对等实现基本就能正常编译使用原生std了。比如,上图缺失 write 函数,我们就补一个 write 函数:
#[no_mangle]pubextern"C"fnwrite(fd:c_int,buf:*constc_void,count:size_t)->ssize_t{unsafe{ocall::write(fd,buf,count)}}
这样,我们转接层对上模拟一个 linux glibc 的行为,对下转接到 sgx 特别实现,可让针对 linux 的编译的 Rust 应用程序跑在 sgx 内。
经验证的确如此,在添加了相应 libc 函数并拆掉一部分特殊代码后,我们 enclave 程序就运行起了。
相应 libc 函数:https://github.com/Phala-Network/phala-blockchain/blob/550843a14bbc96bdf59033dfa2850429cf9b039e/standalone/pruntime/enclave/src/patch.rs§.05 std 和 sgx_tstd 共存
上面提到,拆掉了一部分代码,主要这些代码依赖 sgx_tstd 里面特有功能,比如 sgx_tstd::sgxfs::SgxFile。而开启原生 std 后 sgx_tstd 就因为 rust 的 lang_item 冲突而不能编译了。要想恢复使用这些功能,我们要么自己重新实现(copy)一份,要么让 sgx_tstd 和 std 共存。显然,后者更符合可持续发展原则。因此,我们给 sgx_tstd 打个补丁,让 lang_item 变成一个 feature,不开启它就能与原生 std 共存了。
补丁:https://github.com/Phala-Network/incubator-teaclave-sgx-sdk/commit/ec42fc49ccbfd1174b60c96548de009647ab1f9d§.06 安全考虑
我们 enclave 程序是安全敏感的,如果一股脑将 libc 代理到 ocall 函数,显然是粗鲁的不安全的。因此,我们对每个代理的函数都会根据我们业务需求对其安全性做思考,调整其实现行为。§.07 getrandom
随机数安全性尤其重要,直接关系到我们的密钥安全。rust 的 rand crate 会调用 getrandom 函数来获取随机熵源。我们将 getrandom函数代理到 sgx_read_rand,sgx_read_rand 在HW 模式下会通过 CPU 硬件获取真随机数。实现如下:
#[no_mangle]pubextern"C"fngetrandom(buf:*mutc_void,buflen:size_t,flags:c_uint)->ssize_t{ifbuflen==0{return0;}letrv=unsafe{sgx_read_rand(bufas_,buflen)};matchrv{sgx_status_t::SGX_SUCCESS=>buflenas_,_=>{ifflags&libc::GRND_NONBLOCK!=0{set_errno(libc::EAGAIN);}else{set_errno(libc::EINTR);}-1}}}§.08 权限相关
支持了std,我们需要严格控制enclave内代码的权限,以最大限度降低安全风险。越权访问代码最好是在编译构建时阻拦下来,次之是在运行时限制越权访问。
转接层对权限的控制策略如下:
§.09 链接错误排查
由于我们现在的原生 std 依然存在部分功能缺失, 当我们引入新的依赖到 SGX 时,少数情况有可能会遇到链接错误,比如依赖中有网络操作,报了如下错误:
简单情况下,我们可以去检查源码,发现是哪部分功能引入了这个依赖。但很多情况下,其实我们看到的代码虽然会经过编译,但最终进入 binary 的只有其中一小部分,那些静态不可达的代码都会被编译器/链接器丢弃掉。因此,我们可能很难根据原始源码判断出实际生效的依赖关系。
因此,我们需要从最终输出的 binary 出发来分析上图中的 connect 究竟是怎么被引入进来的。我们写了一个 callerfinder.py 来辅助分析此类问题。
callerfinder.py:https://github.com/Phala-Network/phala-blockchain/blob/b91e11e00e97dbf9485fdc863fb916ff00d538f8/standalone/pruntime/callerfinder.py
第一步先把这些 undefined reference 都用一个空的占位符号补上,使其能编译通过:
然后使用 callerfinder 库查找输出二进制文件中 undefined 函数的依赖关系:
In[1]:fromcallerfinderimportCallerFinderIn[2]:finder=CallerFinder("./enclave/enclave.so")In[3]:finder.print_callers('std::net::tcp::TcpStream::connect',14)std::net::tcp::TcpStream::connect_timeout::h79c6c1fec8ad56c5http_req::request::Request::send::h8ea00de7a9d4e562enclaveapp::create_attestation_report::h08c59df2ec69ab65enclaveapp::prpc_service::get_runtime_info::h5ee8ea7c8422d583phala_enclave_api::...::dispatch_request::hd1bf94703ec9513eecall_prpc_requestsgx_ecall_prpc_request
这样我们就很清晰地找到网络操作的来源,根据情况采取对应的措施,比如这里我们把 http_req 换成原 http_req-sgx 移植版本即可。§.10 CPUID 指令问题
我们将 enclave 代码迁移到 std 后用 SGX_MODE=SW 模式顺畅运行,SGX_MODE=HW 环境下则出现多处崩溃。
经排查,这些崩溃均指向同一个函数 rand::thread_rng(),而 rand::thread_rng() 其内部实现使用了 std::is_x86_feature_detected 宏来检测 CPU 对 SIMD 的支持程度。该宏使用了 SGX 环境禁止的 CPUID 指令,导致程序崩溃。
一方面 SGX 环境出于安全考虑禁止了 CPUID 指令,另一方面,应用程序使用 CPUID 检测 CPU 对 SIMD 的支持情况是很常见的“正当行为”。虽然 CPUID 触发崩溃虽然没有泄露信息,没有越权访问,没有触发 Unsound 等安全问题,是一种运行时安全守卫措施。但这种“正当行为”而触发运行时崩溃显然不能接受,如果我们代码依赖中有相关检测逻辑,在我们的业务随时有宕机风险。因此,我们要么让 std::is_x86_feature_detected 适配 sgx 环境,要么让保证我们整体代码不触及 CPUID。
Teaclave 的 sgx_tstd 中重新实现了 is_x86_feature_detected 宏 来避免触及 CPUID。而我们采用原生 std 的方案,问题就稍微复杂一点了,我们无法像前文那样通过 libc 重新实现 is_x86_feature_detected(除非给std打补丁)。另外,单解决一个 is_x86_feature_detected 显然也不能避免代码直接内嵌 CPUID 汇编指令的情况。因此,我们暂且选择让代码不触及 CPUID。
具体方法为,我们增加一个编译后处理步骤:
https://github.com/Phala-Network/phala-blockchain/pull/437/commits/b91e11e00e97dbf9485fdc863fb916ff00d538f8
通过反汇编输出的 enclave.so 来检查其中是否含有 CPUID 指令。如果有我们让 make 报错并打印出函数名:
然后可利用前述 callerfinder.py 找出哪些函数导致依赖了 CPUID:
然后,我们可以顺藤摸瓜找到对应的代码实现。如果是必要的,我们可以 patch 对应的 crate 让他使用 teaclave 提供的 is_x86_feature_detected, 比如 rand::thread_rng();如果是能砍掉的功能我们就砍掉,比如上图中的 env_logger 我们 href=”https://github.com/Phala-Network/phala-blockchain/pull/434/commits/8ec0a9d2343a65008e28cd3eaabf71617cb81e16″> 关掉其 regex feature 即可消除此依赖。
rand::thread_rng():https://github.com/Phala-Network/phala-blockchain/pull/434/commits/9a37919c2e40d95b866e364048b7bf4d635ce127
https://github.com/Phala-Network/phala-blockchain/pull/434/commits/8ec0a9d2343a65008e28cd3eaabf71617cb81e16§.11 关于其它 SGX 非法指令
既然 CPUID 存在此问题,那么是否可能碰到其它 SGX 特别禁止的指令呢?理论上当然是可能碰到的,从intel 的指南看看还有哪些特殊指令。
指南描述一些SGX环境的非法指令如下:
对于其2和其3,除了 INT/SYSCALL/SYSENTER 等系统调用指令之外,其余都应只出现在系统内核代码中。而系统调用相关功能除非极其个性的程序,否则不论是 Rust 还是 C/C++ 生态都应调用 libc 的相关函数或 syscall 函数来完成,而不是直接嵌入汇编指令。
我们着重需要关注 1 中这些指令:
CPUID 常用于检测 CPU 对 SIMD 的支持,以便使用不同 SIMD 指令集处理计算密集任务,需要关注。
GETSEC 是一个 leaf funtion 总入口,有很多子功能,都是特殊用途的。一般程序不会用到
RDPMC:读取性能计数器,特殊用途,perf之类的工具使用,不必关注。
RDTSC/RDTSCP:读取CPU timestamp计数器,可能被应用程序使用,加入指令检测脚本。
SGDT – Store Global Descriptor Table Register(仅操作系统使用)
SIDT – Store Interrupt Descriptor Table Register(仅操作系统使用)
SLDT – Store Local Descriptor Table Register(仅操作系统使用)
STR – Store Task Register(仅操作系统使用)
VMCALL/VMFUNC 虚拟化相关指令,不用关注
为保险起见,我们把这些指令全都加入后处理检查脚本中,禁止其使用。§.12 小结
添加 std 支持后,我们用 Rust 开发 SGX 程序变得和开发普通 Rust 应用程序无太大差异,也能直接使用 Rust 标准工具链的单元测试等设施,开发效率上升一个台阶。About Phala
Phala Network 是一个 Web3.0 共享云平台,旨在解决计算云中的信任问题。基于TEE可信硬件的分布式计算,Phala 云计算可在不牺牲数据机密性的情况下实现大规模云计算处理,其计算系统是可信的。Phala 通过将共识机制与计算分离,确保处理能力具有高度可延展性。不同于传统云服务平台,Phala 的计算节点哪怕不在数据中心也可提供安全、机密性好、边缘化的云服务,为强大安全和可扩展的无信任计算云创建了共享经济模型的基础设施。