学习笔记

1、安卓设备的远程攻击面

  • 通过互联网发起的攻击:通常需要进行一些交互
    • 点击一个链接
    • 接收一个邮件
    • 发送即时消息
    • ……
  • 通过临近网络发起的攻击:更容易做到零点击
    • NFC
    • WiFi
    • 蓝牙
    • ……

3、安卓设备攻击技巧

  • 通过一个浏览器漏洞,获得一个渲染线程的权限,把漏洞转化为UXSS漏洞,之后安装任意应用,包括恶意应用。
    • 谷歌已经改变了在页面上安装应用的流程,会弹出提示框,让输入密码
    • 谷歌实现了site isolation的技术,不同的站点运行在不同的进程里面,导致了RCE2UXSS技术失效
  • 通过一个浏览器漏洞,然后通过libgralloc模块内的漏洞,获得系统用户权限
  • ……

4、梯云纵漏洞链概览

梯云纵漏洞链通过三个漏洞能够很轻易地获取手机Root权限’

  1. 通过以下两个漏洞可以拿到Chrome APP 权限
    • Chrome渲染进程漏洞CVE-2019-5877
    • ChromeGPU进程漏洞CVE-2019-5870
  2. 通过以下漏洞获取到内核权限
    • 高通GPU驱动漏洞CVE-2019-10567

5、漏洞链中三个漏洞的细节

CVE-2019-5877

Torque是Chrome v8的一种语言,它用来实现Chrome中很多内建的函数和内建的一些对象,最后会被翻译成CSA,最终集成到snapshot文件里面,影响到Chrome的安全性。

这个漏洞与JSFunction的内存布局有关:

  • JSFunction是Javascript的函数对象的内部实现结构
  • JSFunction的大小是可变的,它可能拥有PrototypeOrInitalMap这样一个成员
  • 如果有这个成员的话,PrototypeOrInitialMap是JSFunction的最后一个成员

JSFunction构造方法

function functionName(arg0,arg1,....,argN){statements} //function语句
var function_name = new Function(arg0,arg1,....,argN,function_body);//Function构造函数
var func = function(arg0,arg1,....,argN){statements}//函数直接量
  1. 函数实际上是功能完整的对象,Function类可以表示开发者定义的任何函数。
  2. 函数名只是指向函数的指针,因此函数可以作为参数传递给另一个函数。
  3. 所有的函数都应看作Function类的实例。
  4. 如果定义的函数没有参数,那么可以只需给构造函数传递一个字符串(函数的主体)即可。

Array函数的内存布局

d8> %DebugPrint(Array);
 DebugPrint: 0x2b7bcbd91b79: [Function] in OldSpace
 - map: 0x2f2f62402ff1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x2b7bcbd82091 <JSFunction (sfi = 0xc83e79480e9)>
 - elements: 0x3b058dc40bf9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: 0x2b7bcbd91dc9 <JSArray[0]>
 - initial_map: 0x2f2f62403041 <Map(PACKED_SMI_ELEMENTS)> //initial_Map
 - shared_info: 0x0c83e79547c9 <SharedFunctionInfo Array>

 0x2f2f62402ff1: [Map]
 - type: JS_FUNCTION_TYPE
 - instance size: 64
 - callable
 - constructor
 - has_prototype_slot
 - constructor: 0x2b7bcbd822e1 <JSFunction Function (sfi = 0xc83e7954449)>

parseInt函数的内存布局

d8> %DebugPrint(parseInt)
 DebugPrint: 0x2b7bcbd8b999: [Function] in OldSpace
 - map: 0x2f2f624003e1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x2b7bcbd82091 <JSFunction (sfi = 0xc83e79480e9)>
 - elements: 0x3b058dc40bf9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: <no-prototype-slot> //没有prototype initialMap成员
 - shared_info: 0x0c83e79557a1 <SharedFunctionInfo parseInt>

0x2f2f624003e1: [Map]
 - type: JS_FUNCTION_TYPE
 - instance size: 56
 - callable
 - constructor: 0x3b058dc401b1 <null>

Proxy的内存布局

d8> %DebugPrint(Proxy)
 DebugPrint: 0x2b7bcbd8d6d1: [Function] in OldSpace
 - map: 0x2f2f62401d31 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x2b7bcbd82091 <JSFunction (sfi = 0xc83e79480e9)>
 - elements: 0x3b058dc40bf9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: <no-prototype-slot>
 - shared_info: 0x0c83e795e749 <SharedFunctionInfo Proxy> 0x2f2f62401d31: [Map]

 - type: JS_FUNCTION_TYPE
 - instance size: 56
 - callable
 - constructor //是一个构造函数
 - constructor: 0x3b058dc401b1 <null>

有一个假设:如果一个函数是一个构造函数,那么它就有prototype initial_Map这个成员;如果不是构造函数就没有这个成员。该假设在绝大多数情况都是成立的,也有极端情况不成立。比如Proxy对象是一个构造函数,但是没有这个成员。这些边缘情况很容易导致漏洞。

RCE漏洞位置

没有检查prototype_or_initial_map是否存在就去访问,因此是一个越界的访问

macro GetDerivedMap(implicit context: Context)(target: JSFunction, newTarget: JSReceiver): Map {
    try {
        //访问constructor
        const constructor = Cast<JSFunction>(newTarget) otherwise SlowPath;
        //访问prototype_or_initial_map
        const map = Cast<Map>(constructor.prototype_or_initial_map) otherwise SlowPath;
        if (LoadConstructorOrBackPointer(map) != target) {
            goto SlowPath;
        }
        return map;
    }
    label SlowPath {
        return runtime::GetDerivedMap(context, target, newTarget);
    }
 }

触发漏洞

var malformedTypedArray = Reflect.construct(Uint8Array, [4], Proxy)

将newTarget设置成Proxy,最终会调到有缺陷的getDerivedMap Torque代码

如何利用

可以看RCE漏洞位置,如果经过if判断发现了map和target不同就会跳转到slow path,我们要做的就是让它不走slowPath,让它走fashPath。

  1. 释放紧邻Proxy的一个对象
  2. 用一个UINT8重新占据这个释放的空间。UINT8的map是动态生成的,设它的map是X
  3. 释放UINT8对象,触发一次垃圾回收,使UINT8对象和它的map都被gc标记为垃圾,并且开始sweep
  4. 重点 在UINT8和它的map被sweep前触发漏洞,这样缺陷代码就会走fastPath
  5. Sweep完成后,用一个UINT32Array的map重新占据原来map X所在的空间,可以得到一个畸形的对象,它的map是UINT32Array,内存布局是UINT8Array,用这个畸形对象很容易实现任意内存读写
  6. 打开MojoJS bindings开关触发提权漏洞

CVE-2019-5870

Chrome多进程架构

  • Browser:一般来说权限比较高
  • GPU:一般来说权限比较高
  • Render:一般是高度沙箱化的

进程之间是通过IPC来进行通信,现在Chrome新的IPC机制是Mojo IPC

ContentDecryptionMojo 接口中有个Initialize函数是用来初始化这个Mojo的,这个初始化函数会返回一个cdm id

 interface ContentDecryptionModule {
     SetClient(pending_associated_remote<ContentDecryptionModuleClient> client);
     Initialize(string key_system,url.mojom.Origin security_origin,CdmConfig cdm_config)
     =>(CdmPromiseResult result, int32 cdm_id, pending_remote<Decryptor>? decryptor);
     SetServerCertificate(array<uint8> certificate_data)(CdmPromiseResult result);
 ......
 };

Initialize函数实现

 void MojoCdmService::Initialize(conststd::string& key_system,
                                 const url::Origin& security_origin,const CdmConfig& cdm_config,
                                 InitializeCallback callback) {
     DVLOG(1) << __func__ << ": " << key_system;
     DCHECK(!cdm_); //In debug version, this DCHECK will be trigger

     auto weak_this = weak_factory_.GetWeakPtr();
     cdm_factory_->Create(
     key_system,security_origin, cdm_config,
	 base::Bind(&MojoCdmService::OnSessionMessage, weak_this),
	 base::Bind(&MojoCdmService::OnSessionClosed, weak_this),
     base::Bind(&MojoCdmService::OnSessionKeysChange, weak_this),
     base::Bind(&MojoCdmService::OnSessionExpirationUpdate, weak_this),
     base::Bind(&MojoCdmService::OnCdmCreated, weak_this,
     base::Passed(&callback)));
 }

如果Mojo初始化成功的话,它会调用一些回调函数,其中包括一个onCdmCreate的函数,这个函数会对cdm进行一个注册:

 void MojoCdmService::OnCdmCreated(InitializeCallback callback,
                                   const scoped_refptr<::media::ContentDecryptionModule>& cdm,
                                   const std::string& error_message) {
     mojom::CdmPromiseResultPtr cdm_promise_result(mojom::CdmPromiseResult::New());

     if (!cdm) {
         ......
     }
     cdm_ = cdm;
     if (context_) {
         cdm_id_ = context_->RegisterCdm(this); //register twice here
         DVLOG(1) << __func__ << ": CDM successfully registered with ID " << cdm_id_;
     }
     ...
 }

RegisterCdm函数

int MojoCdmServiceContext::RegisterCdm(MojoCdmService* cdm_service) {
    DCHECK(cdm_service);
    int cdm_id = GetNextCdmId();
    cdm_services_[cdm_id] = cdm_service; //two cdm ids map to one cdm_service
    DVLOG(1) << __func__ << ": CdmService registered with CDM ID " << cdm_id;
    return cdm_id;
}

漏洞原因是没有对接口调用次数做限制,如果接口被调用多次,那么它创建的cmd service就会注册多次,如果cmdService被注册多次,会导致两个cmdID被映射到同一个cmdService。当cmdService被析构的时候,就会导致一个cmdID被映射到一个野指针。

触发UAF

std::unique_ptr<CdmContextRef> MojoCdmServiceContext::GetCdmContextRef(int cdm_id) {
    ......
    auto cdm_service = cdm_services_.find(cdm_id);
    if (cdm_service != cdm_services_.end()) {
        if (!cdm_service->second->GetCdm()->GetCdmContext()) {
            //GetCdm
            NOTREACHED() << "All CDMs should support CdmContext.";
            return nullptr;
        }
        return std::make_unique<CdmContextRefImpl>(cdm_service->second->GetCdm());
    }
    ......
    return nullptr;
}

被UAF的对象是一个很小的对象,它的大小只有48B,一般这么小的对象不太好控制。比较幸运的是它指向一个比较大的对象,这个对象有168B,它的名字是MediaDrmBridge,由于缺乏信息泄露漏洞,所以不采用ROP方法,转而使用更简单的return-to-lib方法。

(gdb) p sizeof(media::MojoCdmService)
	$21 = 48
(gdb) p sizeof(media::MediaDrmBridge)
	$3 = 168 //the size is 160 in release version
(gdb) x/10xw 0xb6993300 //the 内存内部 of MediaDrmBridge
     0xb6993300: 0xca3f3a0c 0x00000000 0x00000100 0xca3f3a4c
     0xb6993310: 0xca3f3a6c 0xb6a90750 0xb6a90750 0xb6a90760
     0xb6993320: 0x00000000 0x00000000
(gdb) x/10xw 0xca3f3a0c //the  虚表 of MediaDrmBridge
    0xca3f3a0c <_ZTVN5media14MediaDrmBridgeE+8>: 0xca237e09 0xca207a79 0xca237fad 0xca2382f9
    0xca3f3a1c <_ZTVN5media14MediaDrmBridgeE+24>: 0xca2384a9 0xca238601 0xca238741 0xca238881
(gdb) info symbol 0xca238881
    media::MediaDrmBridge::GetCdmContext() + 1 in section .text of libmedia.cr.so

why not rop

return-to-libc实现细节:

render进程和gpu进程共享很多动态库,这些动态库的基址都是一样的。其中有一个很有意思的动态库libllvm-glnext.so,这个动态库中用到了一个system函数,所以当这个库被加载时这个库里就会有一个system函数的指针,所以我们可以把mediadrmbridge的虚表指向这个system函数附近,当虚表里的函数被调用时,system函数就可以被调用,而且我们还可以控制它的参数,所以通过return-to-libc的方法能够执行一些shell命令,从而反弹出来一个shell。

Root漏洞 CVE-09-10567

该漏洞与高通GPU的KGSL驱动有关,其中有一个被全局映射的页有关,该页的名字是scratch。

scratch页的特点:

  1. 被映射到所有GPU的context
  2. 可以被CPU空间来操作

scratch页的数据:

  • offset 0x0 length 4*4 [RB0 RPTR,RB1 RPTR, RB2 RPTR, RB3 RPTR]
  • offset 0x10 length 8*4 [RB0 Context Restore Address, RB1 Context Restore Address RB2 Context Restore Address, RB3 Context Restore Address]

前面4个DWORD是环形缓冲区的四个读指针,scratch页可以被gpu和cpu同时控制,可以被gpu普通命令修改

int adreno_ringbuffer_probe(struct adreno_device *adreno_dev, bool nopreempt)
{
    struct kgsl_device *device = KGSL_DEVICE(adreno_dev);
    struct adreno_gpudev *gpudev = ADRENO_GPU_DEVICE(adreno_dev);
    int i;
    int status = -ENOMEM;

    if (!adreno_is_a3xx(adreno_dev)) {
        //scratch is allocated as writable by normal Command Processor instructions
        status = kgsl_allocate_global(device, &device->scratch, 
                                      PAGE_SIZE, 0, KGSL_MEMDESC_CONTIG, "scratch");
        if (status != 0)
            return status;
    }
    ...
}

unsigned int *adreno_ringbuffer_allocspace(struct adreno_ringbuffer *rb,
                                           unsigned int dwords){
    struct adreno_device *adreno_dev = ADRENO_RB_DEVICE(rb);
    unsigned int rptr = adreno_get_rptr(rb); //read rptr from scratch memory
    unsigned int ret;

    if (rptr <= rb->_wptr) {
        unsigned int *cmds;

        if (rb->_wptr + dwords <= (KGSL_RB_DWORDS - 2)) {
            ret = rb->_wptr;
            rb->_wptr = (rb->_wptr + dwords) % KGSL_RB_DWORDS;
            return RB_HOSTPTR(rb, ret);
        } ……
    }

由以上代码可知缓冲区被分配成可以被普通gpu指令修改,所以当KGSL驱动分配一块内存时可以使函数发生一些混淆:

  • 读指针指向GPU执行的下一条指令
  • 写指针指向freespace

环形缓冲区

利用这个漏洞覆盖现有的指令,把rptr改动到比较贴近wptr,再做一次分配可能会成功,并且会把已有的指令给覆盖掉,覆盖成比较关键的指令:

  • CP_NOP
  • CP_SET_PROTECTED_MODE

每次GPU_COMMAND的CP指令序列

  1. CP_SET_PROTECTED_MODE 开启ProtectedMode决定特权级别,如果是关闭的,就可以设置一些特权寄存器
  2. user_profiling 开始
  3. CP_INDIRECT_BUFFER_PFE
  4. user_profiling 结束
  5. CP_SET_PROTECTED_MODE 禁用ProtectedMode

CP_SET_PROTECTED_MODE 类似于trigger,覆盖指令后产生一些CP_NOP,最后执行一句CP_SET_PROTECTED_MODE 把原来打开的ProtectedMode关掉,就有权限修改一些特权寄存器,接下来就可以在特权模式下执行CP_INDIRECT_BUFFER_PFE 中的指令。

独立思考

1. 什么是UAF漏洞?

UAF全称是Use After Free。

假设一个对象A前四个字节(0x40000000)为函数printf,对象被释放后,恶意用户重用这个空间,将前四个字节改为(0x4a000000)system函数,然后来到漏洞触发点,程序由于疏忽调用A的printf,却实际调用了system函数。

2. 什么叫析构?为什么析构后,一个cmdID就被映射到野指针?

析构函数(destuctor)与构造函数相反,当对象结束其生命周期,如对象所在的函数已经调用完毕时,系统自动执行析构函数。析构函数往往用来做”清理善后”工作。

例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存。

因此当两个cdmID指向同一个cdmService时,如果cdmService被析构,其内存空间被回收,其中一个cdmService在析构过程被回收,多余的一个cdmID就指向了cdmService原来的空间,但是对象已经被回收,所以它变成了野指针。

3. 什么叫ROP?

ROP全称是Return-oriented Programming。

它是一种新型的基于代码复用技术的攻击,攻击者从已有的库或可执行文件中提取指令片段,构成恶意代码。攻击者扫描已有的动态链接库和可执行文件,提取出来可以利用的指令片段(gadget),这些指令片段均以ret指令结尾,即用ret指令实现指令片段执行流的衔接。在Linux系统之中,通过%esp和%ebp寄存器维护栈顶指针和栈帧的起始地址,%eip是程序计数器。

ROP攻击则是利用以ret结尾的程序片段,操作这些栈相关寄存器。控制指令的流程,执行相应的gadget,实施攻击者预定的目标。

ROP不同于return-to-libc,ROP以ret指令结尾的函数代码片段,而不是整个函数本身去完成预定的动作。

  1. ROP控制流中,call和ret不操纵函数,而是将函数里的短指令序列串起来。而在正常程序中,call和ret分别代表函数的开始和结束。
  2. ROP控制流中,jmp指令在不同的库函数甚至不同的库之间跳转。而在正常程序中,jmp指令通常在同一函数内部跳转。

ROP需要在一个特定的地址分配它的虚表,在没有信息泄露漏洞情况下不太方便使用。

产生过的疑问

  1. 什么是UAF漏洞?
  2. 什么叫析构?为什么析构后,一个cmdID就被映射到野指针?
  3. 什么叫ROP?