PHP动态网站开发期末考试/网址导航大全
脱壳修复是指在进行加壳保护后的二进制程序脱壳操作后,由于加壳操作的不同,有些程序的导入表可能会受到影响,导致脱壳后程序无法正常运行。因此,需要进行修复操作,将脱壳前的导入表覆盖到脱壳后的程序中,以使程序恢复正常运行。一般情况下,导入表被分为IAT(Import Address Table,导入地址表)和INT(Import Name Table,导入名称表)两个部分,其中IAT存储着导入函数的地址,而INT存储着导入函数的名称。在脱壳修复中,一般是通过将脱壳前和脱壳后的输入表进行对比,找出IAT和INT表中不一致的地方,然后将脱壳前的输入表覆盖到脱壳后的程序中,以完成修复操作。
数据目录表的第二个成员指向导入表,该指针在PE开头位置向下偏移0x80h
处,此处PE开始位置为0xF0h
也就是说导入表偏移地址应该在0xf0+0x80h=170h
如下图中,导入表相对偏移为0x21d4h
。
这个地址的读取同样可以使用PeView
工具得到,通过输入DataDirectory
读者可看到如下图所示的输出信息,其中第二行则是导入表的地址。
这里的0x21d4
是一个RVA地址,需要将其转换为磁盘文件FOA偏移才能定位到导入表在文件中的位置,使用RvaToFoa
命令可快速完成计算,转换后的文件偏移为0x11d4
此处我们也可以通过使用虚拟偏移地址减去实际偏移地址来得到这个参数,由于0x21d4
位于.rdata
节,此时的rdata
虚拟偏移是0x2000
而实际偏移则是0x1000
通过使用2000h-1000h=1000h
,接着再通过0x21d4h-0x1000h=11D4h
同样可以得到相对FOA
文件偏移。
我们通过使用WinHex
工具跳转到11d4
位置处,读者此时能看到如下图所示的地址信息。
如上图就是导入表中的IID
数组,每个IID
结构包含一个装入DLL
的描述信息,现在有三个导入DLL文件,则第四个是一个全部填充为0的结构,标志着IID数组的结束,每一个结构有五个四字节构成,该结构体定义如下所示;
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{union{DWORD Characteristics;DWORD OriginalFirstThunk;} DUMMYUNIONNAME;DWORD TimeDateStamp;DWORD ForwarderChain;DWORD Name;DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
我们以第一个调用动态链接库为例,其地址与结构的说明如下所示:
- 0000 22C0 => OrignalFirstThunk => 指向输入名称表INT的RVA
- 0000 0000 => TimeDateStamp => 指向一个32位时间戳,默认此处为0
- 0000 0000 => ForwardChain => 转向API索引,默认为0
- 0000 244A => Name => 指向DLL名字的指针
- 0000 209C => FirstThunk => 指向输入地址表IAT的RVA
每个IID结构的第四个字段指向的是DLL
名称的地址,以第一个动态链接库为例,其RVA是0000 244A
将其减去1000h
得到文件偏移144A
,跳转过去看看,调用的是USER32.dll
库。
上方提到的两个字段OrignalFirstThunk
和FirstThunk
都可以指向导入结构,在实际装入中,当程序中的OrignalFirstThunk
值为0时,则就要看FirstThunk
里面的数据,FirstThunk常被叫做IAT
它是在程序初始化时被动态填充的,而OrignalFirstThunk
常被叫做INT
,它是不可改变的,之所以会保留两份是因为,有些时候会存在反查的需求,保留两份是为了更方便的实现。
在上述流程中,我们找到了User32.dll
的OrignalFirstThunk
,其地址为22C0
,使用该值减去1000h
得到 12c0h
,在偏移为12c0h
处保存的就是一个IMAGE_THUNK_DATA32
数组,他存储的内容就是指向 IMAGE_IMPORT_BY_NAME
结构的地址,最后一个元素以一串0000 0000
作为结束标志,先来看一下IMAGE_THUNK_DATA32
的定义规范。
typedef struct _IMAGE_THUNK_DATA32
{union{DWORD ForwarderString;DWORD Function;DWORD Ordinal;DWORD AddressOfData;} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
直接使用WinHex
定位到12c0h
地址处,此处就是OrignalFirstThunk
中保存的INT
的内容,如下图,除去最后一个结束符00000000
以外,一共有19
个四字节,则说明User32.dll
中导入了19
个API
函数。
再来看一下FirstThunk
也就是IAT
中的内容,由于User32
的FirstThunk
字段默认值是209C
,使用该值减去1000h
即可得到109ch
,此处就是IAT的内容,使用WinHex
定位过去,可以发现两者内容时完全一致的。
接着我们以第一个导入RVA
地址0000243Eh
,用该值减去1000h
得到143Eh
,定位过去正好是EndDialog
的字符串,同样的方式,第二个导入RVA地址0000242ch
,用该值减去1000h
得到142ch
定位过去正好是PostQuitMessage
的字符串,如下图绿色部分所示。
如上图中我们已第二个函数PostQuitMessage
为例,前两个字节0271h
表示的是Hint
值,后面的蓝色部分则是PostQuitMessage
字符串,最后的0标志结束标志。
当程序被运行前,它的FirstThunk
值与OrignalFirstThunk
字段都指向同一片INT
中,此处我们使用LyDebugger
工具对程序进行内存转存,执行命令LyDebugger DumpMemory --path Win32Project.exe
生成dump.exe
文件,该文件则是内存中的镜像数据。
当程序运行后,OrignalFirstThunk
字段不会发生变化,但是FirstThunk
值的指向已经改变,系统在装入内存时会自动将FirstThunk
指向的偏移转化为一个个真正的函数地址,并回写到原始空间中,定位到dump.exe
文件FirstThunk
输入表RVA地址处209Ch
查看,如下图;
接着定位到OrignalFirstThunk
处,也就是22c0h
,观察可发现,绿色的INT
并没有变化,但是黄色的IAT
则相应的发生了变化
我们以IAT
中第一个0x75f8ab90
为例,使用x64dbg
跟进一下,则可知是载入内存后EngDialog
的内存地址。
当系统装入内存后,其实只会用到IAT
中的地址解析,输入表中的INT
就已经不需要了,此地址每个系统之间都会不同,该地址是操作系统动态计算后填入的,这也是为什么会存在导入表这个东西的原因,就是为了解决不同系统间的互通问题。
有时我们在脱壳时,由于IAT
发生了变化,所以程序会无法被正常启动,我们Dump
出来的文件由于使用的是内存地址,导入表不一致所以也就无法正常运行,可以使用原始的未脱壳的导入表地址对脱壳后的文件导入表进行覆盖替换,以此来修复导入表错误。
要实现这段代码,读者可依次读入脱壳前与脱壳后的两个文件,通过循环的方式将脱壳前的导入表地址覆盖到脱壳后的程序中,以此来实现对导入表的修复功能,如下代码BuildIat
则是笔者封装首先的一个修复程序,读者可自行体会其中的原理;
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>
#include <ImageHlp.h>
#pragma comment(lib,"Dbghelp")DWORD RvaToFoa(PIMAGE_NT_HEADERS pImgNtHdr, LPVOID lpBase, DWORD dwRva)
{PIMAGE_SECTION_HEADER pImgSecHdr;pImgSecHdr = ImageRvaToSection(pImgNtHdr, lpBase, dwRva);return dwRva - pImgSecHdr->VirtualAddress + pImgSecHdr->PointerToRawData;
}void BuildIat(char *pSrc, char *pDest)
{PIMAGE_DOS_HEADER pSrcImgDosHdr, pDestImgDosHdr;PIMAGE_NT_HEADERS pSrcImgNtHdr, pDestImgNtHdr;PIMAGE_SECTION_HEADER pSrcImgSecHdr, pDestImgSecHdr;PIMAGE_IMPORT_DESCRIPTOR pSrcImpDesc, pDestImpDesc;HANDLE hSrcFile, hDestFile;HANDLE hSrcMap, hDestMap;LPVOID lpSrcBase, lpDestBase;// 打开源文件与目标文件hSrcFile = CreateFile(pSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);if (hSrcFile == INVALID_HANDLE_VALUE)return;hDestFile = CreateFile(pDest, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);if (hDestFile == INVALID_HANDLE_VALUE)return;// 分别创建两份磁盘映射hSrcMap = CreateFileMapping(hSrcFile, NULL, PAGE_READONLY, 0, 0, 0);hDestMap = CreateFileMapping(hDestFile, NULL, PAGE_READWRITE, 0, 0, 0);// MapViewOfFile 设置到指定位置lpSrcBase = MapViewOfFile(hSrcMap, FILE_MAP_READ, 0, 0, 0);lpDestBase = MapViewOfFile(hDestMap, FILE_MAP_WRITE, 0, 0, 0);pSrcImgDosHdr = (PIMAGE_DOS_HEADER)lpSrcBase;pDestImgDosHdr = (PIMAGE_DOS_HEADER)lpDestBase;printf("[+] 原DOS头: 0x%08X --> 目标DOS头: 0x%08X \n", pSrcImgDosHdr, pDestImgDosHdr);pSrcImgNtHdr = (PIMAGE_NT_HEADERS)((DWORD)lpSrcBase + pSrcImgDosHdr->e_lfanew);pDestImgNtHdr = (PIMAGE_NT_HEADERS)((DWORD)lpDestBase + pDestImgDosHdr->e_lfanew);printf("[+] 原NT头: 0x%08X --> 目标NT头: 0x%08X \n", pSrcImgNtHdr, pDestImgNtHdr);pSrcImgSecHdr = (PIMAGE_SECTION_HEADER)((DWORD)&pSrcImgNtHdr->OptionalHeader + pSrcImgNtHdr->FileHeader.SizeOfOptionalHeader);pDestImgSecHdr = (PIMAGE_SECTION_HEADER)((DWORD)&pDestImgNtHdr->OptionalHeader + pDestImgNtHdr->FileHeader.SizeOfOptionalHeader);printf("[+] 原节表头: 0x%08X --> 目标节表头: 0x%08X \n", pSrcImgSecHdr, pDestImgSecHdr);DWORD dwImpSrcAddr, dwImpDestAddr;dwImpSrcAddr = pSrcImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;dwImpDestAddr = pDestImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;printf("[-] 原始IAT虚拟地址: 0x%08X --> 目标IAT虚拟地址: 0x%08X \n", dwImpSrcAddr, dwImpDestAddr);dwImpSrcAddr = (DWORD)lpSrcBase + RvaToFoa(pSrcImgNtHdr, lpSrcBase, dwImpSrcAddr);dwImpDestAddr = (DWORD)lpDestBase + RvaToFoa(pDestImgNtHdr, lpDestBase, dwImpDestAddr);printf("[+] 导入表原始偏移: 0x%08X --> 导入表目的偏移: 0x%08X \n", dwImpSrcAddr, dwImpDestAddr);// 定位导入表pSrcImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)dwImpSrcAddr;pDestImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)dwImpDestAddr;printf("[*] 定位原始导入表地址: 0x%08X --> 定位目的导入表地址: 0x%08X \n\n\n", pSrcImpDesc, pDestImpDesc);PIMAGE_THUNK_DATA pSrcImgThkDt, pDestImgThkDt;// 循环遍历导入表,条件是两者都不为空while (pSrcImpDesc->Name && pDestImpDesc->Name){char *pSrcImpName = (char*)((DWORD)lpSrcBase + RvaToFoa(pSrcImgNtHdr, lpSrcBase, pSrcImpDesc->Name));char *pDestImpName = (char*)((DWORD)lpDestBase + RvaToFoa(pDestImgNtHdr, lpDestBase, pDestImpDesc->Name));pSrcImgThkDt = (PIMAGE_THUNK_DATA)((DWORD)lpSrcBase + RvaToFoa(pSrcImgNtHdr, lpSrcBase, pSrcImpDesc->FirstThunk));pDestImgThkDt = (PIMAGE_THUNK_DATA)((DWORD)lpDestBase + RvaToFoa(pDestImgNtHdr, lpDestBase, pDestImpDesc->FirstThunk));printf("\n [*] 链接库: %10s 原始偏移: 0x%08X --> 修正偏移: 0x%08X \n\n", pDestImpName, *pDestImgThkDt, *pSrcImgThkDt);// 开始赋值,将原始的IAT表中索引赋值给目标地址while (*((DWORD *)pSrcImgThkDt) && *((DWORD *)pDestImgThkDt)){DWORD dwIatAddr = *((DWORD *)pSrcImgThkDt);*((DWORD *)pDestImgThkDt) = dwIatAddr;printf("\t --> 源RVA: 0x%08X --> 拷贝地址: 0x%08X --> 修正为: 0x%08X \n", pSrcImgThkDt, pDestImgThkDt, dwIatAddr);pSrcImgThkDt++;pDestImgThkDt++;}pSrcImpDesc++;pDestImpDesc++;}UnmapViewOfFile(lpDestBase); UnmapViewOfFile(lpSrcBase);CloseHandle(hDestMap); CloseHandle(hSrcMap);CloseHandle(hDestFile); CloseHandle(hSrcFile);
}void Banner()
{printf(" ____ _ _ _ ___ _ _____ \n");printf("| __ ) _ _(_) | __| | |_ _| / \\|_ _| \n");printf("| _ \\| | | | | |/ _` | | | / _ \\ | | \n");printf("| |_) | |_| | | | (_| | | | / ___ \\| | \n");printf("|____/ \\__,_|_|_|\\__,_| |___/_/ \\_\\_| \n");printf(" \n");printf("IAT 修正拷贝工具 By: LyShark \n");printf("Usage: BuildIat [脱壳前文件] [脱壳后文件] \n\n\n");
}int main(int argc, char * argv[])
{Banner();if (argc == 3){// 使用原始的IAT表覆盖dump出来的镜像BuildIat(argv[1], argv[2]);}return 0;
}
代码的使用很简单,分别传入脱壳前文件路径,以及脱壳后的路径,则读者可看到如下图所示的输出信息,至此即实现了脱壳修复功能。
本文作者: 王瑞
本文链接: https://www.lyshark.com/post/ff060496.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章:

2.9 PE结构:重建导入表结构
脱壳修复是指在进行加壳保护后的二进制程序脱壳操作后,由于加壳操作的不同,有些程序的导入表可能会受到影响,导致脱壳后程序无法正常运行。因此,需要进行修复操作,将脱壳前的导入表覆盖到脱壳后的程序中,以…...

MybatisPlus插件功能详细介绍 自动分页 通用分页实体
本课程全面讲解了Mybatis框架的使用,从快速入门到原理分析再到实战应用。每一个知识点都有案例进行演示学习,最终通过学习你将全面掌握,从而使Mybatis的开发更加的高效,系统学习 通过项目的开发大家应该能发现,单表的C…...

ES kibana 创建索引快速脚本
删除 DELETE my_test创建索引 创建自定义ngram分词器 PUT my_test {"settings": {"index.max_ngram_diff": "32","analysis": {"analyzer": {"code_analyzer": {"tokenizer": "code_tokenizer&q…...

2023年09月编程语言流行度排名
点击查看最新编程语言流行度排名(每月更新) 2023年09月编程语言流行度排名 编程语言流行度排名是通过分析在谷歌上搜索语言教程的频率而创建的 一门语言教程被搜索的次数越多,大家就会认为该语言越受欢迎。这是一个领先指标。原始数据来自…...

linux对一个文件夹中的所有文件重命名
在Linux中,你可以使用mv命令对一个文件夹下的所有文件进行重命名。下面是几种常见的用法: 方法1: 批量添加前缀或后缀: $ cd 目标文件夹路径 $ for file in *; do mv "$file" "前缀$file"; done # 添加前缀 $ for fil…...

Greenplum执行SQL卡住的问题
问题 今天社区群里面一位同学反映他的SQL语句执行会hang住,执行截图如下。 分析 根据提示信息,判断可能是网络有问题,或者是跟GP使用UDP包有关系。 此同学找了网络检查的人确定网络没有问题,于是猜测跟UDP包有关。 参考文章ht…...

Discourse 的系统日志
Discourse 提供了较为完善的日志查看方式。 用得最多的可能就是 Logster 的基于 Web 的 UI 了。 Logster Discourse 的错误日志面板用的是 logster,采集的是 Rails/Rack 的日志,正常应该用 Rails::Logger 但是 discourse 做了封装。 正常的访问地址为…...

【7z密码】如何给7z压缩包加密、解密?
7z压缩包是压缩率最大的格式,也有很多朋友会使用7z格式,那么7z压缩包如何进行加密、解密?今天给大家介绍详细教程。 7-zip加密 右键文件选择7-zip打开压缩软件进行压缩或者在打开7-zip软件找到需要压缩的文件,点击添加ÿ…...

InnoDB为什么使用B+Tree
分析&回答 1.B Tree的层数较少 B类树的一个很鲜明的特点就是数的层数比较少,而每层的节点非常多,树的每个叶子节点到根节点的距离都是相同的; 2. 减少磁盘IO; 树的每一个节点都是一个数据也,这样每个节点只需…...

【Spring Bean的生命周期实现方式】
文章目录 Spring Bean的生命周期实现方式实例化属性赋值初始化销毁Spring Bean的生命周期实现方式 Spring Bean的生命周期决定了一个Bean的整个生命周期,它分为四个阶段:实例化、属性赋值、初始化和销毁。 实例化 实例化通过构造器实例化和工厂方法实例化两种方式实现;构…...

腾讯云PK阿里云2核2G云服务器租用价格表
2核2G云服务器可以选择阿里云服务器或腾讯云服务器,腾讯云轻量2核2G3M带宽服务器95元一年,阿里云轻量2核2G3M带宽优惠价108元一年,不只是轻量应用服务器,阿里云还可以选择ECS云服务器u1,腾讯云也可以选择CVM标准型S5云…...

【美团3.18校招真题2】
大厂笔试真题网址:https://codefun2000.com/ 塔子哥刷题网站博客:https://blog.codefun2000.com/ 最多修改两个字符,生成字典序最小的回文串 提交网址:https://codefun2000.com/p/P1089 由于字符串经过修改一定为回文串&#x…...

一文带你快速入门『YOLOv8』
前言 本文是 YOLOv8 入门指南(大佬请绕过),将会详细讲解安装,配置,训练,验证,预测等过程 YOLOv8 官网:ultralytics/ultralytics: NEW - YOLOv8 🚀 in PyTorch > ONN…...

# 将PCL点云转换为Eigen向量进行运算
将PCL点云转换为Eigen向量进行运算 在处理点云数据时,我们常需要将PCL中的点云转换为Eigen向量,进行一些矩阵运算。这里介绍PCL点云到Eigen向量的两种转换方法。 点云转换为Eigen数组 对于一个PCL的点云,可以通过getArray4fMap()函数获取Eigen数组表示: // PCL点云 pcl::Po…...

elmentui表单重置及出现的问题
一、表单: 二、代码——拿官方的代码举例(做了一些小改动): 改动:model绑定的字段,由form改为queryParams ref绑定的字段form改为queryFrom 注:model绑定的这个字段用来做数据双向绑定的 注:ref绑定的这…...

游戏平台加盟该怎么做?需要准备什么?
游戏平台加盟是一种合作模式,允许个人或企业以加盟商的身份参与游戏平台,并从中获得一定的权益和收益。以下是一些步骤和需要准备的事项,来考虑如何进行游戏平台加盟: 步骤: 研究市场和平台:了解游戏市场和…...

selenium中定位shadow-root,以及获取shadow-root内部的数据
通过shadow-root的父级定位到shadow-root,再通过语句进行操作 两种方法: 第一种,Python种JS实现 第二种,selenium实现 1.0 案例网站 参考某橘色网站 2.0 js语句定位 可在控制台进行测试 测试语句 document.querySelector("ali-ba…...

OpenCV(三十二):轮廓检测
1.轮廓概念介绍 在计算机视觉和图像处理领域中,轮廓是指在图像中表示对象边界的连续曲线。它是由一系列相邻的点构成的,这些点在边界上连接起来形成一个封闭的路径。 轮廓层级: 轮廓层级(Contour Hierarchy)是指在包含…...

接口自动化测试做线上巡检,如何避免数据污染
在接口自动化测试中,避免数据污染是非常重要的,特别是在线上环境中进行巡检。 1. 使用独立的测试环境:建议使用专门的测试环境来进行接口自动化测试,而不是直接在生产环境中进行。测试环境应该是一个独立的、与生产环境隔离的环境…...

C++ 指针
C 指针 学习 C 的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。 正如您所知…...

SpringBoot集成kubernetes-client升级k8s后初始化失败问题
SpringBoot集成kubernetes-client升级k8s后初始化失败问题 1.问题描述 程序以前使用的k8s版本是1.16,fabric8.kubernetes-client的版本是4.10.2,springboot版本是2.3.5。由于环境切换,这次需要升级k8s的版本,现在将k8s版本升级到…...

MySQL 学习笔记
😀😀😀创作不易,各位看官点赞收藏. 文章目录 MySQL 学习笔记1、DQL 查询语句1.1、基本查询1.2、函数查询1.2.1、单行函数1.2.2、聚合函数 1.3、复杂查询1.3.1、连接查询1.3.2、子查询 1.4、SQL 语句 执行顺序 2、DDL 定义语句2.1、…...

Docker 的常用命令
0 基本命令 概述 [root192 home]# docker --helpUsage: docker [OPTIONS] COMMANDA self-sufficient runtime for containersOptions:--config string Location of client configfiles (default "/root/.docker")-c, --context string Name of the context…...

嵌入式-电子电路四个基本定律
目录 1、欧姆定律 2、焦耳定律 3、基尔霍夫电流定律 4、基尔霍夫电压定律 1、欧姆定律 欧姆定律是关于导体两端电压与导体中电流关系的定律。具体表述为:在同一电路中,通过某段导体的电流跟这段导体两端的电压成正比,跟这段导体的电阻成反…...

【linux命令讲解大全】083.Linux 常用命令ispell , spell , atrm, chattr
文章目录 ispell补充说明语法参数 spell补充说明语法参数 atrm补充说明语法选项参数 实例 chattr补充说明语法选项 实例 从零学 python ispell 检查文件中出现的拼写错误。 补充说明 ispell命令用于检查文件中出现的拼写错误。 语法 ispell [参数] 参数 文件:…...

JAVA实现SAP接口
JAVA实现SAP接口 环境spring-bootmaven 1.maven依赖 <dependency><groupId>com.github.virtualcry</groupId><artifactId>sapjco-spring-boot-starter</artifactId><version>3.1.4</version></dependency>2.配置文件 applic…...

华南理工大学811信号与系统考研分数线,招生人数,报考统计,考情分析,就业,真题,大纲,参考书,华工811
华南理工大学811信号与系统考研分数线,招生人数,报考统计,考情分析,就业,真题,大纲,参考书,华工811 华南理工大学811信号与系统考研分数线,招生人数,报考统…...

Android 字符串 占位符
在 Android 中,字符串中的 % 后面跟着的字符称为格式化占位符,用于指示在运行时将值插入到字符串中的位置,并指定插入的值的类型。常见的格式化占位符类型如下: %s:字符串占位符。它用于插入字符串值。 String name …...

vue页面添加水印(可用于H5,APP)
vue页面添加水印 背景实现新建vue组件使用效果 尾巴 背景 最近实现了一个小功能,就是给页面添加背景水印。实现思路就是定义一个宽高充满屏幕的组件,然后使用绝对定位并通过层级控制让水印显示在页面的最前端。 实现 代码相对简单,相信有点…...

下载git
1.官网下载可能会有访问失败 2.用其他的镜像源下载 快 准 狠 CNPM Binaries Mirror...