逆向分析 FSViewer 并写出注册机

news2024/11/17 4:27:19

逆向分析 FSViewer 并写出注册机

FSViewer是一款老牌的图片管理查看编辑软件, 个人使用免费, 商用收费
本文将逆向分析FSViewer 7.5版本的注册算法并编写注册机

0. 前言

最近在整理之前的资料, 发现了一篇几年前刚学逆向那会儿写的文章, 是跟着看雪一位大牛的文章做的, 但逆向方法不同, 应该算是自己能逆向出算法的第一款软件了, 发出来纪念一下

借鉴了看雪 @深山修行之人 大牛的文章: FastStone Image Viewer注册算法分析+KeyGen

1. 准备工作

1.1 判断程序语言以及加密情况

用ExeInfo PE查看一下:
 

PEInfo


可以看到是32位程序,用Delphi语言编写,未加壳

对于Delphi程序,自然要使用针对delphi的大杀器: IDR(Interactive Delphi Reconstructor)

1.2 定位btnRegisterCliced函数

通过IDR找到注册按钮被点击事件:

BtnRegister

可知: btnRegisterCliced : 0x72F248

1.3 导出并加载map和idc文件

虽然IDR可以识别Delphi的大部分函数,但是只有反汇编,如果要看流程图和反编译还是需要ida

而IDR提供了导出map和idc文件的功能,所以用IDR导出map和idc文件分别供调试器和ida使用:

  • 跑完idc脚本后ida效果还是可以的(需要4-5分钟)
  • 这里我使用x32dbg加载map文件进行动态调试(OD的LoadMapEx总是崩溃,可以在xp下使用)

2. 分析注册流程

Delphi遵循_fastcall调用约定,但是与Windows的_fastcall略有不同,参数顺序为eax为第一个参数、edx为第二个参数、ecx为第三个参数,大于3个的参数通过堆栈传递,大于三个的堆栈顺序从左到右依次压栈,堆栈由被调用者恢复

整体的注册逻辑流程很简单,而且关键点在验证函数,所以不再赘述,这里贴出我分析出的伪C代码:

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

58

59

void OnBtnRegisterClicked(void *this)

{

    String userName = Trim(TControl_GetText(edit)); // [ebp-0x18]->[ebp-0x4]

    String regiCode = Trim(TControl_GetText(edit)); // [ebp-0x20]->[ebp-0x1C]

    String upperCode = upperCase(regiCode); // [ebp-0xC]

    String trueCode = NULL; // [ebp-0x10]

    if((LStrLen(upperCode)-1) >= 0)

    {

        int len = LStrLen(upperCode); // [ebp-0x14]

        for(int i = 0; i < len; i++)

        {

            if (upperCode[i] >= 0x41 && upperCode[i] <= 0x5A) // in [A,Z]

            {

                String toCat = NULL; // [ebp-0x24]

                LStrFromChar(toCat, upperCode[i]);

                LStrCat(trueCode, toCat);

            }

            if (LStrLen(trueCode) == 5 || 11 || 17)

                LStrCat(trueCode, "-");

        }

    }

    String cpTrueCode = NULL; // [ebp-0x8]

    LStrLAsg(cpTrueCode, trueCode); // StrCopy

    /*

    userName = Viking

    trueCode = ABCDE-FGHIJ-KLMNO-PQRST

    */

    if (userName != NULL)

    {

        if ((sub_72E770(this, userName, cpTrueCode, 0) &&

            sub_72EBFC(this, userName, cpTrueCode, 0))

                ||

            (sub_72E770(this, userName, cpTrueCode, 1) &&

            sub_72EBFC(this, userName, cpTrueCode, 1)))

        {

            int res = GetLicenseType(cpTrueCode); // sub_72F030

            if (res > 1)

            {

                if (res == 4999)

                    MessageBox("Corporate Site");

                else if (res >= 5000)

                    MessageBox("Corporate Worldwide");

                else

                    MessageBox("Multiple User");

            }

            else

                MessageBox("Singel User");

        }

        else

            MessageBox("无效用户名或注册码");

    }

    else

        MessageBox("用户名为空");

     

    return 0;

}

总体流程就是:
注册码为20个字母, xxxxx-xxxxx-xxxxx-xxxxx五个一组, 只有纯字母
将用户名和注册码传入sub_72E770和sub_72EBFC这两个验证函数
只要sub_verify1(..., 0) && sub_verify2(..., 0)
或者sub_verify1(..., 1) && sub_verify2(..., 1)有一个分支成立即可


3. 分析验证函数sub_72E770,sub_72EBFC

以userName = Viking
registerCode = ABCDE-FGHIJ-KLMNO-PQRST作为输入

3.1 验证函数sub_72E770(...)

用x32dbg步进sub_72E770:一开始会将userName和registerCode都转为大写
通过动态调试可知,一开始会将注册码中的'-'去掉:
 

strip-


此后会进行某种循环,通过动调和ida静态分析可知:
 

mix


 

mix-ida


以上循环是将userName和registerCode的前8位交叉混合起来
具体逻辑是:(userName[i])占偶数位,(Code前8位[i])占奇数位,超出8位的字符直接补到后面
本例输入在交叉后的字符串即:VAIBKCIDNEGFGH
 
之后将注册码前8位和内置的两个字符串拼接起来,作为某种加密函数sub_71FB90的参数:
 

crypt1


之后有个一判断分支,判断第三个参数arg3是0还是1, 并且也调用了sub_71FB90函数:
 

verify1-if


(这里贴出伪C代码):

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

if (flag)

{

    //0x72E99C

    String toCat[2] = {0,}; // [ebp-0x54,0x58]

    WStrFromLStr(toCat[0], cp8Code);

    WStrFromLStr(toCat[1], n8Str, "96888", toCat[0]);

    String cp8N8Str = NULL; // [ebp-0x50]

    WStrCatN(cp8N8Str, 3, toCat[1]);

    String cp8N8Str1 = NULL; // [ebp-0x4C]

    LStrFromWStr(cp8N8Str1, cp8N8Str);

    DCPcrypt2.encrypt(VMT_723D04_TDCP_sha512, cp8N8Str1);// 0x71FB90

}

else

{

    String toCat[2] = {0,}; // [ebp-0x64,0x68]

    WStrFromLStr(toCat[0], cp8Code);

    WStrFromLStr(toCat[1], n8Str, "96332", toCat[0]); // ??

    String cp8N8Str = NULL; // [ebp-0x60]

    WStrCatN(cp8N8Str, 3, toCat[1]);

    // result: cp8N8Str = cp8Code+96332+n8Str

    // ABCDEFGH96332VAIBKCIDNEGFGH

    String cp8N8Str1 = NULL; // [ebp-0x5C]

    LStrFromWStr(cp8N8Str1, cp8N8Str);

    DCPcrypt2.encrypt(VMT_723D04_TDCP_sha512, cp8N8Str1);// 0x71FB90

}

以上这一块代码是将注册码前8个字符+96332+交叉字符串拼接起来并作为sub_71FB90函数的参数
 
再之后(伪代码和汇编可能有些出入,因为我是按整体逻辑写的,逻辑是正确的):

1

2

3

4

5

6

7

String encodeStr1 = NULL; // [ebp-0x20]

DCPcrypt2.sub_71FEB4(Base64, n8Str, encodeStr1);

// mzvoPqb8etggqNJ9TqI=

String encodeStr2 = NULL; // [ebp-0x6C]

DCPcrypt2.sub_71FEB4(Base64, encodeStr1, encodeStr2);

// 4hQ99VfA1SHNNrjvHQv78MSew2Q=

LStrLAsg(encodeStr1, encodeStr2); // copy

进行两次加密,且输出都是base64
最后就是进行判断了:从加密字符串里取前八个大写字母和输入的注册码的第9-16个字符比较
 

ret


如果相等则返回TRUE,不相等则返回FALSE
那么关键点就在于sub_71FB90和sub_71FEB4这两个函数.

3.1.1 分析sub_71FB90函数

可以看到idr将此函数识别为DCPcrypt2模块的函数
 

cryptName


猜测sub_71FB90和sub_71FEB4这两个函数都是某种密码学算法
可以用插件识别一下程序中的密码学算法有哪些:

1

2

3

4

5

BASE64 table :: 0058677C :: 00987B7C

BLOWFISH [sbox] :: 005810C8 :: 009824C8

SHA1 [Compress] :: 003228C5 :: 007234C5

SHA-512 [init] :: 0032D559 :: 0072E159

IDEA

跟进sub_71FB90函数可以发现一些很明显的特征:
 

sha1Iint


 

initABCDE


以上处是在初始化sha1的链接变量
sub_71FA88将拼接字符串作为输入,判断出是sha1摘要
 

sub_71FA88


此处进行最终sha1
 

final-sha1


根据x32dbg提示可知此函数是一个分组加密函数:
 

blowfish


跟进去可以发现是blowfish初始化s,p_box:
 

blowfish-init


注意:初始化完后,又调用了一次bf主加密函数,参数为8个0:

bf_fn

总体流程即:
用拼接字符串的sha1 hash值作为密钥初始化blowfish的s,p_box,再调用bf主加密函数加密8个0
(伪C代码:)

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

// ebx = arg1, [ebp-4] = arg2, [ebp-8] = arg3

void sub_0071FB90(void *arg1, String encodeStr, void *vtable)

{

    if (arg1[48])

        DCPblowfish.sub_7210D0(arg1, *arg); // init

    int ret = 0;

    if ( (ret = DCPsha1.sub_723928()) < 0) // 恒为160

        ret += 7;

    ret = ret >> 3; // 算数右移 = 20

    BYTE *shaMem = GetMem(ret); // malloc // esi

    TComponent.Create(1, arg1);

    DCPsha1.initABCDE(shaMem); //sub_723A04();初始化链接变量ABCDE

    // 进行sha1

    DCPcrypt2.sub_71FA88(shaMem, encodeStr);//->DCPsha1.sub_723A7   (encodeStr);

    DCPsha1.sub_723B24(shaMem);// 最终sha1,结果在mem

    TObject.Free(...);

    ret = DCPblowfish.sub_720E28(); // 恒为448

    ret1 = DCPsha1.sub_723928(); // 恒为160

    if (ret < ret1) // 恒不会进入的分支

    {

        ...

    }

    DCPblockcipher.sub_720244(arg1, shaMen, 160);// 初始化p,s_box

    Bf_fn();

}

同样的,在if分支里调用的sub_71FB90函数和上面的流程一样,但是传入的虚表变了,是用sha512 hash值作为密钥初始化idea算法

3.1.2 分析sub_71FEB4函数

将交叉码作为参数输入
 

sub_71FEB4


函数体内就两个关键函数:一个加密函数,一个base64函数
 

sub_71FEB4_2


跟进加密函数:发现blowfish加密主函数
 

sub_71FEB4_3


以上为循环处理明文,将加密data(8个0)后的首个字节与明文第[i]个异或,然后将data向左移动1个字节,将异或结果补到最后作为下次bf加密输入
伪C代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

0x71FEFE| call dword ptr ds:[esi+7C] | [esi+7C]:DCPblockciphers.sub_007205D8_007205D8

arg1@eax = this/vtable , arg2@edx = "VA.."待异或字符串 , arg3 = 0xA6FDB18 结果输出

{

    // [ebp-4] = arg3@ecx

    // [ebp-8] = arg2@edx

    for (int i = 0; i < len(arg2); ++i)

    {

        BYTE *ret = ...;

        BYTE var[8] = {全0数据加密后的8bytes};

        DCPblowfish.sub_721104(this, var, result);

        BYTE tmp = result[0]^arg2[i];  

        ret[i] = tmp;      

        move(var[0], var[1], 7);

        var[7] = tmp;

    }

}

最后将结果result进行base64编码输出
同样的,之后调用的sub_71FB90函数和上面的流程一样,传入的待异或字符串是上次的base64字符串,并且是调用的idea算法加密
整体流程伪C代码:

1

2

3

4

5

6

7

String encodeStr1 = NULL; // [ebp-0x20]

DCPcrypt2.sub_71FEB4(Base64, n8Str, encodeStr1);// blowfis+xor+base64

// mzvoPqb8etggqNJ9TqI=

String encodeStr2 = NULL; // [ebp-0x6C]

DCPcrypt2.sub_71FEB4(Base64, encodeStr1, encodeStr2);// ide+xor+base64

// 4hQ99VfA1SHNNrjvHQv78MSew2Q=

LStrLAsg(encodeStr1, encodeStr2); // copy

3.2 验证函数sub_72EBFC(...)

sub_72EBFC与sub_72E770非常相似:

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

//int func@<eax> (String @<edx>, String @<ecx>, int @<stack>)

int sub_72EBFC(String userName, String trueCode, int flag)

{

    /*

    [ebp-0x4] = this

    [ebp-0x8] = userName

    [ebp-0xC] = trueCode

    */

    LStrAddRef(userName);

    LStrAddRef(trueCode); // 增加字符串引用计数

    ....

    //与第一种加密函数相同

    ....

    //sha512摘要作为密钥初始化blowfish

    DCPcrypt2.encrypt(VMT_sha512, "0923284924839834...", vtable);// 0x71FB90

    if (flag)

    {

        // 0x72EDD7

        String code = 拼接注册码前8个字符+96888+交替码

        DCPcrypt2.encrypt(VMT_sha1, code);// 0x71FB90

    }

    else

    {

        // 0x72EE31

        String code = 拼接注册码前8个字符+96332+交替码

        DCPcrypt2.encrypt(VMT_sha1, code);// 0x71FB90

        // sha1摘要作为密钥初始化idea  

    }// 与第一种相同   

    String ecode = name & code 交错;

    int count = regiCode[0] - 0x32;

    if (cout >= 0)

    {

        cout += 1;

        for (int i = 0; i < count; ++i)

        {

            DCPcrypt2.sub_71FEB4(this, ecode, resIdea);// idea+xor+base64

        }

    }  

    DCPcrypt2.sub_71FEB4(this, resIdea, resBlfh);// blowfish+xor+base64

    String final4code = 从resBlfh里取前4个大写字母

    String codeCmpStr = NULL; // [ebp-0x74]

    LStrCopy(tuTrueCode, 17, 4, codeCmpStr); //(最后四个字符) QRST

    if (!LStrCmp(final4code, codeCmpStr)) // 相等

        return 1;

    else

        return 0;

}

此处的加密方式变成了sha512初始化blowfish,sha1初始化idea,并用注册码[0]-0x32作为循环次数,循环进行idea加密
最后进行判断:从加密字符串里取前四个大写字母和输入的注册码的第17-20个字符(最后四个)比较
如果相等则返回TRUE,不相等则返回FALSE


4. 获取注册类型

当验证函数通过后就会调用sub_72F030函数获取注册类型

gettype


伪C代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

int sub_72F030(this, cpTrueCode)

{

    /*

    [ebp-4] = arg2

    */

    String regiCode = cpTrueCode; // [ebp-4]

    regiCode = Trim(UpperCase(trueCode)); // [ebp-0x4]->[ebp-0x14]- [ebp-0x10]  

    if (LStrLen(regiCode) == 23)

    {

        regiCode = regiCode去掉横线; // [ebp-0x10]

    }  

    if (LStrLen(regiCode) == 20)

    {

        /*

        String toCat[] = {0,}; // [ebp-0x2C]

        LStrCopy(regiCode, 4, 1, toCat[0]);

        IntToStr(toCat[0]-0x4D, result);

        */

        取注册码的第4862位的字符

        得到[3]-77,[7]-68,[5]-73,[1]-79拼接而成的字符串

        return 将字符串转为int;

    }

}

注册类型见 2. 分析注册流程 伪代码部分


5. 写注册机

至此流程分析完毕,注册机按注册流程写一遍就出来了,详细代码请见附件, 逆向使用的FSViewer75版本也见附件

keygen


 

fsviewer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1501659.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

三井住友保险中国区信息技术部负责人陈婧,将出席“ISIG-RPA超级自动化产业发展峰会”

3月16日&#xff0c;第四届「ISIG中国产业智能大会」将在上海中庚聚龙酒店拉开序幕。本届大会由苏州市金融科技协会指导&#xff0c;企智未来科技&#xff08;RPA中国、AIGC开放社区、LowCode低码时代&#xff09;主办。大会旨在聚合每一位产业成员的力量&#xff0c;深入探索R…

Linux:预备

计算机结构基础 操作系统: 内核 (管理软硬件) shell(给用户使用操作系统的方式) 操作系统的目标 对硬件抽象 原因:操作系统是对软硬件资源管理的应用软件抽象:内存管理, 进程管理, 文件管理, 驱动管理软件:驱动程序(给软件提供访问硬件的软件)硬件:磁盘(对应文件), 网卡等隔离…

【C++】什么是类与对象?

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:C ⚙️操作环境:Visual Studio 2022 面向对象概述 面向对象是一种符合人类思维习惯的编程思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系。在程序中使用对象来映射现实中的事物,使用对象的关…

基于SpringBoot+Vue+ElementUI+Mybatis前后端分离管理系统超详细教程(四)——前后端数据交互

经过前面几个章节的学习我们掌握了&#xff1a; 1、如何使用Vue快速搭建前端工程化项目&#xff0c;并结合elementUI优化了界面&#xff1b; 基于SpringBootVueElementUIMybatis前后端分离管理系统超详细教程&#xff08;一&#xff09; 基于SpringBootVueElementUIMybatis前后…

软件测试入门

文章目录 一、入门1. 软件2. 软件基本组成3. 软件产生过程4. 软件测试5. 软件测试目的&#x1f3c6; 小结 二、测试主流技能1. 功能测试2. 自动化测试3. 接口测试4. 性能测试&#x1f3c6; 小结 三、测试分类1. 按测试阶段划分2. 按代码可见度划分&#x1f3c6; 小结 三、质量模…

信息系统项目管理师003:信息化(1信息化发展—1.1信息与信息化—1.1.3 信息化)

文章目录 1.1.3 信息化1.信息化内涵2.信息化体系3.信息化趋势 要点总结 1.1.3 信息化 信息化是一个过程&#xff0c;与工业化、现代化一样&#xff0c;是一个动态变化的过程。信息化是指培养、发展以计算机为主的智能化工具为代表的新生产力&#xff0c;并使之造福于社会的历史…

通过sqoop把hive数据到mysql,脚本提示成功,mysql对应的表中没有数

1、脚本执行日志显示脚本执行成功&#xff0c;读写数量不为0 2、手动往Mysql对应表中写入数据十几秒后被自动删除了 问题原因&#xff1a; 建表时引擎用错了&#xff0c;如下图所示 正常情况下应该用InnoDB

7-4 哲哲打游戏(Python)

哲哲是一位硬核游戏玩家。最近一款名叫《达诺达诺》的新游戏刚刚上市&#xff0c;哲哲自然要快速攻略游戏&#xff0c;守护硬核游戏玩家的一切&#xff01; 为简化模型&#xff0c;我们不妨假设游戏有 N 个剧情点&#xff0c;通过游戏里不同的操作或选择可以从某个剧情点去往另…

偶极子和环形天线的辐射机理仿真分析

目录 0 引言 1 偶极子天线的辐射因素分析 1.1 偶极子天线模型设计 1.2 谐振点的出现规律 1.3 天线尺寸对辐射的影响 1.4 天线角度对辐射的影响

浅显易懂:WinForms、WPF和Electron的区别和优缺点

在开发桌面应用的时候&#xff0c;WinForms、WPF和Electron是绕不过去的三个技术栈&#xff0c;本文就详细据介绍了三者的区别和优缺点&#xff0c;帮助老铁们做个抉择。 一、winform wpf Electron 三者区别 WinForms、WPF和Electron是三种不同的框架和技术&#xff0c;用于开…

计算机中丢失缺少mfc100.dll文件该如何解决?

当你打开某个应用程序时&#xff0c;有时候会遇到一个“mfc100.dll丢失”或找不到mfc100.dll的错误信息提示。这种情况表明你的计算机缺少一个名为mfc100.dll的动态链接库文件。这个文件是由Microsoft VC 2010 Redistributable Package提供的&#xff0c;它是一组可重用的组件&…

【校园导航小程序】2.0版本 静态/云开发项目 升级日志

演示视频 【校园导航小程序】2.0版本 静态/云开发项目 演示 首页 重做了首页&#xff0c;界面更加高效和美观 校园指南页 新增了 “校园指南” 功能&#xff0c;可以搜索和浏览校园生活指南 地图页 ①弃用路线规划插件&#xff0c;改用SDK开发包。可以无阻通过审核并发布…

Linux的top命令解析

Top命令是什么 TOP命令是Linux下常用的性能分析工具&#xff0c;能够实时显示系统中各个进程的资源占用状况。 TOP是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止.比较准确的说,top命令提供了实时的对系…

qml中toolbox控件、ComboBox控件、PlainText实现及美化

一. 内容简介 qml中toolbox控件、ComboBox控件、PlainText实现及美化 二. 软件环境 2.1vsCode 2.2Anaconda version: conda 22.9.0 2.3pytorch 安装pytorch(http://t.csdnimg.cn/GVP23) 2.4QT 5.14.1 新版QT6.4,&#xff0c;6.5在线安装经常失败&#xff0c;而5.9版本…

相对于 Linux,Windows Server 存在的意义是什么?

相对于 Linux&#xff0c;Windows Server 存在的意义是什么&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「Linux 的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给…

写作文的ai的软件有吗?分享4款热门的软件!

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到我们生活的方方面面&#xff0c;包括写作领域。许多AI工具如今能够帮助我们快速、高效地创作文章&#xff0c;无论是新闻稿、广告文案还是博客文章&#xff0c;它们都能提供有力的支持。今天&#…

linux安装todesk

xunilToDesk远程桌面软件-免费安全流畅的远程连接电脑手机ToDesk远程控制软件是一款稳定流畅的远程控制电脑手机连接软件,可远程桌面办公,远程协助运维.采用端对端加密,让每一次远程访问都安全可靠。https://www.todesk.com/linux.htmlToDesk远程控制软件是一款稳定流畅的远程控…

20.网络游戏逆向分析与漏洞攻防-网络通信数据包分析工具-数据分析工具数据类型编辑功能的实现

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易道云信息技术研究院VIP课 上一个内容&#xff1a;19.数据分析工具数据类型配置功能的实现 码云地址&#xff08;master 分支&#…

Androidstudio实现登录按钮按下变色

在activity_main.xml中&#xff0c;写如下代码&#xff1a; <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"androi…

华为---MSTP(一)---MSTP生成树协议

目录 1. MSTP技术产生背景 2. STP/RSTP的缺陷 ​编辑 2.1 无法均衡流量负载 2.2 数据使用次优路径 3. MSTP生成树协议 3.1 MSTP相关概念 3.2 MSTP树生成的形成过程 4. MSTP报文 1. MSTP技术产生背景 RSTP在STP基础上进行了改进&#xff0c;实现了网络拓扑快速收敛。但…