
前言
- 复现
balsn2019 ctf中的bank区块链题目 wtcl,复现了一个下午- 具体分析及官方
WP如下: https://x9453.github.io/2020/01/16/Balsn-CTF-2019-Bank/ - 复现地址为:
ropsten@0x85B0446Dc5B5f32cbB674Dc8e49Fc27Ebaff2Ee2 - 外部账户地址为:
0x785a8D0d84ad29c96f8e1F26BfDb3E6CB72cAe9b
Source
1 | pragma solidity ^0.4.24; |
Analyse
- 合约创建后,
slot布局如下
1 | ----------------------------------------------------- |
- 关于
FailedAttempt布局如下,在代码33-36行存在未初始化漏洞,会导致覆盖原先slot0到slot2的位置内容
1 | ----------------------------------------------------- |
- 同样,
SafeBox也是类似分析,会在deposit()中覆盖slot0和slot1
1 | ----------------------------------------------------- |
- 可以看到,如果通过
deposit修改slot0和slot1是没用的,即使修改了owner也没用,因为还有第74行的限制 - 其实题目考查的是结构体未初始化漏洞+数组存储方式+mapping存储方式+控制程序执行流
- 因为
33-36行中如果origin足够大,相当于修改了safeboxes数组的长度,让数组的长度足够大,如果大到可以修改或者覆盖failedLogs,那么就可以通过safeboxes数组去访问failedLogs,其实也相当于failedLogs是数组safeboxes的内容,所以这就对tx.origin有一定的要求 - 假设
safeboxes可以包含failedLogs,同时FailedAttempt中的triedPass可以覆盖Safebox的callback: 因为triedPass我们可以完全控制,所以我们就可以通过triedPass控制callback,进一步控制程序执行流(第64行) - 如果能够控制程序执行流的话,那么我们只需要找到第
75行emit SendFlag(msg.sender);的位置就行了,这个可以通过查看bytecodes对应的opcode找到,如下图,我们先找到require(msg.value >= 100000000 ether);对应的位置,然后再找emit SendFlag(msg.sender);对应的位置,因为在EVM中调用一个函数相当于执行jump操作,而跳到的地方都是以jumpdest开始,所以0x070f就是我们想要跳到的位置,这样的话就可以触发SendFlag事件了

Solution
- 计算
target = keccak256(keccak256(msg.sender||3)) + 2,这里target就是FailedAttempt[0]中的origin(20) | triedPass(12) - 计算
base = keccak256(2),这里base就是safeboxes数组第一个元素在slot的位置 - 计算
idx = (target - base) // 2, 这里idx指的是在safeboxes数组中的索引,因为一个Safebox占据两个slot,所以要除以2 - 计算
(target - base) % 2,如果等于0,说明origin(20) | triedPass(12)刚好可以覆盖unused (11) | hash (12) | callback (8) | done (1) - 计算
(msg.sender << (12*8)),如果< idx,说明safeboxes数组长度合适,可以包含failedLogs; 否则的话从步骤1重新开始 - 调用
deposit(0x000000000000000000000000)并设置msg.value = 1 ether: 这里是让callback指向sendEther, 目的是下一步调用控制safeboxes数组长度 - 调用
withdraw(0, 0x111111111111110000070f00): 这里相当于调用sendEther(0, 0x111111111111110000070f00),将failedLogs[msg.sender]中的triedPass控制,相当于控制了unused (11) | hash (12) | callback (8) | done (1)所在slot的低12字节,修改了callback,其实相当于把failedLogs[msg.sender]对应的内容看成是safeboxes数组的内容,同时修改了数组长度 - 调用
withdraw(idx, 0x000000000000000000000000): 这里就可以执行到emit SendFlag(msg.sender);撒花🎉🎉🎉🎉🎉🎉🎉🎉

题外知识
1 | contract C { |
- 可通过下列函数分别获取
slot内容、mapping内容对应slot、数组第一个元素对应slot
1 | function read_slot(uint k) public view returns (bytes32 res) { |
