
二級標題
二級標題
二級標題
1.不存儲任何敏感信息到鏈上漏洞詳情:基於區塊鏈的透明性,
任何部署到鏈上的合約數據,都是透明可見
contract Eocene{
mapping(address => bytes 32) candidate;
uint private seed = 0x 12341 3d;
function select() public{
bytes 32 result = keccak 256(abi.encodePacked(seed));
if(result == candidate[msg.sender]){
payable(msg.sender).transfer( 1 ether);
}
}
}
,既便是private 修飾的變量也是如此。因為private 的可見性僅僅是對函數和外部合約的而言,任意用戶都可以通過檢索鏈上數據獲取到這些值。這種情況下,任何希望通過基於鏈上private 修飾來保證的機密性操作都是不安全的。
如果存在下面的簡單的抽獎代碼:
二級標題
二級標題
二級標題
2.注意變量默認值
漏洞詳情:
contract Eocene{
mapping(address => bool) unlocked;
uint averageDrop;
address token;
function setAverageDrop() public {
averageDrop = 1000;
}
function drop() public {
if(unlocked[msg.sender] == false){
ERC 20(token).transfer(msg.sender, averageDrop);
}
}
}
在solidity 中,變量的初始值為0/false。這種情況下,在基於某個變量做判斷時如果不考慮變量初始值的影響,可能會導致相應的安全問題。
考慮如下空投解鎖代碼:
二級標題
二級標題
二級標題
3.將不再使用的struct 類型值delete
漏洞詳情:
contract Eocene{
struct Stake{
uint amount;
uint needReceive;
uint startTime;
}
mapping(address => Stake) stakes;
mapping(address => bool) staker;
function getStake() public{
Stake memory _stake = stakes[msg.sender];
(msg.sender).transfer(_stake.needReceive);
staker[msg.sender] = false;
// delete stakes[msg.sender] // need do but don't
}
function calReceive() public{
require(staker[msg.sender],'not staker');
stakes[msg.sender].needReceive = stakes[msg.sender].amount * (block.time - stakes[msg.sender].startTime);
stakes[msg.sender].amount = 0;
}
}
對任何mapping 類型,當value 字段類型為struct 且對應值不再需要使用時, 應當使用delete 置刪除該值。否則該值會依舊殘留在對應slot 中。
考慮如下形式代碼:
上面的合約代碼根據質押數量和質押時間來計算獲取的eth 量,但是在質押完成後,僅僅將staker[msg.sender]的值設置為false,而對應的stakes[msg.sender]依舊存在。所以攻擊者可以無限制調用getStake()函數來獲取eth。修復措施:,否則該值對一直存在於對應slot 中。
二級標題
二級標題
二級標題
1.必須顯示的聲明函數的可見性漏洞詳情:函數默認可見性為public,
對任意函數,都必須顯示的聲明其可見性
contract Eocene{
mapping(address => bool) whitelist;
function _a() {
payable(msg.sender).transfer( 1 ether);
}
function a() public{
require(whitelist[msg.sender],'not in whitelist');
_a();
}
}
,以防止疏忽導致的漏洞問題存在,特別是當函數多層嵌套調用底層函數時,防止因為疏忽導致底層函數沒有被正確賦予可見性。
考慮以下漏洞代碼示例:
二級標題
二級標題
二級標題
2.函數重入攻擊
漏洞詳情:
contract Fund {
mapping(address => uint) shares;
function withdraw() public {
if (payable(msg.sender).send(shares[msg.sender]))
shares[msg.sender] = 0;
}
}
對任何函數,必須考慮在重入後可能導致的問題。這裡的重入包括transfer/send/call/staticall 等外部調用所導致的所有重入問題。
考慮下列代碼形式:
二級標題
三、外部交互
二級標題
二級標題
1.限定外部調用的地址及函數名單
漏洞詳情:
contract Eocene{
function callExt(address _target, bytes calldata data) public{
_target.call(data);
}
function delegateCallExt(address _target, bytes calldata data) public{
_target.delegatecall(data);
}
}
對外部函數的調用,在合理情況下,必須限定調用的合約地址和合約函數
考慮以下代碼形式:
函數callExt 被用於調用任意函數的任意地址,這種情況下,很容易導致重入問題,且一旦該合約在任意錢包中有任何Token 資產,都可以通過該函數直接調用對應Token 的transfer 函數轉走。
二級標題
二級標題
二級標題
2.使用call,send,delegatecall,staticcall 時對外部調用的判斷不能僅僅依賴於異常,還要通過返回值判斷
漏洞詳情:
contract Eocene{
address token; //any token address
function deposit(uint amount) public{
token.call(abi.EncodeWithSignature("transferfrom(address from, address receipt, uint amount)"), msg.sender, address(this), amount);
mint(msg.sender, amount);
}
}
上訴函數並不會因為內部錯誤而導致revert,而是只返回revert。在任何時候使用他們時,必須通過函數返回值來判斷執行是否成功。
考慮下列示例代碼:
對call,send,delegatecall,staticcall 的執行結果的判斷必須基於其返回值,而不是寄望於其是否revert。
二級標題
二級標題
二級標題
1.不基於tx.origin 做身份認證漏洞詳情:
不要基於tx.origin 做身份認證
contract Eocene{
mapping(address=>bool) whitelist;
function freeDeposit() public{
require(whitelist[tx.origin],'not in whitelist');
payable(msg.sender).transfer( 1 ether);
}
}
,tx.origin 是整個交易的發起人,不會隨合約的遞歸調用改變,任何基於tx.origin 的認證,都無法保證tx.origin 是msg.sender。其基於tx.origin 的認證也增加了用戶的賬戶安全性。
考慮下列漏洞示例:
當任何位於白名單中的地址被某些釣魚鏈接誘導調用了任何看似無害的惡意合約地址和函數,而該惡意地址又調用示例代碼的freeDeposit 函數時,本應該屬於該白名單地址的資產會被轉給惡意合約地址。修復措施:二級標題
二級標題
二級標題
2.不基於extcodesize 返回值對eos 賬戶做判斷漏洞詳情:
在合約代碼的初始化階段,即便該地址是合約地址,extcodesize 的返回值也會是 0
contract Eocene{
function withdraw() public{
uint size;
assembly {
size := extcodesize(caller())
}
require(size== 0,"not eos account");
msg.sender.transfer( 1 ether);
}
}
,如果基於該返回值做判斷,所得到的結果是不准確的。
考慮下列代碼形式:
任何時候不要基於外部地址是否會合約地址做判斷,盡可能的保證合約代碼在任意種類賬戶下的功能正常。
二級標題
二級標題
二級標題
1.任何數值運算時,考慮溢出問題
漏洞詳情:
contract Eocene{
mapping(address=>uint) balanceof;
function withdraw(uint amount) public{
payable(msg.sender).transfer(amount);
balanceof[msg.sender] = balanceof[msg.sender]-amount;
require(balanceof[msg.sender] >= 0,'not enough balance');
}
}
溢出問題是指當合約做整數運算時導致的溢出問題。主要原因在於任何數值類型都有其最大長度,兩整數的運算超出其最大值時,超出部分會被截斷,導致問題產生。
考慮下列代碼形式:
對上訴函數,考慮當balanceof[msg.sender] < amount 時,因為balanceof 類型限定為無符號整形,最總計算結果會導致int 類型的負值,而轉換為uint 類型時,就是極大的正值,此時,require 的限制條件被繞過,攻擊者可以從合約匯總竊取任意數量的token。二級標題
二級標題
二級標題
2.在做任何整數運算時,慎重使用int 類型
漏洞詳情:
contract Eocene{
int public result;
uint public uresult;
function cal(uint _a, uint _b) public{
result = int(_a)-int(_b);
uresult = uint(result);
}
}
在做任何整數類型的計算時,慎重將uint 類型轉為int 類型進行計算,除非你需要這種操作。因為當將uint 類型整數轉為int 類型時,一些對於uint 類型為溢出的情況在int 類型中會失效。
考慮下列代碼形式:
使用0.8.0 以上版本的solidity 進行編譯時,如果調用`cal( 0, 1)`,即便` 0-1 ` 在uint 裡造成了溢出,但是在int 類型的計算中並不會引發因為溢出導致的revert(因為0-1 的結果在int 類型的範圍內)。而當再將結果值轉為uint 類型時,則是實際uint 類型計算溢出後的結果值,變相導致了溢出問題的存在。
但是需要注意的是,如果這裡調用cal(type(int).min, type(int).max),依舊會引發revert,因為此時的整數計算也超出了int 類型的範圍修復措施:二級標題
二級標題
二級標題
3.任何可能丟失精度的運算中,通過擴展防止不可接受的精度丟失
漏洞詳情:
contract Eocene {
uint totalsupply;
mapping(address=>uint) balancesof;
uint BasePrice = 1 e 16;
function mint() public payable {
uint tokens = msg.value/BasePrice;
balancesof[msg.sender] += tokens;
totalsupply += tokens;
}
}
做任意整數運算,均考慮精度丟失可能引起的問題,並對其精度進行擴展。
考慮下列形式代碼:
對可能存在精度缺失整數計算中,先通過`* 1 eN` 來對整數進行擴展(N 是需要的精度大小)。
二級標題
二級標題
二級標題
1.不使用任何可猜測/被操作鏈上數據用作隨機數種子
漏洞詳情:
contract Eocene{
function winner(bytes 32 value) public payable{
require(msg.value > 0.5 ether,"not enough value");
if(value == keccak 256(abi.encodePacked(block.timestamp))){
msg.sender.transfer( 1 ether);
}
}
}
由於區塊鏈的特殊性,鏈上不存在任何真正的隨機值,不應該使用任何鏈上數據用作隨機值或隨機數種子,考慮從鏈下獲取隨機值。
代碼示例如下:
對上訴合約來說,使用當前區塊時間標籤來計算隨機值,並和用戶提交的隨機值進行對比,給予相同隨機值用戶獎勵。看起來是基於時間的隨機情況,但實際上任何使用keccak 256(abi.encodePacked(block.timestamp))的用戶都可以通過合約調用計算出該值,並發送給winner 函數的合約代碼,獲取到eth 。此外,我們也應當明白,block.timestamp 時可以被礦工惡意篡改的值,並不是一定公正的。二級標題
七、DOS:
二級標題
二級標題
1.禁止任何將整個狀態變量數組複製給內存變量的操作
漏洞詳情:
contract Eocene{
uint[] id;
function pop(uint amount) public{
require(amount>0,'not valid amount');
uint[] memory _id=id; // this may be revert because of memory space limit
for(uint i= 0;i<_id.length;i++)
{
if(amount==_id[i]){
id[i] = 0;
}
}
}
function push(uint amount) public{
require(amount>0,'not valid amount');
id.push(amount);
}
}
solidity 對函數可用內存大小的使用限制遠低於storage(0x ffffffffffffffff),任何將動態數組整體拷貝到內存的行為,都可能超出可用內存大小,導致revert。
考慮下面代碼形式:
上面代碼中,`uint[] memory _id=id;` 會將storage 中`uint[] id;`的變量值放到內存中,而push 函數可以向`uint[] id;`插入值, 而由於solidity 對內存空間的限制,一旦`uint[] id;`的長度超過`(0x ffffffffffffffff-0x 40)/0x 20-1 `時,就會導致內存佔用過大,revert。也就意味著該合約的pop 函數永遠無法執行成功,或者說,任何存在`uint[] memory _id=id;`操作的函數均無法執行成功。修復措施:二級標題。
二級標題
二級標題
2.任何for 循環中,循環判斷不能基於外部可修改變量
漏洞詳情:
contract Eocene{
uint[] id;
function pop(uint amount) public{
require(amount>0,'not valid amount');
for(uint i= 0;i
{
if(amount==id[i]){
id[i] = 0;
}
}
}
function push(uint amount) public{
require(amount>0,'not valid amount');
id.push(amount);
}
}
任何for 循環的判斷如果基於外部可修改變量,可能會存在外部可修改變量過大導致gas 消耗太高的問題。當gas 消耗高到每個合約調用者的承受時,DOS 攻擊出現。
考慮下面的代碼:
這裡我們刪除了從storage 複製數組到memory 的操作,但是該代碼的另一個問題是for 循環是基於`uint[] id;`的長度,而id 的長度在合約中只能增加不能減少,這意味著pop()函數所消耗的gas 會越來越大,當gas 大到超出執行pop 函數所能承受的最大gas 消耗,很少有人會執行pop,也就實現了DOS 攻擊。二級標題
二級標題
二級標題
3.在循環中,使用try/catch 捕獲無法確定的異常
contract Eocene{
address[] candidates;
mapping(address=>uint) balanceof;
function claim() public{
for(uint i= 0;i
{
address candidate = candidates[i];
require(balanceof[candidate]>0,'no balance');
payable(candidate).transfer(balanceof[candidate]);
}
}
}
漏洞詳情:
在任何循環內部中如果存在可能因為外部地址導致的revert,必須考慮對revert 的捕獲。否則一旦有任意一次內部循環執行失敗,之前所有的gas 消耗都失去意義。而當循環內部的執行失敗與否可以被外部地址控制,如果沒有try/catch 來捕獲可能的異常,就可能會導致循環判斷永遠無法完整進行,實現DOS 攻擊。
在任何for 循環中,如果存在外部調用,並且無法判斷調用是否會revert,必須使用try/catch 來嘗試補貨異常,以防止因為revert 導致的DOS 攻擊。
二級標題
二級標題
一級標題
一級標題
- Solidity
關於我們
At Eocene Research, we provide the insights of intentions and security behind everything you know or don't know of blockchain, and empower every individual and organization to answer complex questions we hadn't even dreamed of back then.