逆向分析 FSViewer 并写出注册机

逆向分析 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于yolov5的番茄检测系统,可进行图像目标检测,也可进行视屏和摄像检测(pytorch框架)【python源码+UI界面+功能源码详解】

功能演示&#xff1a; 基于yolov5的小番茄检测系统&#xff0c;系统既能够实现图像检测&#xff0c;也可以进行视屏和摄像实时检测_哔哩哔哩_bilibili &#xff08;一&#xff09;简介 基于yolov5的番茄检测系统是在pytorch框架下实现的&#xff0c;这是一个完整的项目&…

Vue 项目性能优化指南:提升应用速度与效率

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

【前端】-初始前端以及html的学习

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

(未解决)macOS matplotlib 中文是方框

reference&#xff1a; Mac OS系统下实现python matplotlib包绘图显示中文(亲测有效)_mac plt 中文值-CSDN博客 module ‘matplotlib.font_manager‘ has no attribute ‘_rebuild‘解决方法_font_manager未解析-CSDN博客 # 问题描述&#xff08;笑死 显而易见 # solve 找到…

CleanMyMac X4.15.0专为macOS设计的清理和优化工具

CleanMyMac X 是一款专为 macOS 设计的清理和优化工具。其基本功能和特点主要包括&#xff1a; 系统清理&#xff1a;CleanMyMac X 可以扫描并清除 macOS 系统中的垃圾文件&#xff0c;如缓存、日志、无用的语言文件等&#xff0c;从而释放硬盘空间并提高系统性能。应用程序管…

贪心算法(greedy algorithm,又称贪婪算法)详解(附例题)

目录 基本思想一&#xff09;概念二&#xff09;找出全局最优解的要求三&#xff09;求解时应考虑的问题四&#xff09;基本步骤五&#xff09;贪心策略选择六&#xff09;实际应用 1.零钱找回问题2.背包问题3.哈夫曼编码4.单源路径中的Djikstra算法5.最小生成树Prim算法 基本…

备考银行科技岗刷题笔记(持续更新版)

银行考试计算机部分复习 IEEE 802.11的帧格式 1.1 IEEE 802.11是什么&#xff1f; 802.11是国际电工电子工程学会&#xff08;IEEE&#xff09;为无线局域网络制定的标准。目前在802.11的基础上开发出了802.11a、802.11b、802.11g、802.11n、802.11ac。并且为了保证802.11更…

【电工学笔记】上册第一、二章

电工学 上次考试败在了单位&#xff0c;这次单位 一定要记熟。 第一章 电源或信号源的电压或电流称为激励,它推动电路工作; 由激励所产生的电压和电流称为响应。 复杂电路中,一般无法事先判断某个支路电流的 实际方向或者某个电路元件电压的实际方向 140V/4算不出总电阻的 …

thingsboard如何自定义udp-transport

0、参考netty实现udp的文章 https://github.com/narkhedesam/Netty-Simple-UDP-TCP-server-client/blob/master/netty-udp/src/com/sam/netty_udp/server/MessageDecoder.java 调试工具使用的是:卓岚TCP&UDP调试工具 1、在common\transport下面创建udp模块,仿照mqtt的创…

基于 HBase Phoenix 构建实时数仓(2)—— HBase 完全分布式安装

目录 一、开启 HDFS 机柜感知 1. 增加 core-site.xml 配置项 2. 创建机柜感知脚本 3. 创建机柜配置信息文件 4. 分发相关文件到其它节点 5. 重启 HDFS 使机柜感知生效 二、主机规划 三、安装配置 HBase 完全分布式集群 1. 在所有节点上配置环境变量 2. 解压、配置环境…

海外互联网专线主要解决企业哪些办公问题?

海外互联网专线 是一种专门为跨境企业提供的网络连接服务&#xff0c;旨在解决企业在海外办公过程中遇到的各种网络问题。海外互联网专线如何成为解决企业办公难题的利器&#xff0c;为企业提供稳定、高速的网络连接? 1、跨国远程办公&#xff1a; 随着全球化进程的加速&…

MyBatis Oracle 批量插入数据

MyBatis Oracle 批量插入数据 1.需求描述2.实现方案2.1 循环 insert 插入2.2 insert all 插入2.3 insert union all 插入 3.分析总结 系统&#xff1a;Win10 JDK&#xff1a;1.8.0_351 IDEA&#xff1a;2022.3.3 1.需求描述 在一次项目中实施过程中&#xff0c;后台需要将地区…

垃圾收集器底层算法

垃圾收集器底层算法 三色标记 在并发标记的过程中&#xff0c;因为标记期间应用线程还在继续跑&#xff0c;对象间的引用可能发生变化&#xff0c;多标和漏标的情况就有可能发生&#xff0c;这里我们引入“三色标记”来给大家解释下把Gcroots可达性分析遍历对象过程中遇到对象…

Apache的安装与目录结构

Apache的安装与目录结构 一、Apache是什么&#xff1f;Apache官网Apache下载地址 二、Apache的安装1.Apache在Windows安装1.启动“命令”窗口2.切换目录3.进行安装4.卸载命令5.启动Apache 服务 2.Apache在linux安装1.linux安装代码1.在 Fedora/CentOS/Red Hat Enterprise Linux…

Python 读取写入excel文件

使用Python读取和写入excel的xlsx、xls文件 目录 读取xlsx文件 安装三方库 引入三方库 读取数据 打开文件 表名 最大行数 最大列数 读取一张表 读取整个文件 返回xls整体内容 安装三方包 读取内容 写入xls文件 引入三方库 创建文件并写入数据 报错及解决 报错…

qt自定义时间选择控件窗口

效果如图&#xff1a; 布局如图&#xff1a; 参考代码&#xff1a; //DateTimeSelectWidget #ifndef DATETIMESELECTWIDGET_H #define DATETIMESELECTWIDGET_H#include <QWidget> #include <QDateTime>namespace Ui { class DateTimeSelectWidget; }class DateTim…

给一篇word注音可不可以只要拼音不要汉字 word中如何只保留拼音不要汉字

word中如何只保留拼音不要汉字&#xff0c;如果你想要只保留拼音而去除汉字&#xff0c;可以通过一系列步骤来实现。以下是一个详细的教程&#xff0c;帮助你完成这个任务。 首先&#xff0c;确保你的电脑已经安装了“汇帮注音大师”软件。如果没有&#xff0c;你需要安装一下…

仿12306校招项目业务五(敏感信息模块)

加密存储 数据加密背景 数据加密是指对某些敏感信息通过加密规则进行数据的变形&#xff0c;实现敏感隐私数据的可靠保护。 涉及客户安全数据或者一些商业性敏感数据&#xff0c;如身份证号、手机号、卡号、客户号等个人信息按照相关部门规定&#xff0c;都需要进行数据加密。…

app逆向-ratel框架-sekiro框架的安装使用

文章目录 一、前言二、初次尝试三、案例测试 一、前言 sekiro主要支持多节点的程序调用&#xff0c;所以他归属于RPC&#xff08;Remote Procedure Call&#xff09;框架&#xff1a;API管理、鉴权、分布式、负载均衡、跨语言 开源文档&#xff1a;https://sekiro.iinti.cn/s…

python使用Open3D 对点云重建与c++使用PCL对点云进行重建哪个效率更高

确定哪个库更高效取决于多个因素&#xff0c;包括算法实现、优化程度、硬件配置等。通常情况下&#xff0c;C 的 PCL 库在性能方面可能会比 Python 的 Open3D 库更高&#xff0c;因为 C 语言的编译器可以生成更高效的机器码&#xff0c;并且 PCL 库的底层实现是经过高度优化的。…