Skip to content

Latest commit

 

History

History
352 lines (287 loc) · 15.4 KB

03.md

File metadata and controls

352 lines (287 loc) · 15.4 KB

Intel® Software Guard Extensions (SGX) SW Development Guidance for Potential Edger8r Generated Code Side Channel Exploits 参考译文

White Paper, Revision 1.0, March 2018

Background

2018年2月16日,来自Catholic University of Leuven(KU Leuven)的一个安全研究团队告知Intel由于Intel SGX SDK中Edger8r工具引起的安全问题。这个问题会导致Edger8r工具产生的SGX代码含有严重的side channel安全隐患,使得enclave内的数据被外部攻击者窃取。

Intel SGX程序会使用Edger8r工具来产生边界代码。这些边界代码由一系列proxy函数和bridge函数组成。proxy函数是程序调用ECALL函数时,application一侧的接口函数。bridge函数是ECALL进来之后所执行的函数,是enclave一侧的接口函数,,这个bridge函数最后会调用到ECALL真正所指向的函数。SGX开发者使用EDL文件作为输入,交给Edger8r工具,来生成这些函数。这份EDL文件需要符合Edger8r所规定的语法。这个语法中的两个关键属性:string和sizefunc所产生的代码容易受到此次side channel attack的威胁。开发者一旦使用了这两个关键字来修饰函数参数,那么产生的SGX程序就基本含有spectre漏洞。开发人员应当遵循本文档中提出的建议来最小化潜在的威胁。

这份文档中我们介绍为了缓解spectre,对EDL语法和相应的库以及工具做出的改动。

Intel SGX SDK Changes

为了解决Edger8r的问题,SDK进行了更新。下表展示了SGX SDK(Windows* OS Version 1.9.106 和 Intel® SGX SDK for Linux OS Version 2.1.102)的改动:

SDK更改 影响 受影响的SDK组件
修改了'string'属性的实现。现在'string'修饰的参数的长度在untrusted代码中进行。 使用了'string'属性的代码需要重新使用Edger8r编译,并重新生成整个项目。 Edger8r Tool
移除了'sizefunc'属性 如果项目中使用了'sizefunc'来修饰参数,那么开发人员应该修改函数定义和实现,使用‘size’来指定一个固定长度的输入。此外开发人员应当重新编译和生成EDL文件以及整个项目。 Edger8r Tool
在enclave操作secret memory(即使不被考虑为secret)时停止推测执行。(参考上文) 所有在EDL文件中传递了指针的项目应当使用更新后的Edger8r重新编译,并重新生成整个项目。 Edger8r Tool
更新了SDK样例代码,移除了所有sizefunc的使用 展示了一种不使用'sizefunc'传递参数的办法 SDK样例代码

Intel SGX Developer Guidance

为了利用更新后的SDK,开发人员应使用新版SDK重新编译他们的项目。根据Intel SGX开发手册,开发人员可以选择自增SGX enclave的ISVSVN,以体现这次安全升级。因此,在某些情况下,在需要引用到ISVSVN建立信任和provision secret时,需要更新对应的ISVSVN,以对应于这次安全升级。

如果开发人员使用了sizefunc属性,那么直接重新编译项目是不够的必须手工修复代码,去掉所有的sizefunc属性。这篇文章提供了一种消除sizefunc的参考实现。

接下来的章节描述了Edger8r工具产生代码的变动,以及介绍了一种去掉sizefunc的方法。

Edger8r string Attribute

ECALL函数参数中的'string'属性可能会导致漏洞。使用'string'修饰函数参数会使得自动生成的enclave代码中包括一个strlen函数的调用。strlen返回的长度用于定义参数的buffer长度。

如果'string'修饰的参数指向一个enclave内的地址,那么strlen将直接计算从这个enclave内地址开始的字符串的长度。同时自动生成的代码部分会检查这个输入buffer是否位于enclave内。

攻击者可以在enclave外构造恶意输入,使得'string'修饰的参数指向精心构造的位置,从而触发spectre攻击,使用timing attack泄露enclave内的数据。

Example - Current Edger8r (未更新的)

/* EDL
 * [string]:
 * the attribute tells Edger8r 'str' is NULL terminated string,
 * so strlen will be used to count the length of buffer pointed
 * by 'str'. */
public void ecall_pointer_string([in, string] char *str)

typedef struct ms_ecall_pointer_string_t {
    char* ms_str;
} ms_ecall_pointer_string_t;

当上图所述的EDL代码被Edger8r工具处理时,Edger8r工具会自动生成一个"marshalling structure":ms_ecall_pointer_string_t。这个结构包含了一个指针ms_str指向输入参数(str)。

sgx_status_t ecall_pointer_string(sgx_enclave_id_t eid, char* str)
{
    sgx_status_t status;
    ms_ecall_pointer_string_t ms;
    ms.ms_str = str;
    status = sgx_ecall(eid, 12, &ocall_table_Enclave, &ms);
    return status;
}

上图是对应的Edger8r产生的proxy函数。这里参数包括了一个'string'修饰的参数。在这个函数中,用户提供的指针被填充到marshalling structure对应的域里。这个操作在untrusted部分完成。

#define CHECK_REF_POINTER(ptr, siz) do { \
if (!(ptr) || ! sgx_is_outside_enclave((ptr), (siz))) \
           return SGX_ERROR_INVALID_PARAMETER;\
} while (0)

#define CHECK_UNIQUE_POINTER(ptr, siz) do { \
if ((ptr) && ! sgx_is_outside_enclave((ptr), (siz))) \
           return SGX_ERROR_INVALID_PARAMETER;\
} while (0)

static sgx_status_t SGX_CDECL sgx_ecall_pointer_string(void* pms)
{
    CHECK_REF_POINTER(pms, sizeof(ms_ecall_pointer_string_t));
    ms_ecall_pointer_string_t* ms =
        SGX_CAST(ms_ecall_pointer_string_t*, pms);
    sgx_status_t status = SGX_SUCCESS;
    char* _tmp_str = ms->ms_str;
    size_t _len_str = _tmp_str ? strlen(_tmp_str) + 1 : 0;
    char* _in_str = NULL;
    CHECK_UNIQUE_POINTER(_tmp_str, _len_str);
    // fence after pointer checks
     _mm_lfence();
     if (_tmp_str != NULL && _len_str != 0) {
           _in_str = (char*)malloc(_len_str);
           if (_in_str == NULL) {
                status = SGX_ERROR_OUT_OF_MEMORY;
                goto err;
           }
           memcpy(_in_str, _tmp_str, _len_str);
           _in_str[_len_str - 1] = '\0';
     }
     ecall_pointer_string(_in_str);
err:
     if (_in_str) free(_in_str);
     return status;
}

上图是Edger8r产生的bridge函数。可以看到,strlen操作在enclave里面完成。

Example - Updated Edger8r

Edger8r工具的更新修改了marshalling structure的定义,在上例中,更新后的Edger8r会产生如下的Marshalling structure:

typedef struct ms_ecall_pointer_string_t {
     char* ms_str;
     size_t ms_len_str;
} ms_ecall_pointer_string_t;

这里多了一个成员ms_len_str,用于指明这个字符串的长度。

sgx_status_t ecall_pointer_string(sgx_enclave_id_t eid, char* str) {
    sgx_status_t status;
    ms_ecall_pointer_string_t ms;
    ms.ms_str = str;
    ms.ms_len_str = strlen(str) + 1;
    status = sgx_ecall(eid, 12, &ocall_table_Enclave, &ms);
    return status;
}

上图是更新后的Edger8r工具产生的bridge函数,运行在untrusted部分。可以看到,string的长度是由strlen函数在untrusted部分计算得到的,不涉及到enclave的内存。

#define CHECK_REF_POINTER(ptr, siz) do { \
if (!(ptr) || ! sgx_is_outside_enclave((ptr), (siz))) \
           return SGX_ERROR_INVALID_PARAMETER;\
} while (0)

#define CHECK_UNIQUE_POINTER(ptr, siz) do { \
if ((ptr) && ! sgx_is_outside_enclave((ptr), (siz))) \
           return SGX_ERROR_INVALID_PARAMETER;\
} while (0)

static sgx_status_t SGX_CDECL sgx_ecall_pointer_string(void* pms)
{
     CHECK_REF_POINTER(pms, sizeof(ms_ecall_pointer_string_t));
     // fence after pointer checks
     sgx_lfence();
     ms_ecall_pointer_string_t* ms =
          SGX_CAST(ms_ecall_pointer_string_t*, pms);
     sgx_status_t status = SGX_SUCCESS;
     char* _tmp_str = ms->ms_str;
     size_t _len_str = ms->ms_len_str;
     char* _in_str = NULL;
     CHECK_UNIQUE_POINTER(_tmp_str, _len_str);
     // fence after pointer checks
     sgx_lfence();
     if (_tmp_str != NULL) {
           _in_str = (char*)malloc(_len_str);
           if (_in_str == NULL) {
                status = SGX_ERROR_OUT_OF_MEMORY;
                goto err;
           }
           memcpy(_in_str, _tmp_str, _len_str);
           _in_str[_len_str - 1] = '\0';
     }
     ecall_pointer_string(_in_str);
err:
     if (_in_str) free(_in_str);
     return status;
}

上图是更新后的Edger8r生成的proxy函数。可以看到_len_str直接使用了ms->ms_len_str的值,而不是在enclave内调用strlen计算得到。并且更新后的Edger8r插入了额外的lfence指令。

Edger8r sizefunc Attribute

EDL语法中的sizefunc属性用于允许开发者对一个参数给定一个函数用于计算参数的长度。这个长度计算函数在enclave内执行,并在判断参数是否位于enclave内之前执行。因此,这个用于计算参数长度的函数会受到spectre攻击的威胁。所以,在这次更新中sizefunc属性被移除。为了替换sizefunc,开发者应使用size属性来指定参数的长度,并且开发人员必须保证:在enclave内处理输入参数之前,对输入参数的访问不会越界(看下文例子),并且在处理输入参数过程中访问也不会越界。

Example - Current Edger8r using Sizefunc (有问题的sizefunc)

下图是一个sizefunc的例子

typedef struct _tlv_t {
    uint32_t buf_len;
    uint8_t buf[];
} tlv_t;
/*
 * [sizefunc]:
* the attribute tells Edger8r that calc_size can be used to
 *      calculate the size of varlen_input
 */
public void ecall_sizefunc([in, sizefunc = calc_size] tlv_t* varlen_input);

size_t calc_size(const tlv_t* varlen_input) {
    return ( sizeof(tlv_t) + varlen_input->buf_len );
}
void ecall_sizefunc(tlv_t* varlen_input){
   //process varlen_input
return;
}

这个例子中calc_size用于计算varlen_input参数的长度。Edger8r会自动生成一个Bridge Function(ECALL进来之后执行的第一个函数),用于执行calc_size来计算varlen_input的长度。由于varlen_input这个参数所指向的数据不一定位于enclave外,所以造成calc_size在得知varlen_input是否位于enclave外之前就被执行,具有side channel的风险。事实上,clac_size被执行了两次,一次作用于enclave外的Marshalling structure ms->ms_varlen_input,另一次作用于enclave内的_in_varlen_input。这么做是为了确认传入的参数长度确实等于第一次在enclave外计算的长度。

这里就暴露了一个sizefunc设计上的难题。为了判定传入参数是否位于enclave外,bridge routine必须得知参数的长度。但是这个参数长度的计算只能通过sizefunc进行。

static sgx_status_t SGX_CDECL sgx_ecall_sizefunc(void* pms) {				
CHECK_REF_POINTER(pms, sizeof(ms_ecall_sizefunc_t));
ms_ecall_sizefunc_t* ms = SGX_CAST(ms_ecall_sizefunc_t*, pms);
sgx_status_t status = SGX_SUCCESS;
tlv_t* _tmp_varlen_input = ms->ms_varlen_input;			
size_t _len_varlen_input = ((_tmp_varlen_input) ? calc_size(_tmp_varlen_input) : 0);
tlv_t* _in_varlen_input = NULL;
	CHECK_UNIQUE_POINTER(_tmp_varlen_input, _len_varlen_input);
// fence after pointer checks
_mm_lfence();
	if (_tmp_varlen_input != NULL && _len_varlen_input != 0) {
_in_varlen_input = (tlv_t*)malloc(_len_varlen_input);
if (_in_varlen_input == NULL) {			
              		status = SGX_ERROR_OUT_OF_MEMORY;
			goto err;
}
		memcpy(_in_varlen_input, _tmp_varlen_input, _len_varlen_input);			
          	/* check whether the pointer is modified. */
		if (calc_size(_in_varlen_input) != _len_varlen_input) {
status = SGX_ERROR_INVALID_PARAMETER;
goto err;
}
       // fence after final sizefunc check
       _mm_lfence();
}
ecall_sizefunc(_in_varlen_input);
err:
	if (_in_varlen_input) free(_in_varlen_input);
return status;					
}

以上这个解法与'string'属性的解法非常相似,都需要在untrusted一侧的proxy function里计算参数长度,并且在Marshalling structure里增加一个额外的长度域用于传递这个长度。但是这个解法带来了另一个问题:需要一个enclave内用于计算参数长度的函数,来确保参数传递进enclave之后长度不变。这第二次检查是用于确认真正传入的参数没有超过长度域所指定的长度。

因此这个解决方案要求开发人员编写: Marshalling structure结构体,包含一个额外的长度域。 一个untrusted sizefunc用于计算参数长度 一个trusted sizefunc用于验证参数传递的正确性

但是,sizefunc的本来目的是为了方便开发人员编写ECALL函数,自动产生Marshalling structure。然而现在看来,由于安全起见,开发人员不得不增加工作量以确保安全性。

因此,在更新后的Edger8r中移除了sizefunc属性。开发人员需要使用size属性来传递参数。

Example - Implementation without sizefunc

size属性可以用于解决4.2.1节中的问题。如下图例子。

/*
* [size]:
 *      the attribute tells Edger8r that len is the size of
 *      varlen_input
 */
public void ecall_no_sizefunc([in, size = len] tlv_t* varlen_input, size_t len);

/*
 * ucalc_size:
 *   calculates the size of a tlv_t structure
 */
size_t ucalc_size(const tlv_t* varlen_input) {
return (sizeof(tlv_t) + varlen_input->buf_len);
}

int SGX_CDECL main(int argc, char *argv[])
{
     tlv_t varlen_struct;
     ...
     ret = ecall_no_sizefunc(global_eid,
&varlen_struct,
ucalc_size(&varlen_struct));
     ...
}

Edger8r会为这个ecall_no_sizefunc生成如下的Bridge function。这个函数并不调用任何函数来动态确定varlen_input的长度,而是使用Marshalling structure里的长度域作为输入参数的长度,用于判定varlen_input是否位于enclave之外。在确定varlen_input位于enclave之外后,再将其拷贝到enclave内的buffer中。

static sgx_status_t SGX_CDECL sgx_ecall_no_sizefunc(void* pms) {
    CHECK_REF_POINTER(pms, sizeof(ms_ecall_no_sizefunc_t)); 
    // fence after pointer checks
    sgx_lfence();
    ms_ecall_no_sizefunc_t* ms = SGX_CAST(ms_ecall_no_sizefunc_t*, pms);
    sgx_status_t status = SGX_SUCCESS;
    tlv_t* _tmp_varlen_input = ms->ms_varlen_input;
    size_t _tmp_len = ms->ms_len;
    size_t _len_varlen_input = _tmp_len;
    tlv_t* _in_varlen_input = NULL;
    CHECK_UNIQUE_POINTER(_tmp_varlen_input, _len_varlen_input);

    // fence after pointer checks
    _mm_lfence();

    if (_tmp_varlen_input != NULL && _len_varlen_input != 0) {
        _in_varlen_input = (tlv_t*)malloc(_len_varlen_input);
        if (_in_varlen_input == NULL) {
            status = SGX_ERROR_OUT_OF_MEMORY;
            goto err;
        }
        memcpy(_in_varlen_input, _tmp_varlen_input, _len_varlen_input);
    }
    ecall_no_sizefunc(_in_varlen_input, _tmp_len);
err:
    if (_in_varlen_input) free(_in_varlen_input);
        return status;
}

为了确认拷贝进来的varlen_input确实是得到了正确的值,开发人员必须编写一个enclave内的安全长度计算函数,来确认参数传递是正确的,如下图:

size_t tcalc_size(const tlv_t* varlen_input, size_t maxlen) {
    size_t len = sizeof(tlv_t);
    if (len > maxlen) {
        len = 0;
    } else {
        _mm_lfence(); //fence after maxlen check (CVE-2017-5753)
        len = sizeof(tlv_t) + varlen_input->buf_len;
        if (len > maxlen) {
            len = 0;
        }
    }
    _mm_lfence(); //fence after maxlen check (CVE-2017-5753)
    return len;
}

void ecall_no_sizefunc(tlv_t* varlen_input, size_t len) {
    if (tcalc_size(varlen_input, len) == 0) {
    //record the error
    return;
    }
    //tcalc_size has already performed the fence
    //process varlen_input
    return;
}