在Linux中安装Boofuzz
boofuzz 代码以及官方介绍:https://github.com/jtpereyda/boofuzz
确保安装了正确的环境
sudo apt-get install python3-pip python3-venv build-essential
使用pip安装部署
建议在虚拟环境(venv)中设置boofuzz。首先,创建一个目录来保存boofuzz安装:
mkdir boofuzz && cd boofuzz
python3 -m venv boofuzz
这将在当前文件夹中创建一个新的虚拟环境env。请注意,虚拟环境中的Python版本是固定的,并在创建时选择。与全局安装不同,在虚拟环境中,python被别名为虚拟环境的python版本。
激活虚拟环境:
source boofuzz/bin/activate
更新pip和setuptools:
(boofuzz) $ pip install -U pip setuptools
最后安装boofuzz
(boofuzz) $ pip install boofuzz
要运行和测试模糊脚本,请确保始终事先激活虚拟环境。
boofuzz使用
个人理解,boofuzz已经像一个python包一样,封装好了,上面的步骤相当于开了一个虚拟环境,然后下载了boofuzz,然后可以通过import,就可以像使用其他包一样使用boofuzz。即自己编写.py文件,选择装有boofuzz的解释器就好了。
同时,boofuzz官方提供了很多示例,可以参考学习:boofuzz/examples
boofuzz特点:
易于快速生成数据。
插桩 - 即故障检测。
故障后目标重置。
记录测试数据。
在线文档。
支持任意通信介质。
内置支持串行模糊测试、以太网和 IP 层、UDP 广播。
更好的测试数据记录 - 一致、全面、清晰。
测试结果 CSV 导出。
可扩展的插桩/故障检测。
安装体验更加简单!
错误更少。
boofuzz关键元素
boofuzz的一般步骤:
定义session
定义协议格式
构建协议树(Protocol-Tree):session.connect()
session.fuzz()
可以参考:官方文档
下面先学习关键元素的作用,再实际编写。
API模块学习
session
Session几乎提供了整个Boofuzz功能的设定,同时通过查看Session类初始化的参数可以了解到Boofuzz提供了那些功能。Session类的初始化参数如下:
参数:
session_filename (str): 序列化持久数据到的文件名. Default None.
index_start (int); 设置从哪个索引(index)开始启动testcase.
index_end (int); 设置到哪个索引(index)结束testcase.
sleep_time (float): 运行testcase之间的时间间隔,单位为秒. Default 0.
restart_interval (int): 间隔几个testcase就重启一次目标, 设置为0时表示从不重启目标. Default 0.
console_gui (bool): 在终端使用光标生产一个类似于web界面的静态控制台。未在Windows下测试. Default False.
crash_threshold_request (int): 请求(request)耗尽前允许的最大崩溃数. Default 12.
crash_threshold_element (int): 元素(element)耗尽前允许的最大崩溃数. Default 3.
restart_sleep_time (int): 目标不能被重启时,休眠的时间,单位为秒. Default 5.
restart_callbacks (list of method): 在回调post_test_case_callback失败后,这些注册调用(restart_callbacks)会执行. Default None.
restart_threshold (int): 丢失目标连接时的最大重试次数. Default None(indefinitely).
restart_timeout (float): 重试连接尝试的时间(秒). Default None(indefinitely).
pre_send_callbacks (list of method): 注册的方法将在每个模糊测试用例之前被调用. Default None.
post_test_case_callbacks (list of method): 注册的方法将在每个模糊测试用例之后调用. Default None.
post_start_target_callbacks (list of method): 方法将在目标启动或重新启动后调用,例如,由进程监视器调用。
web_port (int): 通过web浏览器监视模糊测试的端口. Default 26000.
keep_web_open (bool): 会话完成后保持webinterface打开. Default True.
fuzz_loggers (list of ifuzz_logger.IFuzzLogger): 用于保存测试数据和结果。默认日志到标准输出。
fuzz_db_keep_only_n_pass_cases (int): 为了最大限度地减少磁盘使用量,仅保存位于故障或错误之前的n个测试用例的数据。设置为0,表示保存每个测试用例(高磁盘I/O!). Default 0.
receive_data_after_each_request (bool): 如果为True,会话将在发送每个非模糊节点后尝试接收应答. Default True.
check_data_received_each_request (bool): 如果为True,会话将验证在传输每个非模糊节点后是否已接收到一些数据,如果未接收到待验证的数据,则注册失败。如果为False,则不会执行此检查。默认为False。除非receive_data_after_each_request为假,否则仍会进行接收尝试。
receive_data_after_fuzz (bool): 如果为True,会话将在发送模糊消息后尝试接收回复. Default False.
ignore_connection_reset (bool): Log ECONNRESET errors ("Target connection reset") as "info" instead of failures.
ignore_connection_aborted (bool): Log ECONNABORTED errors as "info" instead of failures.
ignore_connection_issues_when_sending_fuzz_data (bool): 忽略模糊数据传输故障。默认为True。这通常是一个有用的启用设置,因为一旦消息明显无效,目标可能会断开连接。
ignore_connection_ssl_errors (bool): Log SSL related errors as "info" instead of failures. Default False.
reuse_target_connection (bool): If True, only use one target connection instead of reconnecting each test case. Default False.
Session大致提供了几个功能:
1)通过session_filename进行实例对象数据序列化并保持到本地;
2)通过index_start和index_end手动指定模糊测试testcase的起始和结束索引;
3)指定时间间隔(发送请求,重启目标等),超时(发送请求等),阈值等;
4)指定回调函数(pre,post,重启目标等);
5)监视模糊测试的web-service设置;
Session常用函数包括:
1)connect,构建协议树时,用来连接消息节点;
2)add_target,添加目标;
3)example_test_case_callback;
4)register_post_test_case_callback,注册一个测试后用例方法。注册的方法将在每个模糊测试用例之后调用。调用的顺序:
pre_send()
↓
req
↓
callback
↓
...
↓
req
↓
callback
↓
post-test-case-callback
5)fuzz,模糊整个协议树(Protocol-Tree)。迭代并模糊所有情况,根据self.skip跳过并根据self.restart_interval重新启动。
如果希望模糊测试结束后,web服务器仍然可用,则程序必须在结尾调用boofuzz.helpers.pause_for_signal();
6)import_file,导入session序列化的本地配置文件;
7)num_mutations,图中的总突变数。此函数会更新成员变量self.total_num_mutations;
8)transmit_fuzz,发送模糊测试请求;
9)transmit_normal,发送正常的请求;
10)render_graph_graphviz,渲染图。使用代码:
with open('somefile.png', 'wb') as file:
file.write(session.render_graph_graphviz().create_png())
目标,Target
目标描述符容器,常用函数set_fuzz_data_logger,设置此对象的模糊数据记录器——用于发送和接收的模糊数据;
可以分为三类:
- Repeater:基础的重复器类
- TimeRepeater:基于时间的重复器类。启动计时器,并重复,直到超过持续时间秒。
- CountRepeater:基于数量的重复器。重复固定的次数。
连接,Connection
连接对象,网络层连接描述类。
- ITargetConnection:用于连接模糊目标的接口。
- BaseSocketConnection:该类是套接字(socket)上许多连接的基础类。
- TCPSocketConnection:用于TCP套接字的BaseSocketConnection实现。
- UDPSocketConnection:用于UDP套接字的BaseSocketConnection实现。
- SSLSocketConnection:用于SSL套接字的BaseSocketConnection实现。
- RawL2SocketConnection:用于网络L2层的BaseSocketConnection实现。
- RawL3SocketConnection:用于网络L3层的BaseSocketConnection实现。
- SocketConnection:ITargetConnection使用套接字实现。
- SerialConnection:ITargetConnection实现通用串行端口。
监视器,Monitors
监控器是针对特定行为监控目标的组件。监视器可以是被动的,只是观察和提供数据,或者更主动地与目标直接交互。某些监控器还具有启动、停止和重新启动目标的功能。
根据您在目标主机上可用的工具,检测目标的崩溃或不当行为可能是一个复杂、非直接的过程;这尤其适用于嵌入式设备。Boofuzz提供了三种主要的监视器实现:
- ProcessMonitor,在Windows和Unix上从进程收集调试信息的监视器。它还可以重新启动目标进程并检测故障。
- NetworkMonitor,一种通过PCAP被动捕获网络流量并将其附加到测试用例日志的监视器。
- CallbackMonitor,用于实现可提供给会话类的回调。
具体的
- BaseMonitor:目标监视器的接口。所有监视器必须遵守本规范。
- ProcessMonitor:进程监视器由两部分组成:
- ProcessMonitor类,实现BaseMonitor;
- 要在目标主机上运行的模块,windows平台使用process_monitor.py,Linux等平台使用process_monitor_unix.py;
- NetworkMonitor:网络监视器由两部分组成:
- NetworkMonitor类,它实现BaseMonitor;
- 要在目标主机上运行的模块,使用network_monitor.py
- CallbackMonitor:Session中用于提供回调数组的新型回调监视器。它的目的是在会话类中保留*_callbacks参数,同时通过将这些回调转发到监视器基础结构来简化会话的实现。参数到此类的方法实现的映射关系,如下所示:
- restart_callbacks –> target_restart
- pre_send_callbacks –> pre_send
- post_test_case_callbacks –> post_send
- post_start_target_callbacks –> post_start_target
- 所有其他实现的接口成员都只是存根(stubs),因为会话中不存在相应的参数。在任何情况下,实现自定义监视器可能比使用回调函数更明智。
日志,Logging
Boofuzz提供了灵活的日志记录。所有日志类都实现IFuzzLogger。下面详细介绍了内置日志类:
要同时使用多个记录器,请参阅 FuzzLogger。
- IFuzzLogger:用于记录模糊数据的抽象类。
- IFuzzLogger为Sulley框架和测试编写器提供了日志接口。
- 提供的方法旨在反映功能测试动作。IFuzzLogger提供了一种记录测试用例、通过、失败、测试步骤等的方法,而不是一般的调试/信息/警告方法。
- 这个假设的示例输出给出了如何使用记录器的想法:如Fuzzing-》Set up (pre-fuzzing)-》Post-test cleanup Instrumentation checks-》Reset due to failure等,记录发送的数据、接收的数据、检查、检查结果和其他信息。
Test Case: UDP.Header.Address 3300
Test Step: Fuzzing
Send: 45 00 13 ab 00 01 40 00 40 11 c9 …
Test Step: Process monitor
checkCheck OK
Test Step: DNP
CheckSend: ff ff ff ff ff ff 00 0c 29 d1 10 …
Recv: 00 0c 29 d1 10 81 00 30 a7 05 6e …
Check: Reply is as expected. Check OK
Test Case: UDP.Header.Address 3301
Test Step: Fuzzing
Send: 45 00 13 ab 00 01 40 00 40 11 c9 …
Test Step: Process monitor check
Check Failed: “Process returned exit code 1”
Test Step: DNP Check
Send: ff ff ff ff ff ff 00 0c 29 d1 10 …
Recv: None
Check: Reply is as expected. Check Failed
- IFuzzLoggerBackend:IFuzzLogger的别名
- FuzzLoggerText:此类格式化FuzzLogger数据以用于文本显示。可以将其配置为输出到标准输出或命名文件。
- 使用两个FuzzLoggerText,可以将FuzzLogger实例配置为输出到控制台和文件。
- FuzzLoggerCsv:此类为pcap文件格式化FuzzLogger数据。可以将其配置为输出到命名文件。
- FuzzLoggerCurses:此类使用curses为控制台GUI格式化FuzzLogger数据。这还没有在Windows上测试过。
- FuzzLogger:获取IFuzzLogger对象的列表,并将记录的数据多路传输到每个对象。FuzzLogger还维护概要故障和错误数据。
协议定义,Protocol Definition
推荐使用新的协议定义,官方文档,静态协议定义也可以,但是不推荐。
Requests are messages, Blocks are chunks within a message, and Primitives are the elements (bytes, strings, numbers, checksums, etc.) that make up a Block/Request.
大致意思是Requests是消息序列,Blocks是消息里的一系列组成部分,Primitives是组成一个Block/Request的元素(bytes, strings, numbers, checksums, etc.)。
下面是一个HTTP消息的示例。它演示了如何使用Request、Block和几个Primitives:
req = Request("HTTP-Request",children=(
Block("Request-Line", children=(
Group("Method", values= ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE"]),
Delim("space-1", " "),
String("URI", "/index.html"),
Delim("space-2", " "),
String("HTTP-Version", "HTTP/1.1"),
Static("CRLF", "\r\n"),
)),
Block("Host-Line", children=(
String("Host-Key", "Host:"),
Delim("space", " "),
String("Host-Value", "example.com"),
Static("CRLF", "\r\n"),
)),
Static("CRLF", "\r\n"),
))
-
Request:顶层容器。可以保存任何块结构或原语(Primitives)。
- 这基本上可以被认为是超级块、根块、父块等别名。
-
Blocks:基本构建块。可以包含primitives, sizers, checksums或其他blocks。
-
Primitives
- Static:静态原语是固定的,在模糊化时不会发生变化。
- Simple:只能通过简单手动指定的,模糊字节值。
- Delim:分隔符,它的突变包括重复、替换和排除。分隔符包括:,\r,\n, ,=,>,<等等;
- Group:表示,在突变时将遍历一个指定的静态值列表的每个元素。可以将块绑定到组原语,以指定块应循环遍历组中每个值的所有可能突变。例如,group原语在表示有效操作码列表时非常有用。下面是Group原语表示HTTP请求方法的所有突变的可能。这个原语表示一个静态值列表,在突变时逐步遍历每个值。
with s_block("Request-Line"):
s_group("Method", ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE"])
s_delim(" ", name="space-1")
s_string("/index.html", name="Request-URI")
s_delim(" ", name="space-2")
s_string("HTTP/1.1", name="HTTP-Version")
s_static("\r\n", name="Request-Line-CRLF")
s_string("Host:", name="Host-Line")
s_delim(" ", name="space-3")
s_string("example.com", name="Host-Line-Value")
s_static("\r\n", name="Host-Line-CRLF")
s_static("\r\n", "Request-CRLF")
-
RandomData:生成随机数据块,同时保留原始数据的副本。可以指定随机长度范围。对于静态长度,请将最小/最大长度设置为相同。
-
String:在“坏”字符串库中循环的基元。类变量“fuzz_library”包含所有实例的全局智能模糊值列表。当前我使用的代码中的_fuzz_library如下:_fuzz_library库变量包含特定于实例化原语的模糊值。这允许我们避免在每个实例化的原语中复制大约70MB的_fuzz_library数据结构。
_fuzz_library = [
"!@#$%%^#$%#$@#$%$$@#$%^^**(()",
"", # strings ripped from spike (and some others I added)
"$(reboot)",
"$;reboot",
"%00",
"%00/",
.........
'%0DCMD=$"reboot";$CMD',
"%0Dreboot",
"%n" * 500,
"%s" * 100,
"%s" * 500,
"%u0000",
"& reboot &",
"& reboot",
"&&CMD=$'reboot';$CMD",
'&&CMD=$"reboot";$CMD',
"&&reboot",
"&&reboot&&",
"..:..:..:..:..:..:..:..:..:..:..:..:..:",
"/%00/",
"/." * 5000,
"/.../" + "B" * 5000 + "\x00\x00",
"/.../.../.../.../.../.../.../.../.../.../",
"/../../../../../../../../../../../../boot.ini",
"/../../../../../../../../../../../../etc/passwd",
"/.:/" + "A" * 5000 + "\x00\x00",
"/\\" * 5000,
"/index.html|reboot|",
"; reboot",
";CMD=$'reboot';$CMD",
';CMD=$"reboot";$CMD',
";id",
.........
]
-
FromFile:循环浏览文件中的“坏”值列表。
获取文件名并打开文件以读取模糊化过程中使用的值。文件名可能包含通配符(glob characters)。 -
Mirror:原语用于使用另一个原语保持更新。
-
BitField:位字段原语表示许多可变长度,用于定义所有其他整数类型。
-
Byte:1个字节大小的位字段原语。
-
Bytes:将任意长度的二进制字节字符串模糊化的原语。
-
Word:2个字节大小的位字段原语。
-
DWord:4个字节大小的位字段原语。
-
QWord:8个字节大小的位字段原语。
-
制作自己的块/原语:要创建自己的块/基本体,请执行以下操作:
-
从Fuzzable或FuzzableBlock继承一个自定义块/原语类;
- Fuzzable:自定义块/原语时,需要继承此类。它是所有块/原语的基类。
- FuzzableBlock:设计为具有子元素的可模糊类型。FuzzableBlock重写以下方法,更改基于FuzzableBlock的任何类型的默认行为:
- mutations() ,遍历所有子节点产生的突变。
- num_mutations() ,对每个子节点表示的突变求和。
- encode() ,调用函数 get_child_data().
- FuzzableBlock添加了以下方法:
- get_child_data(),渲染并连接所有子节点。
- push() ,添加额外的子节点;通常只在内部使用。
-
override父类的mutations和/或encode的方法;
-
可选:创建附带的静态原语函数。示例,请参见boofuzz的__init__.py文件。如果您的块依赖于对其他块的引用,那么校验和或长度字段依赖于消息的其他部分的方式,请参阅Size源代码以获取如何避免递归问题,并小心。
-
Overload,Override,Overwrite区别
Overload重载,同一个作用域中,语义功能相似,仅函数名相同;
Override覆盖,继承关系中,Override一般用于多态技术,函数必须实现基类的统一接口;
Overwrite重写,继承关系中,子类函数名与父类相同。
其他模块,Other Modules
-
测试用例会话引用,ProtocolSessionReference:指在单个测试用例的上下文中接收或生成的动态值。将此对象作为原语的default_value参数传递,并确保使用回调设置引用的值,例如,post_test_case_callbacks(请参阅Session部分)
-
ProtocolSession:包含一个session_variables字典,用于存储特定于单个模糊测试用例的数据。通常,session_variables中的值将在回调函数中设置,例如,post_test_case_callbacks(请参阅Session部分)。变量可以在以后的回调函数中使用,也可以由ProtocolSessionReference对象使用。
-
Helpers:该类包含了许多助手函授和小工具。
-
IP Constants:此文件包含IPv4协议的常量。完整路径现在是boofuzz.connections.ip_constants
-
PED-RPC:Boofuzz提供了一个RPC原语来在远程机器上托管监控器。主boofuzz实例充当连接到(远程)运行的RPC服务器实例的客户端,透明地调用在服务器实例的客户端实例上调用的函数,并将其结果作为python对象返回。一般来说,通过RPC接口传递的数据需要能够被pickle。请注意,PED-RPC不提供任何形式的身份验证或授权。建议仅在受信任的网络上运行它。
fuzz示例
对FTP协议的默认端口进行FUZZ的脚本
#!/usr/bin/env python3
"""Demo FTP fuzzer as a standalone script."""
from boofuzz import *
def main():
"""
This example is a very simple FTP fuzzer. It uses no process monitory
(procmon) and assumes that the FTP server is already running.
"""
session = Session(target=Target(connection=TCPSocketConnection("127.0.0.1", 21)))
define_proto(session=session)
session.fuzz()
def define_proto(session):
# disable Black formatting to keep custom indentation
# fmt: off
user = Request("user", children=(
String(name="key", default_value="USER"),
Delim(name="space", default_value=" "),
String(name="val", default_value="anonymous"),
Static(name="end", default_value="\r\n"),
))
passw = Request("pass", children=(
String(name="key", default_value="PASS"),
Delim(name="space", default_value=" "),
String(name="val", default_value="james"),
Static(name="end", default_value="\r\n"),
))
stor = Request("stor", children=(
String(name="key", default_value="STOR"),
Delim(name="space", default_value=" "),
String(name="val", default_value="AAAA"),
Static(name="end", default_value="\r\n"),
))
retr = Request("retr", children=(
String(name="key", default_value="RETR"),
Delim(name="space", default_value=" "),
String(name="val", default_value="AAAA"),
Static(name="end", default_value="\r\n"),
))
# fmt: on
session.connect(user)
session.connect(user, passw)
session.connect(passw, stor)
session.connect(passw, retr)
def define_proto_static(session):
"""Same protocol, using the static definition style."""
s_initialize("user")
s_string("USER")
s_delim(" ")
s_string("anonymous")
s_static("\r\n")
s_initialize("pass")
s_string("PASS")
s_delim(" ")
s_string("james")
s_static("\r\n")
s_initialize("stor")
s_string("STOR")
s_delim(" ")
s_string("AAAA")
s_static("\r\n")
s_initialize("retr")
s_string("RETR")
s_delim(" ")
s_string("AAAA")
s_static("\r\n")
session.connect(s_get("user"))
session.connect(s_get("user"), s_get("pass"))
session.connect(s_get("pass"), s_get("stor"))
session.connect(s_get("pass"), s_get("retr"))
if __name__ == "__main__":
main()
几个问题
如何与目标建立连接的?
涉及到:sessions,connections两块代码
target.py 定义了Target类,实现了open() close() connect() send() recv()用于和目标建立连接和发送测试用例
sessions.connect()用于连接两个request,如果只有一个参数则和root连接
session = Session(
target=Target(
connection=TCPSocketConnection("127.0.0.1", 8021)))
如何生成测试用例?
将测试用例发送给目标
fuzz()
_main_fuzz_loop(self, fuzz_case_iterator)
#fuzz_case_iterator使用以下函数传入
#_generate_mutations_indefinitely() #用于生成变异的测试用例
#_generate_test_case_from_named_mutations()
_fuzz_current_case()
transmit_fuzz()
send(data) #完成向目标发送测试用例
如何变异生成
#boofuzz使用yield来写生成测试用例的函数
_generate_mutations_indefinitely()
_generate_n_mutations()
#两个嵌套的for循环
for _iterate_protocol_message_paths()
for _generate_n_mutations_for_path()
################################
_iterate_protocol_message_paths()
_iterate_protocol_message_paths_recursive()
#一个递归函数,实现了深度优先遍历XXX
################################
_generate_n_mutations_for_path()
_generate_n_mutations_for_path_recursive()
#一个递归函数,实现了深度优先遍历XXX
_generate_mutations_for_request()
get_mutation() #此函数的实现在/boocks/request.py中
################################
### /boocks/request.py
### fuzzable_block.py
### fuzzable.py
get_mutation()
mutations() #实现于fuzzable_block.py中
get_mutations() #实现于fuzzable.py中
mutations() #位于fuzzable.py中,是一个抽象方法给子类自己实现
类继承关系
class Fuzzable
class FzzableBlock
class Requst
class Fuzzable
class Byte
class Bytes
Class String
#...#
#以上这些不同格式的变异字段类分别定义了自己的mutations()函数生成变异
可以利用boofuzz框架自己设计字段格式类,实现变异方法自定义自己的生成方法。
如何从崩溃断点处继续测试?如何重新建立连接?端口号?
session.fuzz()
_main_fuzz_loop()
server_init()
_start_target()
_fuzz_current_case()
_pause_if_pause_flag_is_set()
_open_connection_keep_trying()
_pre_send()
_callback_current_node()
open_test_step()
transmit_normal() or transmit_fuzz()
_check_for_passively_detected_failures()
在_main_fuzz_loop():使用total_mutant_index来表示变异index,在_generate_mutations_indefinitely()中递增。
total_mutant_index小于index_start时跳过,不发送测试用例。
因此可以使用session的index_start参数来指定从第几个用例开始fuzz(可用于跳过已知的可触发崩溃的测试用例)
具体地:index_start和index_end,指定模糊测试testcase开始和结束的索引。测试代码使用了example/http_simple.py。修改如下:
def main():
session = Session(
+++ index_start=5,
+++ index_end=10,
target=Target(connection=TCPSocketConnection("127.0.0.1", 80)),
)
index_start和index_end主要在_main_fuzz_loop函数中起作用,控制着模糊测试testcase开始和结束。
# file: boofuzz\sessions.py
# 其中self.total_mutant_index的值是在生成testcase迭代器过程中而增加的。
def _main_fuzz_loop(self, fuzz_case_iterator):
...
try:
...
for mutation_context in fuzz_case_iterator:
if self.total_mutant_index < self._index_start: # 如果小于index_start则忽略
continue
# Check restart interval
...
self._fuzz_current_case(mutation_context)
self.num_cases_actually_fuzzed += 1
if self._index_end is not None and self.total_mutant_index >= self._index_end: # 如果大于等于index_end则退出Fuzz
break
...
except KeyboardInterrupt:
...
finally:
...
参考资料
https://github.com/jtpereyda/boofuzz
重点参考:https://boofuzz.readthedocs.io/en/stable/user/install.html
https://www.cnblogs.com/snail1502/p/17975139
实战:https://blog.csdn.net/song_lee/article/details/104334096
实战:https://blog.csdn.net/samlirongsheng/article/details/131379531
https://blog.csdn.net/samlirongsheng/article/details/131373560
重点参考:https://blog.csdn.net/weixin_46222091/article/details/119334375