02.Bytedance
2023-09-15 16:59:46 # 22.MetaTrustCTF2023

Bytedance

分析

1.全局观

本题代码量很少,只有一个合约:isOddByte()isByteDance()是pure方法,只能调用checkCode()

2.任务

将状态变量solved设置为true,但是checkCode()中包含delegatecall,因此我们要成功调用checkCode()然后修改slot_0的内容为1

1
2
3
4
5
(bool success,) = _yourContract.delegatecall("");

function isSolved() public view returns(bool){
return solved;
}

3.详细分析

3.1特殊要求

我们先来看两个会被调用到的pure方法:

isOddByte():输入一个字节的数据,要求该数据是奇数

1
2
3
function isOddByte(bytes1 b) internal pure returns (bool) {
return (uint8(b) % 2) == 1;
}

isByteDance():如下代码分析,可以看出,我们不能够让程序进入到isPal := 0,要让他返回true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function isByteDance(bytes1 b) internal pure returns (bool) {
bool isPal = true;
assembly {
let bVal := byte(0, b) // bVal就是b
for { let i := 0 } lt(i, 4) { i := add(i, 1) } // 4次循环
{
// 7-i = x
// bVal 逻辑右移 x 位 = y
// y取最低一位
let bitLeft := and(shr(sub(7, i), bVal), 0x01)
// vVal逻辑右移i位 = x
// x取最低一位
let bitRight := and(shr(i, bVal), 0x01)
// 不能进去,也就是bitLeft和bitRight要相等
if iszero(eq(bitLeft, bitRight)) {
isPal := 0
}
}
}
return isPal;
}

根据此方法的要求,我们可以得到满足条件的bytes1数据:0x81

1
2
3
4
5
6
              [0x81]   |     [0x81]       
init 1000 0001 | 1000 0001
shr(7) 0000 0001 | 1000 0001 shr(0)
shr(6) 0000 0010 | 0100 0000 shr(1)
shr(5) 0000 0100 | 0010 0000 shr(2)
shr(4) 0000 1000 | 0001 0000 shr(3)

再来看主函数checkCode():通过下面的分析,我们可以知道我们需要做的就是自己手动创建一个合约,这个合约的字节码需要满足相关的条件:每一个字节都要是奇数,存在一个字节内容满足isByteDance()

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
  function checkCode(address _yourContract) public {
require(!solved, "Challenge already solved");
bytes memory code;
uint256 size;
bool hasDanceByte = false;

// 那么大概意思就是要让我们用字节码创造一个合约
assembly {
size := extcodesize(_yourContract) // 调用者的代码大小
code := mload(0x40) // 空闲指针
// 修改空闲指针内容,空闲指针指向新的可用内存(将要存储的 size和我们的合约代码 之后的位置)
mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// 在内存中写入size和实际的合约代码内容
// 操作之后的memory: | size | 实际的代码内容 | 空闲指针指向位置 |
mstore(code, size)
extcodecopy(_yourContract, add(code, 0x20), 0, size)
}
// 扫描我们合约字节码的每一个字节的内容
for (uint256 i = 0; i < size; i++) {
bytes1 b = code[i];
// 如果这个字节满足isByteDance(),则返回true
if (isByteDance(b)) {
hasDanceByte = true;
}
// 合约字节码的每一个字节都要是奇数
require(isOddByte(b), "Byte is not odd");
}
require(hasDanceByte, "No palindrome byte found");
// 然后就delegatecall我们的攻击合约,修改slot_0内容为true
(bool success,) = _yourContract.delegatecall("");
require(success, "Delegatecall failed");
}

那么我们现在就来构造这个合约。如果通过正常写合约代码,是无法满足这两个条件的,因为我们无法保证编译器编译出来的字节内容,只能保证功能。因此,我们需要自己手动写字节码,然后部署上去。任务:这个字节码需要实现修改slot_0的内容为true的功能、满足isByteDance()(这个我们前面分析了,用0x81)、每一个字节都是奇数(这就限制了我们使用的操作码的内容)。

3.2构造字节码

核心功能是:用SSTORE将slot_0的内容设置为true,也就是需要stack中包含0,1两个数值,然后用SSTORE写入。

我一开始想的是用PUSH将0和1放进stack,然后SSTORE,最后再停止程序,在程序后面补上0x81。但是PUSH有限制,只能取61,63,65等,并且取了不同的PUSH,输入的内容为1的话,前面会有多余0不符合奇数,输入的数值为0的话,也不符合奇数,因此需要另辟蹊径。

我的想法是用DUP复制,但是也不太可行。便想到用移位和ISZERO来操作行得通:通过下面的步骤就完成了核心功能

1
2
3
4
5
6
7
8
[00]	PUSH2	0101   61 0101 
[03] PUSH2 1101 61 1101
[06] SHL 1B
[07] ISZERO 15
[08] PUSH2 0101 61 0101
[0b] PUSH2 1101 61 1101
[0e] SHL 1B
[0f] SSTORE 55

然后就是要想办法将0x81嵌入进字节码:我的想法是直接用RETURN返回程序,这样就不会报错,并且将0x81嵌入到返回值选取的内容当中

1
2
3
[18]	PUSH2	0101   61 0101
[1b] PUSH2 1181 61 1181
[20] RETURN F3

将操作码连接起来,就成为了我们的字节码:

1
6101016111011B156101016111011B55610101611181F3

最后就是我们需要一个方法来部署这个字节码:

1
2
3
4
5
6
7
8
9
contract Deployer{
function deploy() public returns(address){
bytes memory x = hex"6101016111011B156101016111011B55610101611181F3";
return address(new OurBytecode(x));
}
}
contract OurBytecode{
constructor(bytes memory code){assembly{return (add(code, 0x20), mload(code))}}
}

解题

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;

import "forge-std/Script.sol";
import "./ByteDance.sol";

contract attacker is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("privatekey");
vm.startBroadcast(deployerPrivateKey);

Deployer deployer = new Deployer();
address addr = deployer.deploy();
ByteDance level = ByteDance(0xA3c3cb2FC91412ff3B18C2a795AeC4b816f9bCD2);
level.checkCode(address(addr));

vm.stopBroadcast();
}
}
contract Deployer{
function deploy() public returns(address){
bytes memory x = hex"6101016111011B156101016111011B55610101611181F3";
return address(new OurBytecode(x));
}
}
contract OurBytecode{
constructor(bytes memory code){
assembly{
return (add(code, 0x20), mload(code))
}
}
}