目录
1、默认可见性
2、浮点数精度缺失
3、错误的构造函数
4、自毁函数
5、未初始化指针-状态变量覆盖
1、默认可见性
Solidity 的函数和状态变量有四种可见性:external、public、internal、private。函数可见性默认为 public,状态变量可见性默认为 internal。
可见范围:private < internal < external < public
- private:只有当前合约可见
- internal:外部合约不可见,只有当前合约内部和子类合约可见
- external:只能被外部合约或者外部调用者可见
- public:公共函数和状态变量对所有智能合约可见
solidity 0.4 版本,函数不设置访问修饰符编译不会报错,函数默认的可见性是 public,如果一下敏感函数没有设置访问修饰符,就可能发生越权函数调用
漏洞场景:
敏感函数忘记设置访问修饰符
漏洞代码示例:
pragma solidity ^0.4.5;
contract HashForEther {
function withdrawWinnings() {
// Winner if the last 8 hex characters of the address are 0.
require(uint32(msg.sender) == 0);
sendWinnings();
}
function sendWinnings() {
msg.sender.transfer(this.balance);
}
}
sendWinnings 函数忘记设置函数访问修饰符了,而默认可见性是 public,于是导致任意地址都可以调用改函数而获得转账。
2、浮点数精度缺失
浮点型,定长浮点型——Solidity目前暂时不支持浮点型,也不完全支持定长浮点型。
fixed/ufixed 表示有符号和无符号的定长浮点数,浮点型可以用来声明变量,但不可以用来赋值。
除法运算:除法运算的结果会四舍五入,如果出现小数,小数点后的部分都会被舍弃,只取整数部分
pragma solidity ^0.4.0;
contract C {
uint constant public weiPerEth = 1e18;
uint public token1;
uint256 public token2;
uint256 public token3;
function testC(uint n1, uint n2) external {
token1 = 200 wei / weiPerEth;
token2 = 80 / 10;
token3 = n1 /n2;
}
}
token1 由于除法运算出现了小数(0.0...02),取整数部分,变成了0。当 n1 小于 n2 时,如 8/10 ,token3 也将取 0.8 的整数部分,变成 0。
执行结果:
漏洞场景:
转账发送以太时,以太数量由除法运算结果所得,运算数字可控,可能导致结果精度丢失,最终导致以太丢失
漏洞示例:
pragma solidity ^0.4.23;
contract FunWithNumbers{
uint constant public tokensPerEth = 10;
uint constant public weiPerEth = 1e18;
mapping(address => uint) public balances;
function buyTokens() public payable {
uint tokens = msg.value/weiPerEth * tokensPerEth; // 第一处浮点和精确度问题
balances[msg.sender] += tokens;
}
function sellTokens(uint tokens) public {
require(balances[msg.sender] >= tokens);
uint eth = tokens/tokensPerEth; ?// 第二处浮点和精确度问题
balances[msg.sender] -= tokens;
msg.sender.transfer(eth * weiPerEth);
}
}
3、错误的构造函数
在 Solidity 0.4.22 版本之前,在Solidity中的0.4.22版本之前,所有的合约名和构造函数同名。编写合约时,如果构造函数名和合约名不相同,合约会添加一个默认的构造函数,自己设置的构造函数就会被当作普通函数,导致自己原本的合约设置未按照预期执行,从而造成安全漏洞。
漏洞场景:
- Solidity 0.4.22 前,构造函数与合约名相同,但是大小写不一样
- 构造函数错误地声明为了 public 或者 external
示例 1:
pragma solidity ^0.4.20;
contract OwnerWallet {
address public owner;
function ownerWallet(address _owner) public {
owner = _owner;
}
function () payable {}
function withdraw() public {
require(msg.sender == owner);
msg.sender.transfer(this.balance);
}
}
示例 2:采用更安全的 constructor
pragma solidity ^0.4.0;
contract C {
address owner;
constructor() public {
owner = msg.sender;
}
}
4、自毁函数
Solidity 智能合约中存在一个 selfdestruct() 自毁函数,该函数可以对创建的合约进行自毁,并且可强制将合约里的 Ether 转到自毁函数定义的地址中。
contract DeleteContract {
constructor() payable {}
receive() external payable {}
function deleteContract() external {
// 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
selfdestruct(payable(msg.sender));
}
}
漏洞场景:
合约限制账户转入的以太数量,而攻击者可以使用自毁函数强制转入任意数量,并且使用 this.balance 作为敏感操作的判断条件
漏洞示例:
contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
mapping(address => uint) redeemableEther;
function play() public payable {
require(msg.value == 0.5 ether);
uint currentBalance = this.balance + msg.value;
require(currentBalance <= finalMileStone);
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
return;
}
function claimReward() public {
require(this.balance == finalMileStone);
require(redeemableEther[msg.sender] > 0);
redeemableEther[msg.sender] = 0;
msg.sender.transfer(redeemableEther[msg.sender]);
}
}
攻击者角度分析
由于合约设置参与者每次提交的 Ether 数为 0.5,提交多次后就会达到10 Ether,因此攻击者就可以创建带有 selfdestruct() 函数的合约,通过 selfdestruct() 函数强制给它提供 0.1 Ether,当强制转入的 0.1 Ether 进入条件判断,最终的计算数值将永远不会成为整数,this.balance==finalMileStone 判断将永远不会成立,导致参与者永远不会获得奖励。所有参与者的 Ether 就会永远锁在 EtherGame 合约中。
5、未初始化指针-状态变量覆盖
合约中状态变量存储在 storage 中,会按声明顺序存入卡槽 slot
contract A{
address owner;
B addrB;
}
Solidity 对于复杂的数据类型,在函数中作为局部变量时,会默认存储在 storage 中。当声明的复杂数据类型局部变量未初始化时,它会默认成为指向 storage 的指针,就会指向 slot 0,这时如果声明了状态变量,那么第一个状态变量将会被覆盖。
pragma solidity ^0.4.0;
contract CC {
string public _name1;
string public _name2;
struct NameRecord {
string name1;
string name2;
}
function CC() {
_name1 = "makabaka";
_name2 = "nigubigu";
}
function register(string n1, string n2) public {
// 设置新的NameRecord ,未初始化
NameRecord newRecord;
newRecord.name1 = n1;
newRecord.name2 = n2;
}
}
register 函数中的结构体类型局部变量 newRecord,由于未初始化,默认会指向 slot 0,于是 newRecord.name1 会覆盖状态变量 _name1,newRecord.name2 会覆盖状态变量 _name2