安卓抓包也是一门学问

1. HTTP环境配置

将主机与手机连上同一WiFi,使它们处于同一局域网内。

Kali设置为桥接模式或自定义网卡,编辑 -> 虚拟网络编辑器,更改桥接目标与主机一致。

此时可以查看各自IP地址,看是否处于同一局域网内。虚拟机与手机要能相互ping通,数据包才能互传。

1.1 Charles

下载Charles,可以在 https://www.zzzmode.com/mytools/charles/ 进行破解,但还是推荐支持正版。

运行Charles,Proxy -> Proxy Setting 查看监听端口,默认为8888。

将手机连接上的WiFi设置手动代理,代理服务器主机为虚拟机的IP地址,代理服务器端口为监听端口。

在Charles中会弹出窗口,点击允许。

手机随意浏览网页,Charles就捕获到数据包了。当浏览HTTP网页时,可以将页面的相关内容解析出来。

但浏览HTTPS网页时,Charles只充当一个透明代理,收到什么就转发什么,里面的内容解析不出来。

1.2 Burp Suite

Kali自带,将代理监听器设置为任何IP的8080端口。

将手机连接上的WiFi代理服务器端口修改为8080,即可抓包。

可以去找个Pro版,方便很多。具体安装教程可以参考这篇文章:https://blog.csdn.net/zw05011/article/details/122459723

同样把IP设置为任意。

1.3 HTTP的缺陷

HTTP的缺陷:

  • 通信使用明文(不加密),内容可能会被窃听
  • 不验证通信方的身份,因此有可能遭遇伪装
  • 无法证明报文的完整性,所以有可能已遭篡改

2. HTTPS环境配置

HTTP + SSL + 认证 + 完整性保护 = HTTPS

下图是HTTPS通信完整流程:

2.1 HTTPS中间人抓包核心原理

HTTPS抓包的原理还是挺简单的,简单来说,就是Charles作为“中间人代理”,拿到了 服务器证书公钥 和 HTTPS连接的对称密钥,前提是客户端选择信任并安装Charles的CA证书,否则客户端就会“报警”并中止连接。

有了Charles置于中间之后,本来C/S架构的通信过程会“分裂”为两个独立的通信过程,App本来验证的是服务器的证书,服务器的证书手机的根证书是认可的,直接内置的;但是分裂成两个独立的通信过程之后,App验证的是Charles的证书,它的证书手机根证书并不认可,它并不是由手机内置的权威根证书签发机构签发的,所以手机不认,然后App也不认;所以我们要把Charles的证书导入到手机根证书目录中去,这样手机就会认可,如果App没有进行额外的校验(比如在代码中对该证书进行校验,也就是SSL pinning系列API)的话,App也会直接认可接受。

2.2 Charles配置SSL

Proxy -> SSL Proxying Settings,勾选“Enable SSL Proxying”,在“Include”添加任意IP和任意端口。

Charles开启监听,手机如果想要浏览HTTPS网页,会有警告提示。因为浏览器获得的是Charles的公钥而不是HTTPS网页的服务器公钥。

如果用户不顾警告,点击“高级”,选择继续浏览,有些网站可以,但有些网站使用了HSTS(严格传输安全协议),让浏览器强制使用HTTPS与网站进行通信,以减少会话劫持风险。

如果能继续进入,就可以抓到HTTPS中的一些数据包,比如图片、HTML等,但也有一些红叉叉。

输入账号密码后登录成功,跳转到用户界面,Charles抓到请求响应包,并且可以解析用户界面的内容,比如可以知道学号、姓名等。

点击红叉叉的Notes栏提示我们如果想要这些内容显示需要浏览器或应用程序信任Charles根证书。同样,要想成功进入使用了HSTS的网页,也需要添加Charles根证书。

在浏览器中输入 chls.pro/ssl 下载Charles公钥证书并安装。不同主机下使用Charles抓HTTPS包需要重新安装Charles证书。

此时再次访问HTTPS网页就没有警告提示了,链接显示绿色,并且可以将HTML解析出来。

那些红叉叉也没有了。

2.3 Burp Suite配置SSL

同样,如果Burp Suite抓HTTPS的包也是会显示警告提示,也是要安装Burp Suite公钥证书。

导出的是.der格式的证书,将它转化为.pem格式:

1
openssl x509 -inform DER -in burpsuite.der -out burpsuite.pem

再将.pem格式的证书push到手机中,安装证书即可。可以在用户凭据中查看安装的证书。

再次进入HTTPS网页同样可以解析内容。

3. 抓取App中的HTTPS

上面讲到,无论HTTP还是HTTPS,都是在浏览器中进行的,那抓App中的内容有什么不同吗?

以某个视频App为例子,发现它的网页、图片、音视频都是用HTTP传输的,但登录注册肯定是用HTTPS传输的,要不然用户数据就是在网上裸奔。

在尝试发送验证码时,会弹窗“网络不给力”。

怎么回事呢?我们将代理关掉,重新发送验证码,发现会经过一个安全检测,然后可以发送短信。

说明这不是网络的问题,而是HTTPS传输时发现公钥不匹配导致的。Charles的抓包结果验证了我们的想法。

这是因为在 Android 7.0(API 24)到 Android 8.1(API 27),默认不再信任用户添加的 CA 证书,所以也就不再信任 Charles 和 Fiddler 抓包工具的证书,所以抓取 HTTPS 包时才会失败。而且在 Android 9.0(API 28)及更高版本上,不仅默认只系统预装的 CA 证书,还默认禁止所有明文通信(不允许 HTTP 请求)。

解决方案一:IOS和安卓<7.0的版本没有此问题,因此如果条件允许,可以换为苹果手机或安卓版本低于7.0版本的手机去抓取。

解决方案二:将抓包工具的证书安装到系统凭据中。

打开Charles,Help -> SSL Proxying -> Save Charles Root Certificate ,以.pem格式保存,在root目录下就会生成一个charles.pem证书。

输入以下命令查看证书的哈希值:

1
openssl x509 -subject_hash_old -in charles.pem

将证书重命名为第一行的bc7c86bb,为了防止文件名冲突可以在末尾追加.0,如果根证书文件夹里面已经有这个文件名了,那就将.0改为.1,以此类推。

1
mv charles.pem bc7c86bb.0

将证书导入到系统凭据中:

1
2
3
4
5
6
7
adb push bc7c86bb.0 /sdcard/
adb shell
su
mount -o rw,remount /system
mv /sdcard/bc7c86bb.0 /system/etc/security/cacerts/
chmod 644 /system/etc/security/cacerts/bc7c86bb.0
reboot

Burp Suite的证书同样可以利用这种方法导入到系统凭据中。

现在就可以抓取发送短信验证码的包了。

如果在安全检测中滑动校验时出现“网络不给力”,加载不出拼图也没关系,因为此时我们已经把上面那个包抓到了。

4. Postern + Charles

现在大部分App都只做了客户端校验服务器的操作,也就是HTTPS中间人抓包核心原理的左半部分。由于通信是双方的,所以有些App为了防止被抓包会进行服务器校验客户端的操作。

如果App进行了服务器校验客户端,在通信过程中发现传过来客户端的公钥与存储的公钥不匹配,App会直接退出,终止通信。

目前只有Charles支持通过Postern抓包。单纯使用Charles抓这些App的登录包是抓不住的(应用层抓包),因为App设置了noproxy,根本不走代理,所以此时App还是能成功发送验证码(反正你又抓不到,我为什么不发)。

具体可看赵四大佬的文章:Android安全防护之旅—只需要这几行代码让Android程序项目变得更加安全

怎样才能抓到这个包呢?可以结合Postern,提取码:a9c1。使Charles监听到Postern而不是直接监听App,Postern就是一个VPN,所以App设置不走代理也没用,Postern照样能监听到,然后Postern再转发到Charles上,这样就完成了抓包(传输层抓包)。

4.1 环境配置

在Charles中开启SOCKS Proxy:Proxy -> Proxy Settings,勾选“Enable SOCKS Proxy”,默认端口为8889。

手机中的WiFi代理关掉,打开Postern,在配置代理中编辑代理服务器:

在配置规则中编辑规则:

重启一下VPN,此时Charles就会弹窗,点击允许。此时Charles + Postern已经配置好了。

4.2 VPN抓包

抓某个视频App的登录包也没问题。

在这样一个环境下,如果抓进行了服务器校验客户端的App的登录包,点击获取验证码后App就会直接退出,此时可以看到Charles抓到了包,但是服务器返回的是400状态码,这里就是服务器校验客户端的精髓所在。

1
2
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>

4.3 客户端证书提取转化和导入抓包

服务器校验客户端是HTTPS中间人抓包核心原理的右半部分,这时候与服务器进行通信的已经不是App,而是Charles了,所以我们要将App中内置的证书导入到Charles中去。

那么如何找到这个内置的证书呢?一般进行APK解包后,直接搜索后缀名为.p12的文件即可,一般常用的命令为:

1
tree -NCfhl | grep -i p12

直接打印出P12文件的路径,当然也有一些App比较“狡猾”,比如我们通过搜索“p12”没有搜到证书,然后看jadx反编译的源码得出它将证书伪装成border_ks_19文件,我们找到这个文件用file命令查看,果然不是后缀名所显示的PNG格式,将其改成.p12的后缀名,尝试打开时要求输入密码,可见其确实是一个证书。

但是这样直接搜索很容易漏掉伪装的证书,并且去查看源码也很麻烦。我们知道自签名证书是需要设置密码的,而开发时设置自签名证书的密码用的是KeyStore.load()函数,具体可看:App实现自签名的ssl证书。我们可以hook这个函数得到App的所有证书和密码,将它们dump下来,前提是该App开启了存储权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function hook_KeyStore_load() {
Java.perform(function () {
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
var myArray = new Array(1024);
var i = 0;
for (i=0; i<myArray.length; i++){
myArray[i] = 0x0;
}
var buffer = Java.array("byte",myArray);

var StringClass = Java.use("java.lang.String");
var KeyStore = Java.use("java.security.KeyStore");
KeyStore.load.overload('java.security.KeyStore$LoadStoreParameter').implementation = function (arg0) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
console.log("KeyStore.load1:", arg0);
this.load(arg0);
};
KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (arg0, arg1) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
console.log("KeyStore.load2:", arg0, arg1 ? StringClass.$new(arg1) : null);

if (arg0){
var file = Java.use("java.io.File").$new("/sdcard/Dwonload/certs/" + String(arg0) + ".p12");
var out = Java.use("java.io.FileOutputStream").$new(file);
var r;
while ((r = arg0.read(buffer)) > 0){
out.write(buffer,0,r);
}
console.log("save success!");
out.close();
}
this.load(arg0, arg1);
};

console.log("hook_KeyStore_load...");
});
}
setImmediate(hook_KeyStore_load)

注意这个脚本在Android 8.1.0可行,但Android 10.0.0就不行了,查看Android 10.0.0源码发现已经没有这个KeyStore.load()函数了。

当然证书还有很多格式,后缀名并不一定都是.p12,实际上在Android中是无法使用P12格式的证书的,Android 系统中使用的证书要求是BKS格式。

P12格式的证书可以在Windows上直接打开,而BKS不行。如果实在不知道证书的类型或者不会打开其它类型的证书,可以下载 KeyStore Explorer 证书转换工具,它可以解析证书类型、转换为其它格式的证书。使用该工具的前提是知道证书的密码。

1
2
wget https://github.com/kaikramer/keystore-explorer/releases/download/v5.5.1/kse_5.5.1_all.deb
dpkg -i kse_5.5.1_all.deb

由于Charles只支持导入.p12.pem格式的证书,所以如果不是这两个格式的证书,可以用KeyStore Explorer工具转换。

使用KeyStore Explorer工具转换证书格式

得到证书和密码后,下一步就是在Charles中导入证书。Proxy -> SSL Proxying Settings -> Client Certificates,创建一个安全存储,再进行导入证书。Host和Port可以填成任意,意思是你以“趣充”的身份访问所有网站,因为只有“趣充”才有该证书,而平时我们访问网站是匿名的。也可以按最小权限原则,只在某个特定网站才以“趣充”身份访问,比如在4.2中可知是在“share.equchong.com:9443”中校验客户端证书的,所以只在这个网站中以“趣充”身份访问。

Porxy -> Proxy Settings,按照下图填上端口。

此时再次进行抓包,App没有直接退出,短信发送成功,Charles也抓到包了。

4.4 SSL pinning

有些App并不会默认信任系统根证书目录中的证书,而是在代码里再加一层校验,这就是证书绑定机制——SSL pinning,如果这段代码的校验过不了,那么App还是会报证书错误。

4.4.1 案例——回顾 + 引入

开启VPN抓包后,注册时提示“网络异常”,什么原因呢?第一,我们已经将Charles公钥证书存到了系统凭据中,所以不是客户端校验服务器的问题;第二,我们使用的是VPN抓包,Charles抓到了包但显示不出内容,说明有可能存在服务器校验客户端证书,也有可能进行了SSL pinning,或者二者结合。

尝试找一下App里是否内置了证书。开启App的存储权限,hook KeyStore.load()函数将证书和密码dump下来。

dump下来了两个,但查看它们的MD5值是一样的,说明这两个是同一个文件。

使用KeyStore Explorer查看证书类型,是P12格式的,直接导入到Charles中。

此时抓注册包还是“网络异常”,说明App在代码中还进行了SSL pinning。在代码中进行校验,当然可以用hook方法来bypass。

4.4.2 bypass

在开发中,javax.net.ssl.X509TrustManager接口,用来校验证书是否被信任。通常会校验 CA 是否为系统内置权威机构、证书有效期等。这个接口有三个方法,分别用来校验客户端证书、校验服务端证书和获取可信证书数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 该方法检查客户端的证书,若不信任该证书则抛出异常
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
... ...
}

// 该方法检查服务器的证书,若不信任该证书同样抛出异常
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
... ...
}

// 返回受信任的X509证书数组
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}

因为App默认不信任系统根证书,所以应该hook checkServerTrusted()函数,将其所有重载都置空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hook_ssl() {
Java.perform(function() {
var ClassName = "com.android.org.conscrypt.Platform";
var Platform = Java.use(ClassName);
var targetMethod = "checkServerTrusted";
var len = Platform[targetMethod].overloads.length;
console.log(len);
for(var i = 0; i < len; ++i) {
Platform[targetMethod].overloads[i].implementation = function () {
console.log("class:", ClassName, "target:", targetMethod, " i:", i, arguments);
//printStack(ClassName + "." + targetMethod);
}
}
});
}
setImmediate(hook_ssl)

还有另一种bypass方法,使用objection直接将SSL pinning给disable掉:

1
objection -g com.ninemax.ncsearchnew explore -s "android sslpinning disable"

救了个大命为什么两种方法都不行,还是红叉叉!发送不了验证码。失败的原因是拒绝连接,而不是有关SSL证书什么的。(学会看连接失败的原因很重要)

最后发现关闭了VPN还是注册不了,我猜可能是服务器无了,应该不是我们的问题。那么如果成功后就可以抓到带有手机号的包。

4.4.3 证书绑定开发逆向和混淆后的解绑

很多框架都可以实现SSL pinning,上面使用传统的HttpURLConnection类封装请求,客户端锁定操作需要实现X509TrustManager接口的checkServerTrusted()方法只是其中之一,通过对比预埋证书信息与请求网站的的证书来判断。

使用的objection也是一样,在objection源码里面预设了很多框架的hook SSL pinning的代码,有okHttp3框架、PinningTrustManager框架、TrustManagerImpl框架、SSLCertificateChecker框架等。而我们只需用一条命令就可以实现解绑,非常方便。

瘦蛟舞大佬也写了一个解除安卓证书锁定的工具,覆盖到了更多的框架。

4.4.4 混淆案例

VPN抓包,抓到了包但显示不出内容,错误提示“需要信任Charles根证书”,但我们已经把Charles根证书放进了系统凭据中,所以是App默认不信任系统的根证书,也就是进行了SSL pinning。

解绑的第一步就是要知道App是用了哪个框架的SSL pinning,或者将上面的SSL Pinning方法全都来一遍,还是抓不到内容可以合理怀疑是做了混淆,使我们hook不到对应的方法。

可以从系统角度出发,客户端或服务器校验证书时是利用证书的哈希值来判断该证书是否有效。计算证书的哈希值就必须要打开证书,所以肯定会有文件打开函数。我们可以使用objection hook文件打开函数,看是否打开了证书。

1
2
android hooking search classes File//查找与文件有关的类
android hooking watch class_method java.io.File$init --dump-args --dump-return --dump-backtrace

hook好后尝试注册,发现确实打开了证书算哈希。上面的checkServerTrusted()等函数用上面的框架方法不能通过,继续往下看发现CertificatePinner.java,它是okHttp里面的。

如果实在不知道的话可以用WallBreaker查看z1.g这个类的方法和常量。

1
2
3
plugin load ./~Wallbreaker//加载插件
plugin wallbreaker classdump z1.g//将这个类的常量和方法dump下来
plugin wallbreaker objectsearch z1.g//查找类的实例
1
plugin wallbreaker objectdump --fullname 0x24e6//将类的实例dump下来

由于里面的a()方法是混淆过的,所以需要找到okHttp里CertificatePinner.java中对应的方法。又由于a()方法与SSL pinning相关,所以可以在大佬的脚本上查看hook okHttp中的哪个函数,a()大概率就是那个函数。

可以照着大佬的脚本写:

1
2
3
4
5
6
7
8
9
10
11
12
function hookCer(){
Java.perform(function(){
Java.use("z1.g").a.implementation = function(){
console.log("z1.g. was called!")
return
}
})
}
function main(){
hookCer()
}
serImmediate(main)

此时进行抓包也没有问题了。

4.5 VPN抓包对抗

当我们开启VPN后,手机会多出一个叫“tun0”网卡,手机的所有流量都会经过这个网卡,所以使用Charles监听VPN,就可以抓到手机所有的流量。

要想对抗VPN抓包,App可以通过判断java.net.Network.getName()是否等于“tun0”或“ppp0”来判断是否存在VPN。

但绕过这个检测也很简单,hook该API使其返回“rmnet_data1”即可。

5. Socket通信抓包

HTTPS并非是应用层的一种新协议,只是HTTP通信接口部分用SSL和TLS协议代替。通常HTTP直接和TCP通信,当使用SSL时,则演变成先和SSL通信,再由SSL和TCP通信。在采用SSL后,HTTP就拥有了HTTPS的加密、证书和完整性保护这些功能。

Socket的本质是收发包的接口。它不是中间人抓包,而是网卡流量的dump转储下来。Wireshark抓的就是网卡的流量,如果抓HTTPS包,也就是SSL的流量,此时它不是明文转储,在Wireshark中解析不了HTTPS的内容。

Socket分为两种:TCP和UDP。服务器和客户端之间的通信都是通过Socket进行。在Socket眼里,所有流量都是RAW DATA,纯二进制。它不知道跑的是HTTP还是HTTPS,它只是一条通道,传的是二进制数据,只管是否是可靠传输,是否需要重传。

5.1 TCP/IP应用层hook

SSL不止可以跟HTTP搭配,大多数应用层协议都可以跟SSL搭配,比如SMTP/POP/IMAP 、protobuf协议等。这些应用层协议如果跟SSL搭配,都需要解开SSL这一层。也就是说我们真正需要的不是TCP往上的包,因为TCP往上dump下来的是SSL的包,是经过加解密之后的包,是密文。如果我们想要看到明文,需要dump SSL的接口。可以通过hook SSL框架直接获得明文。

将App附加到objection上,查看App中使用到SSL的类:

1
android hooking search classes ssl

将这些有关ssl的类复制到一个TXT文件中,在每个类的前面添加android hooking watch class,也就是构造hook命令,形成脚本。执行脚本:

1
objection -g com.roysue.httpsocket explore -c ssl.txt

在hook的过程中可能会崩溃,去掉崩溃的类即可。最后发现比较可疑的类:

1
2
3
4
com.android.org.conscrypt.SslWrapper.write(java.io.FileDescriptor,[B,int,int,int)
com.android.org.conscrypt.SslWrapper.read(java.io.FileDescriptor,[B,int,int,int)
com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read([B,int,int)
com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.wirte([B,int,int)

以上wirte()read()方法中的byte数组很有可能就是我们需要的明文,尝试来hook试试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function jhexdump(array,off,len){
var ptr = Memory.alloc(array.length)
for(var i=0; i<array.length; i++)
Memory.writeS8(ptr.add(i), array[i])
console.log(hexdump(ptr,{offset:off,length:array.length,header:flase,ansi:false}))
//console.log(hexdump(ptr,{offset:0,length:array.length,header:flase,ansi:false}))
}

function hook_socket(){
Java.perform(function(){
console.log("hook_SSLsocket")
Java.use("java.net.SocketOutputStream").write.overload('[B','int','int').implementation = function(bytearray,int1,int2){
var result = this.write(bytearray,int1,int2)
console.log("HTTP write result,bytearray,int1,int2 => ",result,bytearray,int1,int2)
var ByteString = Java.use("com.android.okhttp.okio.ByteString")
//console.log("bytearray contents => ",ByteString.of(bytearray).hex())
//console.log(jhexdump(bytearray,int1,int2))
console.log(jhexdump(bytearray,int1,int2))
//console.log(this.socket.value.getLocalAddress().toString())
//sonsole.log(this.socket.value.getLocalPort())
//console.log(this.socket.value.getRemoteSocketAddress().toString())
//console.log(this.socket.value.getPort())
return result
}
}

function hook_SSLsocketandroid8(){
Java.perform(function(){
console.log("hook_SSLsocket")
Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream").write.overload('[B','int','int').implementation = function(bytearray,int1,int2){
var result = this.write(bytearray,int1,int2)
console.log("HTTPS write result,bytearray,int1,int2 => ",result,bytearray,int1,int2)
var ByteString = Java.use("com.android.okhttp.okio.ByteString")
//console.log("bytearray contents => ",ByteString.of(bytearray).hex())
//console.log(jhexdump(bytearray,int1,int2))
console.log(jhexdump(bytearray,int1,int2))
return result
}

Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream").read.overload('[B','int','int').implementation = function(bytearray,int1,int2){
var result = this.write(bytearray,int1,int2)
console.log("HTTPS write result,bytearray,int1,int2 => ",result,bytearray,int1,int2)
var ByteString = Java.use("com.android.okhttp.okio.ByteString")
//console.log("bytearray contents => ",ByteString.of(bytearray).hex())
//console.log(jhexdump(bytearray,int1,int2))
console.log(jhexdump(bytearray,0,result))
return result
}
})
}

function main(){
console.log("Main")
//hook_socket()
hook_SSLsocketandroid8()
}
setImmediate(main)

此时可以看到已经获取到HTTPS的内容了,说明我们找的SSL接口是没问题的。

从这些API中可以得到什么信息呢?至少可以得到远程的IP与port,本地IP与port。

5.2 TCP/IP传输层hook

Frida hook Socket

推荐一个r0ysue大佬写的安卓通杀脚本

5.3 TCP/IP网络层抓包

彻底解决抓不到包的问题。自制路由器,这个就算了。

5.3.1 环境配置

在手机上再安装一个Linux操作系统。

下载与手机型号对应的kali nethunter,使用种子下载会快很多。

6. 总结

爱吃菠菜大佬对以上知识点的总结: