雖然這題目很簡單,不過看到How to zero a buffer我嚇到了,原來我之前的觀念不一定正確。
假設我們在程式中有些敏感資料(金鑰/密碼等),希望能夠在使用之後清除掉。通常我們會這麼作。
1 | void dosomethingsensitive(void) |
不過這段程式碼經過最佳化之後,最後的memset被省略不做。
以下是gcc用-O0
和-O2
編譯出來的assembly code,看看差異
- 關閉最佳化版
1 | func: |
- 最佳化版
1 | func: |
可以很明顯看得出來memset消失了,那是因為buffer在stack上,當函數結束的時候,整個stack就沒用了,Compiler認為至此呼叫這個函數是無意義的,因此省略了這個動作。一般情形可以接受,不過如果是敏感資料,問題就大了。
因此尋找如何在最佳化的情形之下,還能正常清理資料的方式,才是正確解答。
幾個方向
- 在memset處轉形成
volatile
,會彈出warning,不過還是沒用。
1 | memset((volatile uint16_t*)key, 0, sizeof(key)); |
- 自行寫一個SecureZeroMemory函數。
1 | static void secure_memzero(void *p, size_t len) |
一樣被Compiler最佳化掉
- 修改上面的版本,不過對ptr加上
volatile
1 | static void secure_memzero(void *p, size_t len) |
在目前三大編譯器下,這段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化掉。
要正確的作對一件事還真是不簡單。