COOP绕过微软最新CFG(Control Flow Guard)

原文链接:https://bbs.pediy.com/thread-217335.htm


最新的漏洞利用开始渐渐脱离基于ROP的代码重用攻击。在过去的两年里,出现了一些关于一种新的代码重用攻击的文章,Counterfeit Object-Oriented Programming(COOP)COOP是一种顶级的针对forward-edge的执行流完整性(CFI) 的攻击方式。在我们把CFI解决方案(HA-FI)整合进我们的终端产品中时,这种攻击吸引了我们的注意力。COOP主要出现在学术界,还没有出现在exloit工具包里。这也许是因为攻击者更趋向于使用更简单的方法。在win10年度更新中微软的Edge使用了执行流保护(CFGControl Flow Guard)。在CFG中,缺少backward-edgeCFI更容易受到攻击。但是当Return Flow Guard (RFG)出现,使得攻击者不能再依靠淹没栈中的返回地址进行攻击的时候,会发生些什么呢?

 

我们对评估COOP在攻击CFI时的效果很感兴趣。这不仅可以使我们保持在学术界和黑客社区中前沿研究中的地位,也可以测试产品的有效性,更改设计,甚至在必要时普遍地提高我们自己的防御能力。在我们的这一系列的两篇博文中的第一篇中,介绍了我们使用COOP函数重用对微软的CFG以及我们自己的HA-CFI进行攻击的评测。


微软执行流保护


已经有大量的论文,博文和会议发言充分地讨论了微软的执行流保护(CFGMicrosoft’s Control Flow Guard)。Trail of Bits在两篇最近的帖子里比较了Clang CFI和微软CFG。第一篇帖子着重Clang,第二篇强调微软对CFI的实现,还有额外的研究提供了CFG的实现的进一步细节。

 

在过去的几年里,绕过CFG也成为了安全会议中中一个流行的主题。在我们引用一些著名的绕过方法之前,最重要的是CFI能够进一步分为两种:forward-edgebackward-edge

Forward-Edge CFI保护间接调用或是JMP位置. Forward-edge CFI解决方案包括微软 CFGEndgameHA-CFI.

Backward-Edge CFI保护返回指令. Backward-edge CFI解决方案包括微软的Return Flow Guard,EndgameDBI exploit防护的一部分以及包括intelCET在内的其他ROP检测。

这个分类帮助我们描绘出了CFG保护位置的轮廓——间接调用位置——以及不打算保护的位置——栈返回地址。例如,一个最近的POC入选了exploit工具包,这个POC针对Edge,使用读/写的原始方法来修改栈中的返回地址。但这并不适用于CFG,不应该作为CFG的弱点来考虑。尽管如此,它成功地证明了CFG的有效性,并使攻击者转向劫持执行流,而不是间接调用的位置。这个例子实际上证明了CFG缺陷包括以下几点:利用未受保护的函数调用位置,重映射包括CFG代码在内的只读内存区域,并使他们指向需要受到检查的代码,在Charka中提到的JIT编码器的资源竞争,使用基于内存的间接调用。COOP或是函数重用攻击,在面对CFI的实现时有着公认的局限,因为“limitations of coarse-grained CFI”,他们并没有入选微软的bypass赏金。也就是说,我们不知道有哪些公有领域的POCs能证明COOP能指定攻击CFG的加固的二进制代码。

CFG对每个受保护的DLL添加了一个__guard_fids_table,它由一系列在二进制代码中合法的RVAs或是间接调用指令中敏感的目的地址组成。一个地址作为CFG bitmap索引的一部分而存在。bitmap里的bits能够根据地址是否是合法的目的地址而进行切换。在此之外,也有一个API能够对bitmap进行修改,例如,为了支持JIT编码的页面:

kernelbase!SetProcessValidCallTargets在使用系统调用更新bitmap之前会调用ntdll!SetInformationVirtualMemory

win10创意者更新有一项新增的功能可以抑制导出,也就是说,现在导出函数能在CFG保护的调用位置被标记为非法目的地址。这一功能的实现需要使用CFG Bitmap中每一个地址的第二位,以及在初始化每个进程的bitmap__guard_fids_table中每一个RVA条目的一个标记字节。

对于64位的系统,地址的第9-63位被用于在CFGbimap中检索一个qword,第3-10位被用于(模64)访问qword中某一指定位。在导出被抑制后,CFG允许一个给定的地址在CFG bitmap中用两位表示。此外,在大多数DLLs__guard_dispatch_icall_fptr现在被设置为指向ntdll!LdrpDispatchUserCallTargetES,在其中一个合法的调用目标必须从CFG bitmap中删去。


当你把动态解析符号表考虑进去的时候,实现这样一个导出表抑制变得有点复杂,因为使用GetProcAddress意味着随后的代码也能调用返回值作为函数指针。只要CFG bitmap中每一个条目没有被标记为敏感的或是不合法的(例如,VirtualProtect, SetProcessValidCallTargets等等),执行流保护可以通过把条目对应的两位从“10”(导出表抑制)改为“01”(合法的调用位置),解决这个问题。最后,一些导出表将会在进行创建时以不合法的间接调用开始,但最终在运行时代码中成为合法的调用目的地址。在今后我们的讨论中,这尤为重要。当这一情况发生时,一个调用栈的样例如下:

00 nt!NtSetInformationVirtualMemory

01 nt!setjmpex

02 ntdll!NtSetInformationVirtualMemory

03 ntdll!RtlpGuardGrantSuppressedCallAccess

04 ntdll!RtlGuardGrantSuppressedCallAccess

05 ntdll!LdrGetProcedureAddressForCaller

06 KERNELBASE!GetProcAddress

07 USER32!InitializeImmEntryTable


COOP概要

Schuster et al.认为COOPCFI实现的一个潜在的弱点。为了在绕过forward-edge CFI的检查之后执行代码,我们可以利用连续的攻击序列和重用已存在的虚函数。在ROP在有一个相似的方法,其结果是一系列小段合法函数,每一段代码实现最低限度的功能(例如,载入一个值进RDX中),但把它们组合在一起,却可以实现一些复杂的任务。COOP的一个基本组成部分就是利用主循环函数,在其中可以迭代对象链表或数组,调用每个对象中的虚函数。然后,攻击者把内存中“伪装”的对象组合起来,在某些情况下,可能会覆盖对象,这样就能在主循环中按攻击者安排好的顺序调用合法的虚函数。Schuster et al.证明了使用COOP payloads的攻击win7 32位和64位上的IE10,以及Linux 64位上的Firefox的方法。这项研究随后被扩展了,证明了递归或是带有许多非直接调用的函数也可以实现这一过程,而不仅仅是循环。随后又继续被扩展到用于攻击Objective-C 运行时环境。

这项前沿研究极其有趣和新奇。我们想要把这一概念应用到一些现代的CFI实现上,以对如下方案进行评估:a)在加固的浏览器中构造一个COOP payload的难度;b)是否能绕过CFGHA-CFIc)是否能改进CFI使其能检测到COOP类型的攻击。


我们的目标


我们使用COOP主要的目标是win10Edge,因为它代表着一个全新的CFG加固应用,并且它能让我们在内存中使用JavaScript来准备我们的COOP payload。弱点始终是我们小组的兴趣,为了这个目标,我们专注于劫持CFI的执行流,并对攻击者作出了下列假设:

1.任意的读-写原语都是从JavaScript中获得的。

2.因为在运行时动态地找到小段代码不是这项研究的内容,因此,允许使用硬编码偏移量。

3.所有微软创意者更新中最近的防御机制都能被使用(例如,ACGCIG,带导出表抑制的CFG)。

4.除了使用COOP以外,攻击者不允许以任何方式绕过CFG

在我们最初的研究里,我们在对微软年度更新(OS build 14393.953)中的Edge的研究中利用了一个Theori中的POC,我们使用创意者更新中的防御机制设计我们的payload,并在开启导出表抑制的win10创意者更新(OS build 15063.138)中对其进行验证。

一个理想的POC会执行一些攻击者的shellcode或是启动一个应用程序。攻击者的一个经典的代码执行模型,就是把一些内存中被控制的数据映射为+X,然后跳转到包含最新修改过的+X区域的shellcode。然后,我们的真实目的是在forward-edge CFI的保护下,产生一个能够执行一些有意义的代码的COOP payloads。这样一个payload提供了能够进行测试和改善我们的CFI算法的数据。进一步说,攻击Arbitrary Code Guard (ACG)或是Edge的子进程的办法超出了我们的研究范围。我们确定对于win10创意者更新研究的最终目标是使用COOP来使CFG无效,使得在DLL内能够跳转或是调用任意位置的代码。因此,我们总结出下面两个主要的COOP payloads

1.对于win10年度更新,以及缺少ACG保护的程序,我们的payload把我们的控制的数据映射为可执行的代码,在使得CFG无效后跳转到我们控制的shellcode所在区域。

2.对于win10创意者更新,我们的最终目标是仅仅是使CFG无效。


寻找COOP片段


下列Schuster et al.设想的蓝图,我们的第一业务是商定COOP各个组成部分的术语。学术论文将每个重用函数称为虚函数片段(virtual function gadget)或是vfgadget,当我们描述每一个特定类型的vfgadget时使用缩写,例如将主循环(main loopvfgadget称为ML-G。我们选择以更为非正式的方式来命令每种类型的gadget。在接下来的帖子中你能找到的术语定义如下:

Looper:对于执行复杂COOPpayloads(论文中的ML-G)至关重要的主循环gadget

Invoker:一个调用vfgadget的函数指针。(论文中的INV-G

Arg Populator:带一个参数的虚拟函数,它将一个值加载到寄存器中(论文中的LOAD-R64-G),或是移动栈指针或是把值加载进栈中(论文中的MOVE-SP-G

与论文相似,我们编写了脚本来帮助我们识别二进制中的vfgadgets。我们使用了IPA Python,推理帮助我们找到了loopersinvokersargument pupulators。在我们的研究中,我们发现了实现COOP的实用的方法就是,在返回到JavaScript之前,把vfgadgets链接到一起并依次执行少量的vfgadgets。根据需要通过额外的COOP payloads重复这个过程。因此,为了我们的目的,我们发现没有必要将二进制代码提升到IR。然而,将大量COOP payload拼接到一起,比如说完全通过重用代码运行一个C2 socket线程,也许会需要提升到IR。对于vfgadget的每个子类型,我们定义了一系列规则,并使用它在Edgechakra.dlledgehtml.dll)的两个二进制文件间进行搜索。这些规则中与looper vfgadget相关的一部分包括:

1.出现在__guard_fids_table中的函数

2.包含一个不带参数的间接调用的循环

3.循环不能影响到参数寄存器


vfgadgets的所有类中,搜索loopers是最耗时的。许多潜在的loopers有一些限制使其难以使用。我们寻找到的invokers不仅需要有调用虚函数指针的vfgadgets,还要能够在单一的counterfeit对象中,一次性又快又容易地填充六个参数的vfgadgets。因此,当尝试调用单个API时,COOP可以使用快捷方式,完全避免对循环和递归的需求,除非需要返回值。在x64程序上能够找到许多寄存器对参数寄存器进行填充。值得一提的是,Schuster et al.COOP论文中根据mshtml提出的大量原始vfgadgets仍然能在edgehtml中找到。然而,我们在我们的成果中添加了一个要求来避免重用这些,而不是为我们的COOP payloads寻找新的vfgadgets


COOP Payloads


通过脚本语言触发COOP,我们实际上能把一些复杂的任务从COOP中移开,因为一次性把所有东西拼接在一起非常的复杂。我们能使用JavaScript来帮助我们,重复调用微型COOP payload序列。这也让我们能把诸如算术和条件操作放回JavaScript中执行,并保留基本的函数重用来为通过COOP调用重要的API做准备。此外,我们展示了这种方法的一个例子,包括在我们劫持到的#1 section中将COOP的返回值传回到JavaScript,并讨论如何调用LoadLibrary

为了简洁,我将只介绍最简单的payloadspayloads的一个公共的主题是需要调用VirtualProtect。因为VirtualProtecteshims(译者注:应该是ieshimsAPIs被标记为敏感的且在CFG中并不是一个合法的目的地址,我们不得不在创意者更新中使用包装函数。正如Thomas Garnier所建议的那样,可以在.netmscoree.dllmscories.dll中方便地找到包装函数,例如UtilExecutionEngine::ClrVirtualProtect。因为微软的ACG可以防止创建新的可执行内存,以及把已有可执行内存改为可写,因此,我们需要一个替代方法。使用VitualProtect可以把只读内存重映射为可写的,所以我借用了2015年黑帽大会里介绍的这种技术,并将包含chakra __guard_dispatch_icall_fptr的页面重新映射为可写,然后重写函数指针,使其指向包含jmp rax指令的chakra.dll中的任意位置。事实上,在大多数DLL中已经存在一个函数__guard_dispatch_icall_nop,它刚好就是一个单一的jmp rax指令。因此,我就能有效地绕过CFG的保护,因为在通过了所有检查之后,在chakra.dll中所有被保护的调用位置将立即跳转到目的地址。想必我们可以采用这种方法进一步探索使用函数重用攻击ACG的方法。为了完成这个小小的链接过程,需要以下满足以下条件:

1.mscoroc.dll载入进Edge进程

2.chakra.dll的只读内存区域调用ClrVirtualProtect +W

3.重写__guard_dispatch_icall_fptr以通过检查

从上面的vfgadgets列表可以看出,对于COOP来说edgehtml是一个重要的库。因此,我们的第一任务就是泄漏edgehtml的基址以及其他必要的组件,例如我们的counterfeit内存区域。这样,payload就能包含硬编码的偏移并在运行时重新定位。使用TheoriPOC中泄漏的bug,我们就能获得我们想要的基地址。

//OS Build 10.0.14393

var chakraBase = Read64(vtable).sub(0x274C40);

var guard_disp_icall_nop = chakraBase.add(0x273510);

var chakraCFG = chakraBase.add(0x5E2B78); //_guard_dispatch_icall...

var ntdllBase = Read64(chakraCFG).sub(0x95260);

 

//Find global CDocument object, VTable, and calculate EdgeHtmlBase

var [hi, lo] = PutDataAndGetAddr(document);

CDocPtr = Read64(newLong(lo + 0x30, hi, true));

EdgeHtmlBase = Read64(CDocPtr).sub(0xE80740);

 

//Rebase our COOP payload

rebaseOffsets(EdgeHtmlBase, chakraBase, ntdllBase, pRebasedCOOP);

 

触发COOP


使用COOP的一个关键部分就是在最初把JavaScript传递进looper中。使用我们假设的R/W原语,我们可以轻易地劫持到chakravtable,使其指向我们的looper,但我们怎么确保looper会开始迭代我们counterfeit的数据呢?对于这个答案,我们需要进looper进行评估,在这里我使用了CTravelLog::UpdateScreenshotStream:



注意在循环前的第一个块中,代码是在+0x30处获取到链表的指针。为了正确启动looper,我们需要劫持JavaScript对象的vtable,使其地址包含在我们的looper 中,然后在对象+0x30处放置一个指针使其指向counterfeit对象列表的首部。实际的counterfeit对象数据可以通过JavaScript进行定义和重新定位。还要注意,循环在对象+0x80h处的的下一个指针列表处进行迭代。当构造counterfeit流时这很重要。此外,请注意,这个间接调用的位置在vtable+0xF8h处。在counterfeit对象中的任意伪vtable都必须指向设计好的函数指针减0xF8h处,这个地址通常是在邻接vtable表的中间。为了启动COOPpayload,我劫持了JavascriptNativeIntArray对象,并地freeze()seal()虚函数进行了重载,如下所示:


劫持#1:调用LoadLibrary


正如前面所描述的那样,我的最终目的是绕过win10创意者更新中Edge浏览器的CFG保护,使其导出表suppresion能够使用。看看在kernel32kernelbase中导出的各种LoadLibrary调用,事实证明,即使是使用了最新的CFG特征,加载一个新的DLL进我们的进程也相当容易。其原因是有两个。一是在 kenel32.dll中,LoadLibraryExW实际上被标记为__guard_fids_table的合法调用目的地址。


二是,kernel32kernelbase中的其他LoadLibrary调用在最开始是suppressed的,但在Edge中,他们最终会变成合法的调用位置。这似乎源于MicrosoftEdgeCP!_delayLoadHelper2的加载延迟,并最终导致GetProcAddrLoadLibraryX APIs中被调用。如前所述,这表明要使所有的函数导出表变成不合法的调用地址是非常困难的。即使这样,其他的LoadLibrary call gates然后会suppressed或是临时开放,要想达成我们的目的,我们可以直接使用kernel32!LoadLibraryExW,因为它在初始化时是一个合法的目的地址。

为了使VirtualProtect包装函数载入进Edge进程中,我们需要调用LoadLibraryExW(“mscoree.dll”, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32)。我们可以在这里走捷径,并利用前面提到的调用器populate所有参数,而不是创建一个传统的COOPpayload,使用loopervfgadget来迭代四个counterfeit对象。


第一次迭代将会在0x800populate r8d

要想在下述汇编代码中populate r8dCHTMLEditor::IgnoreGlyphs是一个不错的vfgadget。参数0x800(LOAD_LIBRARY_SEARCH_SYSTEM32)将会在+0xD8h处载入。

回想一下,我们的counterfeit对象中的下一个指针一定在+0x80h。我们可以在内存中创建四个连续的counterfeit对象,每个对象的大小大于0xD8h,或是把下一个指针放在对象末尾。我选择了后者。在这种情况下,我们会有一个重叠的对象,所以我们必须小心在0xD8处的偏移不会影响到内存中第二个对象的第二次迭代的vfgadget。第一个populating r8dcounterfeit对象如下所示:



一旦从vfgadget返回,looper就会迭代faked的链接列表,且一定会调用另一个vfgadget,而这一次会populate rdx,其值为0x0(NULL)。为了实现这一目标,我使用了Tree::ComputedRunTypeEnumLayout::BidiRunBox::RunType()。我们可以从counterfeit对象+0x28h处载入我们的值(0x0)


现在我们已经为我们的API调用populated了第二和第三个参数,我们需要popolate第一个参数,这个参数是一个指向’mscoree.dll’的字符串,然后调用函数指针转到LoadLibraryExW.为了实现这个目的,需要一个完美的调用器vfgadgetMicrosoft::WRL::Details::InvokeHelper::Invoke()。汇编代码和对应的第三个counterfeit对象如下:


既然LoadLibraryExW已经被调用了,mscoree.dll也已经被加载到进程中, 我们需要获得回到JavaScript的返回地址重定位附加的COOP payloadslooperCFG都使用RAX作为间接分支的目的地址,所以,为了令新加载的模块返回到JavaScript,我们需要找到另一种方式来获得虚拟地址。幸运的是,在退出LoadLibraryExW时,RDX还包含模块地址的副本。因此,为了把RDX返回到内存区域里counterfeit对象列表中,我们可以将最后一个vfgadget放进对象列表中。在最后一次迭代中,我们可以调用CBindingURLBlockFilter::SetFilterNotify()来把RDX复制到当前counterfeit对象-0x88h处的地址。


然后looper会到达列表的末尾,并从劫持到的能把控制权返回到JavaScript代码的seal()调用中返回。到此,第一个COOP payload已经完成了,mscoree.dll已经加载进Edge中,现在我们可以使用下述代码在JavaScript中获得mscoree的基址。

//Retrieve loadlibrary return val from coop region

var mscoreebase = Read64(pRebasedCOOP.add(0x128));

alert("mscoree.dll loaded at: 0x" + mscoreebase.toString(16));


#2:调用VirtualProtect包装函数


在成功完成COOP payload的第一步之后,为了使其可写,现在我们可以重定位第二个COOP payload来在包含chakra!__guard_dispatch_icall_fptr读内存区域中调用ClrVirtualProtect。我们的目标是调用ClrVirtualProtect(this, chakraPageAddress,0x1000,PAGE_READWRITE,pScratchMemory)。这一次我们将演示一个不使用循环或是递归,而是使用单个counterfeit对象来populate所有参数及调用函数指针的COOP payload。我们将像以前一样使用相同的调用器vfgadget,只是这一次,它主要用于将counterfeit对象放入rcx中。



我们在原始的JavascriptNativeIntArray劫持了freeze()虚函数,使其指向Microsoft::WRL::Details::InvokeHelper::Invoke。这个vfgadget将在this+0x10的地址上移动这个指针,并将它当作函数指针.因此,从JavaScriptR/W原语中,除了劫持vtable来调用调用器trampoline函数,还需要覆盖对象+0x10+0x18处的值。

Write64(objAddr.add(0x10), pCOOPMem2);

Write64(objAddr.add(0x18), EdgeHtmlBase.add(0x2DC540));

Object.freeze(objAddr);



请注意,我们的fake对象将加载ClrVirtualProtect所需要的参数,并通过从另一个fake vtable中解析索引+0x100h,把ClrVirtualProtect 的地址populaterax。完成后,这将把chakra.dll我们希望的页映射为可写。




在这一步,我们完成了COOP,在最后一步中,事实上就使chakra.dllCFG失效了。我们可以在包含jmp rax指令的chakra.dll中选择任意地址。一旦识别出来,我们就使用JavaScript的写原语来覆盖chakra!__guard_dispatch_icall_fptr的函数指针,使其指向这个地址。这可以使CFG验证程序变成nop指令,并允许我们从JavaScript中劫持一个chakra vtable跳转到什么地方。

//Change chakra CFG pointer to NOP check

Write64(chakraCFG, guard_disp_icall_nop);

//trigger  hijack to 0x4141414141414141

Object.isFrozen(hijackedObj);

正如下面WinDbg输出列出的那样,使用CFG已经无效了,我们的劫持成功了,当我们试图跳转到一个没有映射进内存的地址0x4141414141414141时程序崩溃了。值得注意的是,因为CFG已经失效,我们可以使用劫持跳转到进程空间的任意地方。相比之下,由于0x4141414141414141bitmap中是不合法的,有CFG的程序会抛出一个异常。我们将在调用栈中看到我们替换掉的原始CFG例程ntdll!LdrpDispatchUserCallTargetES


总结


在这篇帖子中,我讨论了COOP,一种学术界提出的最新的代码重用攻击,并演示怎样使用它攻击现代执行流完整性的实现,例如微软CFG。总而言之,COOP相当容易使用,尤其是当把payload分割成更小的chains时。把各个vfgadgets拼接在一起和汇编ROP gadgets并没有什么不同。也许最耗时的部分就是在目的进程空间中找到并标记各种候选vfgadgets

微软的Control Flow Guard被认为是一个粗粒度的CFI实现,因此更容易受到这里所述的函数重用攻击的影响。相比之下,细粒度的CFI解决方案能够考虑到给定的间接调用的元素,例如预期的VTable类型,验证参数数量,甚至参数类型。权衡两种方法的关键是性能,因为在CFI中引入复杂的策略可能会显著地增加开销。尽管因为应用程序会因为使用forward-edgebackward-edge CFI而变得难以攻破,防御最新的代码重用攻击仍然是很重要的。

为了抵消CFG的一些局限性,微软似乎专注于多样化的预防措施,例如在CFGArbitrary Code Guard中通过导出表supression保护关键call gates,比如VirtualProtect。然而,这篇帖子的一个关键点挑战是用户空间设计和执行防御。正如我们几前前在EMET中看到的那样,研究人员通过重用EMET本身的代码解除了EMET的防御。此外,正如2015黑帽大会里演示的那样,我们同样利用驻留在用户空间的关键CFG函数指针来改变CFG的行为。

相比之下,EndgameHA-CFI解决方案完全由内核和硬件实现,即使容易受到函数重用攻击,但由于特权分离,使其更难篡改。在本系列的每二部分,我将使用我们自己的HA-CFI和正在进行的研究来对COOP进行讨论,以演示我们的检测逻辑如何应对最新的代码重用攻击。


原文链接:https://www.endgame.com/blog/disarming-control-flow-guard-using-advanced-code-reuse-attacks


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值