使用LuaQEMU对BCM WiFi框架进行仿真和利用

本文讲的是 使用LuaQEMU对BCM WiFi框架进行仿真和利用

使用LuaQEMU对BCM WiFi框架进行仿真和利用

LuaQEMU介绍

当处理固件中的复杂代码时,通常需要具有某种动态运行时内省(Introspection)以及即时修改行为的能力。例如,当设计一套逆向工程嵌入式解决方案,如蜂窝基带或自定义操作系统代码时,研究人员通常都需要借助某种二进制分析工具才能查看到运行时的协议栈、操作系统任务和内存的运行能力。同样,当开发二进制组件时,例如自定义模糊器、调试器,都需要通过处理底层系统来完成。

为什么选择LuaQEMU?

通过LuaQEMU自定义的仿真环境能为多个CPU架构上的快速原型设计提供支持,同时为二进制代码进行灵活的API交互提供支持,支持包括包括完整的系统仿真,特定的片上系统(SoC)解决方案的仿真以及对外设的支持。也就是说LuaQEMU是系统仿真器、调试器和动态二进制仪器框架的一个混合模拟器。由于我们在实践中一直缺少完全符合我们需求的解决方案,因此,可以试着用LuaQEMU来满足我们的实验要求。

由于LuaQEMU是一个基于QEMU的框架,所以可以将QEMU内部API暴露给注入QEMU本身的LuaJIT内核。除此之外,还允许目标系统的快速复原,而不附带任何Lua的本地代码。

在初步评估使用LuaQEMU时,我们设定了4个预期的功能:

1. 成熟的多架构支持

2. 全系统仿真支持,包括驱动程序和外设,MMU,中断和定时器

3. 易于长期维护(即使几乎没有QEMU内核修改)

4. 无需本机代码就能轻松实现目标原型(例如特定板的定义)

前两个功能QEMU开箱即用,我们通过在Lua中完全编写板定义(board definition)而不使用本机代码,从而获得目标原型设计的灵活性。实现这一点,就能使得每个硬件架构都带有一个新引入的Lua板(Lua-board,),可以用于与其他本地板( native board)定义进行交互,而不需要修改QEMU内核代码。目前我们发现使用这种方法可以轻松地转移到QEMU支持的其他架构。

我们的API要求很简单:

1.KISS调试API:

   曝光和操纵CPU上下文,如寄存器

   控制流程跟踪(断点,观察点)

   内存读写

2.在内存区域捕获操作(例如对于驱动程序)

3.存储器的脚本映射(例如,对于加载器)

为此,我们专门选择了API,因为它允许使用脚本功能轻松构建更强大的功能,其中包括自定义桩或钩子代码。

为什么选择QEMU?

当涉及仿真功能本身时,基于QEMU的方法会成为我们的第一选择。首先,它是唯一可用的完整系统仿真和虚拟化的免费或开源软件,除了x86/x64(ARM / AArch64,PPC,Mips,Tricore,Xtensa等)之外,还提供了广泛的支持架构列表。

此外,它还支持某些软件的不同版本或兼容级别,不过,它目前不支持PowerPC VLE。然而,QEMU项目本身非常活跃(光2016年超过了7000个)。更重要的是,通用架构的代码要在实践中经过很好的测试。

与其他解决方案相比,它还提供了很好的运行时性能,因为它的二进制翻译和缓存指令通过其翻译模块和微型代码生成器(TCG)。最后,它与其他在实践中非常有用的功能相结合,例如快照,监视器和gdb调试存根。

Unicorn引擎是利用此功能的工具之一,并通过提供QEMU功能的API,在过去两年中大大帮助了逆向工程师的工作。

为什么选择LuaJIT?

从技术上来讲,由于其内置的gdb存根服务与QEMU的仿真交互需要脚本支持。所以我们在早期进行仿真时,实际上是使用QEMU和gdb脚本来实现某些功能的。然而,目前还没有形成大规模应用,对于数量较大的脚本来说,不是很好维护,更重要的运行速度也很慢。所以,我们选择将脚本支持直接添加到QEMU本身。

当向进程中注入脚本功能时,有许多选项,例如由frida.re使用。

Lua C API 的正确用法是将Lua作为一门嵌入式语言,并提供完整的 C API供Lua代码和宿主程序交互,当然,宿主语言最好是C或C++,Lua是一个小巧的脚本语言,非常文档化,可以作为一个配置和命令式编程的语言,但更重要的是,该方法允许我们暂时表达我们认为重要的任何东西。随着LuaJIT的功能不断强大,它还提供了另一个解决方案,因为它的即时编译(JIT)和一个非常强大的FFI API会与本地代码进行交互,具有出色的性能。

Broadcom HNDRTE WiFi堆栈

由于Broadcom WiFi芯片占了移动设备(包括Android设备,iPhone和iPad)市场的大量份额,所以对它进行安全漏洞的调查时最有代表性的。

背景介绍

今年4月初,Google旗下精英团队GoogleProject Zero的Gal Beniamini发表了他关于利用Broadcom WiFi叠层架构的研究。在他的研究过程中,他发现了几个关键问题,攻击者能够利用Broadcom WiFi远程破坏WiFi SoC并最终导致应用处理器被攻击。由于我们一直在关注BCM WiFi堆栈,所以我们会试着使用LuaQEMU来重现实验室环境。我们还将借此强调如何使用LuaQEMU测试此漏洞。

通过这个漏洞,可以很好地说明为什么系统仿真是有用的。具体来说,由于这是一个堆缓冲区溢出,需要一个功能和模拟堆实现(工作中的malloc/free)。当然,也可以通过为某些功能提供封装来抽象出这个细节,但是为了在非分级环境中重现这个漏洞,我们希望堆在真实的设备上运行,而不必完全理解和重新实现其内部逻辑。在设备启动例程期间,堆又被初始化,这又要求仿真环境下的引导代码。

此外,我们希望能为其进行漏洞测试注入其他类型的WiFi帧,即遍历整个WiFi处理代码,而不是仅仅钩住易受攻击的函数。为此,我们选择了三星Galaxy S6设备上的BCM4358固件作为测试目标(MMB29K.G920FXXU4DPGU)。

测试目标

我们测试的具体目标是:

1.触发TDLS设置使用LuaQEMU确认缓冲区溢出

2.模拟功能堆栈或堆的系统启动

3.模拟WiFi接收路径以注入任意帧

当然,为了达到这个目标,我们不会再附加手动逆向工程。

系统启动

为了模拟系统启动并为我们的目标定义一个QEMU板,我们首先需要确定目标架构。研究并评估二进制模式后,我们发现主要的WiFi基带代码在ARM内核上运行。在ARM供电的蜂窝基带领域,基于Cortex-R *和Cortex-M *内核的设计是相当普遍的。

研究表明,Broadcom正在使用Cortex-R4内核,但由于未知的原因,QEMU不正式支持Cortex-r4,由于存在定义其协处理器寄存器,所以就有一些MRC指令导致了未定义的指令异常(BTCM配置MRC / MCR是特定的)。因此,我们使用兼容且不需要补丁的“cortex-r5”作为目标CPU。

由于Broadcom没有广泛使用完整的固件文件,而移动设备通常会在闪存中包含补丁RAM文件,所以RAM文件将被加载到内存中并在内存中被执行。值得注意的是,由于没有对这些进行加密检查,所以这些检查对于将恶意功能注入到运行时的WiFi SoC固件或转储ROM是非常有用的。

然而,正如前文所描述的那样,Broadcom足以让dhdutil进行读写内存。

由于ROM已知地址为零,固定大小为0x180000,因此我们可以利用此工具将Android代码转储到随机的模拟和反向工程中(dhdutil -i wlan0 membytes -r 0x0 0x180000)。NexMon的运行表明,在ROM存储器之后,BCMHD驱动器或patchram会从0x18000开始。

总而言之,我们知道了以下系统参数:

1. ROM起始地址:0x0

2. RAM起始地址:0x18000

3. 从基地开始执行ROM代码

4. ARM Cortex-R4

可以使用ROM和补丁RAM代码的静态逆向工程来验证这些。

板级初始化(Board Initialization)

我们已经提到,希望使用Lua灵活的板定义,而不需要编写本地代码。让我们看看LuaQEMU的电路板如何定义,以下是我们用来模拟BCM WiFi堆栈的电路板的最小定义:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

这是一个非常简单的例子,可以使用库存QEMU来实现类似的功能,不过我们可以用它来说明这个概念。

machine_type会将我们的目标配置为一个cortex-r5,如上所述,它们类似于Cortex-R4的兼容性。

memory_region块注册在QEMU内部的存储器区域,我们可以使用memory_region块来将代码映射到内存。这是使用file_mappings配置条目发生的情况,该条目定义了一组文件及其在内存中的相应地址。LuaQEMU在一开始就负责加载它们,目前保留名称为“内核”,以便于直接映射到QEMU的-kernel命令行选项,我们正在使用它来加载补丁RAM代码。从技术上讲,这里不需要-kernel命令行选项,因为引导代码所需的所有常见功能都包含在ROM,即堆实现和各种libc函数中。可以在这里添加任意数量的文件。

cpu块可用于初始化CPU,其寄存器会决定是否复位以及复位时QEMU将跳转到什么地址。

系统初始化

这当然不足以达到WiFi堆栈的功能状态,甚至不需要初始化堆和其他重要的数据结构。

我们期望BCM WiFi可以设置堆栈,配置中断和陷阱处理程序,配置缓存,配置内存保护(Cortex-r4上的MPU),配置NVRAM,配置DMA/PCIe,初始化内部WiFi接口等。然而,由于我们的目标是根据需要手动反向工程,以达到主要的WiFi处理代码。

下图是赛普拉斯文档的示意图,红色注释为某些组件的功能:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

右下方的WiFi天线用于发送和接收WiFi帧,典型的WiFi适配器只有一个天线,双工器用于将两个信号(RX和TX)分组或组合成一个。然后,由形成物理WiFi层的DSP内核处理信号。对于这些运行,实时功能很重要。 DOT11MAC(D11)通过接收实际信号并注意确认帧等来监控这些功能的实现。

D11内核和Cortex-r4内核通过DMA操作进行通信。在处理实际的WiFi Layer2/3数据之前,r4内核连续地将数据包从共享FIFO中取出。在架构层面上,D11内核的帧不是原始的WiFi帧,而是封装并包含物理帧头。有关这方面的更多详细信息,请参考Nexmon的运行赛普拉斯数据表SoftMAC内核驱动程序

在这里要重点了解的是,在启动初始化代码和实际的WiFi帧接收代码之间,还有一些额外的组件与未知功能有关。所以,为了模拟WiFi帧的接收,我们希望尽可能少逆向工程。

实际上,在通过所有引导代码之后,代码只是等待中断出现,这使得它从D11内核中出现一个帧,检查物理D11帧头并通过wlc_dpc函数开始处理数据包,这可以通过查找wlc字符串或跟随中断处理程序来识别。

因此,虽然我们不知道什么功能完全由系统启动,但手动逆向工程却给了我们一个粗略的想法,让我们知道需要打什么代码来处理框架。因此,如果我们假设在接收路径上的绝大多数代码与实际的帧接收无关,并且我们知道要达到的有效地址,那么我们可以尝试禁止代码直到我们跳过尽可能少的相关功能。

测试中出现的错误

为了实现目标,我们需要跳过不相关的代码并运行重要的代码。LuaQEMU无法很好地解决这个问题,所以此时绝对需要手动逆向工程来跨越代码路径(例如我们的例子中的堆初始化)并了解错误路径的样子。

在这种特殊情况下,我们可以用panic/trap处理程序指明一个问题,并且由某些系统状态引起的无限循环来发现该问题。

使用LuaQEMU对BCM WiFi框架进行仿真和利用

例如,上述代码在启动时会从背板地址(backplane address)读取并预期具体值,直到找到该值为止。对于某些值,上述代码会立即跳入无限循环。手动逆向工程这些部件相当费时间,手动执行也是如此。但是,LuaQEMU可以帮助我们了解内部CPU状态。

为此,我们引入了一个Lua回调,可以在多个指令之后使用,实际上是翻译模块,且内部没有改变CPU状态。这个启发式简单地记录一个执行窗口,产生一个CPU哈希值,并且每当这个哈希值出现时,都会增加一个计数器直到该阈值被找到。一旦被找到,我们就可以确定我们所处的卡住状态。通过手动剖析相应的代码,看看我们是否可以简单地跳过它。

以下是在实践中的一个例子。

我们将阈值定义为cpu初始化的一部分:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

我们进行Lua回调然后继续简单地转储CPU寄存器:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

这使我们能够快速地注意到其中的问题,并尝试解决。跳过几个这样的卡住的位置,并在相关的地方对注册内容和内存进行小的修改就足以达到功能堆状态,这可以通过挂钩malloc / free功能轻松验证。

其中一个方面是内存保护的设置,这对于XN等漏洞利用的缓解也是有意义的。由于init代码跨越保护代码,我们可以使用它来即时转储其配置。

总而言之,这种方法使我们能够将整个系统API变成功能状态(functional state)。然而,我们完全模拟PCIe设备仿真(即内部WiFi接口)时却失败了。因此,我们决定在启动时完全跳过此代码,并专注于在实际的接收路径上仿真所需的数据。

MPU配置

BCM4358中使用的内存保护单元(MPU)是系统启动过程中的重要部分,因为它定义了所使用的内存区域和权限。通过使用CP15和opcode 6搜索MCR指令,可以快速识别相应的代码。尽管可以在运行时转储此配置(如Gal所示),但我们想要跟踪初始化本身。同时,我们能够在仿真中快速转储配置,从而跟踪Broadcom引入的潜在缓解变更。如果能做到这一点,就不用逆转BCM的MPU配置的具体逻辑。

使用LuaQEMU对BCM WiFi框架进行仿真和利用

在LuaQEMU中卸载配置是微不足道的,因为所需要的是Lua回调,以便在相应的指令中转储值和断点:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

在启动时会出现以下输出:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

我们可以看到,这是与Project Zero发现的相同的内存配置,即所有区域都是rwx,XN不被使用。 WiFi SoC之间似乎很少有偏差。将这一部分重用于较新的映像版本将是有趣的,以查看Broadcom在此空间中的任何更改。

WiFi接收路径

回到我们模拟WiFi接收路径的最初目标,重要的是要更好地了解底层代码。基于SoftMAC实现和手动逆向工程,我们知道帧接收功能从名为wlc_bmac_recv的函数开始。通过使用dma_rx来接受帧数据,dma_rx从上述FIFO中导出帧,并通过调用wlc_recv来处理每个帧。

wlc_recv是WiFi帧数据在首次处理的位置,字节被解析。该接收路径通过服务例程处理程序从中断上下文触发。在模拟WiFi帧接收时,我们可以直接模拟中断,即直接对此接收路径进行设置,并绕过中断处理。

与系统启动期间的早期方法类似,我们定义了我们想要防止的错误条件。所以,每当代码观察到错误时,将调用释放WiFi数据包(我们称之为packet_free)的处理程序,并返回到中断循环。只要我们没有碰到我们感兴趣的接收路径,即数据和控制帧处理程序,更具体地说是我们感兴趣的易受攻击的wlc_tdls_cal_mic_chk处理程序,我们就会一直循环。

一旦仿真器卡住等待中断,我们将修改控制流程,直接用我们选择的数据包调用wlc_recv。

WiFi状态

在任何帧分析程序执行任务之前,我们的仿真方法总会出现几个问题。

首先,wlc_recv接收两个参数,一个是数据包有效载荷,包括一个dot11接收头,一个是指向wlc_info结构的指针。这种结构在整个接收路径中被使用,并且还包含其他数据结构的各种指针。

从SoftMAC驱动程序中定义的版本摘要如下:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

该结构相当大且复杂,并且在BCM WiFi固件中也可能不一样。因此,我们不能手工制作副本,其内容也不能被忽视。

例如,osh句柄包含被标识的packet_free函数使用的函数指针。hwrxoff将用于确定原始帧数据的偏移量。该结构还包含有关相关WiFi接入点的信息,另外它还包含其本身的硬件地址(即MAC地址),其在几个地方使用以便确定分组是否被指向适配器(在监视模式的上下文中也是重要的)。

使用LuaQEMU对BCM WiFi框架进行仿真和利用

由于该漏洞的性质,即处理SETUP确认消息,该结构也是重要的。WiFi堆栈利用这种结构来跟踪在处理确认消息之前是否已经发送SETUP请求,几乎可以通过这种结构访问所有状态和WiFi配置设置。很明显,如果忽视这个结构的内容,就会使我们陷入困境。因此,我们首先需要一个副本,然后决定代码的哪些部分必须进行修补,以解决潜在的不必要的值和状态。

为了重播有效的TDLS帧,我们采用了由wpa_supplicant的开发者提供的一个PCAP示例

在运行时检查状态和数据包数据

由于dhdutil允许我们读取和写入WiFi SoC内存,因此我们决定编写一个短的汇编存根。我们将使用它来将内存后面的参数转储到wlc_recv,以便稍后在仿真过程中重用它。我们还使用这种方法来更好地了解原始分组数据的结构,因为它与SoftMAC驱动程序不同。

在初始化之后,系统RAM的一大块将被堆放。我们会随意利用这个空间内的一个静态位置作为一个缓冲区来将内存转储给我们感兴趣的RAM。

以下是我们用于检查数据包数据的程序集存根:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

这可以使用dhdutil -i wlan0 membytes -h 0x0019B710 ….直接写入内存,修补wlc_recv的一部分,该部分评估监视器模式设置,即正常操作期间不相关的代码。正如你所看到的,除非使用监视器模式,否则该代码已经被删除了,这样我们的就可以覆盖它。

使用LuaQEMU对BCM WiFi框架进行仿真和利用

然后可以通过从临时位置读取,使用dhdutil读取复制的内存。这大大有助于加快手动RE工作,让我们在运行时了解相关结构的内容。

由于wlc_info的大量嵌套结构,我们没有重新使用wlc_info并手动将其重新拼接。而是选择了一个ramdump,然后我们在运行时动态地附加到我们的仿真环境中。这可以使用dhdutil coredump和跳过形成某种头文件的0x146字节来完成。

这样,我们可以在不完全理解其内容的情况下获得完全初始化的wlc_info结构。由于堆存在于该空间中,因此要注意的是,这将使我们的功能堆的内容变得有些不同。

调用wlc_recv

此时,我们已经有足够的功能来使用原始数据包调用wlc_recv。以下是我们用于的Lua代码:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

可以看出,我们动态地将ramdump加载到内存中,重新使用上述的暂存空间来存储我们的TDLS数据包,并调整内存中的几个标题字节以将预期值与wlc_recv相匹配,最后将控制流重定向到wlc_recv()。

接收路径问题

接收路径最终可以释放我们需要解决的数据包,使用称为wlc_recvfilter的函数来确定是否根据帧的认证状态和类来导出数据包,其实,我们完全可以绕过这个功能,另外,我们也将跳过根据bsscfg执行检查的几个地方。

wlc_recv_data通过比较MAC地址来检查数据包是否指向自身,我们也跳过这些检查,以便原始数据包不必与我们的模拟适配器相匹配。

TDLS执行了Gal在他的博客中描述的检查,但是从解析或安全角度来看并不相关。也就是说,代码验证分组中包含的Link-ID IE,评估BSSID,并通过将其与存储值进行比较来验证“Snonce”值。

这个过程类似于跳过卡住的代码路径,由于我们知道在packet_free的情况下找不到相关的TDLS处理程序,所以可以选择性地禁用检查,同时尽可能的减少额外的手动反向工程。

那如何跳过LuaQEMU代码呢?我们只需使用断点并调整CPU寄存器。断点可以在运行时使用lua_breakpoint_insert初始化:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

总的来说,要使此方法发挥作用我们总共要修补了17个位置,以获得相关的引导代码,并启用控制和数据帧的接收。我们另外修补了作为TDLS接收路径一部分的7个位置,使用更精确的wlc_info内容。

触发漏洞后,只需要一个代码补丁来调整内存中原始样本数据包的一些值:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

堆追踪

接下来,我们想使用LuaQEMU来检查堆分配状态,不过要注意到TDLS的堆溢出。由于堆的实现,堆溢出可能不会直接可见或以其他方式导致崩溃。所以我们想使用LuaQEMU来跟踪边界(OOB)条件下的线性堆,于是我们做了一个非常简单的实验,看看LuaQEMU能否帮助我们。

通过为它们添加断点存根来跟踪所有相关的malloc和免费通话。

使用LuaQEMU对BCM WiFi框架进行仿真和利用

在每个malloc条目上,我们只需记录分配的大小,函数退出时会返回一个指向分配的缓冲区的指针。现在一个非常简单的堆OOB检测只需要在写入期间留下分配区域时触发回调。更完整的实现将跟踪整个堆区及其分配的块,并捕获所分配的块外的任何访问。要实现这一点,就需要更多关于堆内部的知识。所以为了达到演示的目的,我们决定尝试一个更简单的实现,即只关注检测线性超出条件。

使用LuaQEMU对BCM WiFi框架进行仿真和利用

退出钩计算与分配的堆块相邻的dword的位置,并在该地址上放置一个观察点。一旦这个观察点触发,我们就知道发生了一个堆OOB写入。

LuaQEMU为我们提供了两种触发内存访问的方式:观察点和陷阱区域。前者类似于在虚拟地址触发的调试器中的观察点,而陷阱区域捕获对物理内存区域的读取和写入访问。此外,陷阱区域处理程序可以调用其本身的读写操作。后者也可用于模拟驱动程序或内存映射IO范围,但两者在目的上有些相似。

分配的指针,大小和oob_ptr存储在Lua表中,用于管理目的。免费的钩子正在利用它们来删除插入的观察点:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

现在如果发生OOB访问,我们的bounds_access回调将被触发。 将LuaQEMU中的观察点接收地址,长度和访问类型作为参数,我们可以用它来进行评估。这是一个重要的细节,因为free和malloc本身可以在堆元数据上工作,从而触及我们标记为OOB的数据。

因此,我们在指示OOB条件之前会过滤malloc和free的内存范围。

使用LuaQEMU对BCM WiFi框架进行仿真和利用

触发TDLS设置确认OOB写入

从系统启动到触发堆溢出,这大致是LuaQEMU给我们的全部信息:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

使用LuaQEMU对BCM WiFi框架进行仿真和利用

我们可以使用相同的实现方式来处理其他IE解析问题,而且仿真也不限于数据帧,在我们的测试中,它也处理了控制帧。

数据包加密

当我们第一次尝试这种方法时,我们不知道如何处理加密。特别是Cortex-r4内核是否在加密数据包上运行还不清楚。看看wlc_recv路径上的汇编代码,我们没有看到数据包解密。这也是有道理的,因为这可能是硬件加速的。

事实上,这不是作为WiFi帧解析的一部分发生的:

使用LuaQEMU对BCM WiFi框架进行仿真和利用

在RX FIFO发生之前和TX FIFO发生之后,引擎密码是公开的。 wlc_info结构包含指向原始帧被加密的信息的指针和会话密钥材料,但是帧解析中的实际数据处理完全在纯文本框架上操作,这是非常有意义的。因为在使用帧数据时,这意味着我们的方法不能用于评估加密实现本身。

总结

虽然本文所讲的方法并不会实现完美的全系统仿真,但只要采用合理的方法总会找到我们适用的安全研究的全系统仿真,这当然不意味着所有的代码路径都能正确地运行。实际上,如果你看看上面的日志消息,你就会注意到一个BCM7332芯片已被初始化,这可能是跳过部分初始化的工具。

作为仿真的一个附带作用,我们可以在新的或不同的固件版本上用合理的方法执行类似的仿真,例如跟踪Broadcom可能部署的更改。这也为我们进一步的安全研究提供了良好的环境。模糊WiFi帧,同时也可以进行覆盖分析,也可以使用patchram工具将调试器直接注入WiFi固件。这样就能在绝大多数情况下运行堆栈,并同时能够在Lua中对代码部分进行编码,而不会失去太多的性能。

当然LuaQEMU不能替代手动逆向工程的其他工作,毕竟这些仿真的办法还有待进一步实证和改进。




原文发布时间为:2017年7月19日
本文作者:luochicun
本文来自云栖社区合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值