ACTF 2025 WriteUp-ezFPGA

ACTF 2025 WriteUp-ezFPGA

mmj Lv2

推荐阅读Nepnep战队wp

1.获取加密逻辑

加密逻辑在ezFPGA\src\encryptor.sv文件中,可以将其转为C代码以便于阅读(ai大法)

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#include <stdio.h>
#include <stdint.h> // 为了使用 uint8_t
#include <string.h> // 为了使用 memcpy

// 定义 FLAG 常量 (等同于 Verilog 的 parameter)
// 注意:C 语言不能直接用字符串数组初始化 uint8_t 数组,
// 需要显式转换字符为 ASCII 值。
const uint8_t FLAG[14] = {
'A', 'C', 'T', 'F', '{', 't', 'e', 's', 't', 'f', 'l', 'a', 'g', '}'
// 对应 ASCII: 65, 67, 84, 70, 123, 116, 101, 115, 116, 102, 108, 97, 103, 125
};
const size_t l = sizeof(FLAG) / sizeof(FLAG[0]); // 等同于 $size(FLAG)

// 定义其他常量数组 (等同于 Verilog 的 localparam 或 assign)
const uint8_t ab[4] = {11, 4, 5, 14};
const uint8_t ad[36] = {
116, 174, 193, 124, 102, 100, 11, 193, 115, 4, 127, 139, 98, 214,
197, 145, 97, 151, 31, 30, 117, 15, 230, 179, 235, 25, 244, 202,
73, 222, 15, 191, 119, 140, 94, 32
};
const uint8_t db[8] = {'e', 'c', 'l', 'i', 'p', 's', 'k', 'y'}; // "eclipsky"

// 状态枚举 (等同于 Verilog 的 state_t)
typedef enum { S0, S1, S2, S3, DONE } state_t; // 添加 DONE 状态用于 C 循环控制

// 模拟 Encryptor 模块行为的函数
// cypher_output 必须是一个至少能容纳 36 个 uint8_t 的数组
void encryptor_process(uint8_t cypher_output[36]) {

// 内部变量 (等同于 Verilog 的 reg 或 logic)
uint8_t aa[39];
uint8_t ac[36];
uint8_t ae[36];
uint8_t af[36];
uint8_t ba[256];
uint8_t ca, cb, cg, da; // 状态寄存器
uint8_t cd, ce, cf, ch; // 中间计算值 (组合逻辑)

state_t state;

// --- 1. 初始化 aa 数组 ---
// Verilog 的 generate 块等效于 C 的 for 循环
size_t i;
for (i = 0; i < l; i++) {
aa[i] = FLAG[i];
}
for (i = l; i < 39; i++) {
aa[i] = 0;
}

// --- 2. 计算 ac 数组 ---
// Verilog 的 generate 块等效于 C 的 for 循环
for (i = 0; i < 36; i++) {
// C 语言的 uint8_t 会自动处理溢出 (模 256),与 Verilog 的 logic [7:0] 行为一致
ac[i] = aa[i] * ab[0] + aa[i + 1] * ab[1] + aa[i + 2] * ab[2] + aa[i + 3] * ab[3];
}

// --- 3. 计算 ae 数组 ---
// Verilog 的 generate 块等效于 C 的 for 循环
for (i = 0; i < 36; i++) {
size_t row_start = (i / 6) * 6;
size_t col = i % 6;
ae[i] = ac[row_start + 0] * ad[col + 0] +
ac[row_start + 1] * ad[col + 6] +
ac[row_start + 2] * ad[col + 12] +
ac[row_start + 3] * ad[col + 18] +
ac[row_start + 4] * ad[col + 24] +
ac[row_start + 5] * ad[col + 30];
}

// --- 4. 状态机模拟 ---
// 初始化 (等同于 Verilog 的 initial 或 reset)
// Verilog 的 rst 信号将状态置为 S1,并清零计数器
ca = 0;
cb = 0;
cg = 0;
da = 0; // Verilog 中 da 在 S1 和 S2 中用作循环计数器,复位为 0
state = S1; // 直接从 S1 开始,因为 Verilog 的复位逻辑如此

// 初始化 ba 数组(虽然 S0 被跳过,但 KSA 需要一个初始状态)
// 标准 RC4 KSA 从 ba[i] = i 开始,这里也这样做
for (i = 0; i < 256; i++) {
ba[i] = (uint8_t)i;
}

// 状态机执行循环
// 在 C 中,我们不需要模拟时钟,可以直接按顺序执行状态逻辑
// 直到完成所有计算

// --- 状态 S1: KSA (密钥调度) ---
if (state == S1) {
// printf("进入状态 S1 (KSA)\n");
cg = 0; // 确保 cg 从 0 开始用于 KSA
for (da = 0; ; da++) { // 使用 da 作为循环变量,模拟 Verilog 中的行为
ch = cg + ba[da] + db[da % 8];
// 交换 ba[da] 和 ba[ch]
uint8_t temp = ba[da];
ba[da] = ba[ch];
ba[ch] = temp;
cg = ch;

if (da == 255) break; // 完成循环
}
da = 0; // 重置 da 用于下一个状态
state = S2;
}

// --- 状态 S2: PRGA 与混合 ---
if (state == S2) {
// printf("进入状态 S2 (PRGA & Mix)\n");
ca = 0; // 确保 ca, cb 从 0 开始用于 PRGA
cb = 0;
for (da = 0; da < 36; da++) { // 使用 da 作为循环变量
cd = ca + 1;
ce = cb + ba[cd];
// 交换 ba[cd] 和 ba[ce]
uint8_t temp = ba[cd];
ba[cd] = ba[ce];
ba[ce] = temp;
cf = ba[cd] + ba[ce];
af[da] = ba[cf] + ae[da]; // 混合
ca = cd;
cb = ce;
}
da = 0; // 重置 da 用于下一个状态
state = S3;
}

// --- 状态 S3: 输出 ---
if (state == S3) {
// printf("进入状态 S3 (Output)\n");
for (da = 0; da < 36; da++) { // 使用 da 作为循环变量
cypher_output[da] = af[da];
// Verilog 模块会在 da=36 时将 cypher 输出 0,
// 但在 C 中,我们通常只关心生成的 36 个字节。
}
state = DONE; // 标记处理完成
}
}

// --- 主函数 (用于测试) ---
int main() {
uint8_t final_cypher[36];

printf("原始 FLAG: ");
for (size_t i = 0; i < l; ++i) {
printf("%c", FLAG[i]);
}
printf(" (ASCII: ");
for (size_t i = 0; i < l; ++i) {
printf("%d ", FLAG[i]);
}
printf(")\n");

// 调用加密处理函数
encryptor_process(final_cypher);

// 打印结果
printf("生成的 Cypher (36 字节):\n");
for (int i = 0; i < 36; i++) {
printf("%02X ", final_cypher[i]); // 以十六进制格式打印
if ((i + 1) % 16 == 0) {
printf("\n");
}
}
if (36 % 16 != 0) {
printf("\n");
}

return 0;
}

总结为:

  • 初始化 aa 数组: 使用输入的 FLAG 参数填充 aa 数组的前 l 个元素,其余用 0 填充
  • 线性变换 1 (aaac): 使用系数 ab 进行滑动窗口计算
  • 线性变换 2 (acae): 使用系数 ad 进行更复杂的基于块的变换
  • 类 RC4 :
    • KSA 使用密钥 db = "eclipsky" 打乱数组 ba
    • 伪随机生成一个密钥流 k
    • 混合为密文[i] = 密钥流[i] + ae[i]

2.获取密文

密文也就是af

可以在Linux中用make wave等命令打开GTKWave,查看cypher信号的值,但是只能一个个显示,非常不方便

image.png

这里推荐在VScode中安装插件WaveTrace,将.vcd文件拖入其中即可查看

image.png

得到密文:

1
2
3
ad 00 c0 9f 16 17 ec 25 25 1f 12 e2 7f 9f 37 53
12 ba 8d 38 60 14 1b 31 8e 13 e2 56 0a 1a 25 b9
80 73 8a 60

(”25“占了两格长度,所以为两个数据)

3.求解RC4

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
#include <stdint.h>
#include <stdio.h>

uint8_t ba[256];
uint8_t db[8] = {'e', 'c', 'l', 'i', 'p', 's', 'k', 'y'};

// 初始化ba
void init_ba() {
for (int i = 0; i < 256; i++) {
ba[i] = i;
}
uint8_t cg = 0;
for (int i = 0; i < 256; i++) {
uint8_t ch = (cg + ba[i] + db[i % 8]) & 0xFF;
uint8_t tmp = ba[i];
ba[i] = ba[ch];
ba[ch] = tmp;
cg = ch;
}
}

int main() {
init_ba();

// 密文
uint8_t cipher[36] = {
0xad,0x00,0xc0,0x9f,0x16,0x17,0xec,0x25,0x25,0x1f,0x12,0xe2,0x7f,0x9f,0x37,0x53,
0x12,0xba,0x8d,0x38,0x60,0x14,0x1b,0x31,0x8e,0x13,0xe2,0x56,0x0a,0x1a,0x25,0xb9,
0x80,0x73,0x8a,0x60
};

uint8_t ae[36];

uint8_t ca = 0, cb = 0, da = 0;

for (int i = 0; i < 36; i++) {
uint8_t cd = (ca + 1) & 0xFF;
uint8_t ce = (cb + ba[cd]) & 0xFF;
uint8_t cf = (ba[cd] + ba[ce]) & 0xFF;

ae[i] = (cipher[i] - ba[cf]) & 0xFF;

ca = cd;
cb = ce;
}

// 输出为数组
printf("Recovered ae:\n[");
for (int i = 0; i < 36; i++) {
printf("%d", ae[i]);
if (i < 35) printf(", ");
}
printf("]\n");

return 0;
}

1
2
3
Recovered ae:
[107, 1, 191, 109, 188, 22, 73, 145, 243, 235, 253, 113, 68, 240, 157, 197, 4, 138, 132, 231, 93, 183, 108, 227, 116, 255, 192, 7, 122, 68, 218, 197, 15, 11, 60, 120]

4.求解flag

剩余部分我们可以使用z3约束求解,在这里可能会遇到一个问题,就是python默认是32位加法,而 Verilog 是 8位加法(自动取模256),所以记得每一步都强制 & 0xFF

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
from z3 import *

# 创建一个长度为 39 的 BitVec 数组,每个 BitVec 为 8 位
ANS = [BitVec("ANS%.2d"%i,8) for i in range(39)]

s = Solver()

# 设置 FLAG 的前5个字符为ACTF{
s.add(ANS[0] == 0x41) # 'A'
s.add(ANS[1] == 0x43) # 'C'
s.add(ANS[2] == 0x54) # 'T'
s.add(ANS[3] == 0x46) # 'F'
s.add(ANS[4] == 0x7B) # '{'

# 限制其余字符是可打印字符或者0
for i in range(5,39):
s.add(Or(ULE(ANS[i],0x7e), ANS[i] == 0))

ab = [11,4,5,14]
AC = [BitVec("AC%.2d"%i,8) for i in range(36)]

# 计算AC[i]
for i in range(36):
s.add(AC[i] == ANS[i + 0] * ab[0] + ANS[i + 1] * ab[1] + ANS[i + 2] * ab[2] + ANS[i + 3] * ab[3])

# 填写之前rc4解出来的AE数组值
AE_ANS = [
107, 1, 191, 109, 188, 22, 73, 145, 243, 235, 253, 113,
68, 240, 157, 197, 4, 138, 132, 231, 93, 183, 108, 227,
116, 255, 192, 7, 122, 68, 218, 197, 15, 11, 60, 120]

# 计算 AE[i] 值,注意每次累加时都要取模 0xFF
AE = [BitVecVal(0,8) for i in range(36)]
AD = [
116,174,193,124,102,100,11,193,115,4,127,139,
98,214,197,145,97,151,31,30,117,15,230,179,
235,25,244,202,73,222,15,191,119,140,94,32
]

# 设置AE数组的约束
for i in range(36):
term = BitVecVal(0, 8)
block = i // 6
pos = i % 6
for j in range(6):
term = (term + AC[block*6 + j] * AD[pos + j*6]) & 0xFF
s.add(term == AE_ANS[i])

# 求解
if s.check() == sat:
m = s.model()
flag = ''.join([chr(m.eval(ANS[i]).as_long()) for i in range(39)])
print(f"Recovered FLAG: {flag}")
else:
print("No solution found")

1
Recovered FLAG: ACTF{RC4_4nd_FPGA_w4lk_1nt0_4_b4r}

总结

这道题我本来的方向是偏了,转去研究FPGA的硬件知识了,后来也是在提取密文的部分因为工具(vsc插件)的问题导致了进度缓慢,总之一道题让我学到了很多

  • 标题: ACTF 2025 WriteUp-ezFPGA
  • 作者: mmj
  • 创建于 : 2025-05-09 23:26:21
  • 更新于 : 2025-05-11 23:36:36
  • 链接: https://samzhaohx.github.io/2025/05/09/ACTF 2025 WriteUp-ezFPGA/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论