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)) } } }
|