趁这个机会学学 Move XD
U Can’t Touch This
这题的主要考点是对于 check_admin
函数的绕过,它会检查传入 signer address 的前两位字节是否是 0xf75d
,而 user 的 address 显然不是。翻文档可以知道 object::create_named_object
可以传入 seeds 生成 ConstructorRef
进而生成 signer 来绕过 check_admin
中对前两个字节的校验。
seed 可以通过链下 SDK 暴力枚举得到。
Sage
题目需要传入一个 m 向量和 n 向量参与运算得到的结果与 flag 相等。
观察可以知道是自实现的矩阵运算,需要先用给的前 9 个 next_text 元素解出 X 矩阵的所有情况,然后枚举计算能使所有 m 都有解的 X 矩阵情况。
Sage script
Simple Swap
这题实现了一个简单的 deposit/withdraw 和 swap,可以看 initialize 函数来找初始状态:
- 创建 APT coin (decimals = 6)
- 创建一个 Faucet
- user 可以拿到 5 token (0.000005)
- admin 可以拿到 8 token (0.000008)
- 创建一个 Vault 用来存 APT
- 创建 TokenB coin (decimals = 1)
- 创建
Pool<APT, TokenB>
- APT 20 token (0.000020)
- TokenB 20 token (2)
- 取消 TokenB 的所有 cap (burn, mint, freeze)
- challenger 获得 APT 的所有 cap
- 创建 Share token (decimals = 6)
- challenger 获得 Share 的所有 cap
solve check 是池子里的一种 token 为 0 即可。
观察可以发现 swap 的时候存在精度误差,能一直换到池子空。不过需要 amount >= 6 才能调用这个函数,但用户初始的只有 5 amount。
发现有个 donate 函数可以改变 share token 的 rate;远程还提供了一次 admin deposit 8 amout 的调用,并且也有向下取整的问题,所以可以用如下构造方式从 admin 那里拿一点。
initialize
Admin: 8 APT; User: 5 APT; Vault: None
user deposit 1 APT
Admin: 8 APT; User: 4 APT, 1 Share; Vault: 1 APT
user donate 4 APT
Admin: 8 APT; User: 1 Share; Vault: 5 APT
admin deposit 8 APT
Admin: 1 Share; User: 1 Share; Vault: 13 APT
这样 User 就可以用 1 Share token 换 6 APT 了,然后一直换池子就行。
Swap
题目环境中创建了四种 asset,并为此创建了两个池子,前两种 asset 组成一个 volatile pool, 后两种 asset 组成 stable pool。两个池子计算 amount_out 的方式有区别,不过在计算之前都会以一定比率从 amount_in 里拿取 fees_amount,这个 fees_amount 可以由 fee_manager
获得。
在初始化过程中,给了池子初始的 asset:
pool1 (is_stable = false)
osec: 1337
movebit: 1337
pool2 (is_stable = true)
zellic: 1337
jbz: 1337
然后 user 可以调用 router::free_mint
来获取一笔空投:
osec: 100
movebit: 100
zellic: 10
jbz: 0
router::is_solved
的条件是两个池子里的所有 asset balance 为空且 pool fees amount 也为空。
池子全为空这个条件在正常 K 值模型下的 swap 是不太可能出现的,因此可以考虑从以下几个角度入手寻找漏洞:
- K 值计算错误
- 在正常计算 amount out 的前后引入不正确的 fee 计算
- 精度误差
通过审计代码,可以发现以下几个漏洞点:
- fee_manager 可以被任意 user 设置
在 pool::set_fee_manager
中,本应该验证传入的 fee_manager
为当前池子的 fee_manager
,即 assert!(address_of(fee_manager) == pool_configs.fee_manager, 0)
,但代码中并未验证。
因此我们可以设置自己的 user 为 fee_manager
,从而调用 claim_fees
获得池子的 fee。
- 池子创建时的 seeds 未包含
is_stable
标志
在创建池子的时候的 seeds 是通过两种 asset 的 name 去进行计算的,没有包含 is_stable
标志,所以调用 swap
函数时 is_stable
的值对结果没有影响。
而在 swap
里对于 volatile pool 有对于 K 值不变的检查:
我们便可以凭借这一点绕过这一检查。
- swap 时候可以伪造 asset
以 swap_a_2_b
为例:
这里有一个对 from_token
的判断逻辑:正常情况下传入的 from_token
如果不等于 pool_data.metadata_1
,那么便是 pool_data.metadata_2
,会走到 else 分支;但是如果传入的是一个伪造的 asset,与正常的 asset 有相同的 token name(为了找到 pool 的地址),同样会走到 else 分支进行正常的 withdraw
操作,并且不会通过 deposit
增加 pool 中正常 asset 的数量。
所以我们可以构造任意数量的 asset 去换取池子中的正常 asset。下面的问题是如何把池子换空。
因为我们现在可以 withdraw fees amount,又由于 fees_amount 是直接由 amount_in 的一定比例得到的,所以一个想法便是构造一个数值,使得调用 swap 时 withdraw 的 asset 数量与获得的 fees_amount 之和恰好等于池子中这种 asset 的数量。
编写 Python 脚本进行爆破:
volatile pool brute force
stable pool brute force
执行得到了 swap 时需要传入的 asset 数量。值得注意的是每个池子需要让两种 asset 都空,所以第二次在爆破时候需要在脚本里更新一下当前 pool 的状态 (也就是其中一个 reserve = 0)。
最终 payload:
看到 revenge 时候才知道这题原来还有非预期…
Groth16
不给 hint 之前看到这一堆 math 感觉不会做,放了。 给了 hint 之后发现文档内容:
Note that it’s possible to combine
entry
withpublic
orpublic(friend)
, In this casesample_function
can be called by both the Aptos CLI/SDK by any module declared as a friend.
而更改 flag 的状态函数刚好就是这个情况,直接调用这个函数当入口即可,合约都不用写。
solve:
Flash Loan
题目提供了一个简单的 flash_loan 功能,但是在 repay 的时候并没有检查 asset 的 balance,所以可以 repay 个 0 balance 的 asset 回去。