前言:找Ver👴想复现下qwb final的区块链。Ver👴给我发了这个比赛下面的一道题,发现这个比赛里面有很多高质量的智能合约题。从这里开始写一些不错的题目。
看到题目名字就知道了题目考点: 沙盒 给出合约 BabySandbox.sol
pragma solidity 0.7.0;
contract BabySandbox {
function run(address code) external payable {
assembly {
// if we\'re calling ourselves, perform the privileged delegatecall
if eq(caller(), address()) {
switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00)
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
// ensure enough gas
if lt(gas(), 0xf000) {
revert(0x00, 0x00)
}
// load calldata
calldatacopy(0x00, 0x00, calldatasize())
// run using staticcall
// if this fails, then the code is malicious because it tried to change state
if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)) {
revert(0x00, 0x00)
}
// if we got here, the code wasn\'t malicious
// run without staticcall since it\'s safe
switch call(0x4000, address(), 0, 0, calldatasize(), 0, 0)
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
// revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
}
}
Setup.sol
pragma solidity 0.7.0;
import "./BabySandbox.sol";
contract Setup {
BabySandbox public sandbox;
constructor() {
sandbox = new BabySandbox();
}
function isSolved() public view returns (bool) {
uint size;
assembly {
size := extcodesize(sload(sandbox.slot))
}
return size == 0;
}
}
Setup.py中的isSolved()进行了是否成功解决challenge的check. 这里我不是很熟悉.slot这种用法,所以自己随便部署了一个进行试验。 应该就是取了题目合约的整个字节码。要求把合约变成一个账户。或者直接让合约自毁应该也可以。 然后我们分析下Sandbox中的各种方法
if eq(caller(), address()) {
switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00)
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
这里说的是如果caller也就是调用者是自己的话。那么就会直接调用。 delegatecall,也就是如果这里能设置出一些东西那么就可以成功改变合约状态了。
if lt(gas(), 0xf000) {
revert(0x00, 0x00)
}
// load calldata
calldatacopy(0x00, 0x00, calldatasize())
// run using staticcall
// if this fails, then the code is malicious because it tried to change state
if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)) {
revert(0x00, 0x00)
}
// if we got here, the code wasn\'t malicious
// run without staticcall since it\'s safe
switch call(0x4000, address(), 0, 0, calldatasize(), 0, 0)
case 0 {
returndatacopy(0x00, 0x00, returndatasize())
// revert(0x00, returndatasize())
}
case 1 {
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
第一行检测了gas是否够用,然后calldatacopy 从调用数据的位置 f 的拷贝 s 个字节到内存的位置 t 之后他就会利用staticall继续进行检测,但是我们可以发现,他从这里进入的staticcall 是进入了 自己的合约。 相当于对自己进行了一次重入。重入之后的调用方,就是msg.sender了。也就是可以正常进入delegatecall了。 但是他利用的是staticcall在外层,所以还是不能改变合约的原有状态。
但是通过之后 他利用call进行了第二次的合约使用。也就是这里的delegatecall就可以完成任何想做的事情了。也就是我们想要的合约销毁。
那么到这里 整体的思路就很清晰了:
- 首先进入run(address target)中,delegatecall无法进入,进入staticcall
- staticall中进入delegatecall 完成一次调用。
- call中进入delegatecall完成一次调用。
- 需要一个函数在staticcall中不改变合约状态,在call中改变。
- delegatecall的target只需要直接selfdestruct就可以了。
那么现在就考虑怎么给出一个办法,使得两次调用所执行的方法不同? 尝试思路:
}
也就是利用类似上述的伪代码。这里是不可做的。
2. 利用特征进行判断。但是我们可以看到每次进行交易不管是传的gas还是什么所有的call和staticall中的特征都完全相同。 所以这个方法也很难进行bypass。
if(gas>value){ return ; } else{ selfdestruct(msg.sender); }
3. 考虑使用call外部变量进行改变,这种是可行的一个办法。我们可以通过在外部合约设置一个方法 我们利用内部的call方法进行请求,如果能正确返回状态值则代表当前状态就是call了。
因为外部Call方法的状态即使revert()他也会只返回一个状态码0,并不会直接阻断整个交易的正常运行。
fallback()external payable{ bool success; (success,)=address(0x3c725134d74D5c45B4E4ABd2e5e2a109b5541288).call(""); if(!success){ return; } else{ selfdestruct(address(0)); }
}
![](https://md.buptmerak.cn/uploads/upload_7c5b602324701175c7f645f86f0611fa.png)
这样就成功绕过了沙箱
4. 这个是从github的官方wp中学到的 ,感觉应该和3的意思相同? 用等同于python的语法try catch 这样可以直接避免直接revert()
contract Setup { BabySandbox public sandbox;
constructor() {
sandbox = new BabySandbox();
}
function isSolved() public view returns (bool) {
uint size;
assembly {
size := extcodesize(sload(sandbox.slot))
}
return size == 0;
}
}
学到了很多opcode以及call staticcall delegatecall的知识。
## Rever
一道很巧妙的opcode构造题。给出源码:
pragma solidity 0.8.0;
contract Deployer { constructor(bytes memory code) { assembly { return (add(code, 0x20), mload(code)) } } }
contract Challenge { address public fwd; address public rev;
function safe(bytes memory code) private returns (bool) {
uint i = 0;
while (i < code.length) {
uint8 op = uint8(code[i]);
if (
op == 0x3B // EXTCODECOPY
|| op == 0x3C // EXTCODESIZE
|| op == 0x3F // EXTCODEHASH
|| op == 0x54 // SLOAD
|| op == 0x55 // SSTORE
|| op == 0xF0 // CREATE
|| op == 0xF1 // CALL
|| op == 0xF2 // CALLCODE
|| op == 0xF4 // DELEGATECALL
|| op == 0xF5 // CREATE2
|| op == 0xFA // STATICCALL
|| op == 0xFF // SELFDESTRUCT
) return false;
if (op >= 0x60 && op < 0x80) i += (op - 0x60) + 1;
i++;
}
return true;
}
function flip(bytes memory a) private returns (bytes memory) {
bytes memory b = new bytes(a.length);
for (uint i = 0; i < a.length; i++) {
b[b.length - i - 1] = a[i];
}
return b;
}
function deployOne(bytes memory code) private returns (address) {
require(code.length < 101, "deployOne/code-too-long");
require(safe(code), "deployOne/code-unsafe");
return address(new Deployer(code));
}
function deploy(bytes memory code) public {
fwd = deployOne(code);
rev = deployOne(flip(code));
}
}
contract Setup { Challenge public challenge;
constructor() {
challenge = new Challenge();
}
function test(string memory what) public view returns (bool) {
return test(challenge.fwd(), what) && test(challenge.rev(), what);
}
function test(address who, string memory what) public view returns (bool) {
bool ok;
assembly {
ok := staticcall(gas(), who, add(what, 0x20), mload(what), 0x00, 0x00)
if ok {
if iszero(iszero(returndatasize())) {
let ptr := mload(0x40)
returndatacopy(ptr, 0x00, returndatasize())
ok := mload(ptr)
}
}
}
return ok;
}
}
``` 我们首先分析challenge合约中的函数。 首先safe函数是一个用于检测字节码的函数。它里面会ban一些特定指令,但他不会搜索全部字节码。下面有一个 当op在0x60-0x80间,他会对其进行0x1-0x21的字节码的跳转不进行这其中字节码的检测。 flip函数就是单纯的把字节码全部颠倒0x32ff变为0xff32 deployOne 先进行了2个检测,一个是字节码长度不超过100,另一个是通过safe检测。然后就会部署你的Opcode并且返回合约地址 deploy函数是用于同时部署两个合约的。正向字节码以及反向字节码。进入DelpoyOne进行检测。
Setup中第一个test函数时用来检测正向反向两个合约的。需要两个合约都通过检测。其中可以输入一个字符串。进入下面的test函数。
下面的test函数中,首先进行了staticcall 并且把string作为数据输入进去。 如果交易成功那么进入下一层判断。 下一层是如果返回的datasize()如果不为0 还会进行操作。如果为0就可以直接让ok=1了。所以说我们可以知道让他成功不一定需要全部走完。但是还是要看这里。 这里首先把指针指向mem[0x40:0x60],之后把返回的data copy到了这里。然后ok取mem[0x40:0x60]。也就是如果有返回值需要返回1.