工控安全PLC固件逆向三

2023-11-20

之前我们详细分析了bootram和Vxworks的基本启动流程,这篇文章中我们把视线转到plc的网络部分,同时来复现我们第一个、第二个工控安全漏洞。

一、VxWorks的网络设备驱动

一般我们说有三种设备:块设备、字符设备、网络设备,但是考虑到有些特殊设备的重要性和常用性,VxWorks的设备驱动分为了六大驱动,分别是字符设备驱动、串口驱动、块设备驱动、Flash设备驱动、网络设备驱动、USB设备驱动,其中关于串口驱动的知识实际上我们在上篇文章中已经接触到了一些(还记得/tyCo/1和/tyCo/0吗?)。

在六大驱动中,网络设备驱动最为特殊,因为网络设备和IO不打交道,有些人可能会疑惑,我们读写网络数据不就是对于网络设备的IO操作吗?是这样没错,但是我们所说的它和IO不打交道主要是指它没有普通文件的接口,或者说它根本没有对应的设备节点。我们不是像操作磁盘那样,open一个设备,read数据,而是通过socket来进行操作,在socket的基础上再去read、write。

网络设备作为一种特殊的外设,享受到了与flash、磁盘等Vxworks常见的外设不同的待遇,除了基础的驱动程序之外,还在驱动程序与协议栈之间设置了MUX接口层。这样的设置让驱动层不再需要关注协议层需要什么,只需要提供最基本的读写接口给MUX层,从MUX层读写数据即可;而协议层也不需要关心底层驱动到底是啥样的,有什么特殊性,只需要调用MUX层给的接口来实现数据从MUX的读写即可,这也是操作系统中常见的“解决不了的就加个中间层”的思想。

如下图所示:

其实在早期的Vxworks中,采取的是协议栈与驱动直接交换数据的方式,但很显然,并不好用,所以后期发展成了这样的形式,当然除了这样的模型外还有满足BSD socket等的网络模型,但考虑到MUX的广泛运用,我们这里还是以MUX为主。

在该体系下,对于网络设备驱动来说,又可以分为如下两种:

  • END,Enhanced Network Driver,增强型网络驱动,它基于帧传递数据,其实和我们日常在Windows、Linux上接触到的网络驱动较为类似。
  • NPT,Network Protocol Toolkit,网络协议工具,它相当于是END的改良版或者是进化版,它不再保留链路层信息,以包的形式传递数据。

在这两种网络驱动的基础上,到了我们的MUX层,虽说是MUX向上对接协议栈,但要注意,我们所说的协议栈往往是不包括链路层和物理层部分的,这一部分我们更愿意将其视为驱动和设备要完成的功能,我们的协议栈是纯软件的协议栈,如果非要拿TCP/IP来说的话,我们的MUX更像是插在了网络层和链路层之间,如下图所示:

当然,你可以不太明白这到底是怎么做到的(比如:ARP之类的怎么办?),没关系,后面的逆向过程中我们再细聊这一部分。

为了更好的分析我们的固件,我们先大致看一下标准的网络初始化过程,为我们下面的逆向打好基础。

  • 加载网络设备,因为有了mux层,所以我们需要将驱动程序注册到此处,这样网络设备的注册实际上就分了两个部分,一是设备的加载(驱动程序层),使用endLoad();一个是mux的加载,muxDevLoad()
  • 启动网络设备,同样需要再驱动层endStart(),在mux层muxDevStart()
  • 初始化协议栈,说是协议栈,不过一般也就是TCP/IP了,通过usrNetProtoInit调用
  • 加载网络协议,我们在完成了设备和mux之间的互动后就该让设备与协议栈联系起来了, ipAttach就是来实现这一步的

完成以上步骤后就可以开始进行网络通信了,通信的调用链一般如下:

muxReceive()-> ipReceiveRtn()-> ip_input()->…-> tcp_input()-> recv()

send()->…->tcp_output()->…->ip_output()->ipOutput()->…->ipTxRestart()->ipTxStartup()->muxSend()->send()

二、固件主逻辑的全部逆向

网络初始化及隐藏的危险

我们从usrRoot进入usrNetworkInit函数,这个函数是一切网络初始化的开始,上面我们所说的加载网络设备、启动网络设备等等工作都是在此进行的。

可以又调用了一堆init函数,不要心急,我们一个一个来看。首先是我们说过关于协议初始化的usrNetProtoInit函数。

 

同样又是一堆调用,但这次比较有规律,大多数都是xxxLibInit格式,我们首先调用了usrBsdSockLibInit函数,我们前面说xxxLibInit一般是指库的初始化,但要注意凡是usr开头的函数,我们都尽量要去看看,因为里面很可能被用户做了某些自定义的操作。

前面我们说过了,当ioGlobalStdSet将标准输入、输出重定向到串口后,我们就可以使用printf一类的函数了,所以这里报错不再是之前的log或是专门的err函数,而是通过打印字符串(当然这部分报错是可以显示给用户的,如果是比较“难”的错误还是会采取log的形式),在逆向过程中,这些字符串可以帮助我们推理出函数的大致流程。

这里可以看到uVar2作为返回值,首先进行sockLib库的初始化,失败了会打印相应的错误,并将uVar2设置为0xFFFFFFFF,下面同理,sockLibAdd实际上就是在根据用户的需要初始化bsdSockLib。只有当所有步骤都成功了才将uVar2置为0。

这里要注意,父函数中并没有对返回值进行检验,起初我以为是Ghidra的反汇编问题,但是查看汇编后发现确实是没有检验,查阅Vxworks给的源码发现同样没有检验,也就是说这里只会打印错误信息,哪怕初始化失败了也不影响系统的下一步运行。

回到usrNetProtoInit,往下都是常规的初始化操作,包括了host table、udp等的lib,这里就不再赘述了。再向上回到usrNetworkInit,进入usrEndLibInit函数。

end是我们上文提到过的增强型网络驱动,首先使用了muxAddrResFuncAdd添加了arpsolve函数作为地址解析功能,也就是实现了plc的arp功能,所谓arp就是在网络中,将ip地址转换为mac地址的协议,我们可以通过arpsolve进一步分析arp的功能实现,这里不再赘述。

往下是个大循环,很显然循环变量为endDev_Table,每次加6,也就是说这个Table应该是五个一组的,而local_18看起来就是个普通的计数器。

而while循环内部,我们看到,muxDevLoad函数用来加载驱动到mux层,它的参数依次是table的0、1、2、3、4项,所以我们可以把这当做是突破点,我们看一下该函数的定义:

void * muxDevLoad
    (
    int                          unit,        /* unit number of device */
    END_OBJ * (* endLoad) (char* ,
    void*                        ),           /* load function of the driver */
    char *                       pInitString, /* init string for this driver */
    BOOL                         loaning,     /* we loan buffers */
    void *                       pBSP         /* for BSP group */
    )

显然table[0]代表的应该是驱动的编号,除此之外我们还要关注,table[1]则是驱动的方法,table[2]是驱动的方法,而在muxDevLoad成功装载后,会将table[5]设置为1,也就是标志位。之后再调用muxDevStart来启动设备。

我们在汇编部分可以看到,实际上驱动的函数就是Fec860EndLoad,Fec是fast Ethernet controller的简写,860指明我们的cpu型号。

向下走是usrNetworkBoot函数,该函数主要是处理网络的地址、设备名。

前三个函数都非常简单,分别是获取地址、掩码,usrNetDevNameGet函数用来获取网络设备名称。最后调用了usrNetworkDevStart来进行设备的启动。

主要是1个物理网络接口以及1个本地回路接口。其中还有包括读取用户设置等操作。

回到usrNetworkInit,接下来会进行Remote的初始化,主要是设置主机和创建Remote连接。

完成上述步骤之后,我们的设备就算是“连上网了”。然后就终于到了网络初始化中和我们用户最最最最有关系的usrNetAppInit了,看这个酷似usrAppInit的名字我们就该意识到,这是在Vxworks网络方面用户自定义的部分。比如,我们希望在设备上开启nfs(network file system 用于远程文件访问)服务,我们在Tornado中添加NFS组件,INCLUDE_NFS_SERVER,之后会在该函数中自动生成相关的初始化函数。

rpc为Remote Procedure Call 远程过程调用,这是Vxworks默认会初始化的网络服务,毕竟,远程调用是一个系统要提供的最基本的服务了。

telnet协议是TCP/IP协议族中的一员,是Internet远程登录服务的标准协议和主要方式,同样是默认的。

ftp则是File Transfer Protocol,用于在网络传输文件。

ping估计大家就更熟悉了,不再赘述。

snmp是Simple Network Management Protocol 简单网络管理协议,主要用来支持网络管理系统。

估计上面说的几个大家多少都听过,但像是sntpc这种估计就懵了,实际上这是Simple ntp client,ntp是最古老的网络协议之一,主要是用来同步时间的。

CVE-2011-4859、CVE-2011-4860漏洞出处

这些都是初始化一类的函数,显然不是我们该关注的,而这个usrSecurity就比较有意思了,我们点进去看看。

loginInit创建了一张login的表,用来保存后续的login信息,而shellLoginInstall则是类似hook的一种函数,它的第一个参数是一个函数,用来替换shell登录时的函数,我们可以简单看一下主要部分(为了方便大家观看我对部分函数进行了重命名,有兴趣的可以自己对这些函数进行逆向,并不困难)。

主要就是在时限内读取了login name和login pass,并检查是否正确,如果正确就登录成功了,当然中间有很多“插曲”,有兴趣有的可以自己探索一下。

最后usrSecurity调用了loginUserAdd。

首先去检查上面我们建立的usr表,如果有的话就直接报错,没有的话添加该用户到usr表里。这里就出现大问题了,由于loginUserAdd的参数都是明文字符串,那么我们只要找到登录的地方,是不是就可以直接按照该用户名和密码进行登录呢?

事实上确实是如此,我们暂时跳回到usrAppInit中,同样存在此类情况。

这就是CVE-2011-4859,著名的施耐德硬编码漏洞,如果我们通过后门账户进行登录,危害性可想而知。而这也是2018工控比赛的一道题目,有兴趣的朋友可以找找那场比赛的相关wp。 

 是不是很兴奋?经过我们不懈努力,我们终于成功找到了我们的第一个工控漏洞,虽然说漏洞年代有点久远,而且漏洞偏简单,但这也是巨大的收获。

如果你有这款plc设备的话,可以利用升级时的bug,来实现让plc瘫痪的功能。使用osLoader软件,该软件用来升级plc的固件版本,只需要输入设备的ip,然后会利用现有的账号密码(其实就是这些我们发现的后门账户)来尝试登录设备然后进行升级,我们只需要指定一个错误的固件,就可以实现plc宕机了。

觉得这样就够了?其实这张小小的一张截图中还有一个CVE!这就是CVE-2011-4860,图中ComputePassword函数存在的漏洞。

该函数涉及到了两个参数,我们首先向上看看这俩参数是何方神圣。

可以看到eth实际上是调用GetEthAddr,该函数如下:

检测标志位是否为-1,如果是就获取到了mac地址,这里的mac地址并不是我们熟悉的格式,而是数组的形式进行存储。

下面的设备创建、文件系统建立过程我们暂且略过(留到下一篇文章中),看到sprintf,将eth划分了六部分,按照”.2X“的格式排列,实际上就是格式化mac地址。

最后调用ComputePassword进行运算,参数1就是mac地址的数组,参数2保存运算后的密码。

可以看到逻辑非常简单将全局变量copy到pass,如下图所示,即开头为0x。

接着将数组的第三部分拼接到pass,然后调用strtoul,该函数将字符串转换为无符号整数,其中参数一为源,参数二为目标,参数三是基数,这里是0x10,也就是以16进制进行转换(这也就是为什么先把pass的开头部分置为0x的原因了)。

最终进行简单的位处理和异或操作,然后用sprintf将pass置为全局变量所给出的格式。

这就是最后的pass了,也就是说,我们只需要在知道mac地址的情况下只需要对该”算法“(简单到我都不知道能不能叫它算法)进行逆向即可得到密码。

mac地址的获取方法就多了,最简单的,知道ip了发送arp,即可得知设备的mac地址,然后就可以通过后门账户成功登陆了。

三、Vxworks的设备驱动

在逆向过程中,我们经常会遇见xxxCreate、xxxDrv、iosDrvInstall之类的函数,这其实都是在对设备进行操作,更恰当的说,是对设备及其驱动进行操作。之前我们详细说明了设备驱动中最为特殊的网络驱动,这篇中我们就对其余的驱动进行简单的介绍,当然我们只是为了方便逆向不是为了编写驱动,对驱动编写有兴趣的可以去查查相关资料。

这里的上层io主要就是我们经常使用的open、write等等已经完全告别底层的接口,io子系统则是分发器兼抽象器,对于不同设备的读写进行进一步分发,对于操作进行抽象后为上层提供好用的接口。向下就是具体的驱动了。

首先是我们最熟悉的字符设备,所谓字符设备即以字节流的方式来读写数据的设备,像是我们平常用的键盘就属于这一类,I2C、SPI、UART等接口也都可以作为字符设备驱动,字符设备受制于字节流的读写方式,我们可以很容易处理数据(都一个一个来了,每次顺序操作即可),所以Vxworks并没有再为我们提供中间层,直接由我们写的驱动来对设备进行操作。

串口设备,实际上串口这个概念非常大,在Vxworks的驱动部分,串口相当于“除了几类特殊串口以外的串口”,对于这类设备因为其广泛性与差异性(串口设备用的多、用法还各有不同),所以Vxworks设置了tty中间层,串口通过,当我们向设备发送数据时,io子系统并不会对我们的数据进行任何处理,而是转给tty中间层,再由该层去找对应的驱动程序来进行具体处理。说到这大家可能就注意到了,串口设备驱动应该要和tty层“认识”,要不然tty如何去找驱动呢?这其实就是一个注册的过程,涉及到了ttyOpen、ttyDevCreate等函数,第一个注册的设备往往就当做了标准输入输出(还记得我们之前分析过程中有个函数更改了标准输入输出后我们才能进行printf操作吗?)。

块设备,我们平常用的硬盘就是典型的扩设备,它以数据块的方式进行数据读取,我们往往是在这上面建立文件系统进而对设备进行操作的(像是Windows的ntfs、FAT,Linux的ext4等等),Vxworks主要提供了两种文件系统:

  • dosFs,即兼容MS-DOS的文件系统,我们看到的文件结构就类似于windows的形式。
  • rawFs,不做处理,相当于一整块硬盘就是一个文件。

Flash设备,是一种非易失的闪存技术,我们经常用它来存储代码(特别是嵌入式领域,如果你做过iot方面的开发对它就一定不陌生),它实际上还是属于块设备,但是由于其广泛性、特殊性和重要性(用的多、存代码、擦写方式与硬盘有所不同),所以Vxworks为它在块设备文件系统下又加了一个中间层——TFFS(True Flash File System)。

USB设备,这是非常麻烦的一种设备,因为这种设备往往需要双方协调(比如u盘,u盘内部也需要有硬件、驱动、软件系统,还要和主机端进行协调,最终才可以实现数据传输),我们需要针对硬件、软件做出适配。以下是usb设备的抽象结构:

假设我们用u盘插入设备,那就是如上图的两个USB设备在进行数据交换,很显然需要做好的是连接工作和usb的控制工作,连接我们不必考虑,那主要就是控制器了,我们需要对控制器进行驱动编写,然后往上建立USB栈,这样就完成了整个USB的驱动工作。

对于Vxwork来说,我们经常会看到对于设备的操作(比如xxxDevVCreate),实际上这都是设备和操作系统产生联系,也就是建立上图模型的过程,一般需要:

创建设备——设备初始化——使用设备

而在创建过程中对于不同的设备又包括了将设备注册到io子系统、建立文件系统等等操作,初始化中包含了可选项的初始化、基础设置等等,我们在逆向过程中会看到具体的代码。

逆向分析

我们从上一次继续,进入usrAppInit进行下一步的分析。

首先是各类的初始化,首先创建了ram设备,也就是我们上面说的块设备,然后在RAM1建立了文件系统,初始化相关设置。然后又建立了tffs设备,也就是flash设备,由于我们之后要从flash设备中读取运行程序、配置信息等重要文件(在后面会提到),一旦建立失败后果严重,所以失败了我们就直接死循环。

接着到了FTP_User_Add函数,我们进入查看,这里为了方便大家理解,我已经修改好了变量名。

可以看到首先打开了/FLASH0/ftp/ftp.ini文件,如果失败了,那就添加一个默认的账户(也就是说如果用户没有设置ftp.ini时,我们可以通过该后门用户直接登录)。然后去依次读取ini文件中的用户名、密码,最后进行添加和核对,如果过程中出现了错误就打印相关错误,但是没有设置检查,及时出错也不会导致系统出现问题。

在读取完ftp的ini文件后就会进行ftp的初始化操作,虽然这是Vxworks为我们提供的api,但实际上里面隐含了许多有趣之处,我们进去看看。

开始创建了socket,并对socket进行选项设置、bind、listen操作等等,当然还掺杂着一堆信号量的操作来保证同步与互斥,实际上就是实现了网络通信的基础。

然后会调用taskSpawn函数来创建新的任务,这个函数我们之前已经说过了,第六个函数就是创建的新任务的”main“函数。这里如果调用失败了debug输出错误信息。从这里开始我们就要”多线操作“了,逆向的工作会稍微复杂一点。

我们可以点进去简单看一下,首先是一堆变量的初始化,有常见的keepintvl、keepidle等等,都是tcp传输中的重要选项,往下则是一个大的while循环,里面还嵌套了一个循环。我们按照逆向循环的一般思路,首先找到循环变量,这里第一个循环是死循环,但是子循环中list_head在一直在通过lstNext在迭代,同时client_inet就等于list_head,也在同步迭代。并且每次拿到拿到client_inet后会做一定的处理。

大循环方面首先是debug输出等待用户连接的提示语,接着以client_info作为socket的连接对象进行accept操作(这里的socket编程就不再多做介绍了),然后会将client的ip转换为我们熟知的形式debug输出,然后到小循环进行下一步。到这我们就大致了解到这就是ftp的连接操作,进一步的分析这里就不再赘述了,毕竟咱是工控入门,不是协议分析。

往下走PortA_Init用来初始化端口,CrashLogStartup则是对于log的设置,rebootHookAdd是我们之前提到过的hook操作,它将重启的操作进行hook,resetHardWare就是hook后的函数,其中也主要是一些设置操作。回到usrAppInit,接着调用的是bpi_init函数。

关灯,然后初始化一堆东西,开灯,简洁明了。其中有我们前面非常熟悉的modbus的初始化,此外eos也值得关注,125则是个”特例“,这些东西后面我们会具体来查看。再回到”main“函数。 

将power_up_done置为0,然后循环执行new_poll_bp_token函数,power_up_done会在该函数内部进行变化(我们可以通过交叉引用来查看),所以这并不是死循环,我们进入该函数。

首先是获取信道2out的状态,和0xff进行”与“操作,看结果是不是0x40,这是在硬件编程中很常用的手段,0x40即对应0000 0100,同于”与“检查state的第三位是否为1,如果是则进入bp_isc_c,不是则调用process_modnet,我们先来看看不是的情况。

首先会取得信道1out的信息,然后做一系列时间的操作,最终输出收到modnet command的时间,然后就是上图中的内容,检查信道1内容的0xb的位置,如果为0x03则读取信息,如果是0x04则写信息,我们这里就选取读信息作为例子来看看,注意这里传入的参数是msg+8(下面使用msg_8表示)。

这里检查msg_8的2、5是否为0x5、0x00,如果不是的话直接调用mbus_err_resp__FP9ERROR_RSPUc,返回值固定为0xd,如果是的话则将msg_6作为地址,并用tickGet函数获取时间,打印相关信息。然后根据地址为1或3进行不同的操作,主要是一系列赋值,1的话会返回0x38,3的话为0x18。

回到new_poll_bp_token,这次我们进入bp_isr_c函数来进行研究。

可以看到通过程序通过token的type来进行分发处理,注意这里power_up_done会因为token_type=1而设置为1,打破了之前的while循环。这里涉及到了modbus、eos、user_logic等多个逻辑,要想全部写完,估计够写好几篇文章的了,我们这里就选择处理modbus消息的这一部分来做简单分析,有兴趣的可以自己尝试分析一下。

首先会检查nb_mb_port是否为0,这个变量代表的就是port的数量,不是的话则拿到端口的列表,循环,循环变量为用来计数的counter和port的结构体,循环内容是检查port_stru的第五项是否为0,并且第一项不能为login_prtnum(最开始为空,后面会变化),如果符合则以port的结构体为参数调用put_mbus_msg。

该函数首先通过dequeue函数,拿到了mbus_queue里的各个msg,操作类似于链表,mbus_queue的每个节点的[0]为之后的msg的数量,[1]为下一个节点,dequeue函数将[1]付给了mbus_msg,然后让mbus_queue的[1]再指向下一个节点。实现了msg的遍历,注意这里没有循环,因为循环的过程是在外面的函数进行的。

然后检查msg的[8]是否小于0x100,不是就直接将整成非法信息了。然后是一系列的检验,不论怎么样实际上我们都是要将传进来的port的结构体赋给局部变量。

往后是我们的port结构体的[6]设置为msg[0],然后用read_mbus_svars函数,访问内存,取出内容赋给msg。其余的操作较为繁琐,大致就是将消息给存到了别处,并没有做进一步处理。然后再通过port_stry_00作为参数,对该函数进行递归调用。

到这里我们就看完了modbus_port_FV函数,我们回到bp_isr_c,其实这个函数里包含了大量的消息处理函数,但是篇幅所限我们就不再做更进一步的说明了,建议大家可以继续研究。我们继续看”main“函数。

接着的几个函数也是用来做一些初始化工作的,当这些全部完成后,会打印”Starting Root Task.“,然后通过taskSpawn创建名为NOERoot新任务。

该任务首先会打开某个灯,然后会初始化Device Manager(设备管理器),新建一个DM的任务,同时执行用户指定的程序。

DM创建的任务会读取消息后按照消息的种类,实现停止所有指令、重启等操作,可以说是相当于是linux的root权限了。

首先是复制了一个路径,然后将NOEScript拼接,打开,成功了就执行该文件,也就是说该文件就是后面的具体操作了。到此,我们已经完成了对整个固件的主逻辑的全部探索,从一开始的开机、亮灯到最后的root,我们基本都做了介绍,当然还有很多很多地方我们没有分析到,有兴趣的同学可以继续探索。

modbus_125_handler

我们前面复现了两个洞,实际上该固件一共有三个CVE,但是由于第三个CVE并没有出现在主逻辑里,所以逆向过程中我们没有遇到。

CVE-2011-4861,SchneiderElectricQuantumEthernet模块中的modbus_125_handler函数中存在漏洞。远程攻击者可借助MODBUS125函数代码在TCP502端口上安装任意固件更新。写的很简单,我们来详细看看它到底是怎么回事。

首先会检查拿到消息的function code是不是125,不是的话会把board_id设置为0x1,是的话会使用switch对code的[2]进行分发,也就是说[2]代表的是子功能码,可以看到内容包括读取硬件id、进入内核模式等等权限极高的操作,但是这还不算是太大的危害,真正可怕的是后面的内容。

当检查到子功能码为6时,会检查指令是否为8,如果是的话即进行下面的操作:首先构造一个a.bin文件的目录的字符串,尝试open,成功则进行合法性校验,然后又开始读取该文件的header,失败则报错,接着尝试进入/FLASH0/wwwroot/conf/exec目录,成功后通过简单的检查确定文件是否为下载的内核bin,是的话进行改名,将a.bin改为新的kernel.bin,最后创建内核更新的任务,打开led灯。

更新任务就是打印了提示语句,然后对任务进行推迟,然后重启。可以看到在整个流程中并没有进行严格的用户检测和固件检测,我们仅仅需要对固件进行简单的处理,然后构造流量包,即可完成对于设备固件的替换,进而导致设备瘫痪或者刷入有漏洞的固件版本进行进一步攻击。

本篇将是施耐德NOE77101固件的完结篇。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

工控安全PLC固件逆向三 的相关文章

  • linux shell进行数值计算

    出于项目需要 需要用脚本执行计算 最简单的方法1 这里写算式 可以写变量 Desktop cat test sh a 102 c a 123 echo a a echo a 123 c Desktop test sh a 102 a 123
  • 【软件测试】用例篇

    一 什么是测试用例 测试用例 向被测试系统发起的一组集合 这组集合包含测试数据 测试步骤 测试平台 预期结果 二 为什么在测试前要设计测试用例 三 基于需求设计测试用例 3 1测试是我们测试人员进行测试的依据 3 2测试人员首先要分析需求
  • A-LOAM总结-(前端+后端)算法流程分析

    文章目录 scanRegistration cpp 雷达信息预处理进程 laserOdometry cpp laserMapping cpp A LOAM算法流程 主要运行以下3个cpp文件 流程框图在文末 scanRegistration
  • (C语言)输出数组的最大值及其对应下标的最小值

    本题源自pintia cn 题目要求 代码 测试结果图 PTA测试结果 题目要求 本题要求编写程序 找出给定的n个数中的最大值及其对应的最小下标 下标从0开始 输入格式 输入在第一行中给出一个正整数n 1
  • 为什么学完Python后的薪资这么高?

    人工智能和大数据概念的兴起 带动了Python的快速增长 Python语言逻辑简洁 入门简单 生态丰富 几乎成为几个新兴领域的不二选择 而除了这两个领域 Python还有更多的适用领域 爬虫 web 自动化运维等领域都非常适合Python发
  • 详细的Python Flask的操作

    本篇文章是Python Flask 建站框架入门课程 编程实战微课 w3cschool微课的学习笔记 根据课程整理而来 本人使用版本如下 Python 3 10 0 Flask 2 2 2 简介 Flask是一个轻量级的可定制的web框架

随机推荐

  • 推荐|5种商业AI产品的技术架构设计!

    来源 达观数据 概要 今天我们就特别推荐达观数据的几个商业产品设计技术架构 希望对于广大技术有帮助 做任何一个商业产品设计 技术架构都是首先要考虑的 特别是面对海量数据的AI商业项目更是如此 今天我们就特别推荐达观数据的几个商业产品设计技术
  • Vue中key

    相信很多小伙伴跟我一样在使用v for的时候对key值的存在和必要性有疑问 通过ESlint进行代码检查的时候不加上key还会报错 想知道key为什么存在可以先想想key为什么产生 会不会是尤雨溪灵光一闪就给Vue添加上了key 也有可能
  • 大数据简介

    预备篇 目录 知识 大数据简介 计算机单位 大数据的五个 v Hadoop Hadoop概述 Hadoop的历史 Hadoop三大发行版本 1 Apache Hadoop 2 Cloudera Hadoop 3 Hortonworks Ha
  • 野外偷拍_野外紧急设计

    关于本系列 本系列文章旨在为人们经常讨论但难以捉摸的软件体系结构和设计概念提供新的视角 通过具体的示例 尼尔 福特为您提供了进化架构和紧急设计的敏捷实践的坚实基础 通过将重要的架构和设计决策推迟到最后一个负责任的时刻 可以防止不必要的复杂性
  • 武汉大学空间智能化处理复习

    空间数据处理智能化的重要性 提高地理信息处理的效率 减轻人在地理信息处理中的劳动量 使一般的地理信息用户也能让专家一样解决问题 大型的空间决策服务需要归纳 分析多种方案 智能化处理方法的来源 常常来自于人工智能学科的研究成果 如 知识工程
  • CTK框架介绍和环境搭建

    CTK框架介绍和环境搭建 概述 本人第一次接触CTK的时候是看的一去 二三里大佬的博客 CTK框架实际应用比较可靠 但网上资料很少 官网提供的API链接也已经无法打开了 目前还没有找到很好的官方的使用介绍 网上目前找到的CTK的介绍或者是博
  • Sklearn笔记--逻辑回归调参指南

    1 逻辑回归概述 p y
  • 建站记录2-CSS文件未加载-已解决-Resource interpreted as Stylesheet but transferred with MIME type text/plain

    网站链接 http 139 199 169 122 在本地加载正常 上传到服务器之后 网页没有样式 10 最终正确方案 重装了服务器系统 在别人的服务器上传同样的文件 发现正确 问题锁定在服务器设置中 找研究后端的马同学检查配置 发现是少了
  • python---collections模块

    目录 namedtuple 具名元组 deque 双端队列 OrderedDict 有序字典 defaultdict 默认值字典 Counter 计数 在内置数据类型 dict list set tuple 的基础上 collections
  • 差分数组及应用

    差分数组是啥 我也不是很清楚 大概就是一个数组吧 b战上有很多讲解的视频 我写下我学到的 差分数组就是 原本有个数组 然后你要修改某个区间的数值 正常人会想到遍历去修改 这是没错的 但吃力不讨好 用差分数组可以很快搞定 例如 有一个全为0的
  • CrimeKgAssitant-master的案件分类模块思路整理(多标签分类模型)

    根据liuhuanyong模型改动 尝试推广到其它领域 原始模型连接 GitHub liuhuanyong CrimeKgAssitant Crime assistant including crime type prediction an
  • TOOLS_Pandas根据日期列进行分组统计及绘图的使用示例

    Pandas根据日期列进行分组统计及绘图的使用示例 导入所需要的库 coding utf 8 from logging import warning import os sys import datetime import numpy as
  • linux端口被占用,netstat查看无进程号,端口状态一直停留在FIN_WAIT1以及CLOSE_WAIT状态

    环境信息 CentOS 6 5 现象 同事启动程序发现端口被占用 netstat查看之后发现如下现象 发现端口处于FIN WAIT1状态以及CLOSE WAIT状态 无法释放 问题分析 FIN WAIT1以及CLOSE WAIT状态的原理以
  • 如何通过电脑向ipad传电影视频(不用越狱)

    前言 为了能在ipad上看电脑上已下载的电影 我可是试了各种方法 心好累 还好终于解决了 不得不说 网上没一个靠谱的答案 方法一 如果电影本身的格式是 mp4 mov m4a格式 通过官方的itunes软件 直接导入媒体库 然后在ipad上
  • 聚宽源码17

    原文策略源码如下 导入函数库 from jqdata import import talib from math import isnan def initialize context 设置参数 set parameter context
  • React实现关键字高亮

    先看效果 实现很简单通过以下这个函数 highLight text keyword gt return text split keyword flatMap str gt span keyword span str slice 1 展示某段
  • SQLite3 获取最小可用ID,ID无需包含1

    SQLite3 获取最小可用ID ID无需包含1 一 语法 二 解析 1 判断最小ID是否为1 2 判断最小ID递增后是否存在 3 范围限定 三 总结 一 语法 获取1 到 500 范围间的最小可用ID select CASE WHEN s
  • Visual Studio 2017无法登录问题解决

    前两天登录VS2017的时候遇到无法登录问题 截图如下 期间试了各种方法 更新重启 添加一系列站点至信任域等等 还是没有效果 同时突然想起来自己的IE浏览器一直无法正常上网 不确定和VS无法登录是否有一定的联系 抱着试试看的心态上网搜索了一
  • IGWO-SVM:改良的灰狼优化算法改进支持向量机。 采用三种改进思路:两种Logistic和Tent混沌映射和采用DIH策略

    IGWO SVM 改良的灰狼优化算法改进支持向量机 采用三种改进思路 两种Logistic和Tent混沌映射和采用DIH策略 采用基于DIH维度学习的狩猎搜索策略为每只狼构建邻域 增强局部和全局搜索能力 收敛速度比GWO更快 适用于pape
  • 工控安全PLC固件逆向三

    之前我们详细分析了bootram和Vxworks的基本启动流程 这篇文章中我们把视线转到plc的网络部分 同时来复现我们第一个 第二个工控安全漏洞 一 VxWorks的网络设备驱动 一般我们说有三种设备 块设备 字符设备 网络设备 但是考虑