ASU-CSE365-网络安全导论笔记-全-
ASU CSE365 网络安全导论笔记(全)
1:课程介绍与教学大纲 📚
在本节课中,我们将学习课程的基本框架、教学大纲、评分结构以及学术诚信政策。我们将通过一个真实的网络安全事件作为引子,探讨网络安全的重要性与复杂性。
课程概述与案例引入
大家好,欢迎来到CSE 365网络安全导论课程。我是Yan,这位是Connor。我们将通过一个真实案例来开启我们的网络安全之旅。
2015年7月,黑客组织“Phineas Fisher”入侵了网络安全公司Hacking Team,并通过其Twitter账户泄露了该公司的全部源代码、内部邮件和监控系统数据。这次攻击展示了即使是最注重安全的公司,也可能因为一个微小的漏洞而导致全线崩溃。
Phineas Fisher的攻击步骤如下:
- 侦察:她发现Hacking Team一个对外服务器的固件可供下载。
- 利用零日漏洞:通过分析该固件,她发现了一个未被公开的漏洞(零日漏洞),并以此作为进入内网的入口。
- 内部侦察与横向移动:进入内网后,她进行被动监听和扫描,发现了一个物理安全监控系统服务器存在安全缺陷。
- 权限提升:通过访问备份服务器,她获取了邮件服务器的备份,并从中提取了仍在使用的管理员凭证。
- 达成目标:利用获取的凭证,她安装了键盘记录器,最终窃取了开发人员的密码,获得了核心源代码的访问权限,并完成了数据泄露。
这个案例表明,在网络安全中,防御者必须每次都成功,而攻击者只需成功一次。本节课我们将探讨此类攻击的原理,并在后续课程中学习如何识别、利用和防御这些漏洞。
关于网络安全的伦理思考 🤔
上一节我们看到了一个攻击案例,本节中我们来探讨其背后的伦理问题。

Phineas Fisher的行为是正义的“吹哨人”行为,还是非法的黑客攻击?这个问题没有简单答案。在网络安全领域,法律与个人伦理有时存在冲突。

本课程的核心伦理与法律规则是:
- 绝对不要入侵你没有明确获得授权入侵的系统。
- 许多公司设有“漏洞赏金计划”,明确规定了允许测试的范围。如果你不确定,就不要行动。
- 作为学术研究的一部分,在受控环境下进行安全研究通常受到法律保护(如《数字千年版权法》的研究豁免条款)。
在本课程中:
- 所有课程挑战(
challenges)都是允许并鼓励你们去“攻击”的。 - 课程核心基础设施(如存储学生信息的服务器)是禁止攻击的。
- 我们的目标是学习,在安全、合法的环境中实践技能。
课程结构与教学大纲详解 📖
了解了伦理边界后,我们来看看本课程的具体运作方式。本课程的结构与传统课程有很大不同。
课程形式与安排
- 大班混合教学:所有章节(周一、周三、在线班)合并为一个大型班级。周三的讲座是新课,不会重复周一内容。所有学生都需要观看每周的两次讲座(可直播或观看录像)。
- 无考试:成绩100%来源于实践作业。
- 主要学习平台:我们不使用Canvas作为主要平台。所有课程材料、作业和成绩都发布在
Pwn.College网站上。Canvas仅用于同步截止日期和最终成绩显示。
作业与评分
课程将包含大约10个作业。每个作业由一系列挑战(challenges)组成。
评分分为两部分:
- 检查点:在作业发布约一周后截止。你需要完成该作业约30%的挑战。这是一个“通过/不通过”的评分,占总成绩的30%。目的是督促大家尽早开始。
- 最终提交:在作业最终截止日期前,你需要完成尽可能多的挑战。这部分占总成绩的70%。
最终成绩计算公式为:
最终成绩 = 检查点分数(30%) + (完成挑战数 / 总挑战数) * 70%
迟交政策:在最终截止日期后,作业的70%部分仍可提交并获得50%的分数,直到学期成绩截止日(约12月16日)。检查点过期不候。
辅导课与帮助渠道
- 线下辅导:每周一至周五下午4:30 - 5:20,在BYENG 209教室。这是获得面对面帮助的最佳场所,你可以随时去任何一天的辅导课。
- Discord社区:课程主要的在线交流平台。可以异步提问、讨论概念、分享提示(非代码)。在这里帮助他人还可以获得额外学分。
- 线上同步辅导:每周六在Discord语音频道进行。
额外学分
你可以通过以下方式获得最多15% 的额外学分:
- 制作优质梗图:每周在课程Discord的
#memes频道发布与课程内容相关、有洞察力的梗图,最多可获得0.5%的额外学分。发布低质量或抄袭梗图会导致“梗图监狱”,失去本途径的额外学分资格。 - 帮助同学:在Discord上有效帮助其他同学解决问题,当对方公开感谢你时,即可累积帮助次数。根据公式
额外学分 = 1.337 ^ (log₂(帮助次数))计算奖励。 - 解决CTF挑战:在
Pwn.College的CTF存档中解决挑战,并提交详细的原创解题报告,每个挑战可获得0.5%的额外学分。 - 负责任地披露漏洞:如果你在课程基础设施中发现并负责任地报告了安全漏洞,也可获得额外学分。
严禁滥用系统(如刷感谢、互刷帮助),我们将进行监控和数据统计分析,违者将按学术不端处理。
学术诚信与合作政策
严禁作弊。我们拥有先进的监控系统,可以追溯检测作弊行为。
允许的合作包括:
- 在公开的课程Discord频道或辅导课上,基于概念进行讨论、分享思路和提示。
- 不允许的合作包括:
- 分享代码、直接答案或旗帜(
flag)。 - 在任何其他私人或非课程官方的Discord服务器、SMS群组等私下讨论作业。
- 与修过本课程往届学生合作。
- 分享代码、直接答案或旗帜(

请使用我们提供的官方渠道进行交流。违反政策将导致严重的学术后果。

给学生的实用建议
- 立即开始:第一个作业(Linux Luminarium)包含84个挑战,已发布并将于周日截止。请现在就登录
Pwn.College开始。 - 避开截止日期高峰:服务器在作业截止前几小时可能会因负载过高而变慢或不稳定。请提前规划,不要拖到最后一刻。
- 善用资源:遇到困难时,首先查阅
Pwn.College上的“Getting Started”指南,然后利用辅导课和Discord寻求帮助。 - 关于特殊安排:本课程无考试且出勤不强制,能满足多数学业调整需求。由于作业环环相扣,延长截止日期通常会导致后续学习困难,不建议申请。
总结与下节预告 🎯

本节课我们一起学习了网络安全的一个经典案例,探讨了相关的伦理问题,并详细解读了本课程独特的教学大纲、评分结构、支持资源以及严格的学术诚信政策。


核心要点回顾:
- 网络安全是攻防不对称的领域,小漏洞可引发大问题。
- 必须在法律和伦理框架内进行安全实践。
- 本课程成绩100%源于实践挑战,请务必关注检查点和最终截止日期。
- 充分利用官方Discord和每日辅导课获取帮助。
- 严禁作弊,合作需在公开场合进行概念性讨论。
下节课(周三),我们将深入技术内容,并演示如何使用Pwn.College平台。现在,你的首要任务是完成平台设置并开始第一个作业。
祝大家好运,我们周三见!
2:课程概述与作业入门指南 🚀














在本节课中,我们将一起回顾课程的基本结构,并重点介绍前两个作业(Linux Luminarium 和 Talking Web)的入门知识。我们将学习如何使用课程平台(Pwn College Dojo)、理解命令行基础以及初步探索HTTP网络请求。



课程结构与作业状态 📊
上一节我们介绍了课程的基本框架,本节中我们来看看当前课程作业的完成情况。
本课程由 Connor 和 Yan 两位教授共同授课,所有学生被视为一个大班。如果你错过了周一的课程,需要尽快通过录播视频和课程大纲进行补课。
目前有两个作业已发布:
- Linux Luminarium:Linux 命令行入门。
- Talking Web:HTTP 请求入门。
这两个作业都将在本周日晚上 11:59 截止。强烈建议你尽早开始,原因如下:
- 作业具有一定挑战性,可能需要数小时完成。
- 临近截止日期时,服务器负载会很高,可能导致访问缓慢。





根据当前数据,大部分同学已开始 Linux Luminarium,但仍有不少同学尚未开始 Talking Web。我们建议先完成 Linux Luminarium。




Pwn College Dojo 平台使用指南 💻
现在我们已经了解了作业的截止日期和重要性,接下来看看如何高效地使用我们的学习平台。

Pwn College Dojo 是我们完成挑战的主要平台。每个挑战都可以在网页的图形化桌面环境中完成。
快速提交与导航技巧
以下是提高平台使用效率的几个技巧:
- 网页内快速提交:在挑战页面,点击右上角的小旗子图标,你可以在弹出的窗口中直接阅读题目描述、输入并提交Flag,无需在页面间反复跳转。
- 使用SSH连接:如果你更喜欢在本地终端工作,可以使用SSH连接到Dojo服务器。这能提供更流畅的挑战体验,尤其是在服务器负载较高时。
SSH连接设置步骤
设置SSH连接只需简单几步:
- 在Dojo的“Getting Started”部分,按照指引生成SSH密钥对。命令通常是
ssh-keygen。 - 将生成的公钥(如
key.pub文件内容)复制到Dojo网站的设置(Settings)-> SSH Keys 页面中。 - 启动一个挑战后,在终端使用
ssh hacker@pwn.college命令并指定你的私钥文件即可连接。
连接成功后,你的SSH会话会自动绑定到当前活跃的挑战环境。当你完成一个挑战并开启下一个时,SSH连接会自动刷新到新挑战的shell中,实现快速切换。

Linux 命令行核心概念 🐧
在熟悉了平台操作后,我们需要掌握本课程的核心工具——Linux命令行。本节将介绍其基本哲学和构成。
计算机早期通过开关、打孔卡与文本终端交互。尽管后来出现了图形界面,但命令行因其高效和强大的脚本能力,至今仍是计算领域的基石。
在终端中,你看到的是 提示符(Prompt),它通常显示用户名、主机名和当前目录,并以 $ 符号结尾。后面的光标等待你输入命令(Command)。
命令的语法与参数
命令行的语法类似于自然语言,具有特定的结构:
- 命令:你输入的第一个词。它告诉计算机要运行哪个程序。
- 参数:命令之后的所有词,也称为参数(Arguments)。它们会改变命令的具体行为。
例如,在终端中输入:
echo hello there
echo是命令,它的功能是将参数回显到屏幕。hello和there是传递给echo命令的两个参数。- 因此,输出结果是:
hello there。
不同的命令以不同的方式解释其参数。例如,cat 命令将其参数解释为文件名,并显示这些文件的内容。
cat /home/hacker/world.txt
这条命令会显示位于 /home/hacker/ 目录下 world.txt 文件的内容。

文件系统基础

Linux文件系统是一个单一的树状结构,所有文件和目录都从 根目录 / 开始。你的个人文件通常位于 /home/你的用户名/ 目录下。路径使用正斜杠 / 来分隔目录层级。




Talking Web:HTTP 请求初探 🌐

掌握了命令行基础后,我们就可以开始探索第二个作业“Talking Web”了,它关乎网络通信的基础——HTTP协议。

我们每天使用浏览器访问网站时,都在发起 HTTP 请求。这个协议实际上是人类可读的文本协议,建立在 TCP 网络连接之上。

使用 Netcat 模拟网络通信
我们可以使用 netcat (或 nc) 工具来模拟最基础的网络通信。它可以在两台计算机之间建立原始的 TCP 连接。
- 启动服务端(监听):在一个终端中运行以下命令,表示在 4242 端口监听连接。
nc -l 0.0.0.0 4242 - 启动客户端(连接):在另一个终端中运行以下命令,连接到本机(
127.0.0.1代表自己)的 4242 端口。nc 127.0.0.1 4242 - 进行通信:此时,两个终端建立了连接。在任一终端输入文字并按回车,消息就会显示在另一个终端中。这演示了最基本的双向通信。
理解 HTTP 请求与响应
HTTP 协议规定了客户端(如浏览器)和服务器之间通信的文本格式。
- HTTP 请求:当你在浏览器地址栏输入
http://127.0.0.1:4242并访问时,浏览器会向该地址发送一个类似下面的文本块:GET / HTTP/1.1 Host: 127.0.0.1:4242 User-Agent: Mozilla/5.0... ...(其他头部信息) - HTTP 响应:服务器(比如我们用
netcat模拟的)需要按照 HTTP 协议回复一个响应。一个最简单的响应如下:
浏览器在收到这个响应后,就会在页面上显示HTTP/1.1 200 OK Content-Length: 10 HelloWorldHelloWorld。
除了使用浏览器,你还可以用命令行工具 curl 来发送 HTTP 请求,这在自动化测试和调试时非常有用。
curl http://127.0.0.1:4242
总结与后续安排 🎯

本节课中我们一起学习了以下内容:
- 课程节奏:了解了前两个作业(Linux Luminarium 和 Talking Web)的紧迫性,以及“尽早开始”的重要性。
- 平台熟练度:掌握了 Pwn College Dojo 平台的高效使用技巧,特别是网页内提交和 SSH 连接的方法。
- Linux 核心:理解了命令行的基本哲学、命令与参数的概念,以及 Linux 文件系统的树状结构。
- Web 基础:通过
netcat工具直观感受了网络通信,并初步了解了 HTTP 请求与响应的文本格式。
从下周开始,主要课程内容将通过录播视频发布。而像今天的直播课程将侧重于深入讨论特定主题和答疑。
如果你在完成作业时遇到困难,请记住:
- 仔细阅读作业描述和附带的讲座视频。
- 利用课程 Discord 频道进行提问。
- 参加工作日的 Pwn College Power Hour(下午 4:30),那里有助教可以提供面对面帮助。



祝大家在 Linux Luminarium 和 Talking Web 中顺利获得 Flags!我们下周将进入更精彩的模块。再见,黑客们!
3:Web Security - 课程回顾与入门

在本节课中,我们将回顾课程前几周的状态,并正式开启新的“Web安全”模块。我们将探讨如何培养网络安全所需的对抗性思维,并学习如何开始应对新的挑战。
课程状态回顾
上一节我们完成了Linux和H模块的学习。现在,我们正式进入“Web安全”模块。这个模块结合了之前的知识,并引入了一些更具挑战性的概念。
以下是关于新模块的一些关键信息:
- 模块内容:包含27个挑战,探索Web应用程序的各种安全漏洞。
- 截止日期:作业将于9月15日到期。
- 检查点:本周日晚上前需完成30%的挑战,否则将损失3%的最终成绩。
- 难度提示:每个Web安全挑战的个体难度将显著高于之前的Linux挑战,请预留充足时间。
培养对抗性思维
在网络安全中,培养对抗性思维至关重要。这意味着我们需要像攻击者一样思考,找出开发者所做的、但在现实中可能不成立的假设。
让我们通过一个简单的脚本示例来理解这种思维。假设我们有一个名为 your_file_formatter.sh 的脚本,其核心功能是读取用户主目录下的指定文件。
#!/bin/bash
# 初始版本:存在漏洞
cat "$HOME/$1"
攻击者会思考:脚本假设 $HOME 变量是可信且安全的。但如果攻击者能控制这个变量呢?例如,通过设置 HOME=/,攻击者就能尝试读取系统根目录下的文件,如 /flag。
修复尝试1:开发者决定不使用 $HOME,而是硬编码一个路径。
cat "/home/user/$1"
然而,攻击者可能通过输入 ../../../flag 作为参数 $1 来进行路径遍历攻击。
修复尝试2:开发者尝试过滤掉参数中的点号。
cleaned_input=$(echo "$1" | tr -d '.')
cat "/home/user/$cleaned_input"
但攻击者可能利用符号链接(symlink)或竞争条件等更底层的系统特性来绕过防护。

这个例子说明,安全是一个持续的过程。开发者修复一个漏洞时,可能会引入新的问题,或者未能从根本上解决某一类漏洞。作为安全研究者,你需要不断深入思考,探究系统底层的工作原理和所有可能的交互方式。
Web安全挑战入门指南
本节中我们来看看如何开始应对Web安全挑战。理解挑战的运行机制是第一步。
每个挑战本质上是一个小型Python Web应用程序(使用Flask框架)。你可以通过以下方式与之交互:
- 在终端使用
curl命令。 - 在浏览器中访问提供的地址。
为了理解挑战的逻辑,实践模式 是你的得力工具。在实践模式下,你可以查看并修改服务器源代码,插入调试语句。
例如,在第一个Web安全挑战中,你可以修改 web_security_1.py,添加打印语句来查看关键变量:
@app.route('/<path:filepath>')
def serve_file(filepath):
print(f"[DEBUG] app.root_path: {app.root_path}")
print(f"[DEBUG] Requested filepath: {filepath}")
# ... 原有代码 ...
然后重启挑战服务器,你的请求细节就会在终端输出,帮助你理解程序如何处理输入。
此外,充分利用文档、Discord社区、Sensei AI助手以及课程提供的预录讲座。当Sensei给出建议时,请批判性地思考,并将其作为启发思路的工具,而非绝对正确的答案。
课程文化与资源
本课程鼓励通过“梗图”进行交流和学习。每周发布与课程内容相关的原创梗图可以获得额外学分。这些梗图不仅能活跃气氛,也常常包含解决挑战的关键提示或对课程的有效反馈。
在寻求或提供帮助时,请善用Discord的“点赞”功能。对有帮助的回复进行点赞,可以为帮助者积累额外学分,这是对他们贡献的认可和激励。
请记住,尽早开始 完成作业。这能让你有充足的时间思考、调试,并在截止日期前从容地寻求帮助,避免服务器和帮助渠道在最后时刻过载。
总结

本节课中我们一起学习了如何从Linux基础过渡到Web安全领域。我们强调了培养对抗性思维的重要性,即通过质疑假设和深入系统底层来发现漏洞。我们还介绍了开始Web安全挑战的实用技巧,特别是使用实践模式进行调试。最后,我们回顾了课程的协作文化和可用资源。现在,是时候将这些知识付诸实践,开始你的Web安全探索之旅了。祝你破解顺利!
4:Web安全 - 答疑与调试方法

在本节课中,我们将学习如何高效地调试和分析网络安全挑战,特别是针对SQL注入和路径遍历漏洞。我们将通过实际例子,讲解如何将复杂的Web应用简化为可交互的脚本,以及如何系统地定位漏洞并学习必要的技术知识。

调试SQL注入挑战
上一节我们介绍了课程的基本情况,本节中我们来看看如何具体调试一个SQL注入挑战。当面对一个包含Web前端的复杂应用时,直接通过HTTP请求进行测试可能非常繁琐。一个有效的方法是剥离Web部分,创建一个可以直接与核心漏洞代码交互的简化环境。


以下是创建一个简化调试环境的步骤:
- 定位核心代码:在挑战文件(如
sqli2.py)中找到执行数据库查询的关键部分。 - 剥离无关代码:删除所有与Flask Web框架、路由处理、HTTP请求/响应相关的代码。
- 创建交互脚本:将核心的SQL查询逻辑提取到一个独立的Python脚本中。使用命令行参数或直接修改代码来提供输入。
- 使用交互式Python:通过
python -i命令运行脚本,进入交互式Python环境,可以方便地执行查询、检查结果和测试不同的注入载荷。



例如,原始Web应用中的查询代码可能如下:
query = f“SELECT * FROM users WHERE username = ‘{username}’ AND password = ‘{password}’”
result = db.execute(query)
可以简化为:
import sqlite3
conn = sqlite3.connect(‘:memory:‘)
# ... 初始化数据库 ...
username = input(“Enter username: “)
password = input(“Enter password: “)
query = f“SELECT * FROM users WHERE username = ‘{username}’ AND password = ‘{password}’”
print(“Debug Query:“, query)
cursor = conn.execute(query)
print(“Result:“, cursor.fetchall())


这种方法让你能快速验证注入点,而无需处理网络层的问题。



系统性的漏洞分析方法




在解决了如何调试的问题后,我们需要一个系统性的方法来定位漏洞。无论面对何种挑战,第一步总是明确最终目标:获取flag。







以下是分析漏洞的通用步骤:



- 明确目标:在代码或应用描述中搜索
flag,确定获取flag的条件(例如“以admin身份登录”)。 - 逆向推理:思考如何满足该条件。这通常意味着需要绕过身份验证(如窃取密码或利用登录逻辑漏洞)。
- 定位可疑代码:根据挑战主题(如SQL注入、路径遍历),在代码中搜索可能存在问题的地方(如动态拼接的SQL语句、用户控制的文件路径参数)。
- 聚焦测试:集中精力测试最可疑的代码片段。如果毫无进展,再考虑其他可能性。






以路径遍历挑战为例:
- 目标:读取
/flag文件。 - 推理:应用本身没有直接输出
flag的功能,因此需要利用其文件读取功能。 - 定位:查找用户输入影响文件路径的代码,例如
open(path)中的path变量。 - 分析:检查对
path变量的过滤或清理逻辑(如使用strip(‘./’)),并研究其确切行为。




快速学习未知技术




在分析过程中,你肯定会遇到不熟悉的技术或框架(如Flask、SQLite)。网络安全要求具备快速学习的能力。



以下是高效学习的方法:






- 善用官方文档:遇到未知函数(如Flask的
abort()或Python的strip()),第一时间查阅官方文档。 - 把握抽象层次:无需立即深入每个细节。先理解其基本功能和在该上下文中的安全影响。只有当其他路径都走不通时,才深入挖掘。
- 针对性搜索:使用准确的关键词进行搜索(例如“python sqlite3 cursor execute”而非泛泛的“db execute”)。



例如,你不必精通Flask的所有细节,但需要快速了解abort(403)会向浏览器返回一个“403 Forbidden”HTTP错误,并可能附带消息。这种程度的知识可能就足以理解漏洞的利用方式。

现实意义与总结


本节课中我们一起学习了如何通过简化代码来调试SQL注入,以及一套系统性的漏洞分析方法。更重要的是,我们探讨了在网络安全领域中快速学习未知技术的必要性和方法。这些技能不仅适用于Pwn College的挑战,也直接对应现实世界中的安全漏洞(如CVE列表中的大量SQL注入和路径遍历案例)。通过练习,你将培养出“快速学习,足够深入”的能力,这是安全研究员最关键的核心技能之一。


请记住,对于Web安全模块的检查点,你需要在截止日期前完成一定数量的挑战。如果某个挑战卡住,不妨暂时跳过,先解决其他问题以确保拿到基础分数。
5:Web安全 - 课程回顾与解题方法
在本节课中,我们将回顾Web安全模块的学习情况,并深入探讨一种系统性的解题方法。我们将学习如何分析一个复杂的系统,识别其安全策略,并找到违反该策略的漏洞。课程将涵盖命令注入、SQL注入和跨站脚本等核心概念,并通过实例演示如何分解问题、调试代码和构建攻击链。
课程回顾与观察
上一节我们介绍了Web安全的基础知识。本节中,我们来看看同学们在实践中的表现和遇到的常见问题。
Web安全并不像想象中那么难。事实上,本模块的完成情况相当不错:近三分之二的同学达到了检查点要求,甚至有部分同学已经完成了所有挑战。然而,我们也观察到一些同学将任务拖到了最后期限,这反映出安全挑战需要深入理解,而理解所需的时间是难以预测的。
对于本模块,大部分同学在周六下午之后才解决了第一个挑战。理想情况下,我们希望同学们能更早开始,以便有足够时间发现自己知识的不足。Linux和Talking Lab模块可能相对直接,导致一些同学低估了Web安全的难度。请不要低估后续模块的挑战性,我们已经进入了一个将持续一段时间的难度水平。
在帮助方面,我们的助教团队(如Rob、Veify、Hanto noodles等)发挥了巨大作用。请记住在Discord上获得帮助时感谢他们。寻求帮助的最佳方式是:理解给出的提示或指导,并尝试自己重构从提示前到提示后的思考路径。如果你对“我该如何想到这一点”感到困惑,可以询问帮助你的人。很多时候,他们也是通过试错才找到答案的。

我们真正要教授的,不仅仅是触发命令注入的技巧,更是如何跳入一个现有的复杂系统、理解漏洞并推理其影响。这是一项很难直接传授的技能,我们主要通过“暴露疗法”来让大家掌握。
同时,你需要理解问题的关键点,而不必深入到每一个细节。例如,要进行SQL注入,你并不需要完全理解SQL的历史。只要你有足够的知识来发现和推理漏洞,这就足够了。当然,我们应尽可能多地学习,但也要注意平衡,避免钻牛角尖。



在调试过程中,一个常见的错误是忘记启动服务器,却花费数小时调试解决方案。请务必注意这一点。此外,服务器代码并非不可更改的“青铜巨像”。你可以利用它来帮助你调试和利用漏洞。服务器代码底部有选择端口的代码,如果你不以root身份运行,可以复制出来修改并运行在新端口上,这样你就可以插入调试语句或修改它以辅助攻击。

核心挑战与解题思路
现在,让我们将焦点转向具体的挑战和系统性的解题方法。我们将以命令注入1为例,然后将其思路应用到更复杂的SQL注入1上。
分析方法:以命令注入1为例
首先,请完整阅读挑战描述和代码。我们的分析始于一个核心问题:这个脚本应遵守的安全策略是什么?而我们又要如何违反它?



对于命令注入1,脚本的安全策略是:允许用户使用其预期功能(列出目录),但不允许违反标志文件的机密性。我们的攻击目标正是读取标志文件。
确定了安全属性后,我们接着问:如何实现? 知道这是一个命令注入,并且看到用户输入被直接拼接进字符串,我们就知道问题所在。我们的注意力转向如何利用这个问题。
这一切都要求我们首先理解挑战文件和源代码。理解你正在攻击的程序与确定初始的安全属性同等重要。为了推理攻击,我们需要理解这个服务器,因为它以root身份运行,可能有权访问标志文件。
理解代码意味着逐行阅读,至少在高层次上了解其功能。例如:
import subprocess:用于启动和与进程交互的模块。Flask:一个Python网络服务器模块。f"ls {directory}":这是一个Python格式字符串,会将用户输入添加到字符串中。subprocess.run:用于生成进程。
理解了代码后,我建议动态地与系统交互。这意味着实际运行并操作它。许多同学仅使用curl命令,这就像试图用叉子喝汤一样。系统是为特定交互方式设计的,对于Web漏洞,使用浏览器(如Firefox)通常是更直观的调试工具。



如果你在使用curl或类似工具时遇到困难,无法理解发生了什么,那么就需要开始调试。一个强大的技巧是:分解问题。

例如,命令注入1涉及两种技术:HTTP和Shell命令行。一旦你确定了内部问题(命令注入),你可以创建一个代理挑战,只探索那个问题,而无需处理HTTP。你可以复制服务器代码,删除所有与Web(Flask)相关的部分,只保留核心的命令拼接与执行逻辑,然后通过命令行直接与之交互。这允许你在不处理HTTP的情况下,专注于理解漏洞的本质。
注意:在修改代码时,请注意上下文。原始服务器在启动时会更改工作目录(例如到/challenge)。在你的简化版本中,可能需要保持一致,以确保行为与原始挑战一致。

方法应用:SQL注入1


现在,让我们将同样的分析方法应用到更复杂的SQL注入1。许多同学的做法是:复制所有72行代码和描述,粘贴到ChatGPT并询问如何解决。ChatGPT或许能帮你解决第一个挑战,但如果你不从中学到东西,那么到了SQL注入2或更后面,当你的概念知识严重滞后时,挑战将变得无法解决。请不要这样做。
让我们系统性地分析SQL注入1:
- 安全目标:我们的目标是获取标志(flag)。这指导我们的一切行动。
- 如何获取标志?代码显示,需要以
admin用户身份登录。 - 分析登录机制:
- 用户名从Flask会话cookie中获取,而Flask会话受服务器密钥保护,无法伪造。
- 查看数据库创建语句:有一个
admin用户和一个pin码;还有一个guest用户,pin为1337。 - 尝试以
guest/1337登录成功。这验证了我们对程序工作方式的理解,并帮助我们建立与代码匹配的心智模型。
- 寻找漏洞:查看处理登录的代码,发现这一行:
这是一个将用户输入(query = f"SELECT * FROM users WHERE username = '{username}' AND pin = {pin}"pin)直接放入的格式字符串,然后该字符串被用作SQL查询。这很危险。 - 验证漏洞:尝试以
guest身份登录,pin输入一个分号;。服务器崩溃,说明存在SQL语法错误,证实了SQL注入点就在pin参数处。
现在,我们可以在浏览器中直接测试和调试这个注入点,这比使用curl更方便。
如果我们想更深入地理解SQL查询本身,而不受Web部分干扰,可以再次使用分解法。创建一个简化脚本,移除所有Flask和HTTP相关代码,只保留数据库连接和查询逻辑。我们可以让脚本从命令行接收pin输入,然后打印出构建的查询和结果。这样,我们就有了一个专注于SQL交互的“调试器”。


更进一步,我们可以直接与挑战创建的数据库文件交互。服务器会在/tmp目录下创建一个SQLite数据库文件。我们可以用另一个终端,使用sqlite3命令行工具直接打开这个文件并执行任意SQL查询。这让我们能够完全绕过Web界面,直接操作数据库,从而更好地理解数据结构并测试我们的攻击载荷。




方法推广:XSS与CSRF



这种分解问题、隔离核心漏洞的思想可以推广到其他类型的挑战,如跨站脚本和跨站请求伪造。

以XSS为例,挑战可能涉及服务器、客户端浏览器(受害者)以及它们之间的交互。你的攻击链可能包含多个步骤。你可以:
- 单独测试组件:像处理SQL一样,你可以直接操作后端数据库,插入测试数据,验证攻击的最终效果是否可行。
- 逆向构建:如果你知道最终目标(例如,让管理员浏览器执行特定JavaScript),你可以先确保这一步能独立工作。然后倒退一步,构建触发这一步的HTTP响应,并确保各个环节衔接无误。
- 修改“受害者”:在一些有模拟受害者浏览器的挑战(如XSS2或CSRF)中,“受害者”本身也是一个脚本(例如使用Selenium)。在练习模式下,你可以修改这个受害者脚本,例如移除“无头”模式,让它显示浏览器窗口,或者添加暂停,以便你观察攻击发生时浏览器内的具体情况。

关键在于认识到,这些挑战都是由多个部分组合而成的复杂系统。你可以与不同部分进行不同方式的交互。分解并隔离它们,可以让你与更小、约束更强的问题进行交互,从而可以精细地调整攻击的每一步。之后,再将各个步骤组合起来,完成端到端的攻击。



总结
本节课中,我们一起学习了应对Web安全挑战的系统性方法。我们首先强调了理解代码和明确安全目标(违反何种策略)的重要性。接着,我们深入探讨了如何通过动态交互和问题分解来分析和调试漏洞,具体包括:
- 使用合适的工具(如浏览器)进行交互。
- 创建简化版的“代理挑战”,隔离核心漏洞(如命令注入、SQL逻辑)。
- 直接与系统组件交互(如用
sqlite3操作数据库文件)。 - 在涉及受害者浏览器的挑战中,修改受害者行为以辅助观察。
这项技能的本质是:对一个相对复杂的目标进行安全分析时,你必须拆分问题。这类似于做几何证明,你需要从目标(获取flag)出发,逆向推导出潜在的步骤,并逐一探索和验证。本课程要求你快速自学多种技术(如SQL、HTTP、浏览器自动化),并具备消化文档的能力。通过有意识地运用这些分析方法,你将能更有效地解决未来的安全挑战。

课程名称:CSE365 网络安全导论
章节编号:06
章节名称:Web安全 - 课程回顾与解题方法
6:Web Security
概述


在本节课中,我们将学习Web安全模块的核心概念,特别是跨站脚本攻击。我们将通过分析一个具体的挑战(XSS 5)来理解如何识别漏洞、构建攻击链,并最终获取目标标志。课程将涵盖从理解应用程序结构到利用XSS漏洞执行管理员操作的完整流程。


课程结构说明
在深入挑战之前,我们先回顾一下本课程的结构。课程大纲提供了模块和挑战的概览,但具体内容可能会有所调整。每个模块都包含一系列挑战,难度逐渐增加。本模块涉及路径遍历、命令注入、身份验证绕过、SQL注入、跨站脚本和跨站请求伪造等主题。


跨站脚本5是一个较难的挑战,它建立在之前挑战的基础上。如果你对某个概念感到困惑,例如如何开始一个挑战,本节将为你提供指导。
启动挑战

以下是开始本挑战的步骤:
- 访问挑战页面,你会看到一个服务器。
- 你必须启动服务器。许多同学忘记这一步,导致无法连接。
- 服务器启动后,会显示运行在
localhost:80。 - 你可以通过浏览器访问
http://localhost:80来查看Web应用程序。
每个挑战的核心都是一个Python Flask服务器。理解服务器代码是解决挑战的关键。你可以修改代码来添加调试信息,帮助你理解程序流程。
分析应用程序
上一节我们介绍了如何启动挑战,本节中我们来看看如何分析目标Web应用程序。
首先,阅读挑战描述至关重要。对于XSS 5,描述指出目标不仅仅是弹出一个警告框,而是需要利用JavaScript在受害者浏览器中发起新的HTTP请求,伪装成受害者,以读取管理员用户的未发布草稿中的标志。
接下来,我们需要查看服务器源代码,以理解应用程序的数据结构和功能。

数据结构
服务器初始化时创建了数据库表。核心数据结构如下:
- posts表:存储帖子内容。包含
content(内容)、author(作者)和published(是否发布)字段。 - users表:存储用户信息。包含
username(用户名)和password(密码)字段。
初始数据包括:
- 一个由
admin用户创建的未发布帖子,其content字段包含标志。 - 三个用户:
admin(密码为标志)、guest(密码为password)和hacker(密码为1337)。
我们的目标有两个潜在路径:获取 admin 的密码,或者读取 admin 的未发布帖子。挑战描述指向后者。
功能端点
应用程序提供了四个主要路由(端点):
POST /login:处理用户登录。POST /draft:创建草稿(或已发布)帖子。POST /publish:将当前用户的所有草稿帖子设置为已发布。GET /:根路径,显示欢迎页面和帖子列表。
通过代码分析,我们可以确认:
POST /login和POST /draft使用了参数化查询(?占位符),因此不存在SQL注入漏洞。POST /publish端点有一个关键特性:它可以将当前登录用户的所有草稿帖子发布。如果能让admin用户访问这个端点,那么包含标志的未发布帖子就会变成已发布状态,从而可以被我们查看。
理解漏洞与攻击链
上一节我们分析了应用程序的功能,本节中我们来看看如何将这些功能点串联成攻击链。
攻击的核心思路是:利用跨站脚本让管理员浏览器执行我们控制的JavaScript代码,该代码会以管理员的身份访问 /publish 端点。
攻击链构建
- 目标:获取
admin的未发布帖子中的标志。 - 等价条件:如果
admin用户访问了POST /publish端点,那么他的未发布帖子就会变成已发布。 - 问题:我们无法直接以
admin身份登录。 - 突破口:应用程序存在XSS漏洞。我们可以在帖子内容中注入JavaScript代码。当
admin用户(通过victim脚本自动登录)浏览帖子列表时,我们的代码会在他的浏览器中执行。 - 利用:编写JavaScript代码,在
admin的浏览器环境中向/publish发起请求。由于请求是从admin的浏览器发出的,会携带他的会话Cookie,因此服务器会认为这是admin本人发出的发布请求。

关键代码分析


在根路径 / 的处理函数中,应用程序会查询并显示所有帖子。对于每个帖子,它会直接输出 author 和 content 到HTML页面中,而没有对 content 进行任何过滤或转义。这就是XSS漏洞的根源。

# 简化示例,展示漏洞点
for post in posts:
page += f"<div>Author: {post['author']}</div>"
page += f"<div>Content: {post['content']}</div>" # 危险!直接输出用户控制的content
我们可以提交一个帖子,其 content 为 <script>alert(1)</script>。当任何用户(包括admin)查看帖子列表时,这段脚本就会执行。



开发漏洞利用程序
上一节我们构建了攻击链,本节中我们来看看如何具体实现利用。
我们需要将弹窗的PoC(概念验证)脚本升级为能够发起HTTP请求的脚本。JavaScript的 fetch() API 可以用于此目的。
使用Fetch API
fetch() 函数用于发起网络请求。一个简单的GET请求如下:

fetch('/publish', {
method: 'POST',
credentials: 'include' // 关键:确保发送Cookie
})
参数 credentials: 'include' 至关重要。默认情况下,跨域请求不会发送Cookie等凭证信息。虽然我们这里是同源请求,但明确指定 credentials: 'include' 可以确保会话Cookie随请求一起发送,这样服务器才能识别出这是 admin 用户的请求。
整合利用
因此,我们最终提交的帖子内容应该是一个包含 fetch 调用的脚本:

<script>
fetch('/publish', {method: 'POST', credentials: 'include'});
</script>
当 admin 用户通过 victim 脚本登录并浏览页面时,这段脚本会自动执行,向 /publish 发送一个POST请求,从而发布他所有的草稿(包括包含标志的那一篇)。之后,我们以 guest 身份登录,就能看到已发布的完整标志。

调试与验证

在开发漏洞利用程序时,不断验证你的假设至关重要。以下是一些有效的调试方法:
- 修改服务器代码:在
/publish端点添加print语句,输出当前会话的用户名,以确认请求是否真的以admin身份到达。 - 使用浏览者开发者工具:检查网络请求,确认
fetch请求是否按预期发出,以及Cookie是否被携带。 - 使用
curl或 Pythonrequests库:手动模拟请求,理解应用程序的交互流程。使用curl -v可以查看详细的HTTP请求和响应头。 - 注意上下文转义:在Bash命令行、HTTP参数和SQL语句等多个上下文中,特殊字符(如
&、?、引号)的行为不同。确保你的payload在最终执行时是正确的。
总结
本节课中我们一起学习了Web安全中一个复杂的跨站脚本攻击案例。我们从分析一个Flask应用程序的源代码开始,理解了其数据模型(用户、帖子)和业务逻辑(登录、发帖、发布)。通过代码审计,我们识别出一个存储型XSS漏洞,并发现了一个关键的业务接口 /publish。
我们构建的攻击链是:利用XSS在管理员浏览器中执行JavaScript,该脚本会以管理员的身份调用 /publish 接口,从而将其私密草稿(内含标志)公开。最后,我们探讨了使用JavaScript fetch() API实现该攻击的具体方法,并强调了携带凭证(credentials: 'include')的重要性,以及在整个过程中进行调试和验证的必要性。


这个挑战综合了代码审计、逻辑推理和漏洞利用开发的能力,是Web安全学习的经典范例。
7:拦截通信
在本节课中,我们将学习网络通信的基础层,包括以太网、IP协议和TCP协议。我们将了解数据包的结构,以及如何使用工具如Wireshark和tcpdump来捕获和分析网络流量。特别地,我们将关注ARP协议及其在本地网络中的安全含义。

网络协议层概述
上一节我们介绍了Web安全,它位于技术栈的较高层。本节中,我们将深入网络层,了解数据如何在网络中传输。
网络通信通常被组织成多个层次,就像一个洋葱。每一层都有其特定的职责:



- 以太网层:处理与最近邻居(如直接连接的设备)的通信。它使用MAC地址来标识设备。
- IP层:负责在网络间路由数据包,处理跨多个“跳”的通信。它使用IP地址。
- TCP层:在IP之上提供可靠的、面向连接的通信。它使用端口号来区分同一设备上的不同服务。
这些协议都是二进制协议,这意味着它们的数据由特定位置的字节组成,而不是像HTTP那样的纯文本。


网络接口与命名空间
要理解网络通信,首先需要了解网络接口。网络接口是软件对物理或虚拟网络端口的抽象。
以下是查看网络接口信息的命令:
ip a
在我们的实验环境中,运行挑战时会进入一个特殊的命名空间。这创建了一个隔离的网络环境。在这个环境中:
- 用户身份变为
root。 - 网络接口(如
lo和eth0)是全新的,与外部环境隔离。 - 所有在此命名空间内启动的进程共享同一个网络视图。
重要提示:如果你在桌面环境或不同终端中分别启动挑战,它们将处于不同的命名空间,无法直接通信。要在同一命名空间内获得多个终端,可以在挑战启动后的shell中使用 xfce4-terminal & 命令。

使用工具捕获和分析数据包

我们可以使用图形化工具Wireshark或命令行工具tcpdump来查看流经网络接口的原始数据包。
使用Wireshark
Wireshark允许我们以图形化方式深入查看数据包的每一层。
- 在挑战环境的shell中(拥有root权限),运行
wireshark。 - 选择要监听的接口(例如
lo或eth0)。 - 进行一些网络活动(例如,使用
netcat建立连接并发送消息)。 - 在Wireshark中,你可以点击任何数据包,并展开查看其以太网帧、IP包头和TCP段的详细信息。
- 要查看一个完整TCP会话的纯文本内容,可以右键点击相关数据包,选择 Follow -> TCP Stream。
使用tcpdump
tcpdump是一个强大的命令行数据包分析器。


以下是tcpdump的一些常用命令示例:
# 监听所有接口的流量
tcpdump -i any




# 显示IP地址而非主机名
tcpdump -n


# 以十六进制和ASCII格式显示数据包内容
tcpdump -XX
运行tcpdump后,它会在终端实时显示捕获的数据包,包括协议类型、源/目的IP和端口、标志位等信息。
理解数据包结构:一个Netcat示例


让我们通过一个简单的 netcat 通信示例,来观察数据包的各层。
- 在终端A中启动一个监听服务器:
nc -l 0.0.0.0 4242 - 在终端B中连接到该服务器:
nc 127.0.0.1 4242 - 在终端B中输入
hello并回车。

现在,在Wireshark或tcpdump中,你可以看到类似以下内容的数据包:
- 以太网层:包含源和目的MAC地址(在回环接口
lo上可能全为0)。末尾的Type字段(如0x0800)指明下一层是IPv4。 - IP层:包含源IP(如
127.0.0.1)和目的IP(如127.0.0.1)。Protocol字段(如0x06)指明下一层是TCP。 - TCP层:包含源端口(如
4242)和目的端口(由内核随机分配,如49182)。还有标志位,例如PSH(Push) 表示应立即将数据传递给应用,ACK表示确认,FIN表示连接终止。 - 数据部分:最后才是我们发送的实际内容,例如
hello\n。

这个结构清晰地展示了协议的封装关系:TCP段是IP包的数据,IP包是以太网帧的数据。

ARP协议:连接IP与MAC地址
在本地网络中,设备需要知道目标IP地址对应的MAC地址才能进行以太网通信。ARP(地址解析协议) 就是用来完成这个映射的。
ARP的工作方式很简单,但也带来了安全风险:
- 设备A想与IP地址为
10.0.0.3的设备通信,但不知道其MAC地址。 - 设备A向局域网内广播一个ARP请求:“谁有IP地址
10.0.0.3?请告诉10.0.0.2(设备A的IP)。” - 拥有该IP的设备(如
10.0.0.3)会向设备A发送一个ARP回复,告知自己的MAC地址。 - 设备A将这个IP-MAC映射存入本地ARP缓存,后续通信直接使用此MAC地址。
安全风险:任何设备都可以对ARP请求进行回复,声称自己拥有某个IP地址。如果恶意设备声称自己拥有网关或其他重要设备的IP,它就可以拦截发往该IP的所有流量,实现“中间人攻击”。这就是本模块“拦截通信”的核心议题之一。
你可以使用 ping 命令触发ARP请求,然后在Wireshark中过滤 arp 来观察ARP请求和回复数据包。
总结

本节课中我们一起学习了网络通信的基础层。我们了解了以太网、IP和TCP协议如何协同工作,封装数据包。我们实践了使用Wireshark和tcpdump工具来捕获和分析网络流量。最后,我们探讨了ARP协议的工作原理及其在本地网络中可能引发的安全风险,即通过欺骗ARP响应来拦截通信。理解这些底层机制是识别和防范网络层攻击的关键第一步。
8:拦截通信

在本节课中,我们将要学习网络通信的基础知识,包括IP地址、域名解析(DNS)、路由以及如何利用工具进行网络诊断。这些概念是理解后续网络安全挑战的关键。

课程状态与社区更新
上一节我们介绍了课程的整体进度,本节中我们来看看社区互动和学习工具的使用情况。
课程社区非常活跃,许多同学乐于助人。请记住,在Discord上公开讨论问题有助于所有人学习。严禁通过私信(DM)讨论课程作业相关内容,这违反学术诚信规定。所有帮助应在公开频道进行。
关于学习工具,我们观察到一些同学过度依赖AI助手(如Sensei/GPT)。请注意,AI工具应用作快速概念检查或替代谷歌搜索,不能替代你独立思考和解决问题。过度依赖会阻碍你真正理解计算机科学知识,长远来看不利于你的发展。
新模块:拦截通信介绍
在完成了充满挑战的Web安全模块后,我们现在进入“拦截通信”模块。虽然本模块的初始关卡可能看起来直接,但其中涉及的概念,如ARP、IP、TCP以及像Scapy这样的Python库,在概念上可能比JavaScript更复杂。
不要拖延。尽早完成检查点,避免在最后期限前遇到难以逾越的障碍。请利用“练习模式”来调试和修改挑战,这是深入理解问题的好方法。
网络基础实战
现在,让我们从实践角度深入了解网络。你每天都在使用网络,但让我们看看其下的运作机制。
IP地址与本地主机

在Dojo环境中启动挑战时,你会看到类似 你的IP地址是 10.x.x.x 的提示。IP地址是网络设备的标识符。
在Linux中,可以使用 ip addr 命令查看所有网络接口的IP地址。
ip addr
输出会显示如 eth0(以太网接口)和 lo(环回接口)的信息。环回接口的地址通常是 127.0.0.1,其主机名是 localhost。这意味着计算机可以通过这个地址与自己通信。

当你使用Netcat监听时:
nc -lvp 1337
它默认监听 0.0.0.0,这表示监听所有可用的网络接口。如果连接来自本机,日志会显示 connection received on localhost。
主机名解析:/etc/hosts 与 DNS

计算机如何知道 localhost 对应 127.0.0.1?它首先检查 /etc/hosts 文件。这个文件将主机名映射到IP地址。
cat /etc/hosts
你可以在这里添加条目,例如 127.0.0.1 mywebsite.local。
如果主机名不在 /etc/hosts 文件中,系统会使用域名系统(DNS)进行查询。工具 dig 可以用于DNS查询。
dig google.com
这个命令会向DNS服务器查询,并返回 google.com 对应的IP地址(例如 142.250.69.14)。

本地网络通信:ARP

在本地网络中(例如Dojo容器网络),设备通过ARP(地址解析协议)来发现彼此的硬件(MAC)地址。这允许它们在物理(或虚拟)链路上直接通信。
使用 arp 命令可以查看当前已知的邻居及其MAC地址。
arp -a
你会发现只能看到同一本地网络(如 10.0.0.0/24 或 172.18.0.0/16)中的设备。对于像Google这样的外部IP,它不会出现在ARP表中。
与外部网络通信:路由
那么,如何与不在同一本地网络的IP(如 142.250.69.14)通信呢?这时就需要路由。
使用 ip route 命令查看路由表。
ip route
路由表告诉系统:对于目标网络X,通过网关Y发送数据。默认网关(通常标记为 default via ...)负责处理所有发往未知网络的数据包。
数据包通过网络跳转到达目的地。可以使用 traceroute 工具可视化这个路径。
traceroute google.com
输出显示数据包从你的机器出发,经过本地网关、ISP的路由器,最终到达目标服务器的路径。每一跳都代表一个路由决策点。
网络诊断工具包
以下是几个实用的网络诊断命令:
ping:测试与目标主机的连通性。ping -c 4 google.comdig:查询DNS信息。arp:查看本地ARP缓存。traceroute:追踪数据包路径。nc (netcat):网络界的“瑞士军刀”,可用于建立TCP/UDP连接和监听。
当网站无法访问时,按顺序使用 ping 和 traceroute 可以帮助你定位问题是出在本地网络、中间链路还是目标服务器。
总结与下一步
本节课中我们一起学习了网络通信的核心基础。我们了解了IP地址和localhost的概念,知道了系统如何通过 /etc/hosts 文件和 DNS 将主机名解析为IP地址。我们探讨了本地网络如何使用 ARP 进行设备发现,以及数据包如何通过路由和网关穿越网络到达外部主机。最后,我们介绍了一套实用的网络诊断工具。
请记住,“拦截通信”模块的检查点将于周日晚上截止。这些基础概念是理解后续关卡(如网络嗅探、中间人攻击)的基石。建议你尽早开始,充分利用练习模式,并在公开社区讨论中解决问题。

祝你学习顺利,我们下次课见!
9:拦截通信实践

在本节课中,我们将学习网络通信的基础概念,并通过实际操作演示如何利用网络协议的特性来拦截通信。我们将重点关注ARP协议的工作原理及其潜在的安全风险。

课程概述
网络通信依赖于一系列分层协议,如以太网、IP和TCP。在这些协议中,ARP负责将IP地址解析为物理MAC地址。理解这些协议的工作方式是识别和防御网络攻击的关键。本节我们将通过搭建实验环境,演示ARP欺骗的基本原理。
网络基础回顾
上一节我们介绍了网络分层模型。本节中,我们来看看如何在实践中观察和分析网络流量。
我们首先使用Docker创建一个隔离的网络环境。Docker容器可以看作轻量级的虚拟网络命名空间,非常适合模拟网络实验。
以下是启动一个Python服务器容器的命令:
docker run -it --rm --name server python:latest bash
在容器内,我们安装必要的网络工具并启动一个简单的HTTP服务器:
apt update && apt install -y iproute2 net-tools
python -m http.server 8000
现在,我们从宿主机或其他容器访问这个服务器。通过curl命令,我们可以测试连通性并观察背后的网络过程。
观察网络流量
为了理解数据包如何传输,我们使用tcpdump工具来捕获和分析网络流量。
在另一个终端进入同一个Docker网络命名空间,并运行tcpdump:
docker exec -it server bash
apt install -y tcpdump
tcpdump -i any -n
当我们从宿主机执行curl 172.17.0.2:8000时,可以在tcpdump的输出中看到TCP三次握手的过程:
- SYN:客户端发送同步包。
- SYN-ACK:服务器回复同步-确认包。
- ACK:客户端发送确认包,连接建立。



ARP协议与安全风险


TCP/IP通信建立在IP地址之上,但在局域网内,实际的数据传输依赖于MAC地址。ARP协议就是用来查询“某个IP地址对应的MAC地址是什么”的。
以下是ARP请求的核心过程:
- 主机A想与IP地址为
172.17.0.2的主机B通信。 - 主机A广播一个ARP请求:“谁有
172.17.0.2?请告诉172.17.0.1。” - 主机B收到请求后,单播回复一个ARP应答:“
172.17.0.2的MAC地址是aa:bb:cc:dd:ee:ff。” - 主机A将这条映射关系存入本地ARP缓存,后续通信将使用这个MAC地址。
安全风险在于:ARP协议本身没有认证机制。任何连接到网络的主机都可以对ARP请求进行应答,甚至可以主动发送ARP应答(即“ gratuitous ARP”),声称自己拥有某个IP地址。
演示ARP欺骗
现在,我们演示一个攻击者如何通过ARP欺骗来拦截通信。
首先,我们启动一个名为“攻击者”的Docker容器,并赋予其特权以执行网络操作:
docker run -it --rm --privileged --name attacker python:latest bash
在攻击者容器中,我们安装scapy工具,它是一个强大的数据包操作库。
apt install -y scapy
攻击者的目标是“毒化”受害者(宿主机或其他容器)的ARP缓存,让受害者误以为攻击者的MAC地址对应着目标服务器(172.17.0.2)的IP地址。
以下是使用scapy发送欺骗性ARP应答的一种方法:
from scapy.all import *
# 构造一个ARP数据包,操作码2表示“应答”
# 声称IP地址 172.17.0.2 在攻击者的MAC地址上
arp_response = ARP(op=2, pdst="172.17.0.1", hwdst="受害者MAC", psrc="172.17.0.2", hwsrc="攻击者MAC")
send(arp_response)
执行此操作后,当受害者试图访问172.17.0.2时,网络流量实际上会被发送到攻击者的MAC地址。如果攻击者进一步开启IP转发,并监听或修改流量,就能实现中间人攻击。
防御措施简介
了解攻击原理后,我们可以探讨一些防御策略:
- 静态ARP条目:在关键设备上配置静态的IP-MAC地址映射,但维护成本高。
- 网络监控:使用IDS/IPS系统检测局域网内异常的ARP流量。
- 端口安全:在交换机上启用端口安全功能,限制每个端口学习的MAC地址数量。
- 加密通信:使用HTTPS、SSH、VPN等加密协议,即使流量被拦截,攻击者也无法轻易解密内容。

课程总结
本节课中我们一起学习了网络通信的基础,特别是ARP协议的工作原理。我们通过Docker搭建实验环境,演示了如何利用tcpdump观察网络流量,并深入探讨了ARP协议缺乏认证机制所导致的安全风险——ARP欺骗。通过scapy工具,我们模拟了攻击者如何发送虚假ARP应答来劫持网络流量。理解这些底层机制是构建安全网络、识别潜在威胁的第一步。在后续课程中,我们将学习如何利用密码学来保护通信内容,即使在不安全的信道中也能保障安全。
10:拦截通信实战教程

概述
在本节课中,我们将学习网络攻击中的一个核心概念:中间人攻击。我们将通过实际操作,演示如何拦截并篡改两台主机之间的网络通信。课程将涵盖从基础概念到使用自动化工具进行攻击的完整流程,并解释其背后的网络原理。
网络设置与目标
首先,我们需要搭建一个简单的实验环境。这个环境包含三个角色:
- 服务器:持续发送包含“flag”的数据。
- 客户端(受害者):不断向服务器请求数据。
- 攻击者:位于服务器和客户端之间,目标是拦截通信并将服务器返回的真实“flag”替换为伪造的信息。
以下是服务器和客户端的设置命令:
服务器端命令:
yes "PwnCollege{some_flag}" | nc -l -k 1337
这个命令使用 yes 持续输出字符串,并通过 netcat 在1337端口进行监听。-k 参数使服务器在接受一个连接后继续保持监听。
客户端命令:
while read flag; do echo $flag | nc -q0 172.17.0.3 1337; done
这个 Bash 循环持续从标准输入读取(这里实际上是一个占位符),然后通过 netcat 连接到服务器(IP: 172.17.0.3)的1337端口。-q0 参数使客户端在发送完数据后立即关闭连接。

手动攻击与自动化工具
上一节我们介绍了基本的网络环境。本节中我们来看看两种发起中间人攻击的方法:手动使用Scapy和利用自动化工具。
手动使用Scapy等工具构造ARP欺骗数据包,虽然能加深对协议的理解,但步骤繁琐。因此,安全研究人员和攻击者通常会使用自动化工具。
以下是两种主流工具的对比:
- Ettercap:一款历史悠久的综合网络攻击工具套件,功能全面但界面相对陈旧。
- BetterCap:一个更现代、功能强大的网络攻击与监控框架,支持模块化扩展。


本次演示将主要使用BetterCap,因为它提供了更友好的交互体验和强大的功能。


使用BetterCap实施攻击
BetterCap功能强大,但需要正确配置。以下是实施ARP欺骗并启动代理拦截流量的关键步骤。


首先,我们需要启动BetterCap并设置网络嗅探与主机发现:
- 启动BetterCap交互界面:
bettercap - 开启网络嗅探:
net.sniff on - 开启主机发现:
net.recon on - 查看发现的网络主机:
net.show

接下来,配置并启动ARP欺骗,这是将攻击者插入通信路径的关键:
- 设置ARP欺骗的目标(服务器和客户端IP):
set arp.spoof.targets 172.17.0.3,172.17.0.5 - 开启ARP欺骗功能:
arp.spoof on




此时,攻击者的机器会向网络不断发送伪造的ARP响应包,欺骗服务器和客户端,让它们误以为攻击者的MAC地址是对方的MAC地址,从而将所有流量发送到攻击者这里。

最后,为了拦截并修改流量,我们需要设置一个代理:
- 设置代理将流量重定向到本地的某个端口(例如8080):
set any.proxy.address 172.17.0.4; set any.proxy.port 8080; set any.proxy.srcport 1337 - 启动任意协议代理:
any.proxy on
启动代理后,发往服务器1337端口的流量会被重定向到攻击者本地的8080端口。攻击者可以在此端口运行一个自定义的程序(例如一个简单的Python HTTP服务器)来接收请求,将其转发给真实服务器,并在返回给客户端前将响应内容中的真实flag替换为伪造的flag。
故障排除与核心概念
在使用工具的过程中,可能会遇到各种问题。例如,ARP欺骗不生效、代理未正确拦截流量等。大部分网络问题,90%可能由DNS引起,但在此类中间人攻击场景中,更常见的原因是:
- 系统ARP缓存:目标机器的ARP缓存未被更新。可以尝试在目标机器上手动清除ARP缓存(例如在Linux上使用
arp -d *)。 - 防火墙或安全软件:可能阻止了伪造的ARP包或代理端口的通信。
- 工具配置错误:例如IP地址设置错误、代理端口冲突等。
理解以下核心概念对于调试至关重要:
- ARP协议:一个无状态的协议,主机可以接受未经请求的ARP回复(即“免费ARP”),并据此更新自己的ARP缓存表。这正是ARP欺骗得以实现的基础。
- TCP流与数据包转发:在成功进行ARP欺骗后,攻击者主机需要开启内核的IP转发功能(
sysctl net.ipv4.ip_forward=1),才能将截获的数据包正确地转发到目的地,否则网络连接会中断。


总结
本节课中我们一起学习了中间人攻击的完整流程。我们从搭建实验环境开始,了解了服务器与客户端的通信模式。然后,我们对比了手动攻击与使用自动化工具的优劣,并重点演示了如何使用BetterCap工具实施ARP欺骗和流量代理拦截。最后,我们探讨了实战中可能遇到的常见问题及其背后的网络原理(如ARP协议的工作方式)。通过本课,你不仅掌握了一种具体的攻击技术,更重要的是理解了网络通信的底层机制以及如何利用协议特性实现攻击,这是构建坚实网络安全知识基础的关键一步。
11:密码学基础

在本节课中,我们将要学习密码学的基础知识。密码学是网络安全的核心,它通过数学方法保护信息的机密性、完整性和真实性。我们将从简单的概念入手,逐步理解现代密码系统是如何工作的。
上一节我们介绍了网络通信拦截,了解到网络本身并不安全。本节中我们来看看如何利用密码学在公开的、不安全的信道上保护我们的通信。
概述:密码学的重要性
我们的社会严重依赖密码学算法的安全性。例如,SHA-256哈希算法直接负责比特币的安全。如果SHA-256被攻破,整个比特币系统将崩溃。同样,从HTTP到HTTPS的转变,依赖于TLS协议中的加密算法(如AES)来保护网络通信。与可以修复漏洞的应用程序不同,基础加密算法的漏洞可能带来灾难性、无法修补的后果。


密码学算法家族

现代密码学主要建立在几个核心算法家族之上,本模块将涵盖其中最重要的几个:
- AES(高级加密标准):一种对称加密算法,使用相同的密钥进行加密和解密。其核心是复杂的矩阵数学运算。
- Diffie-Hellman密钥交换:一种允许双方在不安全的信道上协商出一个共享密钥的算法,为后续的对称加密(如AES)提供密钥。
- RSA:一种非对称加密算法,使用公钥和私钥对。公钥可以公开,用于加密信息;只有持有私钥的一方才能解密。
- SHA(安全哈希算法):用于生成数据的唯一“指纹”(哈希值),保证数据完整性。
这些算法共同构成了TLS等安全协议的基础,保护着我们的日常网络活动。
数学基础:有限域与模运算

许多密码算法(如Diffie-Hellman和RSA)的数学基础是有限域和模运算。理解这些概念对学习本模块至关重要。
模运算可以理解为“时钟算术”。在一个模为 p 的系统中,数字范围是 0 到 p-1。任何超过 p-1 的计算结果都会“绕回”到这个范围内。
公式:a mod p 表示 a 除以 p 后的余数。
例如,在模 31 的世界里:
17 + 15 = 32,而32 mod 31 = 1,所以结果是1。1 - 15 = -14,而-14 mod 31 = 17(因为-14 + 31 = 17)。
加法和减法在模运算下依然成立。我们可以利用这个性质构建简单的加密系统,例如凯撒密码的变种:
代码:简单的模运算加密示例
def encrypt(letter, key, p=26):
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
index = alphabet.index(letter.upper())
new_index = (index + key) % p # 应用模运算实现循环
return alphabet[new_index]
def decrypt(letter, key, p=26):
# 解密是加密的逆过程
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
index = alphabet.index(letter.upper())
new_index = (index - key) % p
return alphabet[new_index]
# 示例
key = 13
cipher = encrypt('Z', key) # 输出 'M'
plain = decrypt(cipher, key) # 输出 'Z'
然而,这种简单替换密码可以通过分析字母频率轻易破解。现代密码学需要更强大的数学工具。
Diffie-Hellman 密钥交换原理
Diffie-Hellman 算法的巧妙之处在于,它允许双方(假设为Alice和Bob)在不安全的信道上,仅通过公开对话就能生成一个只有他们俩知道的共享秘密密钥,即使窃听者听到了所有公开信息也无法计算出该密钥。

其核心运算在模 p 的有限域中进行,其中 p 是一个很大的质数,g 是它的一个生成元(通常取2)。
算法步骤:
- Alice 选择一个私有数字
a,计算A = g^a mod p,并将A发送给 Bob。 - Bob 选择一个私有数字
b,计算B = g^b mod p,并将B发送给 Alice。 - Alice 收到
B后,计算共享密钥s = B^a mod p。 - Bob 收到
A后,计算共享密钥s = A^b mod p。
根据幂运算的性质 (g^a)^b = g^(a*b) = (g^b)^a,双方计算出的 s 是相同的:s = g^(a*b) mod p。
安全性:窃听者只能看到公开的 p, g, A, B。想从 A = g^a mod p 反推出私有密钥 a,需要解决“离散对数问题”。对于大质数 p,这在计算上是不可行的。因此,双方可以在公开场合协商出一个秘密密钥。
从Diffie-Hellman到RSA
Diffie-Hellman 在质数模 p 下运作良好。RSA 算法则更进一步,它使用一个合数 n 作为模数,n 是两个大质数 p 和 q 的乘积。
RSA 的安全性基于“大数分解难题”:将公开的 n 分解为 p 和 q 极其困难。
RSA 的密钥生成涉及欧拉函数 φ(n) = (p-1)*(q-1)。公钥是一个数对 (e, n),私钥是一个数对 (d, n),它们满足 e * d ≡ 1 (mod φ(n))。
加密:对于消息 m,计算密文 c = m^e mod n。
解密:对于密文 c,计算明文 m = c^d mod n。
因为 (m^e)^d = m^(e*d) = m^(1 + k*φ(n)) ≡ m (mod n)(根据欧拉定理),所以解密可以恢复原始消息。
关键在于,只有知道 p 和 q(从而知道 φ(n))的人,才能计算出私钥 d。这就实现了非对称加密:任何人都可以用公钥 (e, n) 加密,但只有持有私钥 (d, n) 的人才能解密。
模块挑战指南


本模块包含31个挑战,旨在帮助你实践这些概念。
以下是挑战的大致阶段:
- 数据表示:前几个挑战涉及十六进制、Base64等数据编码方式,这是与密码系统交互的基础。
- 一次性密码本与XOR:理解XOR操作和理想的一次性密码本模型及其局限性。
- 初代密码分析:开始实施一些基础的密码攻击。
- 核心算法实战:深入AES、Diffie-Hellman、RSA的实际应用和攻击场景。
建议:努力在第一个星期完成到第9个挑战左右(即第一个检查点)。这将为你理解后续更复杂的攻击(如填充预言攻击)打下良好基础,并使你有充足时间探索模块后半部分的内容。
总结与警告
本节课中我们一起学习了密码学的基础地位、核心算法家族(AES, Diffie-Hellman, RSA, SHA),以及支撑它们的数学原理——有限域和模运算。我们看到了Diffie-Hellman如何实现安全的密钥交换,以及RSA如何利用大数分解难题实现非对称加密。

最重要的启示是:密码学非常脆弱且容易误用。即使算法本身坚固,实现上的微小失误(例如泄露一个比特的信息)也可能导致整个系统被完全攻破。因此,在现实世界中,除非你是专家,否则应该使用经过严格审查的密码学库,而不是自己发明或实现加密算法。
本模块的挑战将让你亲身体验攻击这些系统的各种方法,从而深刻理解正确使用密码学的重要性。现在,是时候开始你的密码学探索之旅了。
12:密码学进阶
在本节课中,我们将继续深入学习密码学。我们将详细探讨RSA加密算法的原理与实现,并介绍高级加密标准(AES)及其分组密码工作模式。课程内容将涵盖从非对称加密到对称加密的关键概念。
回顾:Diffie-Hellman密钥交换
上一节我们介绍了Diffie-Hellman密钥交换协议。它允许通信双方在不安全的信道上安全地协商出一个共享密钥。其核心在于离散对数问题的计算困难性。
以下是Diffie-Hellman的基本步骤:
- 双方公开约定一个大素数 P 和一个原根 g。
- 通信方A选择一个私密数字 a,计算
A = g^a mod P并发送给B。 - 通信方B选择一个私密数字 b,计算
B = g^b mod P并发送给A。 - A计算共享密钥
s = B^a mod P。 - B计算共享密钥
s = A^b mod P。
由于离散对数问题,攻击者即使截获了 A 和 B,也难以计算出 a 或 b,从而无法得到共享密钥 s。
深入RSA加密算法
Diffie-Hellman主要用于密钥交换。如果我们希望直接加密和传输数据,并且无需双方事先交互,就需要非对称加密算法,例如RSA。
RSA的安全性基于大整数分解的困难性。接下来,我们详细拆解RSA的各个部分。
RSA的数学基础:欧拉函数


RSA运算涉及模指数运算。在一个模 n 的运算中,指数部分的运算实际上是在模 φ(n) 下进行的,其中 φ 是欧拉函数。
- 对于一个素数 p,其欧拉函数值为 φ(p) = p - 1。
- 对于两个不同素数 p 和 q 的乘积 n = p * q,其欧拉函数值为 φ(n) = (p-1)*(q-1)。







这个性质是RSA加解密能够成立的关键。
RSA密钥生成与加解密过程
以下是RSA算法的核心步骤:





1. 密钥生成
- 选择两个大素数 p 和 q。
- 计算 n = p * q 和 φ(n) = (p-1)*(q-1)。
- 选择一个整数 e,满足
1 < e < φ(n)且 e 与 φ(n) 互质(通常取65537)。 - 计算 d,使得
(d * e) mod φ(n) = 1,即 d 是 e 在模 φ(n) 下的乘法逆元。 - 公钥为 (n, e),私钥为 (d) 或 (p, q, d)。
2. 加密过程
若要对消息 M(需转换为小于 n 的整数)加密,使用公钥 (n, e) 计算密文 C:
C = M^e mod n
3. 解密过程
使用私钥 d 解密密文 C,恢复原始消息 M:
M = C^d mod n
数学原理保证了 (M^e)^d mod n = M^(e*d) mod n = M^(k*φ(n)+1) mod n = M。
RSA实践与注意事项
在实践中,直接使用RSA加密长消息效率很低。通常的做法是:
- 使用RSA加密一个随机生成的对称密钥(如AES密钥)。
- 使用该对称密钥加密实际的数据。
此外,必须使用标准的、安全的库来生成RSA密钥对,切勿自己实现或使用来源不可靠的素数。
对称加密与AES简介
与计算密集型的非对称加密(如RSA)相比,对称加密算法速度更快,适合加密大量数据。高级加密标准(AES)是目前最常用的对称加密算法。
AES是一种分组密码,它将固定长度的明文块(128位,即16字节)转换为相同长度的密文块。密钥长度可以是128位、192位或256位。
在Python中,我们可以使用 pycryptodome 库进行AES加密:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os





key = os.urandom(16) # 生成一个128位(16字节)的随机密钥
cipher = AES.new(key, AES.MODE_ECB) # 创建一个AES加密器对象(使用ECB模式,仅作演示)
plaintext = b“Hello, AES World!”
padded_data = pad(plaintext, AES.block_size) # 填充数据至块大小的整数倍
ciphertext = cipher.encrypt(padded_data) # 加密
# 解密
decrypted_padded_data = cipher.decrypt(ciphertext)
decrypted_data = unpad(decrypted_padded_data, AES.block_size)
print(decrypted_data) # 输出:b“Hello, AES World!”
分组密码的工作模式
由于AES一次只能处理一个固定大小的数据块,为了加密任意长度的消息,需要定义“工作模式”。电子密码本模式是最简单的一种。
ECB模式
ECB模式直接对每个明文块独立加密。其最大问题是,相同的明文块会产生相同的密文块,这会泄露数据的模式信息(例如,加密一张图片后,轮廓依然可见)。因此,ECB模式在实际中不应被使用。


为了解决ECB模式的问题,引入了更安全的工作模式,如密码分组链接模式。
CBC模式
在CBC模式中,每个明文块在加密前,会先与前一个密文块进行异或操作。第一个块则与一个随机生成的“初始化向量”进行异或。这样,即使明文相同,加密后的密文也会不同,从而隐藏了数据模式。


异或操作是许多加密模式中的基础操作。它的特性是:如果 A xor B = C,那么 C xor B = A。这种可逆性被巧妙地用于链接加密块。
总结

本节课我们一起深入学习了现代密码学的两个核心组成部分。
- 我们首先回顾了Diffie-Hellman密钥交换协议。
- 然后,我们详细剖析了RSA非对称加密算法的数学原理、密钥生成以及加解密过程,理解了其基于大数分解困难性的安全性。
- 接着,我们介绍了对称加密算法AES,了解了其作为分组密码的基本特性,并探讨了不同的工作模式,特别是ECB模式的缺陷和CBC模式的改进原理。
掌握这些基础知识,是理解如何构建安全通信系统、分析密码学挑战的关键。在接下来的实践中,你将应用这些概念来解决具体问题。
13:密码学进阶




在本节课中,我们将继续学习密码学模块,重点探讨哈希函数、暴力破解的概念,以及如何通过巧妙的方法将原本不可行的搜索空间变得可行。我们将通过具体的例子和代码来理解这些核心概念。




哈希函数:单向加密

上一节我们介绍了对称和非对称加密,它们都允许数据在密钥的作用下进行双向转换。本节中,我们来看看密码学中的另一个基础概念:哈希。


哈希函数是一种单向函数。它将任意长度的输入数据(如一段文本)转换成一个固定长度的输出,通常称为“哈希值”或“摘要”。其核心特性是不可逆性:给定哈希值,你无法(在计算上不可行)推导出原始的输入数据。这与加密有本质区别。
一个流行的哈希算法是 SHA-256。它总是产生一个 256 位(即 64 个十六进制字符)的输出。
import hashlib
hash_object = hashlib.sha256(b"Join me at my birthday party")
hex_digest = hash_object.hexdigest()
print(hex_digest) # 输出固定长度的哈希值

哈希函数还有另一个重要特性:雪崩效应。即使输入数据发生微小的改变,产生的哈希值也会变得截然不同,看起来毫无关联。这个特性对于保证数据的完整性至关重要。
工作量证明与暴力破解


哈希函数的不可预测性使其成为“工作量证明”机制的理想选择,例如在比特币等区块链技术中应用。其核心思想是要求计算机完成大量计算工作来证明其投入。
一个典型的工作量证明问题是:“找到一个输入,使其 SHA-256 哈希值以特定数量的零开头”。由于无法预测输出,唯一的解决方法就是暴力破解——即系统地尝试所有可能的输入,直到找到符合条件的那个。
以下是实现该思路的简化代码:
import hashlib
import itertools

def find_hash_with_leading_zeros(target_zeros):
prefix = "happy birthday "
for i in itertools.count():
guess = f"{prefix}{i}".encode()
hash_output = hashlib.sha256(guess).hexdigest()
if hash_output.startswith('0' * target_zeros):
print(f"Found! Input: {guess}, Hash: {hash_output}")
return guess, hash_output
return None





# 尝试寻找哈希值以3个零开头的输入
find_hash_with_leading_zeros(3)
这个过程本质上是枚举整个搜索空间。虽然可以通过并行计算加速,但如果搜索空间本身过于巨大,单纯增加计算核心也无济于事。
AES加密与搜索空间的难题

现在,让我们将暴力破解的思路应用到加密上。假设我们有一个使用 AES-ECB 模式加密的密文,我们不知道密钥,也不知道明文(比如“happy birthday”),但知道明文长度和可能的字符集(例如小写字母加空格)。
如果我们想通过暴力破解恢复明文,将面临两个巨大的搜索空间:
- 密钥空间:对于 16 字节的 AES 密钥,有
256^16种可能。 - 明文空间:对于 14 字符、27 种可能字符的明文,有
27^14种可能。
这两个数字都大到无法在合理时间内通过枚举完成。因此,在这种“黑盒”场景下,暴力破解 AES 加密是不可行的。这恰恰是加密算法应该具备的安全性。


预言机攻击:缩小搜索空间
然而,在某些实际场景中,攻击者可能获得一个“加密预言机”。例如,一个 Web 服务器可能使用固定密钥加密用户提供的数据(如前缀)拼接上秘密数据(如密码),然后将结果返回给用户。攻击者可以控制输入的前缀,并观察对应的加密输出。
这改变了攻击局面。攻击者不再需要猜测密钥,密钥被隐含在预言机中。现在,攻击目标缩小为仅破解明文。
但 27^14 的明文空间仍然太大。这时,AES-ECB 模式的分块特性(将数据分成独立的 16 字节块进行加密)和可控制的输入前缀,为我们提供了突破口。
利用ECB模式进行逐字节破解
以下是攻击的核心步骤,我们通过一个例子来说明:
-
确定块大小与对齐:首先,发送不同长度的输入,观察输出密文长度的变化,确定块大小为 16 字节。目标是让秘密数据的最后一个字节单独占据一个块的第一个字节位置。
- 假设秘密
SECRET为 14 字节。发送 13 个 ‘A’ 作为前缀,则第一个加密块为13*A + S[0],第二个块为S[1-14] + 填充。这没有帮助。 - 发送 14 个 ‘A’,则第一个块为
14*A,第二个块为S[0-13] + 填充。仍然不理想。 - 发送 15 个 ‘A’,则第一个块为
15*A + S[0],第二个块为S[1-13] + 填充。此时,秘密的第一个字节S[0]位于第一块的末尾。
- 假设秘密
-
破解最后一个字节:发送 15 个 ‘A’ 后,我们得到两个密文块。我们关注第二个密文块,它对应
S[1-13] + 填充。但我们暂时不直接攻击它。- 更有用的是,我们可以发送 14 个 ‘A’ + 15 个已知字节(例如 15 个
0xFF)。这样,第一个块是14*A + S[0] + 0xFF[0],第二个块是0xFF[1-14] + 填充。 - 我们不知道
S[0],但我们可以暴力枚举所有 256 种可能值(如果知道字符集,则枚举更少),替换上面结构中的S[0],并请求预言机加密。 - 当某个枚举值产生的密文的第二个块与我们之前得到的“第二个密文块”匹配时,我们就破解了
S[0],即秘密的第一个字节。
- 更有用的是,我们可以发送 14 个 ‘A’ + 15 个已知字节(例如 15 个
-
迭代破解:知道
S[0]后,我们可以调整前缀长度(例如发送 13 个 ‘A’),构造包含S[0](已知)+ S[1](未知)+ 14个已知字节的块,然后暴力枚举S[1]。如此反复,每次只需枚举 256 次(或更少),即可逐个破解秘密的所有字节。
通过这种“选择前缀攻击”,我们将搜索空间从 27^14(不可行)降低到了 27 * 14(完全可行)。这演示了如何通过密码学原语的误用和巧妙的攻击思路,将理论上的安全弱点转化为实际的攻击路径。
总结
本节课中我们一起学习了:
- 哈希函数的单向性和雪崩效应,及其在工作量证明中的应用。
- 面对 AES 等加密算法时,暴力破解原始密钥或明文搜索空间在计算上是不可行的。
- 当存在一个加密预言机时,攻击局面可能发生变化。结合 AES-ECB 模式的分块特性,通过精心构造输入并控制数据对齐,可以发起选择前缀攻击。
- 该攻击的核心是将指数级大的搜索空间,通过逐字节破解的方式,降维成线性可解的多个小搜索空间,从而使破解成为可能。

理解这些概念的关键在于始终思考搜索空间的大小,并寻找将大问题分解为可管理的小问题的方法。在解决相关挑战时,添加调试信息以可视化数据块是如何对齐和加密的,将非常有帮助。
14:密码学深入 - Cipher Block Chaining (CBC) 与 Padding Oracle 攻击


在本节课中,我们将深入学习密码块链接模式及其一个关键的安全漏洞。上一节我们介绍了AES-ECB模式及其局限性,本节中我们来看看更安全的CBC模式是如何工作的,以及攻击者如何利用其解密过程中的一个特性来发起攻击。


CBC模式的工作原理




CBC模式通过引入一个初始化向量和将前一个密文块与当前明文块进行异或操作,来解决ECB模式中相同明文块产生相同密文块的问题。


以下是CBC加密的核心公式描述:

- 加密过程:
Ciphertext[i] = Encrypt(Plaintext[i] XOR Ciphertext[i-1], Key)。对于第一个块,Ciphertext[i-1]由 初始化向量 替代。 - 解密过程:
Plaintext[i] = Decrypt(Ciphertext[i], Key) XOR Ciphertext[i-1]。同样,第一个块使用IV。
在代码中,使用PyCryptodome库创建CBC模式密码器的示例如下:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
key = os.urandom(16) # 生成随机密钥
iv = os.urandom(16) # 生成随机初始化向量



cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = b"Happy Birthday Students"
padded_plaintext = pad(plaintext, AES.block_size)
ciphertext = cipher.encrypt(padded_plaintext)
# 完整的密文通常将IV预置在真正的密文前
full_ciphertext = iv + ciphertext
CBC模式的比特翻转攻击

CBC模式的一个特性是,攻击者可以通过修改密文(或IV),可控地影响解密后的明文。这是因为在解密过程中,密文块在解密后会被与前一个密文块进行异或。
攻击原理如下:如果攻击者将密文块 C[i] 修改为 C[i] XOR X,那么解密后对应的明文块 P[i+1] 将变为 P[i+1] XOR X。而前一个明文块 P[i] 则会因为AES解密过程被破坏而变成乱码。

这意味着,攻击者可以在不知道密钥的情况下,通过精心构造的异或值 X,使特定位置的明文变成他们想要的值,代价是破坏前一个明文块。如果前一个块的内容不重要(例如图片数据、注释),这种攻击就可能生效。

Padding Oracle 攻击详解
Padding Oracle攻击是CBC模式一个更严重的漏洞。它利用了当解密后填充格式不正确时,服务器可能会返回一个错误信息(如“填充错误”)这一特性。
以下是攻击的核心步骤:
- 设定目标:攻击者截获了一段CBC加密的密文,并希望解密它,但不知道密钥。
- 利用Oracle:攻击者能够向一个使用相同密钥的解密服务(Oracle)发送修改后的密文,并观察其响应是“填充正确”还是“填充错误”。
- 逐字节解密:攻击者从最后一个密文块开始,通过暴力尝试修改倒数第二个密文块的最后一个字节,并发送给Oracle。当Oracle返回“填充正确”时,攻击者就能推导出最后一个明文字节的值。
- 迭代过程:在解密了最后一个字节后,攻击者可以调整目标,去解密倒数第二个字节,如此反复,直到解密整个块。这个过程可以向前迭代,解密所有密文块。
以下是一个简化的Padding Oracle攻击中,暴力破解单个字节的代码概念演示:
def decrypt_last_byte(ciphertext_block, previous_block, oracle):
# 假设我们正在解密 ciphertext_block 的最后一个字节
# 我们通过修改 previous_block 的对应字节来试探
for guess in range(256):
modified_prev_block = bytearray(previous_block)
# 在相应位置异或我们的猜测值
modified_prev_block[-1] ^= guess
test_ciphertext = bytes(modified_prev_block) + ciphertext_block
if oracle(test_ciphertext) == "PADDING_OK":
# 通过猜测值和填充值(例如0x01)计算出明文字节
plaintext_byte = guess ^ 0x01
return plaintext_byte
return None

本节课中我们一起学习了CBC加密模式的工作原理,认识了其相对于ECB模式的改进。然而,我们也深入探讨了CBC模式存在的安全风险:比特翻转攻击和致命的Padding Oracle攻击。理解这些漏洞强调了在实现密码学协议时,不仅需要选择强算法,还必须谨慎处理错误和边界情况。在后续的挑战中,你将有机会亲自实践这些概念。
15:密码学基础教程

📚 课程概述
在本节课中,我们将学习密码学的基本概念,并构建一个简化的TLS(传输层安全)握手协议。我们将结合对称加密、非对称加密、密钥交换和数字签名等核心概念,理解现代网络安全通信的基础。
🔐 对称加密与非对称加密
上一节我们介绍了课程的整体目标,本节中我们来看看密码学的两大基石:对称加密和非对称加密。

对称加密使用相同的密钥进行加密和解密。其核心属性是,没有密钥就无法读取密文。AES(高级加密标准)是一个典型的对称加密算法。

公式:C = E(K, P) 和 P = D(K, C),其中 C 是密文,P 是明文,K 是密钥,E 是加密函数,D 是解密函数。

非对称加密使用一对密钥:公钥和私钥。公钥可以公开,用于加密;私钥必须保密,用于解密。反之,用私钥加密(即签名),则可用公钥解密(即验证)。RSA是典型的非对称加密算法。





公式:C = P^e mod n(加密)和 P = C^d mod n(解密),其中 (e, n) 是公钥,(d, n) 是私钥。


我们的目标是建立一个加密的通信信道。虽然对称加密(如AES)速度快,适合加密大量数据,但它面临一个关键问题:如何安全地交换密钥?
🤝 密钥交换与中间人攻击
上一节我们了解了加密的基础,本节中我们来看看如何安全地交换密钥,以及可能面临的威胁。
Diffie-Hellman密钥交换协议允许双方在不安全的信道上共同建立一个共享密钥。
公式:双方公开交换 g^a mod p 和 g^b mod p,然后各自计算共享密钥 (g^b)^a mod p = (g^a)^b mod p = g^(ab) mod p。

然而,如果攻击者Mallory不仅能监听,还能主动篡改网络流量(主动攻击者),她可以进行中间人攻击。Mallory可以分别与通信双方Alice和Bob建立独立的Diffie-Hellman交换,从而获得两个密钥,并解密所有经过她的通信。
为了抵御主动攻击者,我们需要引入信任机制。
🏛️ 引入信任与RSA
上一节我们看到了主动攻击的威胁,本节中我们通过RSA和信任机制来解决这个问题。
我们假设客户端已经通过某种可信方式获得了服务器(例如Pwn College)的RSA公钥 (e, n)。客户端可以生成一个随机的AES密钥,并用服务器的公钥加密后发送过去。
代码示例:
# 客户端生成并加密会话密钥
session_key = get_random_bytes(16)
key_as_int = int.from_bytes(session_key, 'big')
encrypted_key = pow(key_as_int, e, n) # 使用服务器公钥加密
# 发送 encrypted_key 给服务器
只有拥有对应私钥 d 的服务器才能解密获得这个会话密钥。这样,即使Mallory截获了数据,她也无法解密。随后,双方可以使用这个会话密钥进行快速的AES加密通信。
但这个方案有一个缺陷:如果服务器的私钥在未来某天泄露了,那么Mallory就可以解密她所记录的所有历史通信。这被称为缺乏“前向保密性”。
🔄 实现前向保密性
上一节我们构建了一个基础系统,但存在长期风险。本节中我们通过结合Diffie-Hellman和RSA来实现前向保密性。
我们不直接发送用RSA加密的AES密钥,而是发送用RSA加密的Diffie-Hellman公开值。
流程简述:
- 客户端生成临时私钥
a,计算g^a mod p。 - 客户端用服务器的RSA公钥加密
g^a mod p,然后发送给服务器。 - 服务器用私钥解密,得到
g^a mod p。 - 服务器生成临时私钥
b,计算g^b mod p。 - 服务器用其RSA私钥对
g^b mod p进行签名(即计算(g^b)^d mod n),然后发送给客户端。 - 客户端用服务器公钥验证签名,得到
g^b mod p。 - 双方分别计算共享密钥
g^(ab) mod p,并将其作为会话密钥。
这个方案的精妙之处在于:
- 身份验证:只有真正的服务器(拥有私钥
d)才能正确签名g^b mod p。 - 前向保密性:临时私钥
a和b在会话结束后被丢弃。即使未来服务器的长期RSA私钥泄露,攻击者也无法从存储的密文中恢复出过去的会话密钥g^(ab) mod p。

📜 证书与信任链

上一节我们解决了通信过程中的安全问题,但遗留了一个根本问题:客户端最初是如何获得并信任服务器的公钥的?本节中我们通过数字证书和信任链来解决。
数字证书将实体的身份(如域名)与其公钥绑定在一起,并由一个可信的第三方(证书颁发机构,CA)进行数字签名。
证书简化结构:证书数据 = (域名, 服务器公钥, 有效期等信息)
签名:signature = (SHA256(证书数据))^d_ca mod n_ca,其中 (d_ca, n_ca) 是CA的私钥。

客户端收到证书后:
- 使用CA的公钥
(e_ca, n_ca)对签名进行验证:hash_recovered = signature^e_ca mod n_ca。 - 自己计算证书数据的SHA256哈希值。
- 比较两个哈希值。如果匹配,则证明该证书确实由可信的CA签发,因此可以信任其中的服务器公钥。
那么,我们如何信任CA呢?这形成了一个信任链。操作系统的根证书存储中预置了一些顶级根CA的公钥。这些根CA可以签发中间CA的证书,中间CA再签发服务器证书。客户端通过逐级验证签名,最终信任服务器的公钥。
在实际中,像Let‘s Encrypt这样的CA通过验证申请者对域名的控制权(例如,要求在特定URL下放置指定内容)来签发证书。
🎯 课程总结

本节课我们一起学习了构建一个安全通信系统所需的核心密码学概念。
我们首先回顾了对称加密和非对称加密的基本原理。然后,我们探讨了密钥交换的挑战以及中间人攻击的威胁。接着,我们通过结合RSA和Diffie-Hellman协议,构建了一个既能验证服务器身份又能实现前向保密性的密钥交换流程。最后,我们解释了数字证书和信任链如何解决公钥的初始信任问题,从而形成了完整的TLS握手协议基础。
理解这些构建模块如何协同工作,是理解现代互联网安全(如HTTPS)的关键。
165:访问控制与自动化交互教程

概述
在本节课中,我们将学习Linux访问控制的核心概念,包括文件权限、Linux能力机制以及如何使用Python的pwntools库自动化地与挑战程序进行交互。课程内容将涵盖从基础权限管理到高级进程交互的实用技巧。
访问控制基础与Linux能力机制
上一节概述了课程目标,本节中我们来看看Linux文件权限的基本操作。我们创建一个文件,并修改其权限。
# 创建一个文件并查看其默认权限
touch myfile.txt
ls -l myfile.txt
# 输出类似:-rw-r--r-- 1 user user 0 Oct 16 12:00 myfile.txt
# 移除文件的所有权限
chmod 000 myfile.txt
ls -l myfile.txt
# 输出:---------- 1 user user 0 Oct 16 12:00 myfile.txt



# 尝试读取文件(权限被拒绝)
cat myfile.txt
Linux中,root用户(用户ID为0)可以覆盖大多数文件权限检查。即使文件没有任何读取权限,root用户也能读取其内容。
# 切换到root用户后,可以读取无权限的文件
sudo cat myfile.txt
这种能力并非简单的“用户ID为0即放行”。现代Linux内核使用能力机制来实现细粒度的权限控制。每个进程拥有一组能力,这些能力决定了它可以执行哪些特权操作。例如,覆盖文件权限检查的能力是CAP_DAC_OVERRIDE。
我们可以查看进程的能力集。
# 获取当前shell进程的能力
cat /proc/$$/status | grep Cap
能力分为几个集合:
- Effective (E): 当前进程实际生效的能力。
- Permitted (P): 进程被允许使用的所有能力(上限)。
- Inheritable (I): 可以传递给其子进程的能力。
这种模型是一种基于角色的访问控制的实现。我们可以将特定的能力赋予给可执行文件本身,而不是让整个程序以root身份运行,这更安全。

# 将CAP_DAC_OVERRIDE能力赋予给`cat`命令(需要root权限)
sudo setcap cap_dac_override=ep /bin/cat

# 现在,即使用户身份不是root,使用这个cat也能读取无权限的文件
/bin/cat myfile.txt

# 查看已赋予能力的文件
getcap /bin/cat
系统中有一些程序已经通过能力机制获得了特权,例如ping命令,它拥有CAP_NET_RAW能力以发送原始网络数据包,而无需设置为setuid root程序。
Linux文件访问的有趣特性
了解了基础权限和能力后,本节中我们来看看Linux文件访问中两个重要的行为特性。


1. 权限检查的时机
Linux内核通常在打开资源时进行一次权限检查。一旦获得文件描述符,后续操作可能不再重复检查。
# 示例:在Python中打开一个文件后,即使文件权限被更改,仍可通过已打开的描述符读取
import os
import time
# 首先,创建一个有内容的可读文件
with open(“test.txt”, “w”) as f:
f.write(“Secret Flag\n”)
# 在另一个终端或进程中,运行以下命令移除读取权限
# chmod 000 test.txt
# 然而,如果在此Python脚本中先打开文件
f = open(“test.txt”, “r”)
# 此时再在外部执行 `chmod 000 test.txt`
print(f.read()) # 仍然可以成功读取内容
f.close()
2. 目录权限与文件权限
对目录的写权限允许你在该目录内创建、删除或重命名文件,即使你对目标文件本身没有写权限。/tmp目录通常设置了粘滞位,这可以防止用户删除或重命名其他用户的文件。
# 创建一个目录并设置粘滞位
mkdir shared_dir
chmod 1777 shared_dir
ls -ld shared_dir
# 输出中权限部分包含 ‘t’, 例如:drwxrwxrwt
# 在设置了粘滞位的目录中,用户只能删除或重命名自己拥有的文件。
使用Pwntools进行自动化交互

访问控制挑战通常需要与一个正在运行的程序自动交互。本节中我们将学习使用pwntools库来完成这个任务。pwntools是网络安全领域常用的Python库,它简化了进程创建、数据发送和接收的过程。
以下是使用pwntools的基本步骤和常见模式。
首先,导入库并启动一个进程。
from pwn import *

# 启动目标程序
p = process(‘./challenge_binary’)
与进程交互的核心是发送数据和接收数据。
# 接收数据,直到遇到指定的字符串(例如提示符)
output = p.recvuntil(b“Enter your name: “)
print(“Received:”, output)
# 发送一行数据(自动添加换行符)
p.sendline(b“Yan”)
# 接收一行数据
response = p.recvline()
print(“Response:”, response)
# 接收所有剩余输出,直到进程结束
final_output = p.recvall()
print(“Final:”, final_output)
在开发自动化脚本时,调试至关重要。pwntools提供了方便的调试模式。
# 在命令行运行脚本时启用调试输出,可以看到所有发送和接收的字节
python3 -m pdb your_script.py
# 或者,在脚本中通过环境变量启用(需在导入pwntools前设置)
import os
os.environ[‘PWNLIB_DEBUG’] = ‘1’
from pwn import *
以下是处理一个典型“密码验证”挑战的完整示例。
#!/usr/bin/env python3
from pwn import *
# 启动进程
p = process(‘./password_checker’)
# 1. 接收初始提示,直到出现“Password:”
p.recvuntil(b“Password: “)
# 2. 发送密码
p.sendline(b“MySecretPassword123”)
# 3. 接收并打印结果(例如包含flag的行)
result = p.recvall()
print(result.decode())
# 确保进程结束
p.close()
使用pwntools时,务必管理好进程生命周期,避免资源泄漏。使用with语句是推荐的做法。
with process(‘./challenge_binary’) as p:
p.sendline(b“input”)
print(p.recvall().decode())
虽然Python标准库的subprocess模块也能创建子进程,但其交互接口更为繁琐,缺乏pwntools为CTF挑战设计的便捷功能(如recvuntil、模式匹配、调试输出等)。对于网络安全任务,pwntools是更高效的选择。

总结
本节课中我们一起学习了Linux访问控制的深入知识,包括root用户如何通过能力机制超越普通权限,以及文件描述符和目录粘滞位对安全的影响。更重要的是,我们掌握了使用pwntools库自动化与命令行程序交互的核心技能,这是完成后续许多实践挑战的关键工具。请记住在开发脚本时积极使用调试功能,并妥善管理进程资源。
17:计算基础 101 🖥️
在本节课中,我们将回顾前两个模块(密码学与访问控制)的成绩情况,并正式开启“计算基础 101”模块的学习。我们将了解计算机程序在底层是如何工作的,特别是汇编语言的基础知识,为后续的软件逆向工程和漏洞利用打下坚实基础。
课程回顾与成绩分析
上一节我们完成了密码学和访问控制模块。现在,我们来快速回顾一下这两个模块的情况。

密码学模块的难度超出了预期,中位成绩为54分。不过,93%的学生都达到了检查点要求。考虑到该模块占总成绩的10%,实际中位成绩更接近7.5分。为了鼓励大家复习,本学期我们将密码学模块的迟交惩罚从扣50%调整为扣25%(截止日期为12月16日)。

访问控制模块的情况则截然不同,中位成绩高达94.74分,说明难度可能偏低了。我们将在下学期对这两个模块的难度进行调整。
以下是当前班级的成绩分布概况:
- 大约16-17%的学生成绩为D或E(不及格),这在三年级课程中是预期范围内的。
- 其余学生(A、B、C等级)分布良好。
- 目前所有成绩均未计入额外加分,我们正在努力实现加分自动化。

引入计算基础 101 🚀
上一节我们回顾了过往成绩,本节中我们来看看新的学习内容:“计算基础 101”。这个模块将涵盖汇编语言和计算机内部工作原理的基础知识。
理想情况下,这些知识应在《计算机组成原理》课程中学到。但由于种种原因,我们需要在本课程中提供一个速成班,以便大家能顺利学习后续的软件逆向工程、内存错误利用等高级主题。
“计算基础 101”模块包含6个子模块,共65个挑战。检查点设在完成21个挑战。我们有两周时间来完成它。我的建议是尽早开始推进,不要拖到最后一个周末,否则可能会像密码学模块那样遇到困难。
以下是该模块的六个部分:
- 你的第一个程序:引导你编写第一个汇编程序。
- 汇编速成课:练习各种汇编指令和逻辑实现。
- 调试复习:学习当汇编代码出错时如何调试。
- 构建一个Web服务器:一个完整的期末项目,用汇编语言实际构建一个能提供网页的服务器。
深入汇编语言:从高级语言到底层 🔍
上一节我们介绍了“计算基础 101”的概况,本节中我们来深入探讨汇编语言本身,理解程序在底层是如何运行的。
我们都熟悉 cat /flag 命令。cat 是一个程序,其核心功能是读取文件并输出到终端。我们可以用Python简单地实现它:
#!/usr/bin/env python3
import sys
print(open(sys.argv[1]).read())
但程序最终是如何与操作系统交互的呢?我们可以使用 strace 工具查看 cat 与操作系统的交互(系统调用),例如 open、read、write。
程序内部的逻辑是由一系列计算机指令构成的。我们可以用 objdump 工具将 cat 程序反汇编,查看其底层的汇编指令。这些指令直接映射到CPU执行的二进制代码。
为了更清晰地理解,我们可以用C语言写一个简化版的 cat(我们称之为 kitten),直接使用系统调用:
#include <unistd.h>
int main() {
int fd = open("/flag", 0); // 只读模式打开文件
char buf[1024];
int n = read(fd, buf, 1024);
write(1, buf, n); // 1 是标准输出的文件描述符
return 0;
}
编译 kitten.c 后,再反汇编它,我们可以看到比完整版 cat 少得多的汇编指令。这些指令(如 mov, call)直接对应着我们C代码中的操作。CPU就是逐条读取并执行这些由二进制字节表示的指令。
无论运行的是Python、JavaScript还是其他语言,最终你的CPU都是在疯狂地读取内存中的字节,将其解释为汇编指令并执行。
编写与运行汇编代码 💻
上一节我们了解了程序如何被分解为汇编指令,本节中我们来看看如何实际编写和运行汇编代码。
汇编指令操作的是CPU内部的寄存器。寄存器可以看作是CPU的“工作记忆”,数量有限(x86架构下通用寄存器约15-16个),用于临时存储和操作数据。


汇编指令(如 mov rax, 60)在文件中以二进制形式存储(例如字节序列 BF 01 00 00 00)。汇编器(如 as)负责将人类可读的汇编文本转换成机器可执行的二进制代码。
这里需要注意汇编语法。主要有两种:
- AT&T 语法:由GCC默认产生,操作数顺序为“源,目的”,寄存器前加
%。 - Intel 语法:本课程推荐使用,操作数顺序为“目的,源”,更清晰易读。
在“计算基础 101”的前几个挑战中,你只需直接在挑战界面输入汇编代码(如 mov rax, 60),系统会帮你完成汇编和执行。
对于后续需要自己生成二进制文件的挑战(如“汇编速成课”),步骤会稍微复杂一些。以下是基本流程:
- 用Intel语法编写汇编代码(
solution.s),开头使用.intel_syntax noprefix指令。 - 使用汇编器
as生成目标文件:as solution.s -o solution.o - 使用
objcopy提取纯二进制代码:objcopy -O binary --only-section=.text solution.o solution.bin - 将二进制文件传递给挑战程序:
cat solution.bin | ./challenge
调试与学习路径总结 🛠️
上一节我们学会了如何生成和运行汇编二进制文件,本节中我们来看看当代码出错时如何调试,并对本模块的学习路径进行总结。
在“汇编速成课”中,如果代码未按预期运行,可以使用 int3 指令进行调试。当程序执行到 int3 时,挑战环境会暂停并输出当前所有寄存器和部分内存的状态,帮助你定位问题。
“调试复习”模块则会教你使用更强大的调试器 GDB,这将成为你后续课程中不可或缺的工具。

最后,“构建一个Web服务器”项目将综合运用你在本模块学到的所有汇编知识,从零开始构建一个可运行的程序,深刻理解从汇编到可执行程序的完整链条。
那么,我们为什么需要学习汇编?
在网络安全领域,我们经常需要分析没有源代码的二进制程序(逆向工程),或者理解漏洞的底层原理并编写利用代码。虽然高级语言可以轻松编译成汇编,但从二进制完美还原回高级语言是极其困难的。因此,阅读和理解汇编代码的能力至关重要。本模块的目标就是让你尽快掌握这项能力。


本节课中我们一起学习了“计算基础 101”模块的引入背景、汇编语言的基本概念(寄存器、指令、语法)、编写与运行汇编代码的实践方法,以及模块内的学习路径。我们了解到,理解汇编是进行软件逆向工程和漏洞分析的基础。请务必观看所有相关视频讲座,并尽早开始完成模块中的挑战。
18:编写真实汇编程序
在本节课中,我们将深入学习汇编语言,从零开始编写一个能实际工作的程序。我们将把之前用C语言编写的“kitten”程序(一个简化版的cat命令)翻译成汇编语言,并学习在此过程中如何调试和解决问题。
上一节我们介绍了如何将C程序编译成汇编代码并进行分析。本节中,我们将亲自动手编写汇编程序。
课程管理与反馈
首先,有几个课程管理事项需要说明。
关于密码学和访问控制模块的问卷调查仍在进行中。从数据来看,密码学模块的难度明显超出了预期,学生花费的中位时间达到了25小时,这比我们希望的要高。未来我们计划调整课程内容,例如将“填充预言攻击”这类较复杂的主题移至后续课程。
另一方面,访问控制模块的内容需要适当加强,特别是Linux权限管理部分,很多内容是复习性质。感谢大家的反馈,尚未填写问卷的同学请记得完成,所有问卷总计可获得0.3%的额外学分。
此外,计算101模块新增了一个挑战关卡,专门练习如何构建可执行文件。这对于后续的汇编学习至关重要。请注意,这个新挑战不计入检查点评分,完成原有19个关卡的要求不变。
从C程序到汇编程序
现在,让我们进入今天的核心内容:编写真实的汇编程序。我们将从回顾上周用C语言编写的kitten.c程序开始。

以下是kitten.c程序的基本逻辑,它尝试每次读取100个字符并输出:

// kitten.c 基础版本
// 打开文件,读取100字节,写入标准输出
然而,这个基础版本只能读取文件的前100个字节。为了读取整个文件,我们需要一个循环。在C语言中,我们通常使用while循环,并根据read系统调用的返回值来判断是否已到达文件末尾。
read系统调用在成功时返回读取的字节数。当返回0时,表示已到达文件末尾(EOF)。因此,正确的循环逻辑是:
// kitten.c 完整版本
// 循环读取,直到 read 返回 0
while (1) {
num_read = read(fd, buffer, 100);
if (num_read == 0) {
break; // 到达文件末尾,退出循环
}
write(1, buffer, num_read); // 将读取的内容写入标准输出
}
使用strace工具跟踪系统调用,可以清晰地看到程序读取文件、遇到EOF(返回0)然后退出的过程。
从零开始编写汇编程序
接下来,我们的目标是将上述C程序逻辑用汇编语言实现。我们将这个汇编程序命名为poppy.s。
首先,我们从最简单的部分开始:让程序正确退出。这能验证我们的工具链和基本流程是否正常。




; poppy.s - 基础退出程序
global _start
section .text
_start:
mov rdi, 0 ; 退出状态码为 0
mov rax, 60 ; 系统调用号 60 代表 exit
syscall ; 调用内核
使用以下命令汇编和链接:
nasm -f elf64 poppy.s -o poppy.o
ld poppy.o -o poppy
运行./poppy,程序将安静地退出,返回状态码0。使用strace ./poppy可以验证它只调用了exit系统调用。
实现文件打开与读取
现在,我们为程序添加打开和读取文件的功能。这需要用到open和read系统调用。
我们需要查阅系统调用表。对于64位x86架构(x86-64),open的系统调用号是2,read的是0。
首先,我们硬编码要打开的文件名(例如/flag)。在汇编中,我们需要将文件名字符串的地址作为参数传递给open。
; poppy.s - 添加打开和读取功能
global _start
section .text
_start:
; 打开文件
lea rdi, [rel filename] ; 文件名地址 -> 第一个参数 (rdi)
mov rsi, 0 ; 标志位:O_RDONLY (0) -> 第二个参数 (rsi)
mov rdx, 0 ; 模式 (mode) -> 第三个参数 (rdx),open通常忽略
mov rax, 2 ; 系统调用号:open
syscall
; 此时 rax 中保存了 open 返回的文件描述符 (fd)
mov rbx, rax ; 将 fd 保存到 rbx 寄存器,避免被后续 syscall 覆盖
; 读取文件 (第一次,循环稍后添加)
mov rdi, rbx ; 文件描述符 fd -> 第一个参数 (rdi)
lea rsi, [rsp] ; 栈顶地址作为缓冲区 -> 第二个参数 (rsi)
mov rdx, 100 ; 读取 100 字节 -> 第三个参数 (rdx)
mov rax, 0 ; 系统调用号:read
syscall
; 写入标准输出
mov rdi, 1 ; 文件描述符:1 (标准输出)
lea rsi, [rsp] ; 缓冲区地址 (同样是栈顶)
mov rdx, rax ; 写入的字节数 = read 返回的字节数 (rax)
mov rax, 1 ; 系统调用号:write
syscall
; 退出
mov rdi, 0
mov rax, 60
syscall
section .data
filename: db "/flag", 0 ; 以空字符结尾的文件名字符串
关键点说明:
lea(Load Effective Address):用于获取标签(如filename)的地址。[rel filename]是一种与位置无关的寻址方式,在注入代码等场景中更可靠。- 保存文件描述符:
open返回的fd存储在rax中。因为后续的syscall(如read,write)也会使用并覆盖rax寄存器,所以我们必须立即将其保存到另一个通用寄存器(如rbx)中。 - 使用栈作为缓冲区:我们简单地将栈指针
rsp指向的地址用作读取缓冲区。这是一种简化的做法,在实际程序中需要更谨慎地管理栈空间。
此时,运行./poppy可以成功读取并输出/flag文件的前100个字节。
实现循环控制
为了让程序能读取整个文件,我们需要添加循环。在汇编中,没有高级的while或for关键字,只有跳转指令。
基本思路是:
- 在读取代码前设置一个标签(如
read_loop),作为循环开始点。 - 在读取操作后,检查
read的返回值(rax)。 - 如果
rax == 0,说明到达文件末尾,跳转到程序结束标签(如done)。 - 否则,执行写入操作,然后无条件跳转回
read_loop标签。
我们使用test rax, rax指令来设置标志位,然后使用条件跳转指令jz(Jump if Zero)在结果为0时跳转。
; poppy.s - 添加循环逻辑
global _start
section .text
_start:
; ... (打开文件的代码与之前相同) ...
mov rbx, rax ; 保存 fd 到 rbx
read_loop:
; 读取文件
mov rdi, rbx ; fd
lea rsi, [rsp] ; 缓冲区
mov rdx, 100 ; 读取大小
mov rax, 0 ; syscall: read
syscall
; 检查是否读到文件末尾 (rax == 0)
test rax, rax
jz done ; 如果 rax 为 0,跳转到 done
; 写入标准输出
mov rdi, 1 ; fd: stdout
lea rsi, [rsp] ; 缓冲区
mov rdx, rax ; 写入大小 = 读取的字节数
mov rax, 1 ; syscall: write
syscall
; 跳回循环开始
jmp read_loop
done:
; 退出
mov rdi, 0
mov rax, 60
syscall
section .data
filename: db "/flag", 0
现在,./poppy程序已经能够完整地读取并输出整个/flag文件的内容,并在完成后正常退出。
处理命令行参数
目前,文件名是硬编码在程序数据段(.data)中的。一个更实用的cat版本应该能从命令行参数中获取文件名。在C语言中,main函数的argv参数包含了这些信息。在汇编中,我们需要从进程启动时的栈上获取这些参数。
当Linux执行一个程序时,它会将参数信息压入新进程的栈中。布局大致如下(地址从高到低增长):
argc(参数个数,8字节)argv[0](程序自身路径的地址,8字节)argv[1](第一个命令行参数的地址,8字节)argv[2](第二个命令行参数的地址,8字节)- ...
- 环境变量指针数组
因此,在_start标签处,栈指针rsp指向的是argc的值。argv[1]的地址就存储在rsp + 16的位置(跳过argc的8字节和argv[0]的8字节)。
我们需要通过两次间接寻址来获取实际的参数字符串:
- 从
rsp + 16获取一个地址(这个地址指向参数字符串)。 - 再从这个地址读取内容,得到真正的文件名。
; poppy.s - 支持命令行参数
global _start
section .text
_start:
; 获取第一个命令行参数 (argv[1]) 的地址
mov rdi, [rsp + 16] ; rsp 指向 argc, +8 是 argv[0], +16 是 argv[1]
; 现在 rdi 中存储的是 argv[1] 字符串的地址
; 打开文件 (使用命令行参数)
; rdi 已经是文件名地址,无需改变
mov rsi, 0 ; O_RDONLY
mov rdx, 0
mov rax, 2 ; syscall: open
syscall
; ... (后续的保存fd、循环读取写入、退出代码与之前完全相同) ...
; 注意:我们不再需要 .data 段中的硬编码 filename
现在,我们可以像使用普通cat命令一样使用poppy:
./poppy /etc/passwd

调试技巧:strace 与 gdb
在编写汇编程序时,调试至关重要。我们主要使用两个工具:
-
strace:跟踪程序执行的所有系统调用及其参数、返回值。这是理解程序与操作系统交互的宏观视图。
strace -o trace.log ./poppy /etc/passwd -
gdb:GNU调试器。可以单步执行汇编指令,检查寄存器、内存内容。当程序行为异常(如无限循环、崩溃)时,
gdb是必不可少的。gdb ./poppy (gdb) starti /etc/passwd # 启动程序并停在第一条指令 (gdb) layout regs # 显示寄存器窗口 (gdb) ni # 执行下一条指令 (next instruction) (gdb) x/s $rdi # 以字符串形式检查 rdi 寄存器指向的内存 (gdb) info registers # 显示所有寄存器值
在今天的课程中,我们就是通过strace发现程序错误地使用了文件描述符,并通过gdb单步执行,观察到rax寄存器在write系统调用后被意外覆盖,从而定位了必须保存fd到其他寄存器的根本原因。
总结

本节课中我们一起学习了如何从零开始编写一个功能完整的汇编程序。我们首先用C语言明确了程序逻辑,然后将其逐步翻译成汇编语言,依次实现了退出、打开文件、读取文件、写入输出、循环控制以及处理命令行参数等功能。在整个过程中,我们重点掌握了系统调用的使用、寄存器的管理、循环与条件跳转的实现,以及如何使用strace和gdb工具进行调试。通过动手实践这个“简化版cat”程序,你对汇编语言如何与操作系统交互,以及如何构建底层软件有了更深入的理解。这些技能是后续学习软件漏洞与利用的基础。
19:计算基础与Web服务器构建 🖥️

在本节课中,我们将学习如何通过系统调用来构建程序,并重点探讨构建一个Web服务器所需的核心概念。我们将使用汇编语言和GDB调试器,通过实际操作来理解程序如何与操作系统内核交互。


系统调用:程序与内核的桥梁
上一节我们回顾了puppy程序的基本结构。本节中,我们来看看程序如何通过系统调用与操作系统内核进行通信。
系统调用是用户空间程序请求内核服务的接口。在汇编程序中,我们通过设置寄存器并执行syscall指令来发起调用。


- 选择系统调用:
RAX寄存器存放系统调用号。例如,open的系统调用号是2。 - 传递参数:前三个参数分别存放在
RDI、RSI、RDX寄存器中。 - 接收返回值:系统调用的结果通常返回到
RAX寄存器中。返回一个负数(如0xfffffffffffffffe)通常表示调用失败。
以下是一个open系统调用的汇编代码框架:
mov rax, 2 ; syscall number for 'open'
mov rdi, filename ; pointer to the filename string
mov rsi, 0 ; flags (0 for read-only)
mov rdx, 0 ; mode (ignored for opening existing files)
syscall ; invoke the kernel
; After syscall, check rax for the file descriptor or error



使用GDB进行动态调试 🔍
在编写和调试汇编程序时,GDB是一个不可或缺的工具。它允许我们实时检查程序状态、内存内容和寄存器值。
以下是使用GDB的一些基本命令:
starti:启动程序并停在第一条指令。display/4i $rip:持续显示即将执行的4条指令。stepi(si):执行一条指令。print $rax(p $rax):打印RAX寄存器的值。x/8bx $rdi:以十六进制格式显示RDI指向内存地址的8个字节。x/s $rdi:将RDI指向的内存解释为以空字符结尾的字符串并显示。
通过GDB,我们可以验证程序逻辑是否正确,例如检查传递给系统调用的参数是否正确设置。
内存与指针:数据的两种表示方式
程序中的数据通常存储在内存中,并通过指针来引用。处理内存数据时,有两种常见范式:
- 显式长度:同时传递数据的起始地址和大小(字节数)。例如,
read和write系统调用使用这种方式。 - 隐式终止:只传递起始地址,数据以一个特定的终止符(如空字节
\0或空指针NULL)结束。C语言字符串和命令行参数数组argv就采用这种方式。
例如,argv是一个指向指针数组的指针,它同时包含了元素数量(argc)和以空指针结尾的标记,提供了双重信息。
查找系统调用常量的值



在汇编中使用系统调用时,我们需要知道各种标志(如O_RDONLY)对应的具体数值。这些常量定义在系统的头文件中。





查找O_RDONLY值的方法如下:
grep -r "#define O_RDONLY" /usr/include/
输出通常会显示类似#define O_RDONLY 0的内容,表明O_RDONLY的值为0。对于其他常量(如O_RDWR、O_CREAT),也可用同样方法查找。




文件描述符:统一的操作句柄


许多系统调用成功后会返回一个文件描述符。它是一个小的非负整数,作为内核中某个对象的句柄。




以下系统调用都会返回文件描述符:
open:打开一个文件。socket:创建一个网络通信端点。accept:接受一个网络连接。


一旦获得文件描述符,就可以使用像read和write这样的通用系统调用来操作它,无论它背后代表的是普通文件、网络套接字还是其他资源。这种设计提供了简洁统一的抽象。
实现循环:处理多个命令行参数
我们的puppy程序目前只能处理一个文件参数。为了模拟cat命令的拼接功能,我们需要添加循环逻辑来处理argv中的所有文件名参数。
通过GDB检查内存布局,我们可以确定:
argc(参数计数)位于$rsp指向的内存地址。- 第一个真正的参数
argv[1]的指针位于$rsp + 16的位置。argv数组以空指针结束。
实现循环的基本思路如下:
- 从栈中加载
argc到寄存器(如R10)。 - 递减
R10以跳过程序名(argv[0])。 - 设置另一个寄存器(如
R11)指向argv[1]的地址($rsp + 16)。 - 循环开始:如果
R10为0,则跳转到退出。 - 在循环体内:使用
R11指向的指针作为文件名,执行打开、读取、写入操作。 - 处理完一个文件后,
R11增加8(指向下一个参数指针),R10减1,然后跳回循环开始。
在实现过程中,很可能会遇到错误(例如指针计算错误),这正是需要运用GDB进行调试的时候。
总结与下节预告
本节课中我们一起学习了:
- 系统调用的机制及其在汇编中的使用方式。
- 如何利用GDB动态调试程序,观察寄存器与内存。
- 内存数据的两种管理范式:显式长度与隐式终止。
- 如何查找系统调用中使用的常量值。
- 文件描述符作为统一资源句柄的概念。
- 通过分析栈布局,为程序添加处理多个命令行参数的循环逻辑。

我们尝试修改了puppy.s以支持多个文件,但在首次尝试中遇到了错误。调试是编程的核心技能。下节课(周三),我们将继续使用GDB来诊断和修复这个循环中的错误,并更深入地学习GDB脚本等调试技巧。请务必在构建Web服务器模块中积极应用这些调试技术。
20:调试与进程创建 🐛
在本节课中,我们将学习如何调试一个存在缺陷的汇编程序,并深入理解Linux系统中进程是如何通过fork和exec系统调用被创建和执行的。我们将从一个名为puppy(模拟cat命令)的程序开始,修复其多文件处理错误,并最终编写一个极简的shell。
概述与问题引入
上一节我们介绍了如何用汇编语言编写基础程序。本节中,我们来看看一个实际案例:一个存在缺陷的、用汇编编写的cat命令变体puppy。该程序在读取单个文件时工作正常,但在处理多个命令行参数(例如 ./puppy flag1 flag2)时,只会输出第一个文件的内容。
我们的任务是定位并修复这个bug,同时理解其背后的根本原因——系统调用对寄存器的破坏。修复完成后,我们将探讨进程的创建机制,并动手编写一个简单的shell。
调试汇编程序:定位寄存器破坏问题
首先,我们需要理解puppy程序的逻辑。它本应循环遍历所有命令行参数,依次打开每个文件并输出其内容。
以下是程序的核心逻辑结构(伪代码表示):
num_args = argc - 1; // 忽略程序名本身
offset = 16; // 栈上第一个参数(argv[1])的地址偏移量
while (num_args > 0) {
file_path = *(RSP + offset); // 获取文件路径地址
fd = open(file_path, O_RDONLY);
if (fd < 0) exit(1);
// ... 读取并输出文件内容 ...
num_args--;
offset += 8; // 移动到下一个参数地址
}
通过使用GDB进行调试,我们在open系统调用后设置断点,观察关键寄存器的值。我们发现,在第一次成功打开文件后,用于计算参数地址偏移量的寄存器R11的值被意外修改了。
这引出了我们的核心假设:open系统调用破坏了R11寄存器的值。

理解调用约定与寄存器易失性


为了验证假设,我们需要查阅x86-64 Linux下的系统调用约定。根据约定,在系统调用中:
- 参数传递:使用
RDI,RSI,RDX,R10,R8,R9寄存器。 - 返回值:存放在
RAX寄存器中。 - 寄存器保存责任:系统调用(被调用者)保证不会破坏
RBP,RBX,R12,R13,R14,R15寄存器的值。而其他寄存器,如R11,RCX等,则不被保证会被保存,调用者需要自行保护它们。

因此,我们的bug根源在于:程序使用了易失性寄存器R11来存储重要的偏移量,而open系统调用合法地覆盖了它。




修复Bug:使用被保留的寄存器
解决方案很简单:将用于存储关键数据的寄存器从易失性寄存器(如R10, R11)改为非易失性寄存器(如R12, R13)。这些寄存器由系统调用保证不会被破坏。



以下是修改的核心部分:
; 将参数计数和偏移量存入被保留的寄存器
mov r12, [rsp] ; r12 = argc
mov r13, 16 ; r13 = 参数地址偏移量
dec r12 ; r12 = 剩余文件数 (argc - 1)

loop_over_args:
cmp r12, 0
je exit
; 使用 r13 计算文件路径地址
mov rdi, rsp
add rdi, r13
; ... 打开文件 ...
; 循环末尾更新偏移量和计数器
add r13, 8
dec r12
jmp loop_over_args
进行此修改后,puppy程序成功实现了多文件读取功能。
进程的诞生:fork与exec系统调用
修复了puppy的bug后,我们转向一个更基础的问题:在Linux中,像puppy这样的新进程是如何产生的?
当我们输入./puppy并按下回车时,shell(例如bash)会执行以下步骤:
fork:Shell调用fork系统调用,创建一个几乎是自身精确副本的新进程(子进程)。两个进程的唯一区别是fork的返回值:在父进程中返回子进程的PID,在子进程中返回0。exec:在子进程中,Shell调用exec系统调用(如execve)。这个调用会用一个新的程序(puppy)的代码和数据完全替换当前进程(子shell)的地址空间,但保留其PID、文件描述符(如stdin/stdout)等属性。wait:父进程(原shell)通常会调用wait系统调用,暂停自己直到子进程(puppy)执行完毕,然后回收资源并继续显示提示符。
实践:编写一个极简Shell
理解了fork和exec后,我们可以尝试编写一个功能极其简单的shell。它只支持不带参数的命令,并且没有内置路径查找。
以下是其核心逻辑的汇编伪代码:
start:
; 1. 显示提示符 “$ ”
write(stdout, “$ “, 2);
; 2. 读取用户输入(例如 “cat”)
len = read(stdin, buffer, max_len);
buffer[len-1] = ‘\0’; // 去掉换行符
; 3. 创建子进程
pid = fork();
if (pid == 0) {
// 子进程:变成用户输入的程序
execve(buffer, NULL, NULL);
// 如果execve失败,子进程退出
exit(1);
} else {
// 父进程:等待子进程结束
waitpid(pid, NULL, 0);
// 循环回到 start
jmp start;
}
这个简单的shell演示了进程创建的基本模型。更复杂的shell(如bash)会在此基础上添加参数解析、管道、重定向、作业控制等大量功能。
总结
本节课中我们一起学习了两个关键主题:
- 汇编调试与调用约定:我们通过GDB调试,发现并修复了因忽视系统调用对易失性寄存器的破坏而导致的bug。关键在于理解并遵守ABI调用约定,将需要跨调用保存的数据存放在被调用者保留的寄存器(如
R12-R15)中。 - 进程创建机制:我们深入了解了Linux进程创建的“分裂-变身”模型:父进程通过
fork复制自身,子进程通过exec加载新程序。这是所有命令行程序启动、也是网络服务器(如Web服务器)处理并发连接的基础模型之一。我们通过编写一个极简shell实践了这一过程。

掌握这些底层机制,对于理解软件行为、分析安全漏洞(如利用进程内存布局)以及构建高效可靠的系统软件至关重要。
21:二进制逆向工程入门教程
在本节课中,我们将学习二进制逆向工程的基础知识。我们将从最简单的例子开始,逐步了解如何分析一个编译后的程序,理解其内部逻辑,甚至修改其行为。我们将使用多种工具和方法,包括命令行工具、调试器和专业的反汇编器。


概述:什么是逆向工程?
逆向工程是指在没有源代码的情况下,通过分析程序的二进制文件(即可执行文件)来理解其工作原理的过程。这就像侦探调查犯罪现场,通过留下的证据(机器指令)来推断原始事件(源代码逻辑)。在网络安全领域,逆向工程师常用于分析恶意软件、发现软件漏洞或绕过软件保护机制。
上一节我们介绍了逆向工程的基本概念,本节中我们来看看具体的分析方法和工具。
第一步:使用基础工具进行初步分析
在深入复杂的反汇编之前,我们可以先使用一些简单的工具来获取程序的初步信息,这有时能直接揭示关键数据。
使用 strings 命令查找字符串
strings 是一个命令行工具,它可以扫描二进制文件并提取出所有可打印的字符序列(即“字符串”)。这对于快速查找程序中的硬编码密码、错误信息或URL等非常有用。

以下是使用 strings 命令的示例:
strings ./my_program
运行后,你可能会在输出中看到类似 password: 或 CSE! 这样的字符串,这可能是程序用于比较的密码。
注意:如果程序不是直接比较字符串,而是逐字符进行比较,strings 命令可能就找不到明显的密码字符串了。这时,我们需要更深入的分析。




第二步:使用反汇编器查看汇编代码
当简单的字符串搜索无效时,我们需要查看程序的机器指令。反汇编器可以将二进制代码转换回人类可读的汇编语言。
使用 objdump 进行反汇编
objdump 是一个强大的命令行工具,可以显示目标文件的各种信息,包括反汇编代码。
以下是反汇编 main 函数的命令:
objdump -d ./my_program | grep -A 20 "<main>:"
或者直接反汇编整个程序:
objdump -d ./my_program
在输出中,你会看到类似下面的汇编代码片段,它对应着C语言中的字符比较逻辑:
cmp al, 0x43 ; 比较 AL 寄存器中的值是否等于 ‘C‘ (ASCII 0x43)
jne 失败地址 ; 如果不相等,则跳转到失败处理代码
通过阅读这些汇编指令,我们可以手动推导出程序期望的输入。例如,连续比较 0x43、0x53、0x45、0x21 就对应着字符串 "CSE!"。
然而,手动阅读大量汇编代码非常耗时。对于更高效的分析,我们需要图形化工具。

第三步:使用交互式反汇编器(IDA)进行高级分析
IDA Pro(Interactive Disassembler)是一款功能强大的商业逆向工程软件,其免费版本也提供了核心功能。它能以图形化方式展示程序的控制流,并尝试将汇编代码“反编译”成更易读的伪C代码。
IDA 的核心功能
- 图形化控制流图:IDA 可以将函数内的跳转逻辑可视化为流程图,让你一眼看清程序的分支结构。
- 反编译功能:按下
F5键,IDA 会尝试将当前函数的汇编代码反编译成高级语言(类似C语言)的伪代码。这极大提升了分析效率。 - 交互式修改:你可以重命名变量、修改数据类型、添加注释,帮助IDA更好地理解代码,也使你的分析笔记得以保存。
逆向工程中的“语义锚点”

在分析伪代码时,一个重要的技巧是寻找“语义锚点”。这些是程序中具有明确含义的字符串或行为,能帮助我们理解周边代码的用途。

例如:
- 如果程序输出
“Password: “,那么紧随其后的读取操作很可能就是在读取密码。 - 如果程序输出
“Access Denied“,那么导致这条输出之前的条件判断就是认证失败的关键检查点。
在IDA中,你可以根据这些锚点,右键点击变量或函数,选择“重命名”(N键),为其赋予一个有意义的名称(如 user_password),这使得后续分析更加直观。
第四步:动态分析与调试
静态分析(只看代码)有时会遇到瓶颈,特别是当程序逻辑非常复杂或带有反调试技巧时。动态分析则是在程序运行时观察其行为。
使用GDB进行动态调试
GNU调试器(GDB)不仅可以调试自己的程序,也可以用来分析未知的二进制文件。
以下是利用GDB分析密码检查程序的步骤:
- 在关键函数设断点:我们可以在
main函数或strcmp函数处设断点。gdb ./my_program (gdb) break main (gdb) run - 拦截系统调用:更精准的方法是拦截读取输入的系统调用。
(gdb) catch syscall read (gdb) run - 逐步执行与观察:当程序在
read调用处暂停时,你可以查看它准备读取多少数据、存储到哪个内存地址。继续执行后,可以单步跟踪(stepi)指令,观察程序如何比较你输入的字符与预期值。(gdb) info registers rdi # 查看文件描述符 (gdb) x/s $rsi # 查看读取数据的缓冲区地址 (gdb) stepi # 单步执行一条汇编指令
通过动态调试,你可以实时看到内存和寄存器的变化,精确理解程序在每一步所做的决策。
第五步:修改二进制程序(打补丁)
理解了程序逻辑后,我们不仅可以获取正确输入,有时还需要修改程序行为本身。例如,绕过软件的使用期限检查、禁用某些功能,或者像数字保存领域那样,让一个依赖已失效服务器的旧游戏能够运行。
使用IDA修改程序


在IDA中,你可以直接修改汇编指令。
- 找到你想要修改的指令(例如,一个决定跳转到“失败”代码的条件跳转指令
jne)。 - 右键点击该指令,选择“编辑” -> “修补程序” -> “汇编”。
- 将指令修改为其相反逻辑(例如,将
jne改为je),或者直接改为无条件跳转jmp到成功代码处。 - 应用补丁到输入文件。

重要提示:修改二进制文件就像外科手术,需要精确了解修改的影响。错误的修改可能导致程序崩溃。在打补丁前,最好备份原始文件。

现实世界中的应用场景
逆向工程技能在多个领域至关重要:
- 恶意软件分析:安全研究员逆向工程恶意软件以了解其感染方式、通信协议和破坏能力。
- 漏洞挖掘:通过分析软件二进制代码,寻找潜在的内存损坏漏洞(如下一模块将学习的缓冲区溢出)。
- 软件互操作性:让旧软件在新系统上运行。例如,当游戏公司的DRM(数字版权管理)服务器关闭后,爱好者通过修改游戏二进制文件来移除在线验证。
- 遗留系统维护:在企业中,有时会遇到源代码丢失的古老关键业务软件。当需要迁移到新硬件时,工程师可能需要通过逆向工程来理解其内部协议或修复兼容性问题。


总结

本节课中我们一起学习了二进制逆向工程的基础流程:
- 从使用
strings等简单工具进行初步侦察开始。 - 然后使用
objdump或IDA进行静态反汇编分析,阅读汇编代码或反编译出的伪代码。 - 在分析中,善于利用“语义锚点”来理解代码段的功能。
- 通过GDB进行动态调试,在运行时观察和验证程序逻辑。
- 最终,在充分理解的基础上,可以尝试使用IDA等工具对二进制程序进行修改,以改变其行为。

逆向工程是一种需要耐心和细致观察的技能。本模块的挑战将从简单的密码检查逐步过渡到分析复杂的“C图像”文件格式,希望你通过实践,能够掌握这种“通过表象洞察本质”的能力。请务必观看相关的课程视频,并尽早开始动手实践。祝你成功!
22:逆向工程模块核心概念与工具使用教程
概述
在本节课中,我们将学习逆向工程模块的核心概念,包括如何向程序输入非ASCII数据、如何使用高级逆向工程工具(如IDA、Ghidra)分析二进制程序、以及如何从程序中提取和操作数据。我们将通过具体的示例和演示,帮助你理解这些工具的基本工作流程。
输入非ASCII数据
在逆向工程中,程序经常需要处理非ASCII字符(如十六进制值0xFF、0x01等),这些字符无法通过键盘直接输入。本节将介绍几种向程序输入二进制数据的方法。
使用echo命令和管道
echo命令默认输出文本字符串。为了输出特定的字节值,需要使用-e选项来启用转义字符解释。
示例:
echo -e "hello\xff\x01\x02" | ./test
此命令会将字符串"hello"后跟字节0xFF、0x01、0x02输入到程序./test中。-e选项使得\xff等序列被解释为单个字节,而不是字面字符。


使用Python脚本生成二进制文件
对于更复杂或大量的二进制数据,编写Python脚本是更高效的方法。
示例:
with open('output.bin', 'wb') as f:
f.write(b'\x7fELF') # 写入ELF文件魔数
f.write(b'\x50\x18') # 写入其他数据
此脚本会创建一个包含特定字节序列的二进制文件output.bin。你可以通过cat output.bin | ./program或./program < output.bin将文件内容输入程序。
使用xxd验证文件内容
xxd工具可以以十六进制形式查看文件内容,用于验证生成的二进制文件是否正确。
示例:
xxd output.bin
这将显示output.bin文件的十六进制和ASCII表示。

使用逆向工程工具分析程序
上一节我们介绍了如何输入数据,本节中我们来看看如何分析一个接收这些数据的程序。当面对一个需要逆向的二进制程序时,使用图形化工具远比直接阅读汇编代码高效。
为何需要高级工具(IDA/Ghidra)
使用objdump或gdb直接阅读汇编代码对于理解高级程序逻辑来说效率低下且容易出错。图形化逆向工程工具(如IDA、Ghidra)提供了反编译功能,能将汇编代码转换为更易读的类C代码,极大地简化了分析过程。

IDA Pro 基本工作流程
以下是使用IDA分析一个简单程序(例如名为secret的程序)的基本步骤:


- 加载程序:使用默认设置打开二进制文件。
- 定位主函数:IDA通常会自动识别并定位到
main函数。 - 反编译:在汇编视图下,按下
Tab键即可切换到反编译视图(伪代码视图)。 - 分析逻辑:在反编译视图中,你可以清晰地看到程序的控制流、函数调用(如
memcmp)和关键数据比较。 - 查看数据:双击反编译代码中的变量(如
secret),可以跳转到该变量在内存(如.data段或.rodata段)中的定义位置。


从IDA中提取静态数据
如果程序将密钥等数据静态存储在二进制文件中,可以直接从IDA中导出。

操作步骤:
- 在反编译或汇编视图中,找到对关键数据(如
secret)的引用并双击。 - 在数据视图中,选中该数据区域。
- 右键点击,选择
Edit -> Export data。 - 选择输出格式为
Raw binary并指定文件名,即可将数据导出为二进制文件。


动态分析与调试(GDB)
当程序在运行时对输入数据或内部数据进行修改(例如解密、变换)时,静态分析可能不足以获得最终需要匹配的数据。这时就需要使用调试器进行动态分析。

使用GDB提取运行时数据
假设我们通过IDA分析,发现程序在地址0x4011c6处调用memcmp比较数据。

操作步骤:
- 启动调试:
gdb ./secret - 设置断点:
break *0x4011c6 - 运行程序并提供输入:
run < input.bin - 检查寄存器:在断点处停下后,
memcmp的参数通常保存在RDI(用户输入指针)和RSI(程序内部数据指针)寄存器中。 - 查看内存:
x/8xb $rsi # 以十六进制查看RSI指向的8个字节 - 导出内存数据:
这样,我们就得到了程序在运行时用于比较的实际数据。dump binary memory output.bin $rsi $rsi+33 # 将从RSI开始的33个字节导出到文件

其他逆向工具简介
由于IDA免费版依赖云服务反编译,在大量使用时可能受限。以下是其他优秀的替代工具:
Ghidra
- 来源:美国国家安全局(NSA)开源。
- 特点:功能强大且完全免费,反编译能力优秀。界面基于Java,可能需要适应。
- 建议:如果IDA遇到问题,Ghidra是非常可靠的备选方案。



Binary Ninja
- 特点:商业软件,提供免费的有限功能版。界面现代,反编译效果通常很好。
- 优势:对学生相对更实惠。

angr-management
- 来源:学术研究项目(例如ASU的SES实验室)。
- 特点:不仅是一个反编译器,更是一个强大的二进制分析框架,可用于符号执行等高级分析。
工具选择建议:初学者可以主要使用Ghidra,因为它免费且功能完整。也可以尝试IDA免费版,如果其云服务稳定的话。根据任务需求和个人偏好选择即可。



处理复杂数据结构(结构体)


在更复杂的程序中,数据通常以结构体(struct)的形式组织。反编译器可能无法自动识别这种布局,将其显示为一系列独立的变量。


在IDA中定义结构体
为了提高反编译代码的可读性,我们可以手动定义结构体。
- 打开
View -> Open subviews -> Local types。 - 添加一个新的结构体类型(例如
my_struct),并根据逆向分析的结果定义其字段(如char field_0[4]; int field_1;)。 - 回到反编译视图,对相应的变量按
Y键,将其类型更改为你定义的my_struct。 - 反编译代码会随之更新,使用类似
my_var->field_0的语法,使程序逻辑更加清晰。

这能帮助你更好地理解数据布局和程序逻辑。由于时间关系,关于结构体分析的更详细操作将在后续材料中补充。


总结
本节课我们一起学习了逆向工程中的几个关键技能:
- 输入二进制数据:掌握了使用
echo -e和Python脚本向程序输入非ASCII字节的方法。 - 静态分析:学会了使用IDA/Ghidra加载二进制文件、进行反编译、以及从二进制中导出静态数据。
- 动态分析:了解了如何使用GDB在关键点(如
memcmp)中断程序,并导出运行时的内存数据。 - 工具链:认识了IDA、Ghidra、Binary Ninja等多种逆向工程工具及其特点。
- 数据结构:初步了解了如何在逆向工具中处理和理解复杂的数据结构(如结构体)。

掌握这些基础技能,将为你顺利完成逆向工程模块的挑战奠定坚实的基础。请务必动手实践,并善用课程提供的Dojo环境中的工具。
23:逆向工程实战演示

在本节课中,我们将通过一个实战演示来深入理解逆向工程的核心概念。这个演示程序的设计灵感来源于当前模块的作业,旨在帮助大家理清分析此类任务的方法。我们将使用IDA等工具,从高层概览到细节分析,逐步拆解一个未知的二进制程序,目标是理解其逻辑并最终获取标志(flag)。


课程状态更新
在开始演示之前,我们先了解一下课程近况。
上一模块“密码学”的难度超出了预期,中位成绩为55%。为了平衡成绩,我们采取了以下措施:
- 将密码学模块中所有迟交挑战的分数权重从50%提升至75%。
- 为整个密码学模块增加20%的曲线分,这相当于为每位同学的总成绩额外增加2%的学分。

当前我们正在进行第八个模块“逆向工程”。这是一个新模块,部分同学在完成检查点后的几个挑战上遇到了困难。请务必尽早开始,原因如下:
- 这些挑战本身具有一定难度。
- 在截止日期当天,IDA的云端反编译服务可能会因访问量过大而失效。建议考虑使用Ghidra、Binary Ninja或angr等其他反编译工具。

演示程序概览


为了更真实地模拟逆向工程过程(即分析没有源代码的程序),我们使用AI生成了一个程序的源代码,编译后直接进行分析。这意味着,即使是讲师,也不完全清楚程序内部的精确逻辑。
我们有一个名为 challenge_demo 的Set-UID程序。我们的最终目标是让程序输出 /flag 文件的内容。
第一步:高层概览与语义锚点
我们首先将程序载入IDA,并切换到反编译视图(按Tab键),以获得一个C语言风格的高层概览。
逆向工程初期,应优先寻找“语义锚点”,例如字符串和函数名,它们能为我们提供程序意图的线索。
以下是我们的发现:
- 关键字符串:
"failed to read number of rectangles","failed to read rectangle %d","/flag","frame buffer matches"。 - 关键函数名:
render_rectangle,print_frame_buffer,frame_buffer_matches。 - 目标路径:在代码中看到了
fopen("/flag", "r"),这是我们成功时需要触发的路径。

通过分析 frame_buffer_matches 函数,我们发现它最终调用 strcmp 比较两个字符串。为了让程序执行到读取flag的代码,这个比较必须返回0(即字符串相等)。

第二步:分析输入结构与主逻辑
接下来,我们回到 main 函数,分析程序如何接收和处理我们的输入。
以下是程序输入结构的关键发现:
-
读取矩形数量:程序首先使用
fread(&num_rectangles, 4, 1, stdin)读取一个4字节的小端序整数。这决定了后续循环的次数。如果读取失败,会输出错误信息"failed to read number of rectangles"。- 公式表示:
num_rectangles = fread(stdin, 4 bytes, little-endian)
- 公式表示:
-
循环读取矩形数据:程序进入一个
for循环,循环次数为num_rectangles。- 在每次循环中,它使用
fread(buffer, 5, 1, stdin)读取5字节的数据。如果读取失败,会输出"failed to read rectangle %d"。 - 然后,将这5字节的缓冲区作为参数传递给
render_rectangle函数。 - 公式表示:
for i in range(num_rectangles): rect_data[i] = fread(stdin, 5 bytes)
- 在每次循环中,它使用
-
验证与总结:我们通过实际运行程序验证了上述输入结构。例如,输入
\x01\x00\x00\x00(表示1个矩形)后跟ABCDE(5字节),程序不再报错,说明我们正确越过了初始的输入检查。
第三步:理解核心数据结构与比较

在理解了输入如何被接收后,我们需要弄清楚这些输入如何影响最终决定胜负的字符串比较。
-
帧缓冲区(Frame Buffer):我们发现一个名为
frame_buffer的全局数组,它位于BSS段(初始化为0),大小为100字节。render_rectangle函数会操作这个缓冲区。 -
打印帧缓冲区:
print_frame_buffer函数负责输出frame_buffer的内容。它将这100字节视为5行,每行20字节,并在每行末尾添加一个换行符输出,总共输出105个字符(5 * 21)。

- 帧缓冲区转字符串:
frame_buffer_to_string函数将frame_buffer转换为一个字符串。它同样处理5行数据,将每行20字节复制到目标缓冲区,并添加换行符,最后在字符串末尾添加一个空字节(\0)。因此,最终生成的字符串长度为5 * 20 + 5 + 1 = 106字节。


-
字符串比较的真相:在
frame_buffer_matches函数中,IDA的反编译起初显得混乱,似乎传递了多个参数。但通过查看汇编代码,我们确定了核心逻辑:- 该函数接收一个参数,即一个指向预期字符串的指针(我们称之为
hello_world_buffer)。 - 它调用
frame_buffer_to_string,将当前的frame_buffer转换成106字节的字符串。 - 最后,使用
strcmp比较转换后的字符串与传入的hello_world_buffer。 - 核心逻辑公式:
success_condition = (strcmp(frame_buffer_to_string(frame_buffer), hello_world_buffer) == 0)
- 该函数接收一个参数,即一个指向预期字符串的指针(我们称之为
-
目标字符串:我们在程序的只读数据段(.data段)找到了
hello_world_buffer的内容。它是一个106字节的特定模式字符串,包含了“hello world”文本和星号等字符。我们的目标就是通过输入,操纵render_rectangle函数,使得最终的frame_buffer在转换成字符串后,与这个目标字符串完全一致。
第四步:分析 render_rectangle 函数(关键)
这是我们尚未深入分析的部分,也是解决问题的关键。render_rectangle 函数接收我们输入的5字节矩形数据,并根据这些数据修改 frame_buffer。
根据反编译代码的初步观察,这5字节数据很可能代表了一个“矩形”的属性,例如:
- 位置(X, Y坐标)
- 尺寸(宽度、高度)
- 填充字符
函数内部包含循环和条件判断,逻辑相对复杂。要完成挑战,我们需要:
- 逆向分析出这5字节数据的具体格式和含义。
- 计算出需要多少个矩形(以及每个矩形的具体数据),才能将
frame_buffer“绘制”成与hello_world_buffer匹配的图案。
总结与后续
本节课中,我们一起完成了一次逆向工程实战演示的初步分析。

我们首先通过寻找字符串和函数名建立了高层理解,然后逐步分析了程序的输入结构、核心数据流(帧缓冲区)以及决定程序成功与否的关键字符串比较逻辑。我们明确了最终目标:通过精心构造的输入序列,控制 render_rectangle 函数,使 frame_buffer 的内容匹配预设的目标图案。

目前,我们已经掌握了程序的整体框架和输入格式,但最关键的一步——理解 render_rectangle 如何根据5字节数据修改缓冲区——尚未完成。这将是下一阶段分析的重点。掌握这种由目标倒推输入、并逐步验证假设的分析方法,是解决逆向工程挑战的核心技能。请记住,尽早开始作业,并善用多种工具进行探索和测试。
24:逆向工程实战(续) 🧩
在本节课中,我们将继续深入分析一个演示程序,通过逆向工程来理解其内部逻辑,并最终生成特定的输出以获得标志(flag)。我们将学习如何使用IDA Pro等工具分析函数、重构数据结构,并编写脚本来控制程序行为。
逆向工程分析流程回顾
上一节我们分析了程序的主要函数,并确定了我们的目标输出是一个“HELLO WORLD”图案。本节中,我们将深入分析负责渲染矩形的关键函数,并理解其数据结构。
修正函数参数与返回类型
在分析 frame_buffer_matches 函数时,我们发现IDA错误地识别了多个参数。实际上,该函数只接受一个参数。通过将返回类型从 BOOL8 改为 int,我们成功删除了多余的参数,使反编译代码更清晰。
关键操作:
- 在函数名上按
Y键,修改函数原型。 - 将返回类型改为
int。 - 删除错误的参数定义。
- 按
F5刷新反编译视图。

定义矩形数据结构
我们观察到程序使用一个5字节的结构来表示矩形。为了提升代码可读性,我们在IDA中定义了一个对应的结构体。
操作步骤:
- 打开 Local Types 窗口。
- 右键点击,选择 Add Type。
- 使用C语法定义结构体,包含五个独立的
uint8_t成员。
struct rectangle {
uint8_t a;
uint8_t b;
uint8_t c;
uint8_t d;
uint8_t e;
};
- 在
render_rectangle函数中,将相应参数的类型修改为struct rectangle *。 - 在
main函数中,将局部变量的类型修改为struct rectangle。


通过定义结构体,反编译代码中出现了 rectangle->d 这样的访问方式,这比直接使用偏移量(如 *(v3 + 3))更易于理解。
分析 render_rectangle 函数逻辑
这是核心函数,它根据矩形数据向帧缓冲区(frame buffer)写入字符。我们的目标是理解其参数含义,从而控制输出。
通过分析反编译代码中的循环和条件判断,我们推断出矩形五个字节的含义:
- 字节 0 (a): 矩形的起始 X 坐标。
- 字节 1 (b): 矩形的起始 Y 坐标。
- 字节 2 (c): 矩形的宽度。
- 字节 3 (d): 矩形的高度。
- 字节 4 (e): 要写入的像素(字符)。
函数逻辑是:在由 (X, Y) 指定的起始位置,绘制一个宽为 c、高为 d 的矩形区域,并将该区域内的每个像素都设置为字符 e。
帧缓冲区是一个 5行 x 20列 的数组。因此,坐标 (X, Y) 必须满足 X < 20 且 Y < 5。
验证理解并控制输出
为了验证我们的理解,我们通过修改输入数据来测试。
测试用例: 绘制一个位于 (0,0),大小为 1x1,字符为 ‘A’ 的矩形。
对应的字节序列为:0, 0, 1, 1, ‘A‘。
运行程序后,成功在输出画面的左上角看到了字符 ‘A’,这证实了我们的分析是正确的。
生成目标图案并获取标志
现在我们已经完全理解了程序如何工作。目标是让程序输出特定的“HELLO WORLD”图案以通过校验,获得标志。
设计生成脚本
我们不需要合并相邻的相同字符来优化矩形数量。最简单的方法是:为图案中每一个需要绘制的点(包括空格)都单独创建一个 1x1 的矩形。

以下是生成最终输入数据的Python脚本思路:
- 定义目标图案: 将“HELLO WORLD”图案定义为一个字符串列表,每行一个字符串。
- 计算矩形数量: 图案中所有字符的总数(包括空格)。
- 构建数据头: 前4个字节(小端序)存储矩形数量。
- 遍历图案: 对于图案中的每一个字符(非换行符),根据其坐标 (x, y) 和字符本身,生成一个 5字节的矩形数据
[x, y, 1, 1, ord(character)]。 - 输出文件: 将所有数据写入一个文件,作为程序的输入。
goal = [
“* * *** *** * * *** “,
“* * * * * * * * “,
“***** *** *** * * * *** “,
“* * * * ** ** * * “,
“* * *** *** * * * * “
]
num_rectangles = sum(len(row) for row in goal)
data = bytearray()
data += (num_rectangles).to_bytes(4, ‘little‘) # 小端序头
for y, row in enumerate(goal):
for x, pixel in enumerate(row):
# 每个点都是一个 1x1 的矩形: [x, y, width, height, pixel]
data += bytes([x, y, 1, 1, ord(pixel)])
with open(‘solution.bin‘, ‘wb‘) as f:
f.write(data)
运行此脚本生成 solution.bin 文件,将其作为输入提供给挑战程序,程序成功输出了目标图案并返回了标志(flag)。

总结与技巧
本节课中我们一起学习了逆向工程一个具体程序的完整流程:
- 目标定位: 通过字符串引用找到关键比较函数,确定程序成功条件。
- 代码清理: 修正反编译工具产生的错误类型定义,提升代码可读性。
- 数据结构重建: 根据数据访问模式定义结构体,使逻辑更清晰。
- 逻辑分析: 深入分析核心函数,理解每个参数和变量的实际含义。
- 验证与利用: 编写脚本或手动构造输入,验证分析结果并达成目标。

核心逆向技巧:
- 重命名(N) 和 注释(:) 是你的好朋友。
- 善用 交叉引用(X) 来追踪函数或数据的调用关系。
- 类型定义(Y)和结构体定义能极大提升复杂数据流代码的可读性。
- 始终保持“假设-验证”的循环:提出一个关于代码行为的猜想,然后设计输入去测试它。
逆向工程就像解谜,需要耐心、细致的观察和逻辑推理。通过本案例,希望你掌握了从混乱的反编译代码中梳理出清晰逻辑,并最终控制程序行为的基本方法。现在,你可以将这些技巧应用到更多挑战中去。祝你成功!
25:二进制漏洞利用入门教程
在本节课中,我们将学习二进制漏洞利用的核心概念,包括内存错误和Shellcode注入。我们将通过一个简单的缓冲区溢出示例,理解如何利用程序漏洞控制其执行流程。

概述

二进制漏洞利用是网络安全中的一个关键领域,它涉及发现并利用程序中的内存错误,从而控制程序的执行。本节我们将重点学习缓冲区溢出漏洞的原理和利用方法。


缓冲区溢出漏洞原理
上一节我们介绍了二进制漏洞利用的基本概念,本节中我们来看看最常见的漏洞类型之一:缓冲区溢出。
在C语言等低级编程语言中,程序通常不会检查写入缓冲区的数据是否超出其分配的空间。当程序向一个固定大小的缓冲区写入超过其容量的数据时,多余的数据就会“溢出”到相邻的内存区域。


代码示例:一个存在漏洞的函数
void vulnerable_function() {
char buffer[59]; // 分配59字节的缓冲区
int win = 0; // 一个关键变量,初始为0
read(0, buffer, 4096); // 从标准输入读取4096字节到buffer中
if (win != 0) {
give_flag(); // 如果win不为0,则给出flag
}
}
在上述代码中,read函数试图将4096字节的数据读入一个仅有59字节的buffer。这会导致超出buffer范围的数据被写入相邻的内存位置,可能覆盖win变量或其他关键数据。



利用缓冲区溢出
理解了漏洞原理后,我们来看看如何利用它。我们的目标是覆盖win变量,使其值不为零,从而触发give_flag()函数。
以下是利用此漏洞的关键步骤:
- 确定偏移量:首先需要计算出从
buffer起始位置到win变量之间的字节距离。这可以通过静态分析(如使用IDA)或动态调试(如使用GDB)来完成。 - 构造Payload:Payload是指我们输入到程序中的恶意数据。它需要包含足够多的填充字符(如‘A’)来填满
buffer和偏移量之间的空间,然后在win变量所在位置写入一个非零值。 - 发送Payload:将构造好的Payload发送给正在运行的程序。


公式:Payload结构
Payload = [填充字符 * 偏移量] + [非零值(如0x42代表‘B’)]



实践工具与技巧


在实际利用过程中,手动计算偏移量可能容易出错。我们可以借助一些工具来简化这个过程。
- 使用
pwntools的cyclic模式:pwntools是一个强大的CTF框架和漏洞利用开发库。它的cyclic功能可以生成一个具有特殊模式的字符串。当程序崩溃时,通过检查覆盖了返回地址或关键变量的值,并查询该值在cyclic模式中的位置,就能快速确定准确的偏移量。 - 结合静态与动态分析:使用IDA Pro进行反汇编和静态分析,了解程序结构和变量布局。同时使用GDB进行动态调试,在关键点(如
read函数调用后)检查内存状态,验证我们的Payload是否按预期工作。
总结

本节课中我们一起学习了二进制漏洞利用的基础——缓冲区溢出。我们了解了其产生的原因:程序向固定大小的缓冲区写入超量数据。我们学习了利用该漏洞的基本步骤:确定偏移量、构造Payload并发送。最后,我们介绍了一些实用工具和技巧,如使用pwntools的cyclic模式和结合IDA与GDB进行分析,这些都能帮助我们更高效、更准确地完成漏洞利用。在接下来的挑战中,你将有机会亲自实践这些概念。
26:二进制漏洞利用基础与工具使用

在本节课中,我们将学习二进制漏洞利用的核心概念,特别是内存损坏和缓冲区溢出。我们将通过一个简单的C程序示例,演示缓冲区溢出的原理,并介绍如何使用调试器(GDB)和辅助工具(如pwntools、peda、GEF)来分析和利用这些漏洞。
概述
内存损坏是二进制漏洞利用中的一个核心主题。当程序向内存缓冲区写入超出其分配空间的数据时,就会发生缓冲区溢出,这可能导致程序崩溃或被攻击者控制执行流程。本节课将通过一个简单的C程序,逐步演示缓冲区溢出的发生过程,并介绍如何利用调试器进行分析和利用。

内存损坏与缓冲区溢出

上一节我们介绍了内存损坏的基本概念,本节中我们来看看一个具体的缓冲区溢出示例。
首先,我们创建一个简单的C程序 test.c:
#include <stdio.h>
#include <string.h>
void foo() {
char buffer[16];
strcpy(buffer, "Hello, World!");
}
int main() {
foo();
return 0;
}
这个程序定义了一个大小为16字节的缓冲区,并使用 strcpy 函数将字符串 "Hello, World!" 复制到缓冲区中。由于字符串长度(14字节)小于缓冲区大小,因此不会发生溢出。
接下来,我们编译并运行这个程序,使用GDB观察内存状态:
gcc -o test test.c
gdb ./test

在GDB中,我们可以设置断点并查看缓冲区的内存内容:

(gdb) break foo
(gdb) run
(gdb) disassemble foo
(gdb) x/16bx $rbp-0x10
通过观察,我们可以看到缓冲区的内容以及栈上的其他数据,如保存的基指针(RBP)和返回地址(RIP)。
缓冲区溢出的影响
现在,我们修改程序,使其发生缓冲区溢出:
#include <stdio.h>
#include <string.h>
void foo() {
char buffer[16];
strcpy(buffer, "Hello, World!AAAAAAAABBBBBBBBCCCCCCCC");
}
int main() {
foo();
return 0;
}



这次,我们向缓冲区写入超过16字节的数据,导致溢出。编译并运行程序后,使用GDB观察溢出对栈的影响:
(gdb) break foo
(gdb) run
(gdb) x/32bx $rbp-0x10
我们可以看到,超出缓冲区的数据覆盖了栈上的其他数据,包括保存的RBP和RIP。当函数返回时,程序会尝试跳转到被覆盖的RIP地址,这通常会导致段错误(Segmentation Fault)。
利用缓冲区溢出控制程序流程
上一节我们看到了缓冲区溢出如何导致程序崩溃,本节中我们来看看如何利用这种溢出控制程序流程。
假设我们有一个名为 win 的函数,我们希望程序执行这个函数而不是崩溃:
#include <stdio.h>
#include <string.h>
void win() {
printf("U1\n");
}
void foo() {
char buffer[16];
read(0, buffer, 4096); // 从标准输入读取数据,存在缓冲区溢出风险
}
int main() {
foo();
return 0;
}
我们的目标是通过缓冲区溢出,将返回地址覆盖为 win 函数的地址。首先,我们需要确定 win 函数的地址和缓冲区到返回地址的偏移量。
使用GDB计算偏移量:
(gdb) break foo
(gdb) run
(gdb) print $rsp+8 # 返回地址的位置
(gdb) print $rsi # 缓冲区的起始位置(read函数的第二个参数)
假设偏移量为24字节(0x18),我们可以构造如下payload:
from pwn import *
# 获取win函数的地址
elf = ELF('./test')
win_addr = elf.symbols['win']
# 构造payload
payload = b'A' * 24 + p64(win_addr)
# 发送payload
p = process('./test')
p.send(payload)
print(p.recvall())
这样,当 foo 函数返回时,程序会跳转到 win 函数,打印 "U1"。
使用辅助工具简化分析
手动使用GDB进行分析可能比较繁琐,以下是一些辅助工具,可以简化调试过程:
- PEDA (Python Exploit Development Assistance):一个GDB插件,提供颜色高亮、寄存器查看、栈查看等功能。
- GEF (GDB Enhanced Features):另一个GDB增强工具,功能类似PEDA,但界面略有不同。
- pwntools:一个Python库,用于编写漏洞利用脚本,支持进程交互、内存操作、汇编/反汇编等功能。
以下是使用PEDA的示例:
gdb -q ./test
source /path/to/peda.py
break foo
run
PEDA会自动显示寄存器、栈、代码等信息,使调试更加直观。
总结

本节课中我们一起学习了二进制漏洞利用的基础知识,包括内存损坏、缓冲区溢出的原理及其利用方法。我们通过一个简单的C程序演示了缓冲区溢出的发生过程,并介绍了如何使用GDB和辅助工具(如pwntools、PEDA、GEF)进行分析和利用。掌握这些工具和技巧对于理解和利用二进制漏洞至关重要。
27:Shellcode编写与调试 🐚


在本节课中,我们将学习如何编写和调试Shellcode。Shellcode是一小段机器码,通常用于利用软件漏洞,将其注入到目标进程的内存空间并执行。我们将从编写一个简单的“Hello World” Shellcode开始,学习如何提取、注入和调试它。
概述:Shellcode基础

上一节我们介绍了如何通过缓冲区溢出来劫持程序的返回地址。本节中,我们将更进一步,学习如何将我们自己的代码(Shellcode)注入到进程中并执行。Shellcode的本质是一段可以直接被CPU执行的机器指令序列。



从汇编程序到Shellcode
首先,我们从一个简单的汇编程序开始。以下是一个在x86-64架构上打印“Hi”并退出的汇编程序:
global _start
section .text
_start:
; write(1, message, 3)
mov rax, 1 ; 系统调用号 1 代表 write
mov rdi, 1 ; 文件描述符 1 代表标准输出
mov rsi, rsp ; 缓冲区地址(我们将数据放在栈上)
mov byte [rsi], 'H' ; 将字符 ‘H’ 放入缓冲区
mov byte [rsi+1], 'i' ; 将字符 ‘i’ 放入缓冲区
mov byte [rsi+2], 0x0a ; 换行符 ‘\n’
mov rdx, 3 ; 要写入的字节数
syscall ; 执行系统调用
; exit(0)
mov rax, 60 ; 系统调用号 60 代表 exit
xor rdi, rdi ; 退出码 0
syscall
我们使用汇编器(如nasm)和链接器(如ld)将其转换为可执行文件:
nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
运行./hello会输出“Hi”。然而,这个可执行文件(ELF格式)包含大量元数据(如头部、节区信息),而不仅仅是我们的代码。为了将其作为Shellcode注入,我们需要提取出纯指令字节。
提取纯指令字节
ELF文件中的代码通常位于.text节区。我们需要从这个节区提取出原始的机器码字节。
以下是两种提取方法:
方法一:使用 objcopy 工具
objcopy 可以复制或转换目标文件的部分内容。
objcopy -O binary --only-section=.text hello hello_shellcode
这条命令将hello文件的.text节区内容以原始二进制格式提取到hello_shellcode文件中。


方法二:使用 dd 工具
如果我们知道代码在文件中的偏移量(例如0x1000)和大小(例如0x2a字节),可以使用dd。
dd if=hello of=hello_shellcode bs=1 skip=$((0x1000)) count=$((0x2a))
现在,hello_shellcode文件包含了纯粹的Shellcode字节。我们可以用十六进制查看器验证:
xxd hello_shellcode


注入并执行Shellcode
在漏洞利用场景中,目标程序会有一个缓冲区。我们的目标是:
- 将Shellcode字节放入缓冲区。
- 覆盖函数的返回地址,使其指向我们Shellcode在内存中的起始地址(通常是缓冲区的地址)。
由于本课程实验环境禁用了地址空间布局随机化,栈地址在每次运行中是固定的,这使得计算返回地址变得相对简单。
调试Shellcode
Shellcode执行出错时,调试可能比较棘手,因为代码是在运行时动态注入的,而不是原始二进制的一部分。
技巧:在Shellcode中插入断点指令
x86架构的int3指令(机器码为0xCC)会触发一个断点中断。如果程序在调试器(如GDB)中运行,执行到int3时会暂停,让我们可以检查寄存器、内存状态。
我们可以在汇编代码中需要调试的位置插入int3:
; ... 一些指令 ...
int3 ; 调试断点
; ... 后续指令 ...
重新汇编、提取Shellcode并注入。当在GDB中运行目标程序并触发Shellcode时,执行到int3就会中断,此时我们可以使用GDB命令(如info registers, x/10i $rip)进行调试。



附加进程进行调试
为了获得更精确的内存地址(避免GDB自身环境变量对栈地址的影响),一个更好的方法是:
- 在“练习模式”下正常运行目标程序。
- 使用
ps命令或类似方法找到该进程的PID。 - 使用GDB附加到该进程:
gdb -p <PID>。 - 然后继续执行,当Shellcode中的
int3断点触发时,即可进行调试。
这种方法能让我们在接近真实的环境下观察Shellcode的行为。
应对字符串长度检查




在一些挑战(如“string length”)中,程序可能会使用strlen等函数检查输入长度,然后才用memcpy复制到栈缓冲区。strlen在遇到空字节(\x00)时停止计数。
这意味着,如果我们的Shellcode或地址中包含空字节,strlen返回的长度会很短,能通过检查。但随后的memcpy会复制整个输入(包括空字节之后的内容),从而仍然可以实现溢出。
策略:构造Shellcode和地址时,尽量避免使用空字节,或者确保空字节位于strlen检查之后才需要被复制的部分。
总结
本节课我们一起学习了Shellcode的完整流程:
- 编写:用汇编语言编写功能代码(如系统调用)。
- 提取:从生成的可执行文件中提取出纯指令字节。
- 注入:将字节码作为输入送入存在缓冲区溢出的程序,并覆盖返回地址指向它。
- 调试:利用
int3指令和GDB附加调试来排查Shellcode问题。

理解并掌握Shellcode是二进制漏洞利用的核心技能之一。它允许我们将任意代码注入到目标进程中,从而完全控制其行为。请务必在实验环境中动手实践这些步骤。
28:综合安全
在本节课中,我们将学习如何应对融合了多个安全概念的综合性挑战。我们将以“综合安全”模块的第一个挑战为例,演示如何将复杂问题分解为熟悉的单一概念,并逐步构建解决方案。

概述
综合安全模块的挑战融合了之前学过的多个独立概念,例如密码学、二进制漏洞利用、逆向工程和Web安全。解决这些挑战的关键在于,将复杂问题分解回我们熟悉的单一概念,并逐一攻克。
挑战结构分析
我们首先遇到的挑战结合了密码学和二进制漏洞利用。程序分为两部分:
- 分发器:一个加密服务,只接受并加密短于16字节的消息。
- 漏洞程序:一个解密服务,存在缓冲区溢出漏洞,但要求输入的消息必须能成功解密,且头部信息需验证通过。




我们的目标是利用漏洞程序中的溢出漏洞,触发一个能打印标志的win函数。然而,要与之通信,我们必须先通过其解密和验证检查,而生成有效加密消息的唯一途径是使用有长度限制的分发器。


问题分解与逆向推理
我们可以像证明几何题一样,从目标(获取标志)开始逆向推导必要的条件。
- 最终目标:获取标志(
flag)。 - 倒数第二步:触发
win函数。分析二进制文件发现,没有直接调用win的路径,但存在栈溢出漏洞,且没有栈保护(Canary)和地址随机化(PIE),因此可以通过控制流劫持跳转到win函数。 - 倒数第三步:触发栈缓冲区溢出。我们需要找到溢出的点。
- 分析溢出点:通过阅读源代码,我们发现解密函数
EVP_DecryptUpdate可能存在问题。它解密的数据长度来自用户输入,且没有严格的上限检查,而目标缓冲区message在栈上的大小是固定的。因此,如果提供过长的密文,就会导致栈溢出。 - 新的需求:我们需要构造一个长度超过42字节(头部16字节 + 消息缓冲区)的有效加密消息。有效意味着其解密后的前16字节必须是
verified和一个小于16的长度值。
至此,我们遇到了核心矛盾:分发器拒绝加密长消息,但漏洞利用需要长消息。这引入了密码学层面的挑战。
利用实践模式进行概念隔离


在真正解决密码学难题前,我们可以使用“实践模式”来暂时隔离问题,专注于验证和利用二进制漏洞本身。
以下是具体步骤:



- 修改分发器:在实践模式下,我们可以修改分发器程序,移除其长度检查,并使其始终使用固定的已知密钥。这样,我们就能自由生成任意长度的“有效”加密消息用于测试。
# 示例:生成一个长密文用于测试溢出 python3 -c "from pwn import *; print(cyclic(128))" | ./dispatcher_patched

- 验证溢出:将生成的密文喂给漏洞程序。为了绕过其头部验证检查,我们可以使用调试器(GDB)在运行时修改关键寄存器的值。
- 在
memcmp比较后,将结果寄存器(如RAX)设为0,使验证通过。 - 在长度检查处,修改条件,使程序继续执行。
break *0x401c1d commands set $rax = 0 continue end run < your_encrypted_payload - 在



-
计算偏移量:当程序因我们的长输入而崩溃时,查看崩溃时的返回地址(RIP)。使用
cyclic工具可以计算出从输入缓冲区开始到覆盖返回地址的确切偏移量。# 假设崩溃时RIP的值为0x6161616161616168 cyclic -l 0x6161616161616168 -
构造利用载荷:在获得偏移量后,我们就可以构造标准的漏洞利用载荷了:偏移量长度的填充数据 +
win函数的地址。from pwn import * offset = 104 # 假设计算出的偏移量 win_addr = 0x401216 # win函数的地址 payload = b'A' * offset + p64(win_addr)


- 测试利用:用修改后的分发器加密这个载荷,然后发送给漏洞程序。如果一切顺利,应该能成功触发
win函数,在实践模式下获得假标志。

通过以上步骤,我们在“作弊”的环境下完成了一次从构造输入到触发漏洞的完整链条验证。这证明了二进制漏洞利用部分是可行的。

回归综合挑战
在实践模式中验证了漏洞利用路径后,剩下的核心挑战就是:如何在不修改分发器的情况下,构造出一个能通过漏洞程序所有检查的长加密消息?
这需要运用密码学知识来“欺骗”系统。例如,可能需要研究AES-ECB加密模式的特征,利用其块独立性,通过精心构造的输入,使加密后的密文在解密后能产生我们想要的verified头部和虚假的长度值,同时后续部分又能包含我们的溢出载荷。
这正是“综合安全”挑战的精髓——你需要将密码学的攻击手法(如选择明文攻击)与二进制漏洞利用的精准偏移计算结合起来。
总结
本节课中,我们一起学习了应对多概念融合安全挑战的方法:

- 逆向推理:从最终目标出发,反向推导必要条件链。
- 分解问题:将复杂挑战拆解为独立的、已学过的概念(如密码学、溢出利用)。
- 利用实践模式:在可控环境中隔离并验证单个概念的攻击路径(例如,先专注于验证溢出是否可行)。
- 逐步集成:在验证各部分可行后,再研究如何在不“作弊”的情况下,满足所有前置条件,将攻击链完整串联起来。

综合安全模块是对本学期所学技能的一次全面检验。请善用实践模式进行探索和实验,并享受将不同领域知识融会贯通的乐趣。
29:课程总结与未来展望
在本节课中,我们将回顾本学期的精彩瞬间,探讨完成CSE 365课程后的学习路径,并深入了解网络安全领域的职业发展方向。我们将告别本学期的学习,并为未来的探索做好准备。
表情包回顾 🎭
上一节我们介绍了课程的整体安排,本节中我们来看看本学期社区中产生的精彩表情包。这些内容反映了大家在学习过程中的共同经历。
以下是本学期根据互动数量评选出的部分热门表情包:
- 密码学太难:一个经典表情,表达了面对密码学挑战时的普遍感受,并伴随着对成绩曲线的期盼。
- 荣誉教授Hanto:来自社区的热心成员Hanto长期占据排行榜首位,为课程提供了巨大帮助。为表感谢,我们将为他制作一个奖杯。
- 学期心态变化:表情包展示了随着课程内容变难,同学们从充满希望到只能依赖表情包和求成绩曲线的心态转变。
- 服务器问题:学期初平台遭遇的服务器稳定性问题,这体现了构建可扩展教学平台的挑战。
- 实习机会:有同学凭借在本课程中学到的网络安全知识获得了暑期实习。这证明了课程所授技能的实用性。
- 表情包监狱:本学期被关入“表情包监狱”次数最多的一个表情包,内容关于经典的QA测试。
- 网络通信与数据包注入:一个关于网络通信模块的优秀表情包。该模块因技术原因未能全面升级,但计划在下学期进行改进。
- CSE 466 相关:关于后续课程CSE 466的表情包,提示了连续网络安全学习的挑战与乐趣。
- 逆向工程与漏洞利用:展示了从逆向工程到二进制漏洞利用的技能进阶过程。
- 密码学模块:密码学模块的难度可能设置过高,尤其是填充预言攻击部分,未来会考虑调整。
- 综合安全模块:关于最后一个综合安全模块的表情包,该模块融合了多项技能。
后续学习路径 🛤️
在回顾了本学期的趣味瞬间后,我们来看看如何将在这里学到的基础知识转化为持续的技能成长。
持续练习与实践
课程教授了许多基础知识,但这只是一个开始。你可以通过以下方式继续深入学习:


- CTF档案库:在Pwn College上,我们存档了过去10-12年间超过541个来自各类网络安全竞赛的挑战。你可以继续挑战它们。
- 参加实时CTF比赛:访问
ctftime.org查看即将举行的比赛,与朋友组队参加。这非常有趣,也是融入安全社区的好方法。 - 其他学习平台:我维护了一个包含各种练习和教育性题目的网站列表,可以通过
wargame.nexus访问。例如,对密码学感兴趣可以访问cryptohack,对AI安全感兴趣也有相应的资源站。
通过不断参与CTF,你可以积累知识、声誉和热情,最终目标是参与网络安全界的“奥运会”——Defcon CTF。
发现并报告真实漏洞

你可以开始将所学技能应用于实际的网络安全问题,而不仅仅是挑战题目。
- 漏洞赏金计划:许多组织都有漏洞赏金计划。如果你在其产品中发现漏洞并按照规则报告,可能会获得从数百到数万美元不等的奖金。
- 负责任的披露:发现漏洞后,当前公认的伦理做法是进行负责任的披露,即直接告知厂商。这通常比完全公开披露更受认可。厂商可能会为你分配一个CVE编号,这对你的简历很有帮助。
- 零日漏洞竞赛:例如Pwn2Own竞赛,针对特定设备或软件,成功在现场利用漏洞可获得预定奖金,同时漏洞会提交给厂商修复。
重要提醒:发现漏洞后,切勿利用它攻击他人。这不仅是非法的,也会在职业生涯开始前就终结它。务必在法律和伦理框架内行动。
继续学习相关课程
如果你希望在学校课程体系中继续深造,ASU提供了许多网络安全相关课程:

- CSE 466:本课程的直接延续,深入探讨高级缓解措施、绕过技术以及整个系统的安全性。
- CSE 598 软件漏洞利用:一门研究生课程,深入探讨尖端的缓解措施和漏洞利用技术。
- 应用漏洞研究:学习逆向工程和分析大型真实程序(数百万行代码级别),而不仅仅是课程中的小型演示程序。
- 其他课程:还包括数据安全与隐私、软件质量与测试、网络取证等。请多关注课程目录中与安全相关的课程。
- 密码学课程:CSE 539密码学课程将在下学期进行改进。

参与研究
如果你对深入研究网络安全感兴趣,可以考虑加入我们的研究实验室。
- 本科生研究:任何在Pwn College中获得腰带(特别是橙色腰带)的同学,如果对研究感兴趣,我们都欢迎你来交流。我们有很多适合本科生的项目,涉及教学平台开发、自动化程序分析、网络犯罪分析等。
- 全球参与者:此邀请也适用于全球范围内非ASU的课程跟随者。
网络安全职业道路 💼
掌握了技能之后,让我们看看如何在网络安全领域开创职业生涯。这里有一些主要的方向可供选择。
渗透测试员
受雇从外部像攻击者一样评估公司系统的安全性。
- 优点:工作内容多样、有趣,有时甚至涉及物理安全测试。
- 缺点:日常工作有时会变得重复(例如扫描未打补丁的系统),需要撰写大量报告,时间压力可能较大。
企业内部安全工程师
在大公司内部负责确保关键系统的安全。
- 优点:工作通常稳定,有充足的资源和时间。
- 缺点:可能长期专注于少数几个系统,容易感到枯燥;也可能需要处理公司内部政治。
漏洞研究员
专注于寻找软件中的新型漏洞。
- 优点:技术性极强,与优秀的同事共事,探索目标广泛。
- 缺点:收入可能与漏洞发现直接挂钩,压力大;存在将漏洞出售给非原厂商的诱惑,可能引发伦理和法律问题。有些团队隶属于大公司(如Cisco Talos),环境相对稳定。
独立研究员/竞赛参与者
完全自由职业,参加像Pwn2Own这样的竞赛。
- 优点:自由度高,成功回报丰厚。
- 缺点:收入不稳定,压力大;容易陷入对单一高回报产品的持续研究,可能失去兴趣。
网络安全教授/研究员
从事网络安全学术研究和教学工作。
- 优点:学术自由,可以研究自己感兴趣的课题;职位稳定(获得终身教职后);能与优秀的学生一起推动前沿研究。
- 缺点:薪酬通常低于工业界;获得终身教职前工作强度极大;需要处理教学管理事务。
其他职业路径
网络安全领域还有许多其他角色,不一定需要像课程后期那样的高强度技术能力:
- 安全运营中心分析师/威胁分析师:监控公司网络安全状态,分析安全事件。
- 安全管理:结合技术知识和管理技能,领导安全团队。最终可能成为首席信息安全官。
如何进入网络安全领域 🚪
最后,我们来谈谈如何迈出进入网络安全领域的第一步。
网络安全领域存在巨大的技能缺口,全球有数百万个职位空缺,前景广阔。
- 直接申请:如果你觉得技能足够,可以直接搜索并申请网络安全职位。你可能比自己想象的更 ready。
- 克服冒名顶替综合征:如果你觉得自己还没准备好,那可能是冒名顶替综合征在作祟。勇敢尝试。
- 学历与认证:
- 最重要的“认证”仍然是你的学士学位。它通常是求职时最重要的区分因素。
- 专业认证(如CISSP, CEH等)在通过HR筛选时可能有帮助,但技术能力强的面试官通常更看重实际技能。
- 建立声誉:持续参与CTF,在社区中建立你的声誉和网络,工作机会可能会主动找上门。
总结与告别 👋


本节课中我们一起回顾了本学期的学习历程,探讨了从CTF竞赛、漏洞研究到学术深造等多种后续学习路径,并概述了渗透测试、安全工程、漏洞研究、学术研究等多元化的网络安全职业发展方向。
学习网络安全是一段持续的旅程。本课程为你打开了大门,并提供了基础工具。真正的掌握来自于坚持不懈的实践、探索和解决真实世界的问题。
感谢大家本学期的辛勤付出与陪伴。我们将在下个学期以改进后的面貌回归。对于获得了腰带的同学,请关注Discord上关于周二下午3点授带仪式的通知。
再见,黑客们!祝你们在未来的探索中一切顺利。

浙公网安备 33010602011771号