0%

How to clear a buffer in C/C++

雖然這題目很簡單,不過看到How to zero a buffer我嚇到了,原來我之前的觀念不一定正確。
假設我們在程式中有些敏感資料(金鑰/密碼等),希望能夠在使用之後清除掉。通常我們會這麼作。

1
2
3
4
5
6
7
8
9
void dosomethingsensitive(void)
{
uint8_t key[32];

...

/* Zero sensitive information. */
memset(key, 0, sizeof(key));
}

不過這段程式碼經過最佳化之後,最後的memset被省略不做。

以下是gcc用-O0-O2編譯出來的assembly code,看看差異

  • 關閉最佳化版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
leaq -32(%rbp), %rax
movl $32, %edx
movl $0, %esi
movq %rax, %rdi
call memset
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc

  • 最佳化版
1
2
3
4
5
6
func:
.LFB24:
.cfi_startproc
rep ret
.cfi_endproc

可以很明顯看得出來memset消失了,那是因為buffer在stack上,當函數結束的時候,整個stack就沒用了,Compiler認為至此呼叫這個函數是無意義的,因此省略了這個動作。一般情形可以接受,不過如果是敏感資料,問題就大了。
因此尋找如何在最佳化的情形之下,還能正常清理資料的方式,才是正確解答。

幾個方向

  • 在memset處轉形成volatile,會彈出warning,不過還是沒用。
1
memset((volatile uint16_t*)key, 0, sizeof(key));
  • 自行寫一個SecureZeroMemory函數。
1
2
3
4
5
static void secure_memzero(void *p, size_t len)
{
uint8_t *ptr = p;
while (len--) *ptr++ = 0;
}

一樣被Compiler最佳化掉

  • 修改上面的版本,不過對ptr加上volatile
1
2
3
4
5
static void secure_memzero(void *p, size_t len)
{
volatile uint8_t *ptr = p;
while (len--) *ptr++ = 0;
}

在目前三大編譯器下,這段Code都會正常執行,不過Spec裡面有這一條

Accessing an object designated by a volatile lvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.

也就是說有可能這段Code也可能被Compiler最佳化掉。
不過我找到的幾個範例都這麼寫,這點要在確認。

文中還提到幾種可能的方案,例如把檔案放在另外一個Source file,會被Link time Optimization化掉。
利用function pointer的方式,會被Devirtualization化掉。
要正確的作對一件事還真是不簡單。