目前的许多恶意软件已经逐渐开始抛弃了这两年盛行已久的ROP 攻击,在过去两年中,有一种新型代码重用攻击的技术开始流行,即伪造面向对象编程的攻击技术,它全称为Counterfeit Object-Oriented Programming (COOP),代表着针对forward-edge的控制流完整性(CFI)的最先进的攻击。目前COOP基本上只存在于理论中,还尚未出现在实际的攻击利用中。一方面,因为强CFI的实现会让更多的内核漏洞暴露,而另一方面可能是因为攻击者更倾向于使用更容易实现的攻击方法。在Windows 10周年更新中,微软开始启用控制流保护(CFG,Control Flow Guard)。而在CFG中,还没有实施针对backward-edge CFI的攻击。因为这个针对返回地址检测的forward-edge 或backward-edge CFI的实现几乎干掉了代码重用攻击的可能性,但是当返回流量保护(Return Flow Guard ,RFG)出现后并且攻击者不再依赖于破坏堆栈上的返回地址来进行攻击时,代码重用攻击是否还可行吗?

译者注:加州大学和微软公司于2005年提出了控制流完整性的防御机制。其核心思想是限制程序运行中的控制转移,使之始终处于原有的控制流图所限定的范围内。具体做法是通过分析程序的控制流图,获取间接转移指令(包括间接跳转、间接调用、和函数返回指令)目标的白名单,并在运行过程中,核对间接转移指令的目标是否在白名单中,本文就试图对微软的CFG防护和HA-CFI的COOP重用攻击防护进行评估。

译者注:HA-CFI,一种全新的利用硬件辅助的CFI防御机制。

微软的CFG

目前已经有很多文章在讨论CFG了,但比较有分量的文章还是最近的有关Clang CFI和微软CFG的两篇文章。第一篇文章的重点是Clang,第二个重点是微软执行CFI,而另外的一些文章则提供了CFG实施的进一步细节。

过去几年,关于如何绕过CFG一直是安全专家关注的焦点。不过黑客的力量总是让我们惊叹不已,有些黑客已经可以完全绕过CFG了,不过在此我们要强调一点,即CFI可以进一步细分为两种:forward-edge CFI 和backward-edge CFI。

forward-edge CFI:间接保护CALL或JMP站点,forward-edge CFI的解决方案包括Microsoft CFG和Endgame的HA-CFI。

backward-edge CFI:保护RET指令,backward-edge CFI解决方案包括Microsoft Return Flow Guard,EndGame的DBI漏洞利用预防的组件,以及包括Intel CET在内的其他ROP检测。

之所以会选用这种分类,是因为这样有助于描述CFG保护的逻辑顺序,即间接呼叫站点——非保护站点——返回栈。例如,最近的一个Edge的恶意软件就利用POC并最终在使用R / W的原始方法下修改了堆栈上的返回地址。但这种方法不适用于CFG,不应被视为CFG保护的漏洞。不过,这也表明了CFG保护的有效性,并使攻击者转向劫持控制流,而不是间接呼叫站点。同时,这个例子也暴露了CFG的缺陷,即攻击者可以利用未受保护的呼叫站点,重新映射包含CFG代码指针的只读存储器区域,并将其改变为可以通过检查的代码。 

很多forward-edge CFI也被称为粗粒度CFI,即一个特定的调用或者转跳进入一个函数,CFI的实现将允许攻击者调用的数量非常大:包括程序以外的合法的执行调用和在程序和库里的。这些粗粒度实现有两个原因:性能和信息限制。更细粒度的实现通常会为每次调用和转跳带来更多的安全检查。

CFG会向每个受保护的DLL添加__guard_fids_table,由二进制内部的间接调用站点的有效或敏感目标的RVA列表组成。其中一个地址用作CFG位图的索引,它可以根据地址是否有效来切换,还有一个API是用来修改这个位图的,例如为了支持JIT编码的页面:kernelbase!SetProcessValidCallTargets通过调用ntdll!SetInformationVirtualMemory,来更新位图。

Windows 10 Creators Update中的CFG新增函数可以阻止这种调用。换句话说,CFG保护呼叫站点可以把这种调用地址标记为非法目标地址。执行此操作时,需要使用CFG位图每一个地址的第二位,另外在构建每个位图的初始化进程时,需要使用每个RVA条目的__guard_fids_table中的标志字节。

在Windows 64位系统中,位于9-63位的地址是用来从CFG位图检索qword的,并且使用第3-10位的地址来访问qword中的特定位。在调用被抑制后,这些在给定地址的CFG权限将由CFG位图中的两个位表示。此外,大多数DLL中的__guard_dispatch_icall_fptr现在已设置为指向ntdll!LdrpDispatchUserCallTargetES,其中有效的调用目标必须从CFG位图中省略“01”。

1.png

因为使用GetProcAddress意味着后续代码可以将返回值作为函数指针调用,因此实现新的抑制调用函数会变得复杂一些。只要该条目以前未被标记为敏感或未标记为非法,例如VirtualProtect,SetProcessValidCallTargets等,那么CFG就会通过将CFG位图中相应的两位条目从“10”(输出禁止)更改为“01”(有效的呼叫站点)来处理此事件。因此,一些调用将会在进行创建时以不合法的间接调用启用,但借用运行时的代码,最终这些非法的调用将会成为有效的调用目标。下面我们列出7个发生这种情况时的样本所调用的堆栈:

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 

攻击者会将COOP作为进行CFI攻击时的潜在漏洞,如果要绕过所有的forward-edge CFI并同时执行代码,可以利用连续的攻击序列并重用现有的虚拟函数。这样就能使用与ROP类似的方式,单独执行一系列小段合法的函数,例如,将值加载到RDX中,不过也可以把这些各个小段的合法函数组合到一起进行更复杂的任务。

COOP的基本组成部分是利用主循环函数,该函数可以遍历链表或对象数组,并在每个对象上调用虚拟方法。攻击者就是利用内存中伪造的对象,然后把单个对象拼凑在一起,这样就能在某些情况下与真实的对象重叠,覆盖对象后,就能在主循环中按攻击者拼接好的顺序调用合法的虚函数。 起初攻击者会使用COOP有效载荷的方法,攻击目标仅限于Windows 7 32位和Windows 7 64位的Internet Explorer 10,以及Linux 64位的Firefox。随着研究的扩展,攻击者发现使用 COOP有效载荷也可以使用递归或具有许多间接调用的函数,并将攻击范围扩大到Objective-C运行环境中。

总的来说,这方面的研究非常有趣和前沿。我们希望将这一理论能够应用于现代CFI攻击中,以对以下三个方面作出评估:

1.在加固的浏览器中构建COOP有效载荷的难度;

2.是否可以绕过CFG和HA-CFI;

3.是否能改进CFI使其能检测到COOP类型的攻击;

找出COOP有效载荷

COOP的主要目标是Windows 10上的Microsoft Edge,因为它代表了一个完全加固的CFG应用,并且我们还能在它的内存中使用JavaScript来准备我们的COOP有效内容。虽然在目标实现过程中,我们还发现了许多漏洞,但我们只专注于劫持CFI的控制流程,,并对攻击者作出了下列假设:

1.任意的读写原始都是取自于JavaScript;

2.允许使用硬编码的偏移量,因为在运行时需要动态查找小段代码;

3.启用了所有Microsoft最新的Creators更新缓解措施,例如ACG,CIG,可以调用抑制的CFG;

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

根据我们的初步研究,已经有人在Windows 10周年更新(操作系统版本为14393.953)上利用了Edge的POC。我们使用更新中的防御机制设计了COOP的有效载荷,并在启用导出禁用的Windows 10 Creator(操作系统版本为15063.138)更新时,对该POC进行了验证。

理想的POC将执行攻击者的shellcode或启动恶意应用程序,攻击者的经典代码执行模型是将内存中的一些受控数据映射为+ X,然后跳转到新修改的+ X区域中的shellcode。不过,我们的真正目标是生成能够执行的COOP有效载荷,有意义的东西,同时保护forward-edge CFI。这样的有效载荷提供了数据点,我们可以用它来测试和改进我们自己的CFI算法。此外,Arbitrary Code Guard(ACG)或Edge的子进程策略超出我们的研究范围。所以我们就把目标定位了对Windows 10 Creator 更新的使用COOP有效载荷来禁用CFG,以实现在DLL内能够跳转或是调用任意位置的代码。下面,我们就是我们所研究出的了两个主要的COOP有效载荷:

1.在Windows 10周年的纪念更新版本中,缺少ACG,我们的有效载荷可以将我们控制的数据映射为可执行文件,然后在禁用CFG后跳转到受控shellcode的区域;

2.对于Windows 10 Creator更新来说,我们的最终目标就是劫持CFG;

寻找COOP函数小片段

首先我们的第一个任务是确定COOP各个组成部分的术语,学术论文是将每个重用函数称为虚拟函数片段(virtual function gadget)或vfgadget,并且在描述每个特定类型的vfgadget时使用缩写,如将主循环vfgadget缩写为ML-G。本文中,我们会采用以非正式的方式来命名每种类型的小片段。

Looper:在执行复杂COOP有效载荷中,起重要作用的的主循环小片段本文称为ML-G;

Invoker:一个调用函数指针的vfgadget,本文称为INV-G;

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

另外,我们编写了脚本来帮助我们识别给定二进制文件中的vfgadget。我们使用了IDA Python,利用推理帮助我们找到了循环器,调用者和参数弹出器。在我们的研究中,我们发现COOP的一个实用方法,在返回到JavaScript之前,把vfgadget链接到一起并依次执行少量的vfgadget。根据需要通过额外的COOP 有效载荷来重复这个过程。因此,为了实现我们本文的目的,我们没有将二进制代码提升到IR。不过,为了将巨大的COOP有效载荷拼接在一起,比如说完全通过重用代码运行一个C2 socket线程,我们可能需要将二进制代码提升到IR以便将所需的vfgadget拼接在一起。对于vfgadget的每个子类型,我们定义了一些规则,并使用该规则在Edge(chakra.dll和edgehtml.dll)的两个二进制文件中进行搜索。这些规则包括:

1.存在于__guard_fids_table上的函数;

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

3.循环不能让参数寄存器崩溃;2.png

在vfgadget的所有类中,搜索循环器是最耗时的。因为有许多潜在的循环器在使用时都有这样或那样的限制,这使得它很难被使用。鉴于此,我们提出了调用函数指针的vfgadget来寻找, vfgadget的运行速度非常快,可以轻松地从单个伪造的对象中一次性填充多达六个参数。因此,当尝试调用单个API时,除非需要返回值,COOP可以使用其vfgadget的快速运行方式,来完全避免需要循环或递归。在x64程序上能够找到许多寄存器来对参数寄存器进行填充。

COOP有效载荷的利用

通过从脚本语言触发COOP,我们实际上可以在COOP中将一些复杂的任务删除,因为一次性把所有小片段进行拼接可能会让执行变得复杂。我们可以使用JavaScript来实现我们的小片段执行优势,并反复调用精简过的COOP有效载荷序列。这样我们就可以将算术和条件操作等重新转移回JavaScript,并通过COOP将原始函数重用到关键API的准备和调用中。此外,我们会展示一个利用该方法的样本,包括在我们劫持到的#1 部分将COOP的返回值传回到JavaScript,并讨论如何调用LoadLibrary。

为了方便阐述,我们将只介绍一种最简单的COOP有效载荷。本文中的所有有效载荷的最终目标都是要调用VirtualProtect。由于VirtualProtect和eshims API先前已被标记为敏感的,而不是CFG的有效目标,所以我们必须在Windows 10 Creator更新中使用包装器函数,比如可以在.NET库mscoree.dll和mscories.dll中找到一些方便的包装器,例如UtilExecutionEngine :: ClrVirtualProtect。由于Microsoft的ACG可以防止创建新的可执行内存,或者将现有的可执行内存更改为可写入,因此需要一种替代方法。由于只读内存可以通过VirtualProtect进行重写,所以我们可以将包含chakra!__ guard_dispatch_icall_fptr的页面重新映射为可写,然后覆盖函数指针以指向chakra.dll中的任意包含一个jmp rax指令的位置。实际上,在大多数DLL中已经存在一个函数__guard_dispatch_icall_nop,这是一个单一的jmp rax指令。因此,我们就可以有效地禁用CFG,因为chakra.dll中的所有受保护的调用站点将立即跳转到目标地址,以通过所有的安全检查。然后,我们可以据此来进一步探索函数重用来攻击ACG。为了完成这个探索,我们需要满足以下3个条件: 

1.将mscoree.dll加载到Edge进程;

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

3.覆盖__guard_dispatch_icall_fptr以便通过所有安全检查;

从上面的vfgadget列表中可以看出,edgehtml是COOP的重要库。因此,我们的第一个任务便是泄漏edgehtml的基地址以及任何其他必要的组成部分,例如我们伪造的内存区域。这样,COOP有效载荷就能包含硬编码的偏移并在运行时重新定位。使用POC中的信息泄漏漏洞,我们可以获得我们需要的所有基地址。

//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转化为循环器的函数,使用我们假设的读写原语,就可以很容易地劫持一个vfgadget列表中的looper,但是要把我们伪装的数据迭代到looper中,我们还需要使用CTravelLog :: UpdateScreenshotStream。

3.png

注意在循环前的第一个块中,代码是在+ 0x30 h处检索到链表的指针。为了正确启动looper,我们必须劫持一个JavaScript对象的vtable,将地址包含在我们的looper中,然后将一个指针放在对象+ 0x30 h处,指向我们伪装的对象列表的开头。实际的伪造对象数据可以通过JavaScript进行定义和重新定位。还要注意,循环对象会在+ 0x80h处的下一个指针的列表中进行迭代。在制作伪装的对象时,这点很重要。另外,请注意,在偏移量为+ 0xF8h处间接呼叫站点的vtable。我们的伪造对象中的任何假的虚拟vtable都必须在所需函数指针的地址处减去0xF8h,这通常会发生在vtable表中间位置的相邻部分。要启动我们的COOP有效载荷,就需要选择劫持一个JavascriptNativeIntArray对象,专门覆盖freeze()和seal()的虚函数如下:

var hijackedObj = new Array(0);
[hi, lo] = PutDataAndGetAddr(hijackedObj);
var objAddr = new Long(lo, hi, true);
Write64(objAddr.add(0x30), pRebasedCOOP);
Write64(objAddr, pFakeVTable);
Object.seal(hijackedObj); //Trigger initial looper

4.png

劫持方法1:调用LoadLibrary

如前所述,我们的最终目标是绕过启用导出抑制函数的Win10 Creator更新里Edge上的CFG。看看在kernel32和kernelbase中导出的各种LoadLibrary调用,就知道即使是使用了最新的CFG,把一个新的DLL加载到我们的进程也是很容易的。首先,LoadLibraryExW实际上已被标记为kernel32.dll中__guard_fids_table中有效的调用目标。

5.png

其次,kernel32和kernelbase中的其他LoadLibrary调用刚开始是被抑制的,但在Edge中,这些被抑制的调用最终成为有效的调用站点。这似乎源于MicrosoftEdgeCP!_delayLoadHelper2中的一些延迟加载而最终导致的GetProcAddr在LoadLibraryX API上被调用。如前所述,要让所有函数导出无效的呼叫目标是非常困难,即使这样,其他的LoadLibrary 调用门还是会被抑制或是临时开放,要想达成我们的目的,我们可以直接使用kernel32!LoadLibraryExW,因为它在初始化时就是一个合法的目的地址。

为了使我们想要的VirtualProtect包装器能加载到Edge进程中,就需要调用LoadLibraryExW(“mscoree.dll”,NULL,LOAD_LIBRARY_SEARCH_SYSTEM32)。不过我们可以利用上述的调用方法来一次填充我们的所有参数,而不是使用looper vfgadget创建一个传统的COOP有效载荷来迭代四个被假冒对象。

6.png

我们的第一次迭代将使用0x800填充r8d。 要用vfgadget来填充r8d,CHTMLEditor :: IgnoreGlyphs是一个很好的选择。以后,参数0x800(LOAD_LIBRARY_SEARCH_SYSTEM32)都将在+ 0xD8h处加载。回想一下,我们的伪造对象中的下一个指针必须在+ 0x80h。我们可以在内存中创建四个连续的伪造对象,每个对象的大小都大于0xD8h,或者我们可以将下一个指针放在对象的末尾。如果我们选择了将下一个指针放在对象的末尾,可能会出现一个重叠对象,所以我们必须小心,这个+ 0xD8的偏移量不会影响我们在内存中第二个对象上进行的第二次迭代的vfgadget,用于填充r8d的第一个伪造对象如下所示。

7.png

从这个vfgadget返回后,looper就会遍历我们的假链接列表,所以我们现在必须再次调用另一个vfgadget的值0x0(NULL)来填充rdx。为了实现这一点,我们使用了Tree :: ComputedRunTypeEnumLayout :: BidiRunBox :: RunType()。我们可以从我们的伪造对象+ 0x28h处加载我们的值(0x0)。

8.png

现在我们已经为API调用填充了第二个和第三个参数,不过我们还差第一个参数,该参数是一个指向我们的'mscoree.dll'字符串的指针,然后调用函数指针转到LoadLibraryExW.为了实现这个目的,我们需要一个调用器vfgadget,Microsoft::WRL::Details::InvokeHelper::Invoke(),汇编代码和对应的第三个伪装对象如下。

9.png

现在LoadLibraryExW已经调用完毕,不过我们希望将mscoree.dll也加载到我们的进程中,为了实现这个目标,我们需要将返回地址返回给JavaScript以重新附加COOP有效载荷。looper和CFG都使用RAX作为间接分支目标,因此我们需要找到另一种方式来将新加载的模块的虚拟地址重新设为JavaScript。幸运的是,在退出LoadLibraryExW时,RDX还包含模块地址的副本。因此,我们可以将一个最终的vfgadget添加到我们的对象列表中,以便将RDX移回到我们的假冒对象内存区域。为了在最后一次迭代中进行循环,我们将调用CBindingURLBlockFilter :: SetFilterNotify(),它会将RDX复制到我们当前伪造对象的0x88h处。

10.png

打针机然后到达我们的列表的末尾,并从被劫持的密封()呼叫转移控制返回到我们的JavaScript代码。第一个COOP有效载荷已经完成,mscoree.dll已经加载到Edge,现在我们可以从下面的代码片段中的JavaScript获取mscoree的基地址。

然后looper会到达列表的末尾,并从劫持到的seal()中进行呼叫转移,以便将调用中的控制权返回到JavaScript代码。这样,第一个COOP 有效载荷就算完成了,现在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有效载荷后,我们现在可以重定位第二个COOP有效载荷了,以便在包含chakra!__ guard_dispatch_icall_fptr的只读内存区域中调用ClrVirtualProtect,以使其可写,我们的目标是调用ClrVirtualProtect即chakraPageAddress,0x1000,PAGE_READWRITE,pScratchMemory。不过这次我们将不会通过使用单个伪装的对象来填充所有参数并调用函数指针来利用循环或递归。我们将像以前一样使用相同的调用器vfgadget,不过这次,它主要用于将伪造对象移动到rcx中。

11.png

我们从我们原来的JavascriptNativeIntArray中劫持了freeze()虚函数,该函数指向Microsoft :: WRL :: Details :: InvokeHelper :: Invoke。这个vfgadget会根据这个+ 0x10的地址来移动这个指针,并将这个+ 0x18h地址看作一个函数指针。因此,在JavaScript中的R / W原语中,除了劫持vtable来调用调用器trampoline函数,还需要覆盖对象+0x10和+0x18处的值。

Write64(objAddr.add(0x10),pCOOPMem2);
Write64(objAddr.add(0x18),EdgeHtmlBase.add(0x2DC540));
Object.freeze(objAddr);

12.png

请注意,我们伪造的对象将加载ClrVirtualProtect的所有必需参数,并通过从另一个虚假的vtable解析索引+ 0x100h,并将ClrVirtualProtect的地址填充到rax中。这样,chakra.dll中所需的页面就可写:

13.png

在完成了COOP后,最后一步便是禁用chakra.dll的CFG。我们可以在包含指令jmp rax的chakra.dll中选择任意地址,一旦选好地址,我们就可以使用JavaScript的写入原语来覆盖chakra!__ guard_dispatch_icall_fptr的函数指针,使其指向选好的地址。这样CFG验证程序就变为了nop指令,并允许我们从JavaScript中劫持一个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被禁用,我们可能会将此劫持跳转到进程地址空间中的任何地方。另外,由于0x41414141414141在位图中会显示为无效地址,因此CFG会显示为一个异常,我们将在调用堆栈中看到我们迭代的原始CFG进程ntdll!LdrpDispatchUserCallTargetES:

14.png

总结

我们本文所讨论的COOP目前还仅停留在学术界,属于一种比较前沿的代码重用攻击,本文就为大家阐述了如何使用它来攻击控制流完整性(CFI),如Microsoft CFG。总的来说,COOP是非常容易实现的,特别是把复杂有效载荷分解成较小的片段拼接时,因为拼接在一起的vfgadget与经过汇编的ROP gadget的并没有什么本质区别。非要说有区别,那也就是在目标进程空间内查找和标记各种类型的vfgadget要耗费大量时间。

由于Microsoft的Control Flow Guard被认为是一个粗粒度的CFI实现,因此更容易受到函数重用攻击的影响。相比之下,可以通过预防间接调用的一些关键项来预防细粒度的CFI攻击,例如VTable,验证参数数量,甚至参数类型等。

在CFI的攻击策略中会引入太多的复杂性进程,从而让攻击成本增加。但这并不代表攻击者不会使用COOP方法,尽管微软已经使用forward-edge和backward-edge CFI来进行了防护,但对最新的代码重用攻击还是要重点关注。

为了让CFG的防护面更宽一些局,微软也开始专注多种多样化的预防措施,例如在CFG和Arbitrary Code Guard中通过抑制出口调用来保护关键的通话门,如VirtualProtect。

目前来看, HA-CFI解决方案可以完全实现内核和硬件函数的防护,即使在该方案下受到函数重用攻击,使得由于权限分离而让进程更难以被篡改。

目前的许多恶意软件已经逐渐开始抛弃了这两年盛行已久的ROP 攻击,在过去两年中,有一种新型代码重用攻击的技术开始流行,即伪造面向对象编程的攻击技术,它全称为Counterfeit Object-Oriented Programming (COOP),代表着针对forward-edge的控制流完整性(CFI)的最先进的攻击。目前COOP基本上只存在于理论中,还尚未出现在实际的攻击利用中。一方面,因为强CFI的实现会让更多的内核漏洞暴露,而另一方面可能是因为攻击者更倾向于使用更容易实现的攻击方法。在Windows 10周年更新中,微软开始启用控制流保护(CFG,Control Flow Guard)。而在CFG中,还没有实施针对backward-edge CFI的攻击。因为这个针对返回地址检测的forward-edge 或backward-edge CFI的实现几乎干掉了代码重用攻击的可能性,但是当返回流量保护(Return Flow Guard ,RFG)出现后并且攻击者不再依赖于破坏堆栈上的返回地址来进行攻击时,代码重用攻击是否还可行吗?

译者注:加州大学和微软公司于2005年提出了控制流完整性的防御机制。其核心思想是限制程序运行中的控制转移,使之始终处于原有的控制流图所限定的范围内。具体做法是通过分析程序的控制流图,获取间接转移指令(包括间接跳转、间接调用、和函数返回指令)目标的白名单,并在运行过程中,核对间接转移指令的目标是否在白名单中,本文就试图对微软的CFG防护和HA-CFI的COOP重用攻击防护进行评估。

译者注:HA-CFI,一种全新的利用硬件辅助的CFI防御机制。

微软的CFG

目前已经有很多文章在讨论CFG了,但比较有分量的文章还是最近的有关Clang CFI和微软CFG的两篇文章。第一篇文章的重点是Clang,第二个重点是微软执行CFI,而另外的一些文章则提供了CFG实施的进一步细节。

过去几年,关于如何绕过CFG一直是安全专家关注的焦点。不过黑客的力量总是让我们惊叹不已,有些黑客已经可以完全绕过CFG了,不过在此我们要强调一点,即CFI可以进一步细分为两种:forward-edge CFI 和backward-edge CFI。

forward-edge CFI:间接保护CALL或JMP站点,forward-edge CFI的解决方案包括Microsoft CFG和Endgame的HA-CFI。

backward-edge CFI:保护RET指令,backward-edge CFI解决方案包括Microsoft Return Flow Guard,EndGame的DBI漏洞利用预防的组件,以及包括Intel CET在内的其他ROP检测。

之所以会选用这种分类,是因为这样有助于描述CFG保护的逻辑顺序,即间接呼叫站点——非保护站点——返回栈。例如,最近的一个Edge的恶意软件就利用POC并最终在使用R / W的原始方法下修改了堆栈上的返回地址。但这种方法不适用于CFG,不应被视为CFG保护的漏洞。不过,这也表明了CFG保护的有效性,并使攻击者转向劫持控制流,而不是间接呼叫站点。同时,这个例子也暴露了CFG的缺陷,即攻击者可以利用未受保护的呼叫站点,重新映射包含CFG代码指针的只读存储器区域,并将其改变为可以通过检查的代码。 

很多forward-edge CFI也被称为粗粒度CFI,即一个特定的调用或者转跳进入一个函数,CFI的实现将允许攻击者调用的数量非常大:包括程序以外的合法的执行调用和在程序和库里的。这些粗粒度实现有两个原因:性能和信息限制。更细粒度的实现通常会为每次调用和转跳带来更多的安全检查。

CFG会向每个受保护的DLL添加__guard_fids_table,由二进制内部的间接调用站点的有效或敏感目标的RVA列表组成。其中一个地址用作CFG位图的索引,它可以根据地址是否有效来切换,还有一个API是用来修改这个位图的,例如为了支持JIT编码的页面:kernelbase!SetProcessValidCallTargets通过调用ntdll!SetInformationVirtualMemory,来更新位图。

Windows 10 Creators Update中的CFG新增函数可以阻止这种调用。换句话说,CFG保护呼叫站点可以把这种调用地址标记为非法目标地址。执行此操作时,需要使用CFG位图每一个地址的第二位,另外在构建每个位图的初始化进程时,需要使用每个RVA条目的__guard_fids_table中的标志字节。

在Windows 64位系统中,位于9-63位的地址是用来从CFG位图检索qword的,并且使用第3-10位的地址来访问qword中的特定位。在调用被抑制后,这些在给定地址的CFG权限将由CFG位图中的两个位表示。此外,大多数DLL中的__guard_dispatch_icall_fptr现在已设置为指向ntdll!LdrpDispatchUserCallTargetES,其中有效的调用目标必须从CFG位图中省略“01”。

1.png

因为使用GetProcAddress意味着后续代码可以将返回值作为函数指针调用,因此实现新的抑制调用函数会变得复杂一些。只要该条目以前未被标记为敏感或未标记为非法,例如VirtualProtect,SetProcessValidCallTargets等,那么CFG就会通过将CFG位图中相应的两位条目从“10”(输出禁止)更改为“01”(有效的呼叫站点)来处理此事件。因此,一些调用将会在进行创建时以不合法的间接调用启用,但借用运行时的代码,最终这些非法的调用将会成为有效的调用目标。下面我们列出7个发生这种情况时的样本所调用的堆栈:

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 

攻击者会将COOP作为进行CFI攻击时的潜在漏洞,如果要绕过所有的forward-edge CFI并同时执行代码,可以利用连续的攻击序列并重用现有的虚拟函数。这样就能使用与ROP类似的方式,单独执行一系列小段合法的函数,例如,将值加载到RDX中,不过也可以把这些各个小段的合法函数组合到一起进行更复杂的任务。

COOP的基本组成部分是利用主循环函数,该函数可以遍历链表或对象数组,并在每个对象上调用虚拟方法。攻击者就是利用内存中伪造的对象,然后把单个对象拼凑在一起,这样就能在某些情况下与真实的对象重叠,覆盖对象后,就能在主循环中按攻击者拼接好的顺序调用合法的虚函数。 起初攻击者会使用COOP有效载荷的方法,攻击目标仅限于Windows 7 32位和Windows 7 64位的Internet Explorer 10,以及Linux 64位的Firefox。随着研究的扩展,攻击者发现使用 COOP有效载荷也可以使用递归或具有许多间接调用的函数,并将攻击范围扩大到Objective-C运行环境中。

总的来说,这方面的研究非常有趣和前沿。我们希望将这一理论能够应用于现代CFI攻击中,以对以下三个方面作出评估:

1.在加固的浏览器中构建COOP有效载荷的难度;

2.是否可以绕过CFG和HA-CFI;

3.是否能改进CFI使其能检测到COOP类型的攻击;

找出COOP有效载荷

COOP的主要目标是Windows 10上的Microsoft Edge,因为它代表了一个完全加固的CFG应用,并且我们还能在它的内存中使用JavaScript来准备我们的COOP有效内容。虽然在目标实现过程中,我们还发现了许多漏洞,但我们只专注于劫持CFI的控制流程,,并对攻击者作出了下列假设:

1.任意的读写原始都是取自于JavaScript;

2.允许使用硬编码的偏移量,因为在运行时需要动态查找小段代码;

3.启用了所有Microsoft最新的Creators更新缓解措施,例如ACG,CIG,可以调用抑制的CFG;

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

根据我们的初步研究,已经有人在Windows 10周年更新(操作系统版本为14393.953)上利用了Edge的POC。我们使用更新中的防御机制设计了COOP的有效载荷,并在启用导出禁用的Windows 10 Creator(操作系统版本为15063.138)更新时,对该POC进行了验证。

理想的POC将执行攻击者的shellcode或启动恶意应用程序,攻击者的经典代码执行模型是将内存中的一些受控数据映射为+ X,然后跳转到新修改的+ X区域中的shellcode。不过,我们的真正目标是生成能够执行的COOP有效载荷,有意义的东西,同时保护forward-edge CFI。这样的有效载荷提供了数据点,我们可以用它来测试和改进我们自己的CFI算法。此外,Arbitrary Code Guard(ACG)或Edge的子进程策略超出我们的研究范围。所以我们就把目标定位了对Windows 10 Creator 更新的使用COOP有效载荷来禁用CFG,以实现在DLL内能够跳转或是调用任意位置的代码。下面,我们就是我们所研究出的了两个主要的COOP有效载荷:

1.在Windows 10周年的纪念更新版本中,缺少ACG,我们的有效载荷可以将我们控制的数据映射为可执行文件,然后在禁用CFG后跳转到受控shellcode的区域;

2.对于Windows 10 Creator更新来说,我们的最终目标就是劫持CFG;

寻找COOP函数小片段

首先我们的第一个任务是确定COOP各个组成部分的术语,学术论文是将每个重用函数称为虚拟函数片段(virtual function gadget)或vfgadget,并且在描述每个特定类型的vfgadget时使用缩写,如将主循环vfgadget缩写为ML-G。本文中,我们会采用以非正式的方式来命名每种类型的小片段。

Looper:在执行复杂COOP有效载荷中,起重要作用的的主循环小片段本文称为ML-G;

Invoker:一个调用函数指针的vfgadget,本文称为INV-G;

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

另外,我们编写了脚本来帮助我们识别给定二进制文件中的vfgadget。我们使用了IDA Python,利用推理帮助我们找到了循环器,调用者和参数弹出器。在我们的研究中,我们发现COOP的一个实用方法,在返回到JavaScript之前,把vfgadget链接到一起并依次执行少量的vfgadget。根据需要通过额外的COOP 有效载荷来重复这个过程。因此,为了实现我们本文的目的,我们没有将二进制代码提升到IR。不过,为了将巨大的COOP有效载荷拼接在一起,比如说完全通过重用代码运行一个C2 socket线程,我们可能需要将二进制代码提升到IR以便将所需的vfgadget拼接在一起。对于vfgadget的每个子类型,我们定义了一些规则,并使用该规则在Edge(chakra.dll和edgehtml.dll)的两个二进制文件中进行搜索。这些规则包括:

1.存在于__guard_fids_table上的函数;

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

3.循环不能让参数寄存器崩溃;2.png

在vfgadget的所有类中,搜索循环器是最耗时的。因为有许多潜在的循环器在使用时都有这样或那样的限制,这使得它很难被使用。鉴于此,我们提出了调用函数指针的vfgadget来寻找, vfgadget的运行速度非常快,可以轻松地从单个伪造的对象中一次性填充多达六个参数。因此,当尝试调用单个API时,除非需要返回值,COOP可以使用其vfgadget的快速运行方式,来完全避免需要循环或递归。在x64程序上能够找到许多寄存器来对参数寄存器进行填充。

COOP有效载荷的利用

通过从脚本语言触发COOP,我们实际上可以在COOP中将一些复杂的任务删除,因为一次性把所有小片段进行拼接可能会让执行变得复杂。我们可以使用JavaScript来实现我们的小片段执行优势,并反复调用精简过的COOP有效载荷序列。这样我们就可以将算术和条件操作等重新转移回JavaScript,并通过COOP将原始函数重用到关键API的准备和调用中。此外,我们会展示一个利用该方法的样本,包括在我们劫持到的#1 部分将COOP的返回值传回到JavaScript,并讨论如何调用LoadLibrary。

为了方便阐述,我们将只介绍一种最简单的COOP有效载荷。本文中的所有有效载荷的最终目标都是要调用VirtualProtect。由于VirtualProtect和eshims API先前已被标记为敏感的,而不是CFG的有效目标,所以我们必须在Windows 10 Creator更新中使用包装器函数,比如可以在.NET库mscoree.dll和mscories.dll中找到一些方便的包装器,例如UtilExecutionEngine :: ClrVirtualProtect。由于Microsoft的ACG可以防止创建新的可执行内存,或者将现有的可执行内存更改为可写入,因此需要一种替代方法。由于只读内存可以通过VirtualProtect进行重写,所以我们可以将包含chakra!__ guard_dispatch_icall_fptr的页面重新映射为可写,然后覆盖函数指针以指向chakra.dll中的任意包含一个jmp rax指令的位置。实际上,在大多数DLL中已经存在一个函数__guard_dispatch_icall_nop,这是一个单一的jmp rax指令。因此,我们就可以有效地禁用CFG,因为chakra.dll中的所有受保护的调用站点将立即跳转到目标地址,以通过所有的安全检查。然后,我们可以据此来进一步探索函数重用来攻击ACG。为了完成这个探索,我们需要满足以下3个条件: 

1.将mscoree.dll加载到Edge进程;

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

3.覆盖__guard_dispatch_icall_fptr以便通过所有安全检查;

从上面的vfgadget列表中可以看出,edgehtml是COOP的重要库。因此,我们的第一个任务便是泄漏edgehtml的基地址以及任何其他必要的组成部分,例如我们伪造的内存区域。这样,COOP有效载荷就能包含硬编码的偏移并在运行时重新定位。使用POC中的信息泄漏漏洞,我们可以获得我们需要的所有基地址。

//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转化为循环器的函数,使用我们假设的读写原语,就可以很容易地劫持一个vfgadget列表中的looper,但是要把我们伪装的数据迭代到looper中,我们还需要使用CTravelLog :: UpdateScreenshotStream。

3.png

注意在循环前的第一个块中,代码是在+ 0x30 h处检索到链表的指针。为了正确启动looper,我们必须劫持一个JavaScript对象的vtable,将地址包含在我们的looper中,然后将一个指针放在对象+ 0x30 h处,指向我们伪装的对象列表的开头。实际的伪造对象数据可以通过JavaScript进行定义和重新定位。还要注意,循环对象会在+ 0x80h处的下一个指针的列表中进行迭代。在制作伪装的对象时,这点很重要。另外,请注意,在偏移量为+ 0xF8h处间接呼叫站点的vtable。我们的伪造对象中的任何假的虚拟vtable都必须在所需函数指针的地址处减去0xF8h,这通常会发生在vtable表中间位置的相邻部分。要启动我们的COOP有效载荷,就需要选择劫持一个JavascriptNativeIntArray对象,专门覆盖freeze()和seal()的虚函数如下:

var hijackedObj = new Array(0);
[hi, lo] = PutDataAndGetAddr(hijackedObj);
var objAddr = new Long(lo, hi, true);
Write64(objAddr.add(0x30), pRebasedCOOP);
Write64(objAddr, pFakeVTable);
Object.seal(hijackedObj); //Trigger initial looper

4.png

劫持方法1:调用LoadLibrary

如前所述,我们的最终目标是绕过启用导出抑制函数的Win10 Creator更新里Edge上的CFG。看看在kernel32和kernelbase中导出的各种LoadLibrary调用,就知道即使是使用了最新的CFG,把一个新的DLL加载到我们的进程也是很容易的。首先,LoadLibraryExW实际上已被标记为kernel32.dll中__guard_fids_table中有效的调用目标。

5.png

其次,kernel32和kernelbase中的其他LoadLibrary调用刚开始是被抑制的,但在Edge中,这些被抑制的调用最终成为有效的调用站点。这似乎源于MicrosoftEdgeCP!_delayLoadHelper2中的一些延迟加载而最终导致的GetProcAddr在LoadLibraryX API上被调用。如前所述,要让所有函数导出无效的呼叫目标是非常困难,即使这样,其他的LoadLibrary 调用门还是会被抑制或是临时开放,要想达成我们的目的,我们可以直接使用kernel32!LoadLibraryExW,因为它在初始化时就是一个合法的目的地址。

为了使我们想要的VirtualProtect包装器能加载到Edge进程中,就需要调用LoadLibraryExW(“mscoree.dll”,NULL,LOAD_LIBRARY_SEARCH_SYSTEM32)。不过我们可以利用上述的调用方法来一次填充我们的所有参数,而不是使用looper vfgadget创建一个传统的COOP有效载荷来迭代四个被假冒对象。

6.png

我们的第一次迭代将使用0x800填充r8d。 要用vfgadget来填充r8d,CHTMLEditor :: IgnoreGlyphs是一个很好的选择。以后,参数0x800(LOAD_LIBRARY_SEARCH_SYSTEM32)都将在+ 0xD8h处加载。回想一下,我们的伪造对象中的下一个指针必须在+ 0x80h。我们可以在内存中创建四个连续的伪造对象,每个对象的大小都大于0xD8h,或者我们可以将下一个指针放在对象的末尾。如果我们选择了将下一个指针放在对象的末尾,可能会出现一个重叠对象,所以我们必须小心,这个+ 0xD8的偏移量不会影响我们在内存中第二个对象上进行的第二次迭代的vfgadget,用于填充r8d的第一个伪造对象如下所示。

7.png

从这个vfgadget返回后,looper就会遍历我们的假链接列表,所以我们现在必须再次调用另一个vfgadget的值0x0(NULL)来填充rdx。为了实现这一点,我们使用了Tree :: ComputedRunTypeEnumLayout :: BidiRunBox :: RunType()。我们可以从我们的伪造对象+ 0x28h处加载我们的值(0x0)。

8.png

现在我们已经为API调用填充了第二个和第三个参数,不过我们还差第一个参数,该参数是一个指向我们的'mscoree.dll'字符串的指针,然后调用函数指针转到LoadLibraryExW.为了实现这个目的,我们需要一个调用器vfgadget,Microsoft::WRL::Details::InvokeHelper::Invoke(),汇编代码和对应的第三个伪装对象如下。

9.png

现在LoadLibraryExW已经调用完毕,不过我们希望将mscoree.dll也加载到我们的进程中,为了实现这个目标,我们需要将返回地址返回给JavaScript以重新附加COOP有效载荷。looper和CFG都使用RAX作为间接分支目标,因此我们需要找到另一种方式来将新加载的模块的虚拟地址重新设为JavaScript。幸运的是,在退出LoadLibraryExW时,RDX还包含模块地址的副本。因此,我们可以将一个最终的vfgadget添加到我们的对象列表中,以便将RDX移回到我们的假冒对象内存区域。为了在最后一次迭代中进行循环,我们将调用CBindingURLBlockFilter :: SetFilterNotify(),它会将RDX复制到我们当前伪造对象的0x88h处。

10.png

打针机然后到达我们的列表的末尾,并从被劫持的密封()呼叫转移控制返回到我们的JavaScript代码。第一个COOP有效载荷已经完成,mscoree.dll已经加载到Edge,现在我们可以从下面的代码片段中的JavaScript获取mscoree的基地址。

然后looper会到达列表的末尾,并从劫持到的seal()中进行呼叫转移,以便将调用中的控制权返回到JavaScript代码。这样,第一个COOP 有效载荷就算完成了,现在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有效载荷后,我们现在可以重定位第二个COOP有效载荷了,以便在包含chakra!__ guard_dispatch_icall_fptr的只读内存区域中调用ClrVirtualProtect,以使其可写,我们的目标是调用ClrVirtualProtect即chakraPageAddress,0x1000,PAGE_READWRITE,pScratchMemory。不过这次我们将不会通过使用单个伪装的对象来填充所有参数并调用函数指针来利用循环或递归。我们将像以前一样使用相同的调用器vfgadget,不过这次,它主要用于将伪造对象移动到rcx中。

11.png

我们从我们原来的JavascriptNativeIntArray中劫持了freeze()虚函数,该函数指向Microsoft :: WRL :: Details :: InvokeHelper :: Invoke。这个vfgadget会根据这个+ 0x10的地址来移动这个指针,并将这个+ 0x18h地址看作一个函数指针。因此,在JavaScript中的R / W原语中,除了劫持vtable来调用调用器trampoline函数,还需要覆盖对象+0x10和+0x18处的值。

Write64(objAddr.add(0x10),pCOOPMem2);
Write64(objAddr.add(0x18),EdgeHtmlBase.add(0x2DC540));
Object.freeze(objAddr);

12.png

请注意,我们伪造的对象将加载ClrVirtualProtect的所有必需参数,并通过从另一个虚假的vtable解析索引+ 0x100h,并将ClrVirtualProtect的地址填充到rax中。这样,chakra.dll中所需的页面就可写:

13.png

在完成了COOP后,最后一步便是禁用chakra.dll的CFG。我们可以在包含指令jmp rax的chakra.dll中选择任意地址,一旦选好地址,我们就可以使用JavaScript的写入原语来覆盖chakra!__ guard_dispatch_icall_fptr的函数指针,使其指向选好的地址。这样CFG验证程序就变为了nop指令,并允许我们从JavaScript中劫持一个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被禁用,我们可能会将此劫持跳转到进程地址空间中的任何地方。另外,由于0x41414141414141在位图中会显示为无效地址,因此CFG会显示为一个异常,我们将在调用堆栈中看到我们迭代的原始CFG进程ntdll!LdrpDispatchUserCallTargetES:

14.png

总结

我们本文所讨论的COOP目前还仅停留在学术界,属于一种比较前沿的代码重用攻击,本文就为大家阐述了如何使用它来攻击控制流完整性(CFI),如Microsoft CFG。总的来说,COOP是非常容易实现的,特别是把复杂有效载荷分解成较小的片段拼接时,因为拼接在一起的vfgadget与经过汇编的ROP gadget的并没有什么本质区别。非要说有区别,那也就是在目标进程空间内查找和标记各种类型的vfgadget要耗费大量时间。

由于Microsoft的Control Flow Guard被认为是一个粗粒度的CFI实现,因此更容易受到函数重用攻击的影响。相比之下,可以通过预防间接调用的一些关键项来预防细粒度的CFI攻击,例如VTable,验证参数数量,甚至参数类型等。

在CFI的攻击策略中会引入太多的复杂性进程,从而让攻击成本增加。但这并不代表攻击者不会使用COOP方法,尽管微软已经使用forward-edge和backward-edge CFI来进行了防护,但对最新的代码重用攻击还是要重点关注。

为了让CFG的防护面更宽一些局,微软也开始专注多种多样化的预防措施,例如在CFG和Arbitrary Code Guard中通过抑制出口调用来保护关键的通话门,如VirtualProtect。

目前来看, HA-CFI解决方案可以完全实现内核和硬件函数的防护,即使在该方案下受到函数重用攻击,使得由于权限分离而让进程更难以被篡改。

对root权限进行检测

本节会介绍对root权限进行检测的技术思路,这些思路我们都在上文中介绍过。就是基于这些方法,研究人员制作了一个具有root权限属性的检测表,并设计了几个函数(如上所示)来检测它们。在检测表中,研究人员将上文所讲过的第二种检测方法(检测已安装文件)细化为两项检测:检测su二进制文件和检测来自busybox的命令。研究人员并没有将上文所讲过的第七中检测方法(使用shell命令检测root权限)包含在检测表中,因为该方法可以很容易被其他方法替代。研究人员使用的是开源软件实现的这些检测功能,检测表和函数的具体运行过程如下所示:

· detectTestKeys()会检测自定义镜像闪烁,它会reads /system/build.prop并检测ro.build.tags的值是否为“release-key”。如果没有,则假定设备具有root权限;

· checkForSystemProperties()检测系统属性的值,它会调用Runtime().exec(“getprop”)获取系统属性,然后检测ro.secure和ro.debuggable的值;

· checkForBinary ()会尝试查找安装的su二进制文件,它调用exist()

· 检测su二进制文件是否安装在类似/system/(x)bin, /data/local/(x)bin等中;

· checkForBusybox()会检测busybox提供的其他命令存在与否,它通过调用Runtime.exec()来执行find,tail和lsof等命令;

· checkForFilesystem()会检测文件系统属性,它会调用mount命令来检测任何只读文件系统是否具有“write”属性;

· detectRootManagementApps()会调用getPackageInfo()来获取已安装应用程序的列表,并将其与具有rooti权限相关的应用程序列表进行比较;

· detectRootProcess()会调用Runtime.exec(“ps”)获取当前正在运行的进程的列表,并使用root权限检测进程。如果这样的过程不是Zygote进程的子过程,则就假设设备是含有root权限的;

3.jpg

使用NetBeans动态调试Android应用程序

对调试环境进行检测

市场上的Android应用程序通常是在发布模式下构建的,并且在发布模式下会删除调试标志。要启用动态调试,攻击者就需要使用apktool等工具激活该标志。该标志通过将android:debuggable=true插入到AndroidManifest.xml中来激活。

Android应用程序的java级调试通常使用NetBeans进行,调试环境如图3所示。研究人员可以通过检测标志的值来检测调试环境。通过使用Android服务包管理器,存根DEX将获得ApplicationInfo类。这个类的ApplicationInfo.flags Field 字段包含当前运行的应用程序的各种标志值。存根DEX会检测该Field 字段是否包含来自android:debuggable的FLAG DEBUGGABLE。如果是,则终止该应用程序(假设攻击者正在调试),这个过程是使用isDebuggable()方法实现的。

对基于调用栈的那些试图绕过安全检测攻击的检测

调用堆栈是用于执行Java应用程序方法的内存空间。调用时,调用堆栈将为该执行方法分配其中的一些内存。内存通常存储本地变量、参数变量、临时操作数据等。通过对调用堆栈的研究,研究人员可以识别出调用者和被调用者方法并获得一系列方法调用。由于Android应用程序是一种Java应用程序,我们可以通过比较方法名来获取Android应用程序的调用方法信息和挂钩(那些试图绕过安全检测的攻击) 方法。

· 检测表使用的存根DEX方法如下所示:

· 调试的标志用isDebuggable()检测;

存根DEX会在研究人员希望预防的那些试图绕过安全检测的攻击的函数中生成异常,他们会通过调用getStackTrace()方法获取调用堆栈的内容。getStackTrace()会返回StackTraceElement对象数组,其中包含每个被调用方法的类和方法名称。逃脱方法通常要挂钩系统调用或库函数,并修改它们的参数或返回值来更改执行流程。为了在Android上挂钩库函数,许多攻击者使用Xposed。Xposed是一个众所周知的框架,它可以改变Android系统或应用程序的行为。它会扩展了Zygote来加载一个用于挂钩的库,而Zygote加载的每个应用程序都含有这些挂钩库。这些库会提供了一个findAndHookMethod()方法,它会挂钩指定的方法并注册回调方法。每次调用挂钩方法时,都会调用handleHookedMethod()。该函数不但会调用已注册的回调方法,还可以更改调用的参数、调用其他方法并修改结果。

4.jpg

检测与root相关的应用程序时,所调用的存根DEX的堆栈图

图4显示了存根DEX调用detectRootManagementApps()来检测与root相关的应用程序时的调用堆栈。当应用程序启动时,会首先调用onCreate()。然后调用doRootCheck(),它会创建一个包含root权限的检测对象。该对象会先调用isRooted(),然后调用detectRootManagementApps()。该方法会获取一个与root权限相关的应用程序列表,并调用detectResult(),以将其与设备中安装的应用程序进行比较。注意,call stack()是一个生成异常对象并检测其中是否包含调用堆栈内容的方法。

5.jpg

当detectRootManagementApp()被挂钩时,存根DEX的调用堆栈

此时攻击者可能会挂钩detectRootManagementApps(),并修改包含root权限的应用程序列表。从onCreate()到detectRootManagementApps()的方法调用与图4中含有相同。在图5中,存根DEX中并没有使用handleHookedMethod()和invokeOriginalMethodNative()两种方法,因为这些方法属于Xposed,可以修改与root相关的应用程序列表。为了防御这种那些试图绕过安全检测的攻击,存根DEX会在根或调试检测方法的过程中调查它的调用堆栈。通过将调用堆栈中的方法与存根DEX中使用的方法列表进行比较,存根DEX会检测挂钩或那些试图绕过安全检测的攻击,并终止应用程序。

6.jpg

自定义镜像闪烁的堆栈跟踪和检测结果

检测结果的验证

研究人员成功将该方案应用于Android应用程序,验证程序是《部落冲突》(clash Of Clans),是从Google Play下载的。研究人员向其中添加一个存根DEX(称为classes.dex),将原来的classes.dex文件移动到目录资产,并重新打包APK。此时,添加的存根DEX会对原始类执行root权限或调试环境检测、逃脱(挂钩)检测和动态加载。

验证过程是在真正的智能手机Nexus 4((Android 4.4 Kitkat, Linux kernel 3.5)上进行的,另外研究人员还为每个检测列表设置一个root环境,并启动重新打包的验证应用程序来检测是否检测到对应的root环境。最后,研究人员还设置了一个调试环境,如图3所示,这是为了验证是否检测到调试环境。接下来,他们会在验证系统中构建并安装Xposed,以避免root权限或调试检测。通过比较存根DEX的调用栈,就可以证明此预防方案是否可以检测到那些试图绕过安全检测的攻击。

对预防那些使用Xposed试图绕过安全检测的验证

首先,研究人员会检测在无效Xposed的root权限设备上的存根DEX的调用堆栈,该设备一个通过闪烁Cyanogenmod(flashing Cyanogenmod)自定义镜像的root权限设备。图6显示了存根DEX的堆栈跟踪,以及启动重新打包的验证应用程序时的检测结果。存根DEX的每个堆栈跟踪都包括方法调用onCreate()、doRootCheck()和isRooted()。当调用存根DEX中的onCreate()时, doRootCheck()就会被调用,同时doRootCheck()会创建root权限检测对象。

该对象会包含要检测每个具有root权限或调试属性的方法(上文的检测方法中,已经详细列出来了)。这些方法由具有root权限检测对象的isRooted()方法一个接一个地调用。例如,在图6中,isRooted()调用detectTestKeys()来检测ro.build.tags的值。在图6中,detectTestKeys()检测自定义镜像闪烁并输出日志““[-] Detect test-keys(检测验证密钥)”。它还会弹出一条Toast消息并终止该应用程序。

接下来,我们会检测激活Xposed后,存根DEX的调用堆栈。图7显示了使用Xposed的那些试图绕过安全检测的攻击下的调用堆栈。Xposed的handleHookedMethod()和invokeOriginal MethodNative()方法都会出现在调用堆栈中。由于这些方法会修改detectTestKeys()的返回值,因此,isRooted()无法检测自定义镜像闪烁,其他具有root权限属性检测方法的返回值,也会进行了类似的修改。

在调试检测验证中,研究人员在验证应用的AndroidManifest.xml中设置了“android:debuggable=true”标志并重新打包。通过这种方式设置的调试环境,如图3所示,然后启动应用程序即可。此时存根DEX会创建根检测对象,并调用isDebuggable()方法。该方法会检测android:debuggable 标志的值,并输出日志“[-]detection debug mode!!”

7.jpg

逃脱root权限检测时的堆栈跟踪

8.jpg

检测到调试环境时的堆栈跟踪

图8显示了检测到调试环境时的调用堆栈,图9显示了使用Xposed逃脱调试检测时的调用堆栈。与检测那些逃脱root权限检测的过程类似,当调用isDebuggable()时,将调用Xposed的挂钩方法,此时isDebuggable()的返回值被修改,安全检测失败。图10是调试验证应用程序的NetBeans的屏幕截图。

对预防那些基于调用栈逃脱的结果验证

研究人员发现,当攻击者使用Xposed等工具执行那些试图绕过安全检测的攻击时,现有的root权限和调试环境检测技术无法正常工作。在这一部分,研究人员会验证所提出的解决方案是否能检测出那些试图绕过安全检测的攻击。

9.jpg

逃脱调试环境检测时的堆栈跟踪

10.jpg

使用NetBeans调试验证应用程序

11.jpg

在root权限环境中检测进行逃脱攻击

图11可以明显的显示出,新的预防方案是如何保护验证应用程序在root环境中免受那些试图绕过安全检测攻击的。

对于那些试图绕过安全检测的攻击,现有的root属性检测方法,比如detectTestKeys())是不会检测到那些自定义镜像闪烁的,请注意“[+] Device is not custom rom([+]设备不是自定义rom)”日志。但是,Xposed (invokeOriginalMethodNative()和handleHookedMethod())的挂钩方法却出现了。

在调用堆栈中,新的预防方案可以成功检测出基于调用堆栈的逃脱攻击,因为它会成功地检测Xposed的挂钩方法,注意此时是“[-] But this function is hacked!!(这个函数被黑了)”的日志。图12显示了所检测到那些绕过调试环境检测的攻击,你可以看到isDebuggable()进行了挂钩。调试模式检测方法未能检测到调试标志,但新的方案可以发现这种挂钩。

12.jpg

对调试环境中那些试图绕过安全检测攻击的检测

检测时间估计

这个新的检测方案中,存根DEX的大小为21.7KB。至于检测所花费的时间,会根据实际检测的root权限或调试环境来定,其原理就是确定是否有被挂钩的方法以及相应的原始DEX文件是否被加载。

总结

由于Android应用程序是用Java编程语言编写的,并且Java字节码可以使用逆向工程工具轻松地反编译,因此它们很容易受到静态和动态逆向工程攻击。为了保护Android应用程序免受静态逆向工程的影响,研究人员已经采用了混淆、动态代码加载、打包等预防措施。然而,它们却无法预防动态逆向工程攻击。近些年,就已经出现了许多基于真实设备的动态逆向工程攻击,可以转储应用程序代码并对其进行逆向工程。由于这种动态攻击在攻击时,需要在真实设备上获取root权限。

所以在本文中,研究人员提出了一种Android应用程序保护方案。该方案就是实现对root权限或调试环境的检测。这些检测技术可以检测那些使用Xposed框架逃避检测的攻击,如本文所述,该方案还实现了基于调用堆栈的逃脱检测技术,该技术可以检测由Xposed框架启用的挂钩,该方案的优点是它使用的是动态代码加载技术,以最终应用于任何编译(打包)的Android应用程序中。

前言

之所以Android应用程序的逆向工程很简单,是因为它是用高级但简单的字节码语言编写的。字节码(Byte-code)是一种包含执行程序,由一序列op代码或数据对组成的二进制文件,是一种中间码。由于恶意逆向工程攻击,许多Android应用被篡改和重新包装成恶意应用。为了保护Android应用程序不受逆向工程的影响,研究人员已经研究出了诸如混淆、打包(打包可执行文件)、加密和反调试等反逆向工程技术。

不过混淆、打包和加密是针对静态逆向工程的防御技术,并不能防止内存转储和运行时调试(runtime debugging)等动态逆向工程。而现有的针对动态逆向工程的防御技术,通常只会通过确定应用程序是否在基于仿真的分析环境中执行,如果是,则会停止在仿真器上的执行过程来保护应用程序。不过道高一尺魔高一丈,目前攻击者可以直接在真实移动设备上采用动态逆向工程技术,这意味着基于检测仿真器的保护技术已经失效。

本文的研究者试图提出一种适用于真实移动设备的Android应用程序动态逆向工程保护方案。该方案会检测应用程序运行的设备是否是具有root权限的设备或应用程序是否正在调试。如果是,该方案将立即停止执行应用程序。实际验证的结果表明,通过挂钩方式来逃脱根或调试环境检测的攻击技术,都可以被该方案检测到。

该方案的优点之一是它不是作为应用程序源代码的一部分实现的,而是作为单独的可执行文件实现的。这意味着,该方案可以应用于源代码无法实现的应用程序中。

动态逆向工程攻击的介绍

软件逆向工程是分析一个程序结构及其行为过程的方式,其目的是了解程序如何工作和运行。逆向工程可以用于学习程序的工作方式,还可以通过了解现有程序的结构和行为过程,以低成本的方式创建新的应用程序。通过对可执行文件进行反编译或逆向工程,分析人员可以获得程序的源代码,进而了解程序背后的应用技术,最后借鉴现有程序的一些思想设计新的程序。更重要的是逆向工程可以用来篡改或侵入移动应用程序达到非法目的,如绕过认证或支付过程,而入侵或篡改控制医疗设备的移动应用程序甚至会威胁到人类的生命。根据著名应用保护解决方案提供商Arxan Technologies的报告,97%的付费Android应用和80%的免费Android应用都被黑客攻击过。逆向攻击过程一般包括如下5个步骤:

1.选择一个目标应用程序;

2.对应用程序进行逆向分析;

3.提取和窃取机密数据;

4. 将破解的程序重新再创建或对其中的内容进行篡改,目的就是将恶意软件其伪装成正常的应用程序或新版本补丁,来感染目标设备;

5.司机将恶意程序传播出去;

于是程序开发员为了保护Android应用程序的程序代码不受逆向工程攻击,就会研究出许多针对动态逆向工程攻击的Android应用保护方案。

反逆向工程有助于防止软件盗版、保护知识产权和保护应用程序免受恶意攻击。不过,反逆向工程之所以会被如此重视,是因为应用程序开发人员有兴趣保护他们的应用程序。为了防止Android应用程序的版权侵权,目前已有许多关于混淆技术和反静态逆向工程技术的研究。然而,这些技术并没有考虑到动态逆向工程的行为,如内存转储。虽然目前有一些技术可以防止基于仿真器的动态逆向工程,但是这些技术对于基于真实设备的动态逆向工程却起不到任何防护作用。另一个问题是,现有的反动态逆向工程技术是作为应用程序源代码的一部分实现的,这意味着这些技术不能应用于源代码无法实现的应用程序中。

在本文中,研究人员提出了一种保护Android应用程序不受真实移动设备动态逆向工程影响的方案。该方案包括:

1.根或调试环境检测技术;

2.基于调用堆栈的逃脱检测技术,用于检测挂钩攻击(用于逃脱根/调试环境检测);

3.能够保护编译(打包)可执行文件的动态代码加载技术;

实际验证的结果表明,通过挂钩方式来逃脱根或调试环境检测的攻击技术,都可以被该方案检测到。验证结果还发现,该方案可以应用于任何Android应用程序包,而不需要源代码。

针对动态逆向工程攻击技术的预防过程

为了保护软件免受逆向工程攻击,很多研究人员已经进行了研究。目前流行的反逆向工程技术包括程序代码的混淆、可执行文件的加密、反调试等。

ProGuard是一个压缩、优化和混淆Java字节码文件的免费的工具,它可以删除无用的类、字段、方法和属性。可以删除没用的注释,最大限度地优化字节码文件。它还可以使用简短的无意义的名称来重命名已经存在的类、字段、方法和属性。常常用于Android开发用于混淆最终的项目,增加项目被反编译的难度。研究人员Patrick Schulz提出了基于Android平台的几种代码混淆方法,如标识符混淆、字符串混淆、动态代码加载、死代码管理和自修改代码。标识符混淆可以将原始标识符进行模糊化处理,使其不容易被逆向分析出来;字符串混淆会将原始字符串转换为另一个字符串,而动态代码加载方法则会使用两个组件:加密的可执行文件(加密的dex文件)和解密存根。解密存根会执行三个主要发热混淆步骤:第一步是通过从内部数据结构提取加密的dex文件或从远程服务器下载文件,将加密的可执行文件提取到内存中;第二步是解密已加密的dex文件并还原原始dex文件,获取原始dex文件后,最后一步就是将原始dex文件加载到Dalvik虚拟机(DVM)并开始执行。其中的检测方法包含:

1.检测已安装的软件包,目的是检测是否安装了与root相关的应用程序;

2.检测已安装文件,目的是检测二进制文件(如busybox,su)是否存在,这些文件经常出现在安装有root权限的设备上;

3.检测BUILD标签,目的是检测设备上的Android镜像是库镜像还是由第三方开发人员构建的自定义映像;

4.检测系统属性,目的是检测系统属性的值——ro.debuggable和ro.secure是否含有具有root权限的shell;

5.检测目录权限;目的是检测是否为这些目录提供了写入权限,正常情况下目录应该只具有只读权限;

6.检测进程、服务和任务,目的是检测具有root权限的应用程序是否正在运行;

7.使用shell命令检测root权限,目的是检测使用su和ps等shell命令在单独的进程中检测具有root权限的功能;

而Sudipta Ghosh等人提出了一种使程序代码更加复杂的代码混淆技术,该技术专注于增加应用程序控制流程的复杂性,从而起到让分析式感到逆向充满了挑战性。该方法会使得获取嵌入在应用程序中的业务逻辑变得更加困难,不过其缺点是逆向工程专家可以破解出这种模糊技术。

Junfeng Xu等人提出了保护Android应用程序不受动态调试影响的调试状态和调试环境的检测方法。调试状态检测是通过检测父进程、读取进程状态或使用ptrace来检测调试器,如GDB、IDA和strace。调试环境检测是通过验证仿真器特定的属性值和特性来检测仿真器的环境。但是,这些技术不能防御使用Lime工具进行内存转储之类的攻击,Lime可以在Android设备上完全捕获内存,它还可以在采集过程中最大限度地减少用户和内核空间进程之间的交互,从而使其能够产生比其他Linux内存采集工具更为合理的内存捕获。由于内存转储攻击肯定会需要root权限,因此需要检测具有root权限的运行环境以防止动态逆向工程攻击。

San-Tsai Sun等在分析了几种现有的使用root权限的方法后,提出了相应的检测方法,该方法基于Android设备的特点。其中的检测方法就是上面讲过的7种。然而,由于这些root权限检测方法通常需要调用Android API,因此攻击者可以通过使用Xposed之类的框架挂钩API来避免被检测到。因此,我们需要一种能够检测Android设备上的那些试图绕过安全检测的攻击(挂钩)的技术。如何保护Android应用程序不受动态逆向工程的影响

大多数反逆向工程技术都会通过检测含有root权限的环境或调试环境来保护Android应用程序免受动态分析的影响。然而,这些技术很容易被诸如挂钩之类的逃脱方法所攻击。在本文中,研究人员提出了一种针对动态逆向工程的Android应用程序保护方案。该方案包括对含有root权限或调试环境的检测技术、基于调用栈的那些试图绕过安全检测的攻击(挂钩)检测技术和动态代码加载技术。本文所讲的方案,其最终是要实现含有root权限的DEX文件,然后此文件被添加到APK (Android应用程序包),也就是说,APK会被重新打包。启动应用程序时,存根DEX试图检测含有根的调试环境和逃脱方法。如果没有找到,存根DEX将动态加载原始DEX文件(classes.dex)。由于此方案不是作为应用程序源代码的一部分实现的,所以它可以应用于已经打包的apk。

1.jpg

应用本文所讲的APK方案前后的对比结构

存根DEX和动态代码加载

该方案最终是要实现一个名为存根DEX的Android可执行文件。存根DEX的主要任务是含有root权限属性的检测、调试环境检测、调用栈分析和动态代码加载。在应用本文所讲的方案之前,APK的结构如图1左边所示。通常情况下,APK包含AndroidManifest.xml(描述诸如应用程序名称、版本和访问权限等信息)、class .dex(可执行文件)、META-INF目录和assets目录。

而应用了此方案后,如上图的右边所示,存根DEX被添加到APK中。为了在启动APK时首先执行存根DEX,研究人员首先设置了位于AndroidManifest.xml中<application>元素的属性android:name的值,目的就是设置存根DEX(类应用程序或应用程序的一个子类)的应用类。原始可执行文件classes.dex被移动到assets目录,存根DEX被命名为classes.dex。然后,APK被重新包装,并伺机传播。

如果重新打包的APK在用户的智能手机中启动,存根DEX将首先执行。它会执行root权限属性检测和调试环境检测。在这些任务期间,存根DEX会分析它的调用堆栈,以检测是否被挂钩,即调用堆栈中是否出现存根DEX方法以外的任何方法调用。如果存在,存根DEX会终止应用程序,把它假设为一种那些试图绕过安全检测的攻击方法。如果不存在,任务将完成并返回检测结果。根据检测结果,存根DEX会决定是终止应用程序还是加载原始可执行文件assets或classes.dex。

如果没有检测到root权限或调试环境,存根DEX将使用DexClassLoader()为原始可执行文件创建类加载器。然后使用makeApplication()将类加载器替换为新的类加载器。原始可执行文件调用其start组件的onCreate()方法。这个过程如下图所示:

2.jpg

使用动态代码加载改变执行流程

检测表和存根DEX的检测方法:

· 自定义镜像闪烁(custom image flashing)用detectTestKeys ()方法检测;

· 更改系统属性用checkForSystemProperies()方法检测;

· 安装的su二进制文件用checkForBinary ()方法检测;

· 来自busybox的新命令用checkForBusybox()方法检测;

· 文件系统属性用checkForFilesystem ()方法检测;

· 与root相关的应用用detectRootManagementApps ()方法检测;

· 正在运行的root进程用detectRootProcess()方法检测;

本文介绍了这个新保护方案所采用的检测方法和内容部分结构,下一篇,我们会介绍,该方案是如何对root权限和调试环境进行检测,以及在真实设备上验证该方案的实际检测结果。

使用多标签浏览变得越来越普遍,因为人们在Facebook,Twitter,YouTube,Netflix和Google Docs等服务上花费的时间越来越多,甚至已经成为人们日常生活中的一部分。

Quantum DOM:调度是Project Quantum的一个重要功能,其重点是使Firefox操作更加方便,特别是当许多选项卡打开时。在本文中,我将描述在多标签浏览中出现的问题,并找到对应的解决方案以及Quantum DOM在其中所起的作用。

问题1:不同类别的任务优先级排序

由于多处理器Firefox(e10s)在Firefox 48版本中被首次启用,因此Web内容选项卡现在可以在单独的内容进程中运行,以减少给定进程中操作系统资源的拥挤。然而,经过深入的研究,我发现内容进程中主线程的任务队列仍然拥挤了多个任务。内容进程中的任务可能有许多可能的来源,比如,通过IPC(进程间通信)从主进程(例如输入事件,网络数据和vsync)直接进入网页(例如从setTimeout,requestIdleCallback或postMessage),或内容进程内部(例如垃圾收集或遥测任务)。为了更好的响应,我已经学会了为requestIdleCallback和垃圾收集优先处理用户输入和vsync上的任务。

问题2:选项卡之间缺少任务优先级

在Firefox内部,在前台和后台选项卡中运行的任务在单个任务队列中以先到先得的顺序执行。将前台任务优先于后台任务是比较合理的,以提高Firefox用户的用户体验响应能力。

解决方案

来看看我是如何处理这两个安排的挑战的,我会将它们分成一系列导致可实现目标的行动:

1.在类别和标签组的内容进程的主线程上分类和优先处理任务,以提供更好的响应。

2.如果此抢占对用户的运行效率不明显,则抢占运行后台选项卡的任务。

3.由于资源有限,提供了更少的内容进程可供e10s multi选择。

任务分类

2.1.png

为了解决我们的第一个问题,我们将内容进程中主线程的任务队列划分为3个优先级排队:高(用户输入和刷新驱动程序),正常(DOM事件,网络,TimerCallback,WorkerMessage)和低(垃圾收集,IdleCallback)。注意:优先级相同的任务顺序保持不变。

任务分组

在描述第二个问题的解决方案之前,让我们将TabGroup定义为一组通过window.opener和window.parent关联的打开的选项卡。在HTML标准中,这称为相关浏览环境的单位。由于任务是孤立的,如果它们属于不同的TabGroups,则不能互相影响。任务分组确保来自同一TabGroup的任务按顺序运行,同时允许我从背景TabGroups(background TabGroups)中断任务,以便从前台TabGroup运行任务。

在Firefox内部结构中,每个窗口或文档都包含对它所属的TabGroup对象的引用,它提供了一组有用的调度API。这些API使Firefox开发人员更轻松的将任务与特定的TabGroup相关联。

如何将任务分组到Firefox中

以下我会通过几个示例来展示如何在Firefox中的各种类别中分组任务:

1.在window.postMessage()的实现中,一个名为PostMessageEvent的异步任务将被分派到主线程的任务队列中:

2.2.jpg

随着DOM窗口与TabGroup的新关联以及TabGroup中提供的新调度API,我现在可以将此任务与适当的TabGroup相关联并指定TaskCategory:

2.3.jpg

2.除了可以与TabGroup相关联的任务之外,内容过程中还有几种任务,如通过垃圾收集进行遥测数据收集和资源管理,与任何Web内容无关,下图就是垃圾收集开始的方式

360截图16300506327135.jpg

要使分组没有TabGroup依赖项的任务,引入了一个名为SystemGroup的特殊组。然后,可以修改PokeGC()方法,如下所示:

2.5.jpg

我们现在已将此GCTimerFired任务分组到具有TaskCategory :: GC指定的SystemGroup,这允许调度程序中断任务以运行任何前台选项卡的任务。

3.在某些情况下,相同的任务可以通过特定的Web内容或具有内容进程中系统特权的内部Firefox脚本来请求。当任务不绑定到任何窗口或文档时,我就必须决定SystemGroup是否适用于请求。例如,在内容进程中实现DNSService时,可以提供可选的TabGroup-versioned事件目标,以便在解析DNS查询后进行结果回调。如果未提供可选事件目标,则将选择TaskCategory :: Network中的SystemGroup事件目标。我会先假设请求是从内部脚本或与任何窗口及文档无关的内部服务触发的。

2.6.jpg

TabGroup类别

一旦在调度程序中任务分组完成,我们从数据集中分配一个每个选项卡组的协作线程,以便使用TabGroup中的任务。每个协作线程在任何安全点都可以通过JS中断由调度程序预先排除。然后,主线程通过这些协作线程进行虚拟化。

2.7.png

在这种新的协作线程方法中,我会确保一次只能运行一个线程。这将分配更多的CPU运行时间到前台TabGroup,并且还确保Firefox中的内部数据是正确的,其中包括许多服务,管理器和有意设计为单例对象的数据。

任务分组和调度中遇到的障碍

很明显,Quantum-DOM调度的性能高度依赖于任务分组。理想情况下,我期望每个任务只能与一个TabGroup相关联。然而,实际上,一些任务可以同时为多个TabGroups提供服务,这些TabGroups需要提前重构以便支持分组,并且并不是所有的任务都可以在调度程序准备启用之前按时间进行分组。因此,为了在所有任务分组之前积极的启用调度器,当未分组的任务到达时,采用以下设计来临时禁用抢占,因为我不知道该未分组任务属于哪个TabGroup。

2.8.png

任务分组的当前状态

我要感谢来自DOM,Graphic,ImageLib,Media,Layout,Network,Security等各个子模块的许多工程师,他们根据显示的频率帮助清除了这些未分组(未标记)的任务遥测结果。

下表显示了在内容进程中运行任务的遥测记录,这能让你更好地描述Firefox正在运行的任务:

2.9.png

目前超过80%的任务已经清除了任务分组和调度的障碍,但是,仍然有相当数量的匿名任务还未被清除。遥测将有助于检查到达主线程的2个未分组任务之间的平均时间。平均时间越长,利用Quantum-DOM调度程序进行的性能优化就越好。

对于技术大咖来说,调试内核是一件非常有趣且具有挑战的事情,除非调试过程中发生严重的异常,否则在一般情况下,他们是不会轻易放弃的。不过,技术开发商也知道这一点,他们也在想法设法让内核调试变得越来越难。比如苹果公司就已经采取了一些措施,让macOS的内核调试变得越来越难,首先,将有关debug引导参数的文档信息隐藏在lock和key下,然后将内核调试工具包转移到Developer Account-only Downloads部分。虽然目前互联网上有很多关于在macOS上调试内核的文章,但其中很多都已经不实用了,比如有的文章会告诉调试人员,通过设置的NVRAM启动参数,但这个方法已经不再有效。甚至还有的文章停留在 “现在调试人员应该设置一个有效的调试会话”这样的层面!在这篇文章中,我尽我所能为大家提供最准确和最新的调试信息,包括正确的调试命令,正确的boot-args参数,当然还有具体的调试示例。

开始在macOS上进行内核调试

调试时,要做的第一件事就是进行调试环境的设置和对测试设备的配置,调试人员需要有一个能进行调试的内核对象(在本文中,我使用的是iMac 2011作为调试器)和一个用于调试的设备(本文使用的是MacBook Pro 2009)。虽然调试人员可以使用我在以下讨论的各种方式将两者连接起来,但在本文的示例中,最好的方法(也是最可靠的)似乎是通过两者之间的火线接口(firewire)(这是因为我的两台设备都有firewire端口)而不是USB-C。

硬件部分设置好以后,我们还需要运行一些软件。理论上调试人员可以调试RELEASE内核,但调试人员不是技术大咖,只是一个初学者时,调试Development内核就会轻松得多。默认情况下,macOS里会自带一个位于/System/Library/ kernel /kernel中的RELEASE融合内核,其中kernel是Mach-O 64-bit executable x86_64。所以,我们可以通过导航到Apple Developer门户并下载内核调试工具包,来获得macOS版本的Development内核。令人惊讶的是,苹果公司只是简单将该套件置于正常的、免费的Apple开发者账户( Apple Developer Account)的lock中,按着我原来的想法,我还以为苹果公司是将其置于付费的Apple开发者账户下载之中。

无论如何,一旦调试人员进入到Apple Developer Portal下载部分,调试人员就将看到如下内容。

1.png

这个过程非常的重要,这是因为调试人员会在以上的这个列表中,找到适合于他们的特定macOS版本的内核调试工具包,下载后,里面包含调试人员将在调试中启动的内核,如果内核与调试人员的macOS版本不匹配,它将无法启动,甚至对对调试人员的文件、计算机等造成损害。

为macOS版本找到合适的内核调试工具包(Kernel Debug Kit)

要找到正确的内核调试工具包,调试人员必须知道他们的macOS版本和实际的内部版本号。调试人员可以很容易的看到你正在运行的macOS版本,只要打开苹果的图标,按下“关于这台Mac”,然后在窗口中阅读出现的版本信息,例如“10.13.6版本”。

对于实际的内部版本号,调试人员可以单击“关于这个Mac”窗口中的“版本”标签,也可以运行终端命令sw_vers | grep BuildVersion。在本文的示例中,运行sw_vers | grep BuildVersion命令会输出 “BuildVersion: 17G65”。

Last login: Sun Dec  2 03:58:16 on ttys000
Isabella:~ geosn0w$ sw_vers | grep BuildVersion
BuildVersion:    17G65
Isabella:~ geosn0w$

所以,就本文而言,我正在运行的macOS High Sierra(10.13.6)的版本号为17G65。查看刚刚下载的工具包,我可以立即找到和我的版本适应的工具,这样我就可以下载包含安装文件的.DMG文件,文件非常少。

3.png

准备调试器以供调试器调试

在调试器(即要调试其内核的设备)上下载调试工具包后,双击安装DMG文件。在DMG文件中,调试人员将找到一个名为KernelDebugKit.pkg的文件,双击它并按照安装向导进行操作。安装完毕,会出现一个询问调试人员macOS登录密码的界面。你可以不理会这个询问,但请不要将此安装程序删掉,调试人员以后还会需要它。

安装完成后的界面看起来如下:

4.png

安装完成后,调试人员会被导航到/Library/Developer/KDKs。在那里,调试人员将获得一个名为KDK_YOUR_VERSION_BUILDNUMBER.kdk的文件夹。在本文的示例中,该文件夹名为KDK_10.13.6_17G65.kdk。打开文件夹,调试人员会在其中找到另一个名为“System”的文件夹。导航到文件夹后,先进入“Library”,然后进入“Kernels”。在该文件夹中,调试人员将找到一些内核二进制文件,一些Xcode调试符号文件(.dSYM)等。一般情况下,调试人员对名为kernel.development的文件感兴趣。

将kernel.development复制并粘贴到在RELEASE内核二进制文件中运行的/ System / Library / Kernels /中。此时,调试人员的macOS上应该安装应该有两个内核,一个是RELEASE内核,另一个是DEVELOPMENT内核。

禁用调试器上的SIP

为了正确调试,调试人员可能需要在要调试其内核的计算机上禁用SIP(系统完整性保护)。为此,调试人员需在恢复模式下重新启动计算机。要做到这一点,必须重新启动设备,当调试人员看到启动界面打开时,按CMD + R,等待几秒钟,使启动进入恢复模式用户界面,然后点击“Terminal”继续。

在恢复终端中,写入csrutil disable,然后重新启动计算机,此时只需正常启动即可。

设置正确的NVRAM boot-args

由于苹果公司一直在更新boot-args,因此调试人员在互联网上找到的设置参数可能已经没有用了。以下boot-args已经过测试,确定可以在2018年的macOS High Sierra运行。

注意!以下boot-args会假设调试人员直接通过火线接口或利用“Thunderbolt”(雷电)适配器通过火线接口执行操作。

如果调试人员在较旧的Mac上,员直接通过火线接口,那在终端中要运行的命令如下所示。

sudo nvram boot-args="debug=0x8146 kdp_match_name=firewire fwdebug=0x40 pmuflags=1 -v"

如果调试人员是利用“Thunderbolt”(雷电)适配器通过火线接口执行操作,那在终端中要运行的命令如下所示。

sudo nvram boot-args="debug=0x8146 kdp_match_name=firewire fwkdp=0x8000 fwdebug=0x40 pmuflags=1 -v"

区别在于fwkdp=0x8000会告诉OFireWireFamily.kext::AppleFWOHCI_KDP使用非内置firewire < – > thunderbolt适配器进行调试会话。

此时,调试器可以在重新启动后进行调试,但是让我先解释一下启动参数所代表的具体含义:

· debug=0x8146:代表调试员可以进行调试了,并允许他们按下电源按钮来触发NMI, NMI代表不可屏蔽中断(即CPU不能屏蔽)   ,它用于调试器连接;

· kdp_match_name=firewire:允许调试人员通过FireWireKDP进行调试;

· fwkdp=0x8000 :正如我之前解释的那样,该参数是告诉kext使用thunderbolt连接到火线接口,如果你是直接使用火线接口,请不要设置该参数;

· fwdebug=0x40:通过启用AppleFWOHCI_KDP驱动程序,得出更详细的输出信息,这对于排除调试过程中的故障很有用;

· muflags=1 :它会禁用看门狗定时器,看门狗定时器(WDT,Watch Dog Timer)是单片机的一个组成部分,它实际上是一个计数器,一般给看门狗一个数字,程序开始运行后看门狗开始倒计数。如果程序运行正常,过一段时间CPU应发出指令让看门狗复位,重新开始倒计数。如果看门狗减到0就认为程序没有正常工作,强制整个系统复位。

· -v :这个参数虽然最简单的,但它会命令计算机启动时要显示详细设备信息,而不是像平常那样只显示苹果的徽标和启动进度条。不仅在调试人员进行调试时,对于故障排除非常有用,而且在Bootloop(无限重启)时也很有用。

除了本文要设置的这些引导参数之外,macOS还支持更多在/osfmk/kern/debug.h中定义的参数,具体的参数我已在下面列出,这些参数来自xnu-4570.41.2。

.../* Debug boot-args */#define DB_HALT        0x1//#define DB_PRT          0x2 -- obsolete#define DB_NMI        0x4
#define DB_KPRT        0x8
#define DB_KDB        0x10
#define DB_ARP          0x40
#define DB_KDP_BP_DIS   0x80//#define DB_LOG_PI_SCRN  0x100 -- obsolete#define DB_KDP_GETC_ENA 0x200#define DB_KERN_DUMP_ON_PANIC        0x400 /* Trigger core dump on panic*/#define DB_KERN_DUMP_ON_NMI        0x800 /* Trigger core dump on NMI */#define DB_DBG_POST_CORE        0x1000 /*Wait in debugger after NMI core */#define DB_PANICLOG_DUMP        0x2000 /* Send paniclog on panic,not core*/#define DB_REBOOT_POST_CORE        0x4000 /* Attempt to reboot after
                        * post-panic crashdump/paniclog
                        * dump.
                        */#define DB_NMI_BTN_ENA      0x8000  /* Enable button to directly trigger NMI */#define DB_PRT_KDEBUG       0x10000 /* kprintf KDEBUG traces */#define DB_DISABLE_LOCAL_CORE   0x20000 /* ignore local kernel core dump support */#define DB_DISABLE_GZIP_CORE    0x40000 /* don't gzip kernel core dumps */#define DB_DISABLE_CROSS_PANIC  0x80000 /* x86 only - don't trigger cross panics. Only
                                         * necessary to enable x86 kernel debugging on
                                         * configs with a dev-fused co-processor running
                                         * release bridgeOS.
                                         */#define DB_REBOOT_ALWAYS        0x100000 /* Don't wait for debugger connection */...

准备调试器设备

现在既然调试器准备好了,我们就要准备配置运行调试器的设备。为此,我使用了另一台运行El Capitan的macOS设备,但这并不重要。还记得我们在调试器上安装的内核调试工具包吗?我们也需要在调试器设备上安装它。不同之处在于我们不会移动内核,也不会在调试器上设置任何引导参数。我们只是要用到内核而已,因为我们将使用lldb来执行调试。如果调试人员熟悉GDB,则不必担心,这里有一个GDB -> LLDB命令表

注意:即使内核调试工具包没有运行与调试器相同的macOS版本,调试人员应该在调试器上安装相同的macOS内核调试工具包,因为我们不会在调试器上启动任何内核。

安装工具包后,就可以调试内核了。

调试内核

首先,重新启动调试器。此时,调试人员将看到它是在文本模式控制台被启动的,该控制台会输出详细的启动信息。等到屏幕上显示“DSMOS出现!”,然后按一下电源按钮,注意不要按住它不放。在调试器上,调试人员将看到它正在等待连接。

在调试器设备上的连接过程

打开终端窗口并启动fwkdp -v,这是FireWire KDP工具,它将侦听火线接口并将数据重定向到本地主机,这样调试人员就可以将KDP目标设置为localhost或127.0.0.1调试人员也应该得到类似于下面的输出内容。

MacBook-Pro-van-Mac:~ mac$ fwkdp -vFireWire KDP Tool (v1.6)Matched on device 0x00002403
Created plugin interface 0x7f9e50c03548 with result 0x00000000
Created device interface 0x7f9e50c0d508 with result 0x00000000
Opened device interface 0x7f9e50c0d508 with result 0x00000000
Added callback dispatcher with result 0x00000000
Created pseudo address space 0x7f9e50c0d778 at 0xf0430000
Address space enabled.
2018-12-02 05:51:05.453 fwkdp[5663:60796] CFSocketSetAddress listen failure: 102
Created KDP socket listener 0x7f9e50c0d940 with result 0
KDP Proxy and CoreDump-Receive dual mode active.
Use 'localhost' as the KDP target in gdb.
Ready.

现在,在不关闭此窗口的情况下,打开另一个终端窗口并启动lldb调试器,方法是将调试器上安装的kernel.development文件作为内核调试工具包的一部分传递给它。请记住,内核可以在/Library/Developer/KDKs/中找到。在那里,调试人员将会找到一个名为KDK_YOUR_VERSION_BUILDNUMBER.kdk的文件夹。在本文的示例中,该文件夹名为KDK_10.13.6_17G65.kdk,而我所需要的完整内核路径是/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development。

在本文的示例中,新终端窗口中的命令是xcrun lldb /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/ kernel /kernel.development。

Last login: Sun Dec  2 10:37:51 on ttys000
MacBook-Pro-van-Mac:~ mac$ xcrun lldb /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development
(lldb) target create "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development"
warning: 'kernel' contains a debug script. To run this script in this debug session:
command script import "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/kernel.py"

To run all discovered debug scripts in this session:

    settings set target.load-script-from-symbol-file true

Current executable set to '/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development' (x86_64).

如上所示,lldb表示“内核”所包含的调试脚本。在现在打开的lldb窗口中,运行settings set target.load-script-from-symbol-file true来运行脚本。

Last login: Sun Dec  2 10:37:51 on ttys000
MacBook-Pro-van-Mac:~ mac$ xcrun lldb /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development
(lldb) target create "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development"
warning: 'kernel' contains a debug script. To run this script in this debug session:
command script import "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/kernel.py"

To run all discovered debug scripts in this session:

    settings set target.load-script-from-symbol-file true

Current executable set to '/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development' (x86_64).
(lldb) settings set target.load-script-from-symbol-file true

Loading kernel debugging from /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/kernel.py
LLDB version lldb-360.1.70
settings set target.process.python-os-plugin-path "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/lldbmacros/core/operating_system.py"
settings set target.trap-handler-names hndl_allintrs hndl_alltraps trap_from_kernel hndl_double_fault hndl_machine_check _fleh_prefabt _ExceptionVectorsBase _ExceptionVectorsTable _fleh_undef _fleh_dataabt _fleh_irq _fleh_decirq _fleh_fiq_generic _fleh_dec
command script import "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/lldbmacros/xnu.py"
xnu debug macros loaded successfully. Run showlldbtypesummaries to enable type summaries.

settings set target.process.optimization-warnings false
(lldb)

现在,我们终于可以通过编写kdp-remote localhost将lldb连接到 动态内核(live kernel)了。如果一切正常,内核应该已经连接成功,且应该出现以下这样的输出内容。一开始,许多文本会涌入调试人员的lldb窗口,不过过一会,内核进入休息状态。

(lldb) kdp-remote localhost
Version: Darwin Kernel Version 17.7.0: Wed Oct 10 23:06:14 PDT 2018; root:xnu-4570.71.13~1/DEVELOPMENT_X86_64; UUID=1718D865-98B4-3F6E-97CF-42BF0D02ADD7; stext=0xffffff802e800000
Kernel UUID: 1718D865-98B4-3F6E-97CF-42BF0D02ADD7
Load Address: 0xffffff802e800000
Kernel slid 0x2e600000 in memory.
Loaded kernel file /Library/Developer/KDKs/KDK_10.13.6_17G3025.kdk/System/Library/Kernels/kernel.development
Loading 152 kext modules warning: Can't find binary/dSYM for com.apple.kec.Libm (BC3F7DA4-03EA-30F7-B44A-62C249D51C10)
.warning: Can't find binary/dSYM for com.apple.kec.corecrypto (B081B8C1-1DFF-342F-8DF2-C3AA925ECA3A)
.warning: Can't find binary/dSYM for com.apple.kec.pthread (E64F7A49-CBF0-3251-9F02-3655E3B3DD31)
.warning: Can't find binary/dSYM for com.apple.iokit.IOACPIFamily (95DA39BB-7C39-3742-A2E5-86C555E21D67)
[...]
.Target arch: x86_64
.. done.
Target arch: x86_64
Instantiating threads completely from saved state in memory.
Process 1 stopped
* thread #2: tid = 0x0066, 0xffffff802e97a8d3 kernel.development`DebuggerWithContext [inlined] current_cpu_datap at cpu_data.h:401, name = '0xffffff80486a2338', queue = '0x0', stop reason = signal SIGSTOP
    frame #0: 0xffffff802e97a8d3 kernel.development`DebuggerWithContext [inlined] current_cpu_datap at cpu_data.h:401 [opt]

现在我们就连接到动态内核了,不过你可以看到此时进程已停止,这意味着内核已冻结,这就是为什么启动过程会在调试人员离开的位置停止。不过现在调试器已连接,我们只需做到一点就可以安全地继续启动进程到正常的macOS桌面。要做到这一点,我们只需解冻该过程,输入“c” 即可,然后按Enter键直到启动继续(更多文本会出现在调试器屏幕上)。

(lldb) c
Process 1 resuming
Process 1 stopped* thread #2: tid = 0x0066, 0xffffff802e97a8d3 kernel.development`DebuggerWithContext [inlined] current_cpu_datap at cpu_data.h:401, name = '0xffffff80486a2338', queue = '0x0', stop reason = EXC_BREAKPOINT (code=3, subcode=0x0)
    frame #0: 0xffffff802e97a8d3 kernel.development`DebuggerWithContext [inlined] current_cpu_datap at cpu_data.h:401 [opt](lldb) c

一旦调试器完全在macOS中启动,调试人员就可以在桌面上进行任何调试了。要运行调试器命令,调试人员必须再次触发NMI,然后按一下电源按钮。调试器屏幕将冻结,但调试器的lldb屏幕将处于运行状态,此时调试人员可以在动态内核上读/写寄存器,读/写内存,反汇编地址,反汇编函数等。要将其解冻,请再次键入“c” 并在lldb屏幕上按一下Enter键。

内核调试示例

示例1:使用lldb读取所有寄存器并将“AAAAAAAA”写入其中一个寄存器

要读取所有寄存器,按下电源按钮并在打开的lldb窗口中输入register read –all来触发NMI:

(lldb) register read --allGeneral Purpose Registers:
      rax = 0xffffff802f40ba40  kernel.development`processor_master
      rbx = 0x0000000000000000
      rcx = 0xffffff802f40ba40  kernel.development`processor_master
      rdx = 0x0000000000000000
      rdi = 0x0000000000000004
      rsi = 0xffffff7fb1483ff4
      rbp = 0xffffff817e8ccd50
      rsp = 0xffffff817e8ccd10
       r8 = 0x0000000000000000
       r9 = 0x0000000000000001
      r10 = 0x00000000000004d1
      r11 = 0x00000000000004d0
      r12 = 0x0000000000000000
      r13 = 0x0000000000000000
      r14 = 0x0000000000000000
      r15 = 0xffffff7fb1483ff4
      rip = 0xffffff802e97a8d3  kernel.development`DebuggerWithContext + 403 [inlined] current_cpu_datap at cpu.c:220
 kernel.development`DebuggerWithContext + 403 [inlined] current_processor at debug.c:463
 kernel.development`DebuggerWithContext + 403 [inlined] DebuggerTrapWithState + 46 at debug.c:537
 kernel.development`DebuggerWithContext + 357 at debug.c:537
   rflags = 0x0000000000000046
       cs = 0x0000000000000008
       fs = 0x0000000000000000
       gs = 0x0000000000000000
 
Floating Point Registers:
      fcw = 0x0000
      fsw = 0x0000
      ftw = 0x00
      fop = 0x0000
       ip = 0x00000000
       cs = 0x0000
       dp = 0x00000000
       ds = 0x0000
    mxcsr = 0x00000000
 mxcsrmask = 0x00000000
    stmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm8 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm9 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm10 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm11 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm12 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm13 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm14 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm15 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
 Exception State Registers:
3 registers were unavailable.(lldb)

现在让我们对其中一个寄存器进行写入,注意不要在未设置为0x0000000000000000的寄存器中写入,因为调试人员将覆盖某些内容,找一个空的进行写入。在本文的示例中,R13是空的(r13 = 0x0000000000000000),所以我可以其中写入来证明我的观点。为了向寄存器写入一个AAAs字符串,我可以将它的值替换为0x414141414141414141,其中0x41是ASCII字符“A”的十六进制表示。要覆盖寄存器,我可以使用register write r13 0x4141414141414141命令。果然,如果我们再次读取寄存器,就会发现已经覆盖了。

(lldb) register write R13 0x4141414141414141(lldb) register read --allGeneral Purpose Registers:
      rax = 0xffffff802f40ba40  kernel.development`processor_master
      rbx = 0x0000000000000000
      rcx = 0xffffff802f40ba40  kernel.development`processor_master
      rdx = 0x0000000000000000
      rdi = 0x0000000000000004
      rsi = 0xffffff7fb1483ff4
      rbp = 0xffffff817e8ccd50
      rsp = 0xffffff817e8ccd10
       r8 = 0x0000000000000000
       r9 = 0x0000000000000001
      r10 = 0x00000000000004d1
      r11 = 0x00000000000004d0
      r12 = 0x0000000000000000
      r13 = 0x4141414141414141 <-- Yee overwritten this.
      r14 = 0x0000000000000000
      r15 = 0xffffff7fb1483ff4
      rip = 0xffffff802e97a8d3  kernel.development`DebuggerWithContext + 403 [inlined] current_cpu_datap at cpu.c:220
 kernel.development`DebuggerWithContext + 403 [inlined] current_processor at debug.c:463
 kernel.development`DebuggerWithContext + 403 [inlined] DebuggerTrapWithState + 46 at debug.c:537
 kernel.development`DebuggerWithContext + 357 at debug.c:537
   rflags = 0x0000000000000046
       cs = 0x0000000000000008
       fs = 0x0000000000000000
       gs = 0x0000000000000000
 
Floating Point Registers:
      fcw = 0x0000
      fsw = 0x0000
      ftw = 0x00
      fop = 0x0000
       ip = 0x00000000
       cs = 0x0000
       dp = 0x00000000
       ds = 0x0000
    mxcsr = 0x00000000
 mxcsrmask = 0x00000000
    stmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm8 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm9 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm10 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm11 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm12 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm13 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm14 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm15 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
 Exception State Registers:
3 registers were unavailable. 
(lldb)

注意:当调试人员想要读取单个寄存器时,他们不必输入register read –all,调试人员可以简单地用register read [register] 指定寄存器,例如register read r13。

示例2:在运行uname -a时更改内核版本和名称

是时候在内核做一些真正的内存读写了,调试人员可能知道,终端中的uname -a命令会列出了内核的名称、版本、版本号和构建日期,那我们如何将其更改为我们想要的内容呢?

首先,我们不知道内核存储名称、版本、版本号和构建日期信息的位置,因此我们要首先找到信息的存储位置。为此,我们可以使用任何反汇编程序,如IDA Pro,Hopper Disassembler,Jtool,Binary Ninja等。

我在本文的示例中使用的是IDA Pro,要做的就是加载内核。在IDA Pro中开发文件,并让IDA分析它。由于内核太大,分析可能需要一段时间,所以请耐心等待。当IDA完成时,输出应该或多或少看起来像以下这样。调试人员会知道IDA何时完成,因为它会在左下角显示“AU: idle”。

14.png

现在,我们必须找到位置字符串。。我们知道,当在终端中执行uname -a命令时,内核名是Darwin,所以为了在IDA中查找位置字符串,我们要到顶部栏 – >查看 – >打开子视图 – >字符串。这样,我们将看到一个新的字符串窗口,如果调试人员在其中按CTRL + F,搜索框将显示在底部,搜索Darwin,整个字符串都会显示出来。

15.png

双击字符串,调试人员将被重定向到一个名为_version的常量。所以现在我们知道了,常量就是所谓的“version”,这也是我们要寻找的字符串。调试人员可能倾向于从IDA反汇编中复制常量的地址,但这是不对的,因为内核使用KASLR或内核地址空间布局随机化,因此地址将不相同。但调试人员其实是不需要知道地址的,他们可以在调试器设备上用lldb轻松搞定。

16.png

此时得到的就是“version”常量的地址,它实际上非常简单。按下电源按钮触发NMI(如果继续该过程)并写入print &(version)。

(lldb) print &(version)
(const char (*)[101]) $8 = 0xffffff802f0f68f0
(lldb)

在本文的示例中,const char version 位于地址0xffffff802f0f68f0。果然,当我列出字符数组时,它会显示如下内容。

(lldb) print version
(const char [101]) $9 = {
  [0] = 'D'
  [1] = 'a'
  [2] = 'r'
  [3] = 'w'
  [4] = 'i'
  [5] = 'n'
  [6] = ' '
  [7] = 'K'
  [8] = 'e'
  [9] = 'r'
  [10] = 'n'
  [11] = 'e'
  [12] = 'l'
  [13] = ' '
  [14] = 'V'
  [15] = 'e'
  [16] = 'r'
  [17] = 's'
  [18] = 'i'
  [19] = 'o'
  [20] = 'n'
  [21] = ' '
  [22] = '1'
  [23] = '7'
  [24] = '.'
  [25] = '7'
  [26] = '.'
  [27] = '0'
  [28] = ':'
  [29] = ' '
  [30] = 'W'
  [31] = 'e'
  [32] = 'd'
  [33] = ' '
  [34] = 'O'
  [35] = 'c'
  [36] = 't'
  [37] = ' '
  [38] = '1'
  [39] = '0'
  [40] = ' '
  [41] = '2'
  [42] = '3'
  [43] = ':'
  [44] = '0'
  [45] = '6'
  [46] = ':'
  [47] = '1'
  [48] = '4'
  [49] = ' '
  [50] = 'P'
  [51] = 'D'
  [52] = 'T'
  [53] = ' '
  [54] = '2'
  [55] = '0'
  [56] = '1'
  [57] = '8'
  [58] = ';'
  [59] = ' '
  [60] = 'r'
  [61] = 'o'
  [62] = 'o'
  [63] = 't'
  [64] = ':'
  [65] = 'x'
  [66] = 'n'
  [67] = 'u'
  [68] = '-'
  [69] = '4'
  [70] = '5'
  [71] = '7'
  [72] = '0'
  [73] = '.'
  [74] = '7'
  [75] = '1'
  [76] = '.'
  [77] = '1'
  [78] = '3'
  [79] = '~'
  [80] = '1'
  [81] = '/'
  [82] = 'D'
  [83] = 'E'
  [84] = 'V'
  [85] = 'E'
  [86] = 'L'
  [87] = 'O'
  [88] = 'P'
  [89] = 'M'
  [90] = 'E'
  [91] = 'N'
  [92] = 'T'
  [93] = '_'
  [94] = 'X'
  [95] = '8'
  [96] = '6'
  [97] = '_'
  [98] = '6'
  [99] = '4'
  [100] = '\0'
}
(lldb)

实际上,使用x <address>命令,我就可以将内存内容转储到该地址。

(lldb) x 0xffffff802f0f68f0
0xffffff802f0f68f0: 44 61 72 77 69 6e 20 4b 65 72 6e 65 6c 20 56 65  Darwin Kernel Ve
0xffffff802f0f6900: 72 73 69 6f 6e 20 31 37 2e 37 2e 30 3a 20 57 65  rsion 17.7.0: We
(lldb)

它看起来像是对0xffffff802f0f6900的继续,于是我决定继续转储。

(lldb) x 0xffffff802f0f6900
0xffffff802f0f6900: 65 72 73 69 6f 6e 20 36 39 2e 30 30 20 57 65 65  rsion 17.7.0: We
0xffffff802f0f6910: 64 20 4f 63 74 20 31 30 20 32 33 3a 30 36 3a 31  d Oct 10 23:06:1
(lldb)

此时,就可以看到44 61 72 77 69 6e了,这是Darwin这个词的十六进制表示。如果我把它改成十六进制中的“GeoSn0w”,基本上可以改变内核名称,所有的版本均是如此。

所以,我需要一个Text to Hex转换器(一个非常好的文本字符转换16进制小工具,它支持文本转换16进制,也支持16进制转换文本),这个工具可以从网上下载到。但需要注意的是,我们如果要写入更长的字符串,就要先覆盖一些原先的内容。注意,写入的字符串不要超过现有字符串中的字符限制。

经过精心设计,我的十六进制字符串如下所示。

47 65 6f 53 6e 30 77 20 4b 65 72 6e 65 6c 20 56 = "GeoSn0w Kernel V"

65 72 73 69 6f 6e 20 36 39 2e 30 30 20 57 65 65 = "ersion 69.00 Wee"

不过现在,我们还不能把它写成这样的两个地址。因为我们必须在所有字符前面加上“0x”,最终的结果如下。

0x47 0x65 0x6f 0x53 0x6e 0x30 0x77 0x20 0x4b 0x65 0x72 0x6e 0x65 0x6c 0x20 0x56 = "GeoSn0w Kernel V"

0x65 0x72 0x73 0x69 0x6f 0x6e 0x20 0x36 0x39 0x2e 0x30 0x30 0x20 0x57 0x65 0x65 = "ersion 69.00 Wee"

现在我们可以将字节写入内存,让我们从第一个地址开始。在本文的示例中,命令看起来像这样:

(lldb) memory write 0xffffff802f0f68f0 0x47 0x65 0x6f 0x53 0x6e 0x30 0x77 0x20 0x4b 0x65 0x72 0x6e 0x65 0x6c 0x20 0x56
(lldb) x 0xffffff802f0f68f0
0xffffff802f0f68f0: 47 65 6f 53 6e 30 77 20 4b 65 72 6e 65 6c 20 56  GeoSn0w Kernel V
0xffffff802f0f6900: 72 73 69 6f 6e 20 31 37 2e 37 2e 30 3a 20 57 65  rsion 17.7.0: We
(lldb)

以下就是在0xffffff802f0f6900地址中,写入字符串后的结果。

(lldb) memory write 0xffffff802f0f6900 0x65 0x72 0x73 0x69 0x6f 0x6e 0x20 0x36 0x39 0x2e 0x30 0x30 0x20 0x57 0x65 0x65
(lldb) x 0xffffff802f0f6900
0xffffff802f0f6900: 65 72 73 69 6f 6e 20 36 39 2e 30 30 20 57 65 65  ersion 69.00 Wee
0xffffff802f0f6910: 64 20 4f 63 74 20 31 30 20 32 33 3a 30 36 3a 31  d Oct 10 23:06:1
(lldb)

现在让我们在调试器上解冻内核:

(lldb) c
Process 1 resuming
(lldb) Loading 1 kext modules warning: Can't find binary/dSYM for com.apple.driver.AppleXsanScheme (79D5E92F-789E-3C37-BE0E-7D1EAD697DD9)
. done.
Unloading 1 kext modules . done.
Unloading 1 kext modules . done.
(lldb)

在调试器终端运行uname -a命令:

26.png

此时,调试人员会看到显示出来的字符串。

Last login: Sun Dec  2 07:12:19 on ttys000
Isabella:~ geosn0w$ uname -a
Darwin Isabella.local 17.7.0 GeoSn0w Kernel Version 69.00 Weed Oct 10 23:06:14 PDT 2018; root:xnu-4570.71.13~1/DEVELOPMENT_X86_64 x86_64
Isabella:~ geosn0w$

以上就是我在macOS上进行内核调试的示例结果,希望调试人员喜欢它。不要忘记,在完成调试之后,调试人员应该再次将boot-args设置为stock,以便启动正常的RELEASE内核。调试人员可以在调试器的终端上运行sudo nvram boot-args=""命令,然后进入/System/Library/ kernel /删除kernel.development文件。

Isabella:~ geosn0w$ sudo nvram boot-args=""
Password:
Isabella:~ geosn0w$

现在在终端中输入以下两个命令,让kextcache无效:

sudo touch /Library/Extensions
sudo touch /System/Library/Extensions

然后重新启动,此时计算机将启动正常的RELEASE内核。

Mozilla公司正在努力创建一个物联网框架的软件和服务,以把各种分散的设备进行连接。通过为这些设备提供Web URL、标准化的数据模型、API,该公司正在向安全,开放和可互操作的物联网业务转移。

互联网和万维网建立在开放标准之上,这些开放标准是通过设计分散的,任何人都可以自由实现这些标准并连接到网络,而无需中心控制点。这导致了亿万台个人电脑和数十亿智能手机的爆炸式增长,这些智能手机可以通过一个全球网络相互通信。

随着技术从个人电脑和智能手机的发展到世界各地的互联网,我们的家庭,城市,汽车,衣服甚至我们的身体中的新型设备无时无刻都处于网络之中。

物联网(The Internet of Things)

物联网是描述物理对象如何连接到互联网的术语,以便可以发现,监控,控制或与之互动。随着技术的进步,这些创新带来了巨大的新机遇,也带来了新的风险。

Mozilla建立IoT的目的是“确保互联网是一个全球公共资源,所有人都可以开放和访问。一个真正把人放在第一位的互联网,安全和独立的环境非常重要。

当我们周围的一切都被设计成连接到互联网,那带来的安全隐患和互操作性的风险将是一个重大挑战。

连接到互联网的许多设备是不安全的,首先它们本身不会更新来应对新出现的漏洞,其次它们还会收集,存储和使用大量非常隐私的信息。

此外,大多数互联网设备今天使用专有的垂直技术堆栈,这些堆栈都是围绕中心控制点建立的,并且不会出现交集。如果它们要交集时,就需要每个供应商的集成来将这些系统连接在一起。不过制定统一的标准是非常复杂的,目前还没有一个主导模式或市场领导者。

1.1.png

物联网(Web of Things)

Web of Things顾名思义,就是所有的设备都利用web网页,技术上来说,物联网(The Internet of Things)出现之前,万维网就已经存在了。虽然万维网有超文本系统和专有GUI,但互联网缺乏统一的应用层协议来共享和链接信息。

物联网(Web of Things)是从万维网上得出来的经验,并将其应用于物联网(The Internet of Things)。woT是通过在网络上提供Things URL来使其可链接和可发现,并定义标准数据模型和API,使其可以互操作来创建分散的IoT。

1.2.png

woT不仅仅是与现有平台竞争的另一个垂直的IoT技术栈,它旨在作为一个统一的水平应用层,将多个潜在的IoT协议桥接在一起。

woT并不是从头开始建立的,而是建立在现有的经过验证的网络标准上,如REST,HTTP,JSON,WebSockets和TLS(传输层安全性)。woT还将使用新的网络标准,特别是,我们认为需要一种Web Thing描述格式来描述事物,一个REST风格的Web Thing API与它们进行交互,以及可能是对IoT用例进行优化的新一代HTTP,并由资源受限设备使用。

woT不仅仅是Mozilla的一个计划,在IETF,W3C,OCF和OGC上已经有一个成熟的Web of Things社区和相关的标准化项目。 Mozilla已计划成为该社区的参与者,以帮助定义新的网络标准,并推广关于隐私,安全性和互操作性的最佳IoT做法。

从这个现有的工作中,出现了将设备连接到网络的三个关键集成模式,这些模式是由Web of Things API渗透到互联网定义的。

1.3.png

直接集成模式

最简单的模式是设备直接将woT API直接映射到互联网的直接集成模式,这对于可以支持TCP / IP和HTTP并且可以直接连接到互联网(例如WiFi摄像机)的相对高功率的设备是有用的。对于可能需要使用NAT或TCP隧道以便穿越防火墙的家庭网络上的设备,该模式可能是不合适的,另外直接集成模式还可以更直接地将设备暴露给来自互联网的安全威胁。

网关集成模式

网关集成模式对于不能自己运行HTTP服务器的资源受限设备很有用,因此使用网关将其桥接到Web。这种模式对于具有有限功率的设备或使用诸如蓝牙或ZigBee之类的PAN网络技术且不直接连接到因特网尤其有用,例如电池供电的感应门,另外,网关也可用于将各种现有的IoT设备连接到网络。

云集成模式

在云集成模式中,Web of Things API由作为远程网关的云服务器公开,设备使用一些其他协议与后端的服务器进行通信。这种模式对于需要中央协调的广泛地理区域的大量装置特别有用,例如空气污染传感器。

Mozilla项目

在Mozilla的Emerging Technologies团队中,研究人员正在开发一个软件和服务的实验框架,以帮助用户以安全和可互操作的方式将设备连接到网络。

1.4.png

该项目最初会将重点集中于开发三个组件:

1.物网关(Things Gateway ),Web物网关的开源实现,有助于桥接现有的IoT设备到网络

2.物云(Things Cloud),由Mozilla托管的云服务的集合,可帮助在广泛的地理区域管理大量的 IoT设备

3.物框架(Things Framework) ,可重用的软件组件,用于帮助创建直接连接到物联网的IoT设备

物网关(Things Gateway )

目前,Mozilla已宣布推出该项目的第一个组件的原型,并提供了一个软件镜像,你可以使用Raspberry Pi构建自己的woT网关。

1.5.png

到目前为止,这个原型具有的特点如下:

1.轻松发现本地网络上的网关;

2.选择一个网址,通过安全的TLS隧道将家庭设备连接到互联网,在你的家庭网络上需要用到零配置网络服务规范;

3.创建用户名和密码以授权访问你的网关;

4.发现并将商用的ZigBee和Z-Wave智能插头连接到网关;

5.从网关本身托管的网络应用程序打开和关闭这些智能插件;

研究人员在早期的开发过程就发布了这个原型,以便黑客和制造商能够掌握源代码来构建自己的Web of Things网关,具体的过程请看这段视频

这个初始原型是在使用NodeJS Web服务器的JavaScript中实现,但是目前,研究人员正在探索适配器附加系统,以允许用户使用其他编程语言(如Rust)在未来建立自己的物联网适配器。

Web Thing API

研究人员建立这个IoT框架的目标是以创建woT应用为例,帮助推动关于安全性,隐私和互操作性的IoT标准。Mozilla的目标不仅仅是创建一个Mozilla IoT平台,而且是一个Web of Things API的开源实现,任何人都可以使用他们选择的编程语言和操作系统来自由实现。

为此,Mozilla的研究人员已开始研究Web Thing API规范草案,这包括一个简单但可扩展的Web Thing说明格式和默认JSON编码,以及一个REST + WebSockets Web Thing API。他们希望这种务实的方法能够吸引Web开发人员,并帮助他们实现WoT的开发。

Mozilla的研究人员鼓励开发人员尝试在现实生活中使用这个API草案,并提供有效的反馈。

1.6.png

有很多方法可以参与其中,比如:

1.构建Web Thing,构建你自己使用的Web Thing API的IoT设备

2.创建适配器,创建适配器以将现有的IoT协议或设备桥接到Web

802.11标准下常见的WiFi攻击

流量嗅探

实际上,所有的WiFi流量都可以在监控模式下使用适配器进行嗅探。大多数Linux发行版都支持将某些WiFi芯片组放入这个监控的模式中,这样就可以处理所有网络流量。

加密的网络也没有你想象的安全,WEP加密甚至WPA2-PSK都是不安全的,攻击者可以通过欺骗一个deauthentication框架来强制一个新的身份验证过程,从而将你的设备与网络断开。

由于嗅探流量是被动进行的,不能被检测到。所以实际上所有开放或关闭的WiFi通信都是公开的,这就要求在更高层次上进行通信加密,比如HTTPs。

暴力访问

和其他密码一样,无线网络的密码也可以被暴力获取。WEP可以通过分析记录的流量在几分钟内被破解,并被渲染成无用的。所以对于WPA安全网络,黑客只需要一个标准的字典攻击即可达到目的。

实际上,目前大多数暴力破解工具都是针对WiFi流量的。

像流量嗅探一样,这种方法也是可以被检测到的。唯一的保护的方法是使用强密码,避免WEP加密。

WiFi网络干扰

在802.11协议标准下,干扰WiFi网络的方法很简单,就是将相关的通信频率填充大量垃圾。具体过程就是:利用Deauthentication和disassociation框架。

因为deauth框架是管理框架,它们是未加密的,即使没有连接到网络,任何人都可以对修改它。通过在框架中设置“发送器”地址,攻击者可以处于攻击范围内,不但可以发送持续的deauth框架,而且还能监听你的设备发送的指令。甚至干扰器脚本能监测出所有接入点和客户机的列表,同时不断的将deauth框架发送给所有的用户。

wifi1.png

检测干扰器

像nzyme这样的工具将会监测出deauth框架,而Graylog日志监控系统可以在不同寻常级别的框架子类型域中发出警报。

恶意接入点

目前手机自动连接到WiFi网络的方式有两种:

1.手机的信标帧(beacon frame)通过发送的定期发送的信标,可让移动工作站得知该网络的存在,从而调整加入该网络所必要的参数。在基础型网络里,接入点必须负责发送信标帧。信标帧所及范围即为基本服务区域。在基础型网络里,所有连接都必须通过接入点,因此工作站不能距离太远,否则便无法接收到信标。

2. 通过探测请求(Probe Request),移动工作站将会利用探测请求帧,扫描所在区域内目前有哪些 802.11网络。Probe Request帧的格式如下图所示,所有位均为必要。

探测请求帧包含两个位:SSID以及移动工作站所支持的速率(Supported Rates)。收到探测请求帧的工作站会据此判定对方能否加入网络。为此,移动工作站必须支持网络所要求的所有数据速率,并以SSID表明所欲加入的网络。

这样问题就来了,任何设备都可以为任何网络发送信标帧和探测请求帧。如此一来,攻击者就可以利用一个无赖的接入点四处移动,以响应任何需要响应的请求,或者他们刻意为目标公司网络发送信标。

现在的很多设备也都部署了相应的保护机制,如果你准备连接到一个之前加密但当前未加密的网络,那么设备将会给你发出警告提醒。不过,如果攻击者知道你之前所连接的WiFi密码或者说本身他攻击的就是一个开放网络的话,这种保护机制就没有任何效果了。如果你的手机进行了恶意接入点,那攻击者就会实施中间人攻击,监听你所有的通讯或发起DNS等攻击。攻击者甚至可以向你展示一个恶意的强制登录门户(Captive Portal)以收集更多关于你的浏览器的信息。

恶意接入点是非常难以识别的,因为在物理上定位它们很复杂,而且它们通常和现有的接入点基础设施混合在一起。不过可以使用nzyme和Graylog工具来检测它们。nzyme是一个开源工具,负责往Graylog记录和转发802.11标准下的管理帧,用于WiFi安全监控和事件响应。

恶意接入点的5中检测方法

方法1:BSSID白名单方法

和其他网络设备一样,每个WiFi接入点都有一个MAC地址,这是它发送的每个消息的一部分。BSSID是指站点的MAC地址,(STA)在一个接入点,(AP)在一个基础架构模式, BSS是由IEEE 802.11-1999 无线局域网规范定义的。这个区域唯一的定义了每个BSS 。检测恶意接入点的一种简单方法是保存你的可信接入点和他们的MAC地址的列表,并与你在空中看到的MAC地址相匹配。不过,攻击者可以很容易地欺骗MAC地址,绕过这种保护措施。

方法2:非同步的MAC时间戳

每个产生同一网络的接入点都有一个高度同步的内部时钟,这一点很重要。这个时间是毫秒级的,同步增量为25微秒。大多数恶意接入点在尝试进行时间戳同步时往往会出现各种各样的错误,你可以通过检测这种错误来发现恶意热点。

方法3:错误的信道

你可以设置一个列表来存储所有受信任接入点的信道,如果信道不同,则说明该接入点有问题。但是对于攻击者来说,这种保护方式也是能够轻松绕过的:比如对站点进行重新定位,并将恶意接入点配置为只使用已经使用过的信道。

方法4:加密降级

一个不知道网络密码的攻击者可能会启动一个恶意接入点,以打开一个开放的网络。

方法5:信号强度异常

通过分析信号强度寻找异常情况,目前有许多方法可以发现恶意的接入点。如果一个攻击者坐在停车场上,并且伪造一个接入点,包括它的MAC地址(BSSID),则平均信号强度突然有一个改变,因为他远离传感器(nzyme)。

“TestLib,Version = 1.0.0.0,Culture = neutral,PublicKeyToken = 769a8f10a7f072b4”

如果你能看懂上面这行的意思,那你很可能是一个.NET开发人员,同时你也可能知道结束处的十六进制字符串表示的是一个公钥标记。

不错,上面的字符串就是一组.net程序集强名称签名。

但你知道如何计算这个令牌吗?你知道强名称签名的结构吗?在这篇文章中,我们将详细介绍强名称的工作原理及其优缺点。

强名称是由程序集的标记加上公钥和数字签名组成的。其中,程序集的标记包括简单文本名称、版本号和区域性信息(如果提供的话)。也就是说,一个完整的强名称字符串包含4部分:

1.程序集的文件名;

2.程序集的版本号;

3.程序集的区域性信息;

4.一个公钥以及一个数字签名; 

其中前3部分信息会存储在程序集的清单(manifest)中。清单包含了程序集的元数据,并嵌入在程序集的某个文件中。它使用相应私钥从程序集文件生成。 

具有强名称的程序集只能使用其他具有强名称的程序集的类型。 否则,将会危害具有强名称的程序集的安全性。

在.NET框架中强名称程序集是通过公钥和私钥加密来产生这个唯一标记的。

因为强命名程序集使用公钥/私钥对来进行唯一性签名,不同的公司公钥/私钥对不可能相同,所以所生成的程序也不相同,这就解决了以前老出现的DLL Hell问题(两个不同的公司可能开发处具有相同名称的程序集,如果将相同名称的程序集放置到同一个目录下,则会出现程序集覆盖现象,最后安装的程序集会覆盖前面的程序集,从而可能导致应用序不能正常运行)。任何两个强命名的程序集,就算名称一模一样,Windows也知道他们是两个不同的版本,因为他们的唯一标记不一样。并且你可以通过配置文件控制应用程序去正确的加载你想要加载的那个dll。

所以强名称签名还能唯一标记程序集所使用的组件,所以强名称的作用主要有三个:

1.区分不同的程序集;

2.确保代码没有被篡改过;

3.在.NET中,只有强名称签名的程序集才能放到全局程序集缓存中。

公钥标记和强名称签名

公钥是包含在程序集元数据中的#Blob流的一部分,下图就是dnSpy窗口的一部分,dnSpy是一款开源的基于ILSpy发展而来的.net程序集的编辑,反编译,调试神器:

22.1.png

下图列出了构建公钥模块的元素:

22.2.png

22.22.png

上图中记录有公钥标记,开发人员和最终用户看到的也是公钥标记,而不是公钥。

公钥和公钥标记有什么区别呢?

公钥总共占160字节,有32个字节装的是头信息,另外有128个字节装的是数据。公钥对程序开发者来说是可见的。因为公钥占160字节,一个程序集可能会引用很多其他的程序集,所以在最终生成的文件中会有很大一部分空间被公钥占用,在使用的时候不太方便,于是人们提出了一个公钥标记的概念,公钥标记只占8个字节。公钥标记是把公钥进行哈希处理(使用SHA1算法),把哈希处理的结果的后面8个字节进行逆转,得到的就是公钥标记,

本文的测试中所用的公钥标记为:

769a8f10a7f072b4 (SHA-1(00 24 00 00 0c 80 … c8 8a c1 b1) = 9aa4de0a96ada8d83d6d7678b472f0a7108f9a76

怎样对程序集进行RSA签名

1.对PE文件内容进行哈希处理,然后把哈希处理后的值,用私钥进行RSA签名,把签名后的值加入到PE文件的CLR头中去。同时也会把公钥加入到程序集的元数据中去。

2.在生成元数据表FileRef时,CLR会把程序集里面的文件也进行哈希处理,得到一个哈希值,把文件和对应的哈希值同时加到FileRef元数据表中,

22.3.png

为了计算RSA签名,我们需要拥有与公钥相对应的私钥。如果你想看到实现验证的C#代码,请查看dnLib库中的StrongNameSigner.cs文件。

在研究了强名称签名结构之后,让我们来了解一下可以用来创建强名称签名的工具。我们发现生成的.snk文件会存储RSA密钥详细信息(如果你对.snk文件格式的详细信息感兴趣,请查看010编辑器模板):

22.4.jpg

然后我们可以使用C#编译器(csc.exe)或组装链接器(al.exe),这两个两者都可以被注入/ keyfile参数,在这里我们提供了刚生成的.snk文件的路径,如下图:

22.5.jpg

此命令将基于文件内容的SHA-1算法生成签名。不过现在,SHA-1已经不是一种安全的算法了,强烈建议使用SHA-2算法。

如果要使用SHA-2算法对我们的程序集进行签名,我们首先需要提取.snk文件的公钥部分:

22.6.jpg

然后延迟私钥签名(强名称签名块将被清0,强名称签名标志不会被置位)。 csc.exe和al.exe都会被注入/ delaysign +参数用于延迟私钥签名:

csc.exe /keyfile:TestLibPubKey.snk /delaysign+ /t:library TestLib.cs

什么是延迟签名?

如果要生成一个强命名的程序集,那么每次生成都需要进行签名,在开发的时候就会频繁的访问私钥文件,而私钥文件一般都是非常保密的,想要频繁使用可能有些费事。所以就有了延迟签名的机制。延迟签名的意思就是在开发阶段,只把公钥提供给开发人员,只有公钥对程序集进行签名,在最后打包发布的时候,才使用私钥来进行签名。

最后,我们需要使用私钥重新对程序集进行签名:

Authenticode 签名

Authenticode 签名,顾名思义,用于验证程序集的开发者身份另外它还能保护组件的完整性。Authenticode 签名的大小和位置存储在PE可选标题中:

22.7.png

强名称虽然引入了“身份”的概念,但没有包括“信任”机制。例如,使用强名称签署的一个程序集虽然能保证版本兼容性,但不能保证要加载的程序集来自Quilogy。为了用Authenticode数字签名来签署程序集,开发者要使用.NET框架配套提供的命令行实用程序Signcode.exe。程序集使用Authenticode签名进行签署之后,管理员就可创建相应的策略,利用代码访问安全性(CAS)机制,允许它下载到用户的机器上并进行加载。签名将成为CLR的类加载器所使用的身份凭证的一部分,用于判断程序集是否应该加载。

程序集可携带完整的 Microsoft Authenticode 签名。 Authenticode 签名包括建立信任的证书。 

请务必注意强名称不要求代码以这种方式进行签名。事实上,用于生成强名称签名的密钥不需要与用于生成Authenticode签名的密钥相同。

跳过受信任程序集的签名验证

从.NET Framework 3.5 Service Pack 1开始,当程序集加载到完全信任的应用程序域(如 MyComputer 区域的默认应用程序域)时,不会验证强名称签名。 这被称之为强名称跳过功能。 在完全信任的环境中,对于已签名的完全信任的程序集,对StrongNameIdentityPermission的需求总是成功,而不考虑其签名。 这种情况下,强名称跳过功能可避免完全信任程序集不必要的强名称签名验证开销,允许更快地加载程序集。

跳过功能适用于使用强名称进行签名及具有以下特征的任何程序集:

1.完全受信任,无需 StrongName证据(如具有 MyComputer 区域证据);

2.加载到完全受信任的 AppDomain;

3.加载自该 AppDomain 的 ApplicationBase 属性下的某个位置;

4.签名没有延迟;

总结

由于很容易跳过签名验证,你可能怀疑二进制文件的整个结构。不过,考虑一下,如果恶意攻击者获得对二进制文件的访问权,签名将不能保护你的软件安全。不过,签名程序集是唯一的能证明程序集中的代码是合法的证明,并且没有被篡改。

我们应该在客户端接收到二进制文件(这通常由安装程序完成)后执行第一次验证,以确保在传输过程中没有篡改;接下来,在二进制文件所在的文件夹中设置有效的访问权限非常重要,只有被授权的人才能修改文件;最后,每当有我们的应用程序出现问题,我们应该要求客户端在填写错误报告之前验证签名,只有这样我们才能确定该错误是我们本来应该有的真实错误。

电子商务、移动支付的普及,消费者越来越少随身携带现金,人们打趣道“小偷都快失业了”。但在互联网上,靠盗窃用户电子账户资金、虚拟资产的“网络小偷”却十分猖獗。

各种途径泄露的个人信息被加工、转卖,并用于电信诈骗或盗刷等,形成庞大的黑色产业体系,成为互联网世界中的隐秘毒瘤。

目前世界上已经有数十个成熟的并且具有高强度安全水平的电子支付系统;然而随着网络攻击手段和黑客技术的快速发展,电子支付加大了风险,也使得其影响范围也扩大了,某个环节存在的风险对整个机构,甚至金融系统都可能存在潜在的影响。最近巴黎大学的研究人员就针对电子支付系统的安全性专门进行了一项调查,对目前市场上常用的各种电子支付系统的安全性进行了安全测试。研究主要集中于对目前主流的电子支付系统以及新出现的一些具有创新性的支付系统进行了测试、分析,以试图对当前各种支付系统的安全进行升级、改善。

电子支付系统的风险

首先是软硬件系统风险。从整体看,电子支付的业务操作和大量的风险控制工作均由电脑软件系统完成。全球电子信息系统的技术和管理中的缺陷或问题成为电子支付运行的最为重要的系统风险。在与客户的信息传输中,如果该系统与客户终端的软件互不兼容或出现故障,就存在传输中断或速度降低的可能。此外,系统停机、磁盘列阵破坏等不确定性因素,也会形成系统风险。根据对发达国家不同行业的调查,电脑系统停机等因素对不同行业造成的损失各不相同。信息系统的平衡、可靠和安全运行成为电子支付各系统安全的重要保障。

其次是外部支持风险。由于网络技术的高度知识化和专业性,又出于对降低运营成本的考虑,金融机构往往要依赖外部市场的服务支持来解决内部的技术或管理难题,如聘请金融机构之外的专家来支持或直接操作各种网上业务活动。这种做法适应了电子支付发展的要求,但也使自身暴露在可能出现的操作风险之中,外部的技术支持者可能并不具备满足金融机构要求的足够能力,也可能因为自身的财务困难而终止提供服务,可能对金融机构造成威胁。在所有的系统风险中,最具有技术性的系统风险是电子支付信息技术选择的失误。当各种网上业务的解决方案层出不穷,不同的信息技术公司大力推举各自的方案,系统兼容性可能出现问题的情况下,选择错误将不利于系统与网络的有效连接,还会造成巨大的技术机会损失,甚至蒙受巨大的商业机会损失。

最后是交易风险,电子支付主要是服务于电子商务的需要,而电子商务在网络上的交易由于交易制度设计的缺陷、技术路线设计的缺陷、技术安全缺陷等因素,可能导致交易中的风险。这种风险是电子商务活动及其相关电子支付独有的风险,它不仅可能局限于交易各方、支付的各方,而且可能导致整个支付系统的系统性风险。

随着计算机技术的发展,电子支付的工具越来越多。这些支付工具可以分为三大类:

电子货币类,如电子现金、电子钱包等;

电子信用卡类,包括智能卡、借记卡、电话卡等;

电子支票类,如电子支票、电子汇款(EFT)、电子划款等。

所以巴黎大学的研究人员就对目前比较流行的有卡交易(Card-present)及其所使用的EMV支付系统就行了研究,EMV是Europay(已被MasterCard收购)、MasterCard、VISA三个信用卡国际组织联合制定的银行芯片卡借记/贷记应用的统一技术标准,代表着新一代银行卡的主流标准,这一标准是全球IC银行芯片卡的基础。

包括剑桥大学的研究人员也对基于EMV系统的不同攻击类型进行了研究,与磁条卡相比,EMV芯片卡可以储存更多的数据来唯一识别卡片和持卡人。黑客几乎无法解码或篡改卡片。再者磁条卡容易被克隆复制,而EMV芯片卡使用加密的微处理芯片可以保护卡片信息不被复制。这么高级别的安全防护,按理说应该不会被攻击,不过研究者却发现,这其中也存在着很大的不安全因素,虽然银联芯片卡规范在一定程度上确保了有卡交易的安全,但针对逐渐普及的无卡交易及新兴(创新)交易,所以研究者还检查了以下无卡交易支付系统的安全性:

1.无卡交易(Card not present)系统,例如3D SET,3D Secure,SET / EMV和EMV / CAP;

2.支付标记化(Payment Tokenization) 以及基于电子支付系统和电子现金的Blon签名的安全效果,其中支付标记化原理在于通过支付标记(token)代替银行卡号进行交易验证,从而避免卡号信息泄露带来的风险;

3.使用量子密钥分配(QKD)的各种电子支付系统,QKD技术通过单光子传输数字信息,生成绝对安全可靠的密钥,被称为量子密钥,理论上具备绝对安全性,本次测试就是对其安全级别就行测试,看看是不是比传统的密码术更能保证支付系统的安全;

4.NFC支付,NFC技术(Near Field Communication)通过手机等手持设备完成支付,是新兴的一种移动支付方式。支付的处理在现场进行,并且在线下进行,不需要使用移动网络,而是使用NFC射频通道实现与POS收款机或自动售货机等设备的本地通讯;

5.非接触式(Contactless) 支付方式,包括Apple Pay,Android Pay和Google电子钱包;

6.各种电子货币和对等网络(P2P)支付系统,如比特币支付;

在今天这部分,我们将披露出在本次研究中所发现的交易中的3类EMV系统安全漏洞:

对静态数据认证(SDA)的攻击

静态数据认证(SDA)是认证支付卡的最简单的方法,但它能防止数据被非法修改。它的做法是用Hash为这些数据生成一个简短表示做为该数据的摘要, 然后,把这个摘要加密,得到一个"签名"。再把数据和加密后的签名发给终端,终端在这一头先把收到的数据Hash,得到一个签名,然后再把收到的加密签名解密,又得到一个签名。比较两个签名,就可以知道数据有没有被修改过,如果数据被修改过,两个签名是不同的。验证不会通过,由于使用静态证书,所以我们的卡里的信息很容易被复制,让第三者来使用,相信下面的这个例子大家都听过:犯罪分子要用两个设备,读卡器和针孔摄像机。他们把读卡器与银行原来的读卡器固定一起,插卡口对齐,当我们取钱时把卡插进去时,我们的卡上的信息就被他们的读卡器读出,然后他们用一张空白的卡就可以把我们的卡复制出来。针孔摄影机的作用就是当我们取钱时窃取我们的密码,卡有了, 密码也有了,剩下的步骤就是取钱了。这就是静态证书的致命安全漏洞。

1.1.jpg

对EMV的MITM攻击

在介绍对EMV的中间人攻击(Man-in-the-Middle Attack, MITM) 之前,有必要先介绍一下银行卡的验证过程,当你输入正确的PIN码时,银行卡传输层中得到的SW1 SW2就等于‘9000’,否则就等于63Cx(x:客户输入的PIN码的数量)就终止交易。然而,在这个过程中的主要漏洞和缺陷便是终端不能准确地识别是谁给银行卡发送过来的响应。这个缺陷使得MITM攻击得以有机可乘。下图形象的描述了这种攻击。

1.2.png

发送到银行终端的认证数据包括“终端验证结果”(TVR)和“发行方应用数据”(IAD)。 TVR包括所有可能的验证状态下的漏洞,如下面的表格所示。

1.3.png

但是这个验证过程的缺点之一就是在成功认证的情况下,没有提到是使用哪种方法(例如PIN或签名)进行的。

因此,这类攻击的主要特点为, 利用上表的那几个缺陷,黑客在执行MITM攻击的时候,能够充当中间人的角色,拦截银行卡与刷卡点之间的传输,然后通过发送‘9000’响应造成终端已经收到验证的假象,也就是说在交易的过程中,黑客可绕过PIN码验证环节,即输入任意PIN码,均可实现正常交易。然而,因为银行卡并没有接收到假的PIN码,所以尝试输入PIN的次数(即63Cx 中x)不会增加。因此,TVR的安全设置变失效,因为PIN码压根就没有通过正常的传输来进行,所以银行卡和刷卡点都被欺骗了。刷卡点认为PIN码验证在接收到‘9000’响应之后成功了并且然成果通过了TVR的设置,而银行卡则假定在终端不能接收PIN码之后,误以为终端不支持PIN验证方法。

对EMV的其他类型攻击

该研究还发现了对EMV的其他类型的攻击,包括EMV中的不可预测的数量,中继攻击(Relay Attack 是通过在通信双方增加中继来进行窃取信息)和EMV智能卡中密码算法的漏洞。