2.9 PE结构:重建导入表结构
脱壳修复是指在进行加壳保护后的二进制程序脱壳操作后,由于加壳操作的不同,有些程序的导入表可能会受到影响,导致脱壳后程序无法正常运行。因此,需要进行修复操作,将脱壳前的导入表覆盖到脱壳后的程序中,以使程序恢复正常运行。一般情况下,导入表被分为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 程序员,学习指针是很有必要的。 正如您所知…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...

基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...

【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

Ubuntu系统复制(U盘-电脑硬盘)
所需环境 电脑自带硬盘:1块 (1T) U盘1:Ubuntu系统引导盘(用于“U盘2”复制到“电脑自带硬盘”) U盘2:Ubuntu系统盘(1T,用于被复制) !!!建议“电脑…...

VisualXML全新升级 | 新增数据库编辑功能
VisualXML是一个功能强大的网络总线设计工具,专注于简化汽车电子系统中复杂的网络数据设计操作。它支持多种主流总线网络格式的数据编辑(如DBC、LDF、ARXML、HEX等),并能够基于Excel表格的方式生成和转换多种数据库文件。由此&…...

使用SSE解决获取状态不一致问题
使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件,这个上传文件是整体功能的一部分,文件在上传的过程中…...