学习笔记
1、安卓设备的远程攻击面
- 通过互联网发起的攻击:通常需要进行一些交互
- 点击一个链接
- 接收一个邮件
- 发送即时消息
- ……
- 通过临近网络发起的攻击:更容易做到零点击
- NFC
- WiFi
- 蓝牙
- ……
3、安卓设备攻击技巧
- 通过一个浏览器漏洞,获得一个渲染线程的权限,把漏洞转化为UXSS漏洞,之后安装任意应用,包括恶意应用。
- 谷歌已经改变了在页面上安装应用的流程,会弹出提示框,让输入密码
- 谷歌实现了site isolation的技术,不同的站点运行在不同的进程里面,导致了RCE2UXSS技术失效
- 通过一个浏览器漏洞,然后通过libgralloc模块内的漏洞,获得系统用户权限
- ……
4、梯云纵漏洞链概览
梯云纵漏洞链通过三个漏洞能够很轻易地获取手机Root权限’
- 通过以下两个漏洞可以拿到Chrome APP 权限
- Chrome渲染进程漏洞CVE-2019-5877
- ChromeGPU进程漏洞CVE-2019-5870
- 通过以下漏洞获取到内核权限
- 高通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}//函数直接量
- 函数实际上是功能完整的对象,Function类可以表示开发者定义的任何函数。
- 函数名只是指向函数的指针,因此函数可以作为参数传递给另一个函数。
- 所有的函数都应看作Function类的实例。
- 如果定义的函数没有参数,那么可以只需给构造函数传递一个字符串(函数的主体)即可。
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。
- 释放紧邻Proxy的一个对象
- 用一个UINT8重新占据这个释放的空间。UINT8的map是动态生成的,设它的map是X
- 释放UINT8对象,触发一次垃圾回收,使UINT8对象和它的map都被gc标记为垃圾,并且开始sweep
- 重点 在UINT8和它的map被sweep前触发漏洞,这样缺陷代码就会走fastPath
- Sweep完成后,用一个UINT32Array的map重新占据原来map X所在的空间,可以得到一个畸形的对象,它的map是UINT32Array,内存布局是UINT8Array,用这个畸形对象很容易实现任意内存读写
- 打开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
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页的特点:
- 被映射到所有GPU的context
- 可以被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指令序列
- CP_SET_PROTECTED_MODE 开启ProtectedMode决定特权级别,如果是关闭的,就可以设置一些特权寄存器
- user_profiling 开始
- CP_INDIRECT_BUFFER_PFE
- user_profiling 结束
- 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指令结尾的函数代码片段,而不是整个函数本身去完成预定的动作。
- ROP控制流中,call和ret不操纵函数,而是将函数里的短指令序列串起来。而在正常程序中,call和ret分别代表函数的开始和结束。
- ROP控制流中,jmp指令在不同的库函数甚至不同的库之间跳转。而在正常程序中,jmp指令通常在同一函数内部跳转。
ROP需要在一个特定的地址分配它的虚表,在没有信息泄露漏洞情况下不太方便使用。
产生过的疑问
- 什么是UAF漏洞?
- 什么叫析构?为什么析构后,一个cmdID就被映射到野指针?
- 什么叫ROP?