前言
- 复现
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) { |