Func Selector and Arg Encoding
用 Balsn CTF 2020 - Election 来学习,主要是加深 abi 编码的理解。
官方文档 - 应用二进制接口说明
题目目的是执行 giveMeFlag
:
需要满足的条件是
stage
为 3
- 自己的票数最多
- 自己的
policies
应该是 Give me the flag, please
然后 _setStage
和 propose
这两个函数有 auth
修饰:
在 ERC223
的 _transfer
中,会直接 address(contract).call
,那么就可以绕过这个 auth
来调用上面两个函数。
参数编码需要按照 ABI 来构造:(取自官方 WP)
这里需要我们拥有一些固定 suffex 的账号地址,可以用这个网站爆破生成。
Ethereum Storage
Re-Entrancy
该攻击的问题在于:先转账后计费。
比如一下函数:
如果 msg.sender
是一个合约地址,那么就会发生调用,而:
如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配fallback会被调用。或者在没有 receive 函数时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。
于是就可以写一个 fallback 函数来控制流程,在这里因为是先转账,所以就直接再次调用 Bank.withdraw
导致再次取款。但为了防止无限次调用导致 gas 猛涨,所以可以加个状态量限制次数。
值得注意的是低版本 fallback 函数的声明是:function() public payable
,而高版本需要换成:fallback () external [payable]
;并且低于 0.5.0
的版本中并没有 address payable
的出现,之后才有细分还不能隐式从 address
到 address payable
。
由于题目环境在 Ropsten testnet 上,这个测试网已经于 10.5 设置为 read-only
了,所以就本地 remix 简单调调,用低版本写一个 example :
Bank
合约中原有 2 eth,Hacker
合约地址有 1 eth,然后直接执行 hack
:
可以看到取一次操作实际进行了两次转账,而且存款也因为用的 uint256
没上 SafeMath 导致了整型下溢。
这里也学到一个小技巧,如果目标合约没有 payable
的 fallback 函数,直接给它转账会报错,可以自己新建一个合约通过 selfdestruct
强制转账。
Integer Overflow and Underflow
可参照重入攻击。
总结就是超出上限会上溢取模变小,超出下限下溢会变大,该漏洞可以用 SafeMath 库来防御,当发生溢出时会回滚交易。
以 ByteCTF 2019 hf 为例子,认识一下 evm 简单逆向。
丢进 https://ropsten.etherscan.io/bytecode-decompiler 可以得到关键伪代码:
还有个水龙头每次可以发 1 eth。
stor1
分析出来是这个合约的所有者,stor0
是一个值,如果传入的与sto0
相等,z在 1727bb94
中就会先减去 1,再加上 2。可以发现有函数可以直接更改 owner 进而更改 stor0
:
因此更改完后,为了触发整型溢出,首先调用 1727bb94
让 unknown6956604e
为 1,然后故意将 balanceOf
弄成 1,接着调用 ad17b493
触发 balanceOf -= 2
,造成整型下溢。
Randomness
合约编写者可能会使用一些变量来进行随机数的生成。
Storage 变量
无论状态变量的可见性如何,都可在链上直接查询到当前的值。
将以下合约部署到0x4741E6999489bE9d8E64328B2B5261485FfDbd31@goerli
。
随便 store 个 number,然后可以在 web3.py 中调用:web3.eth.get_storage_at('0x4741E6999489bE9d8E64328B2B5261485FfDbd31', 0)
获得 slot 0 的数据内容。
Block 变量
web3.eth.get_block('latest')
。
有 difficulty
, hash
, timestamp
, gasLimit
等信息。
blockhash
变量也有点特殊,查看文档也能看出警告:
不要依赖 block.timestamp
和 blockhash
产生随机数,除非你明确知道自己做的用意。
时间戳和区块哈希在一定程度上都可能受到挖矿矿工影响。例如,挖矿社区中的恶意矿工可以用某个给定的哈希来运行赌场合约的 payout 函数,而如果他们没收到钱,还可以用一个不同的哈希重新尝试。
可能的攻击面:
- 误用,如
block.blockhash(block.number)
恒为零。
- 使用过去区块的有效 blockhash ,可以编写攻击合约获取相同值。
- 将猜数字和开奖的交易分开在两个不同区块中,并且使用猜数字时还不知道的某个区块的 blockhash 作为熵源,则可以等待 256 个区块后再进行开奖,消除 blockhash 的不确定性。
Airdrop Hunting
无权限控制的空投函数+转账函数 = 薅羊毛。
当然如果考虑 gas 的话可以限制每次调用临时恶意合约的数量。
Delegatecall
call 函数簇可以用来实现跨合约的调用。
- call: 调用后内置变量
msg
的值会修改为调用者,执行环境为被调用者的运行环境
- delegatecall: 调用后内置变量
msg
的值不会修改为调用者,但执行环境为调用者的运行环境(相当于复制被调用者的代码到调用者合约),而且该调用访问的存储是依据 Storage 的 slot 进行存储。
所以可以有一个变量覆盖的攻击面:
恶意合约第一次调用 setFirstTime
传入自己的地址,Preservation
合约调用 LibraryContract
将 _time
覆盖给 timeZone1Library
,然后构造恶意合约自己的 setTime
替换 owner,只需要注意 owner 在 slot 2。
Uninitialized Storage Pointer
在函数中未申明 memory
的变量且未初始化时会从 slot 0开始一一覆盖。
虽然但是这一点 remix IDE 直接在编译的时候就给你警告了hh。
Arbitrary Writing
主要跟 Ethereum 的 Storage 存储有关。
看道例子: 2019 Balsn CTF Bank。
看看这合约干了哪些事:
- 构造方法:存入 owner
- onlyPass 修饰器:若
bytes12(sha3(pass)) != safeboxes[idx].hash
,则记录 failedLogs[msg.sender].push(info)
。然而这里的 info
初始化便是一个未初始化的 storage 变量,没有加 memory 标识,这里就村存在数据的覆写。
- deposit 函数:传入 hash 创建一个 Safebox,然后就根据当前用户地址是否是 owner 来判断 box.callback 是否为
sendFlag
还是 sendEther
;这里同样存在一个未初始化 storage 变量,能够覆写。
- withdraw 函数:调用一个 Safebox 的 callback。
- sendEther 函数:合约私有,给合约发 ether。
- sendFlag 函数:合约私有,需要有
100000000 ether
然后发 flag。
结构体未初始化漏洞+数组存储方式+mapping存储方式+控制程序执行流。
首先需要执行 sendFlag
函数进而调用事件 SendFlag
。显然不能直接调用 sendFlag
,需要控制执行流,能够通过更改 callback
来直接调用事件 SendFlag
;而 Safebox
的值可以使用 failedLogs
这一个映射来覆盖。
于是我们需要拿一个地址attack
,计算 failedLogs[attack][0]
的地址为 target = keccak256(keccak256(attack || 3))
,往后第二个 slot 便是 origin(20) | triedPass(12)
。
需要将地址改成 070f
。
然后先 deposit(0x000000000000000000000000)
并传 1 eth 让callback
为 sendEther
;然后调用 withdraw(0, 0x111111111111110000070f00)
目的在于覆写 safeobx[idx]
处的 unused (15) | hash (12) | callback (4) | done (1)
,然后调用 withdraw(idx, 0x000000000000000000000000)
完成触发:
当然,这个是利用 origin(20) | triedPass(12)
来达成覆盖,也可以使用 failedAttempt
的 idx
,因为这一 slot 内容也是完全可控的。
其他