当前位置: 首页 > 工具软件 > Rust SGX SDK > 使用案例 >

Intel® Software Guard Extensions (SGX) SW Development Guidance for Potential Bounds Check Bypass中文

漆雕誉
2023-12-01

Rust-SGX-SDK 阅读intel指导翻译

Enclave输入

三种Enclave输入模式会导致被攻击的风险:

  1. 输入被解释为地址/指针
  2. 输入值参与地址计算
  3. 输入指向的内容被解释为地址,或参与地址运算
  • 攻击者可以改变输入参数,是其直接或简介计算生成的地址指向所泄漏的secret。此时,由于输入会经过一个输入指针是否在enclave外的判断,于是符合了spectre攻击的条件导致推测执行,从而side channel attack
  • 一个enclave是会包含这类参数的,因为在编译过程中,Edger8r会为每个ECALL的参数包装成一个Marshaling structure, 并使用一个指针指向这个结构并作为参数进行传递。
  • Edger8r所生成的代码和tRTS的部分代码一同维护一个ecall table 的数据结构,用于将开发者定义的ECALL转换成indirect call。针对这个地址转换进行的攻击,以及上述marshaling structure指针的攻击,在本次SDK更新中得到缓解。
  • SDK在EDL文件中定义的地址类参数在处理中自动插入缓解指令,只要满足:
    • EDL文件中此参数被定义为指针类型
    • 没有使用user_check关键字或者sizefunc属性来修饰此参数,注:某一个"sizefunc input"是否可以被exploit,依赖这个特定的sizefunc函数,这个特定的sizefunc函数是开发者自己编写的。
    • 这个参数的指针性并没有被typedef所掩盖。注:这情况下,EDL文件中的"isptr"属性可以被用于标识这个参数具有指针性,会被当作指针对待。
  • 即使一个输入的指针参数符合了以上标准,一旦这个指针所指向的数据结构包含了任何一个可以用来计算地址/指针的域,那么,开发者有责任去分析这个enclave代码中所有使用这个“嵌套指针”数据结构的代码,以去除安全隐患。更新后的SDK对此类情况没有帮助,只有一个情况除外:一个使用了sealing库的enclave会引入一个指针,指向seal过的blob,作为输入。sealing库的代码会将这个blob解析为一个含有offset的数据结构。在此次更新的SDK中加入了对应的缓解,以防止产生危险。
  • 在真实世界中,处理可变长参数是开发人员经常遇到的情况,并且需要开发人员仔细分析。可变长参数的头部通常包含一个长度域,常见的做法是借用这个传人的长度作为边界来读取参数的值。但是这样的实现就会导致mis-predict,进一步导致推测执行机制执行本不该执行的代码。
public uint32_t enclave_function([in, size = alloc_size] tlv_t* varlen_input,
                                  uint32_t alloc_size);
// 头文件定义
typedef struct {
     unsigned type;
     unsigned length;
     void* payload;
} tlv_t;
  • SDK检查一段长度alloc_size的内存是否位于enclave之外,本次更新后的SDK加入缓解指令,避免危机的side channel attack. 然而,开发者编写的代码中仍然可能具有根据tlv_t结构中length域对payload进行访问的代码。这样的内存访问所产生的side channel attack风险是不会被SDK的更新所缓解的。例如,开发者往往会先比较varlen_input.length和alloc_size的大小关系,再做相应的数据读取操作。这个比较操作就可以被攻击者利用,导致预测器的mis-predict,进而造成spectre攻击风险。开发人员有责任在确定长度合法之后插入一条LFENCE指令,来防止推测执行机制访问到真实数据。如果性能允许,开发者可以简单的在每个分支之后插入LFENCE指令,避免繁重的路径分析工作,如:
// make sure payload is outside enclave
If (varlen_input.length > alloc_size) {
// error code
... }
Else {
     _mm_lfence();
     //
     // valid length path
     //
     ...
}
  • 其中_mm_lfence是Intel编译器中LFENCE指令对应的函数。其他的编译器使用__builtin_ia32_lfence。

EDL属性中的user_check

  • 告诉SDK其修饰的指针不需要被检查(检查缓冲区是否位于enclave地址空间)。SDK代码会把user_check修饰的参数作为整数对待。因此一旦开发者使用user_check对指针进行修饰,那么开发者需要负责分析所有直接、简介使用这个指针参数的enclave代码,如果参数是user_check指针并且指向enclave外的内存地址,那么以下代码会直接导致side channel攻击风险,加入LEFNCE指令来缓解这种风险,其导致的性能下降是可以接受的。
// EDL
public uint32_t enclave_function([user_check]const uint8_t* user_check_input,
                                 uint32_t user_check_size);

uint32_t enclave_function(const uint8_t* user_check_input,
                          uint32_t user_check_size)
{
     ...
     //
     // make sure input buffer is outside enclave
     //
     int SGXAPI sgx_is_outside_enclave(const void *addr, size_t
     size);
     if (!sgx_is_outside_enclave(user_check_input,
     user_check_size)) {
         // error code
         ...
     }
     else {
          _mm_lfence();
          ...
     }
     ...
}
  • sgx_is_outside_enclave是SDK默认提供的函数,这个函数检查一个由起始地址和长度定义的buffer是否完全处于enclave 地址空间之外。
  • 如果一个user_check输入本身指向理应处于enclave地址空间内的数据结构,攻击者可以篡改这个指针,使其指向enclave内的错误位置。需要检查此类输入参数。

Table/Array Indexing

void victim_function(size_t x) {
     if (x < array1_size) {
          temp &= array2[array1[x] * 512];
     }
}
  • 如果victim_function是EDL指定的受信任的enclave函数,那么SDK的代码完全无法缓解这段代码中含有side channel attack风险,因为SDK并不知道输入会被用于参与地址计算,开发者应当插入对应的检查代码来缓解风险,如:
void victim_function(size_t x) {
     if (x < array1_size) {
          _mm_lfence();
          temp &= array2[array1[x] * 512];
     }
}

sizefunc

EDL属性中的sizefunc使得在EDL进行函数定义时,开发人员可以指定一个函数,这个函数用于计算函数参数的长度。sizefunc所修饰的函数是运行在enclave中的,但由于其负责计算输入的长度,因此在进行常规的输入参数是否位于enclave之外这一检查之前,输入参数就会被访问到。这个过程容易引起side channel 攻击风险,因此sizefunc函数应当被马上从EDL定义中移除,开发人员应该使用更安全的size属性来确定输入参数的长度,从而保证在读取输入参数之前就使得index不会越界。

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 check (CVE-2017-5753)
         len = varlen_input->length;
         if (len > maxlen)
         {
             len = 0;
         }
     }
     _mm_lfence();  //fence after check (CVE-2017-5753)
     return len;
}
void ecall_no_sizefunc(tlv_t* varlen_input, size_t len)
{
      // make sure code won't go past input
      // before processing
     if (tcalc_size(varlen_input, len) == 0)
     {
           //record the error
           return;
     }
     //tcalc_size performs fence
     //process varlen_input
     return;
}

Edger8r工具的安全问题

  • Edger8r工具来产生边界代码,由一系列proxy函数和bridge函数组成。proxy函数是程序调用ECALL函数时,application一侧的接口函数。bridge函数是ECALL进来之后所执行的函数,是enclave一侧的接口函数。
  • bridge函数最后会调用到ECALL真正所指向的函数,
  • SGX开发者使用EDL文件作为输入,交给Edger8r工具,来生成bridge和proxy函数。
  • string和sizefunc所产生的代码容易受此次side channel attack。一旦使用就有风险,spectre。

Edger8r string attribute

  1. ECALL函数参数中’string’属性可能会导致漏洞,使用其修饰函数参数会让enclave代码包括一个strlen函数的调用,strlen返回的长度用于定义参数的buffer长度。
  2. 如果’string’修饰 的参数指向一个enclave内的地址,那么strlen将直接计算从这个enclave内地址开始的字符串的长度,同时自动生成的代码部分会检查这个输入buffer是否位于enclave内。
  3. attacker使用’string’参数指向精心构造的位置,从而触发spectre攻击,使用timing attack泄漏enclave内的数据。
  • example


/* 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;

  • Edger8r工具会自动生成一个marshaling 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.
  • 为了替换sizefunc,开发者应使用size属性来指定参数的长度,并且开发人员必须保证:在enclave内处理输入参数之前,对输入参数的访问不会越界(看下文例子),并且在处理输入参数过程中访问也不会越界。
  • example-using 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外计算的长度。
  • 为了判定传入参数是否位于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;					
}
  • 需要在untrusted 一侧proxy function计算参数长度,并且在marshaling structure里增加一个额外的长度域用于传递这个长度,
  • 另一个问题:需要一个enclave内用于计算参数长度的函数,来确保参数传递进enclave之后长度不变,这第二次检查是用于确认真正传入的参数没有超过长度域所制定的长度。
  • 因此这个解决方案要求开发人员编写: Marshalling structure结构体,包含一个额外的长度域。 一个untrusted sizefunc用于计算参数长度 一个trusted sizefunc用于验证参数传递的正确性。
  • 但是,sizefunc的本来目的是为了方便开发人员编写ECALL函数,自动产生Marshalling structure。然而现在看来,由于安全起见,开发人员不得不增加工作量以确保安全性。
  • 因此,在更新后的Edger8r中移除了sizefunc属性。开发人员需要使用size属性来传递参数。
    example-without sizefunc
/*
* [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;
}
 类似资料: