Bagaimana melindungi proses dan ekstensi kernel pada macOS

Halo, Habr! Hari ini saya ingin berbicara tentang bagaimana Anda dapat melindungi proses dari penyusup di macOS. Sebagai contoh, ini berguna untuk antivirus atau sistem cadangan, terutama mengingat fakta bahwa di bawah MacOS ada beberapa cara untuk "membunuh" proses. Baca tentang itu dan tentang metode perlindungan di bawah kucing.

gambar

Cara klasik untuk membunuh suatu proses


Cara terkenal untuk "membunuh" suatu proses adalah mengirim sinyal tentang proses SIGKILL. Melalui bash, Anda dapat memanggil standar "kill -SIGKILL PID" atau "pkill -9 NAME" untuk membunuh. Perintah kill telah dikenal sejak UNIX dan tersedia tidak hanya pada macOS, tetapi juga pada sistem mirip UNIX lainnya.

Seperti pada sistem mirip UNIX, macOS memungkinkan Anda untuk mencegat sinyal apa pun ke proses kecuali dua - SIGKILL dan SIGSTOP. Dalam artikel ini, sinyal SIGKILL terutama akan dianggap sebagai sinyal yang memunculkan proses pembunuhan.

Khusus MacOS


Di macOS, kill system call di kernel XNU memanggil fungsi psignal (SIGKILL, ...). Mari kita coba untuk melihat apa tindakan pengguna lain di userspace dapat memanggil fungsi psignal. Kami menghilangkan panggilan ke fungsi psignal dalam mekanisme internal kernel (meskipun mungkin tidak trivial, mari tinggalkan untuk artikel lain :) - verifikasi tanda tangan, kesalahan memori, pemrosesan keluar / terminasi, pelanggaran perlindungan file, dll.

Kami memulai ikhtisar dengan fungsi dan panggilan sistem terminate_with_payload. Dapat dilihat bahwa selain panggilan kill klasik, ada pendekatan alternatif yang khusus untuk sistem operasi macOS dan tidak ditemukan di BSD. Prinsip operasi kedua pemanggilan sistem juga dekat. Mereka adalah panggilan langsung ke fungsi kernel psignal. Juga perhatikan bahwa sebelum mematikan suatu proses, suatu pemeriksaan "cansignal" dilakukan - apakah proses tersebut dapat mengirim sinyal ke proses lain, sistem tidak mengizinkan aplikasi apa pun untuk mematikan proses sistem misalnya.

static int
terminate_with_payload_internal(struct proc *cur_proc, int target_pid, uint32_t reason_namespace,
				uint64_t reason_code, user_addr_t payload, uint32_t payload_size,
				user_addr_t reason_string, uint64_t reason_flags)
{
...
	target_proc = proc_find(target_pid);
...
	if (!cansignal(cur_proc, cur_cred, target_proc, SIGKILL)) {
		proc_rele(target_proc);
		return EPERM;
	}
...
	if (target_pid == cur_proc->p_pid) {
		/*
		 * psignal_thread_with_reason() will pend a SIGKILL on the specified thread or
		 * return if the thread and/or task are already terminating. Either way, the
		 * current thread won't return to userspace.
		 */
		psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
	} else {
		psignal_with_reason(target_proc, SIGKILL, signal_reason);
	}
...
}

launchd


Cara standar untuk membuat daemon pada startup sistem dan mengontrol masa pakai mereka adalah launchd. Saya akan menarik perhatian pada fakta bahwa kode sumber adalah untuk versi lama launchctl sebelum macOS 10.10, contoh kode diberikan sebagai ilustrasi. Launchctl modern mengirimkan sinyal launchd melalui XPC, logika launchctl ditransfer ke sana.

Mari kita pertimbangkan bagaimana aplikasi dihentikan. Sebelum mengirim sinyal SIGTERM, mereka mencoba menghentikan aplikasi menggunakan panggilan sistem proc_terminate.

<launchctl src/core.c>
...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Could not terminate job: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Using fallback option to terminate job...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Could not signal job: %d: %s", error, strerror(error));
		} 
...
<>

Di bawah tenda, proc_terminate, terlepas dari namanya, dapat mengirim tidak hanya psignal dengan SIGTERM, tetapi juga SIGKILL.

Pembunuhan tidak langsung - batas sumber daya


Kasus yang lebih menarik dapat dilihat pada pemanggilan sistem process_policy lain . Penggunaan standar panggilan sistem ini adalah batas sumber daya aplikasi, misalnya, untuk pengindeks, ada batas waktu prosesor dan kuota memori sehingga sistem tidak melambat secara signifikan dari tindakan caching file. Jika aplikasi telah mencapai batas sumber daya, seperti yang dapat dilihat dari fungsi proc_apply_resource_actions, sinyal SIGKILL dikirim ke proses.

Meskipun pemanggilan sistem ini berpotensi mematikan proses, sistem tidak cukup memeriksa hak-hak proses yang menyebabkan pemanggilan sistem. Sebenarnya, ada cek , tetapi cukup menggunakan bendera alternatif PROC_POLICY_ACTION_SET untuk melewati kondisi ini.

Oleh karena itu, jika Anda “membatasi” kuota penggunaan CPU oleh aplikasi (misalnya, izinkan hanya 1 ns yang akan dijalankan), maka Anda dapat mematikan proses apa pun dalam sistem. Jadi, malware dapat membunuh proses apa pun pada sistem, termasuk proses antivirus. Yang juga menarik adalah efek yang terjadi ketika sebuah proses terbunuh dengan pid 1 (launchctl) - kernel panik ketika mencoba memproses sinyal SIGKILL :)

gambar

Bagaimana cara mengatasi masalah tersebut?


Cara paling mudah untuk mencegah proses tidak terbunuh adalah dengan mengganti pointer fungsi di tabel panggilan sistem. Sayangnya, metode ini non-sepele karena berbagai alasan

, pertama, simbol yang bertanggung jawab atas posisi sysent dalam memori bukan hanya simbol pribadi dari kernel XNU, tetapi juga tidak dapat ditemukan dalam simbol kernel. Anda harus menggunakan metode pencarian heuristik, misalnya, pembongkaran fungsi secara dinamis dan mencari pointer di dalamnya.

Kedua, struktur entri dalam tabel tergantung pada flag yang digunakan kernel. Jika flag CONFIG_REQUIRES_U32_MUNGING dideklarasikan, maka ukuran struktur akan diubah - bidang tambahan sy_arg_munge32 ditambahkan. Penting untuk melakukan pemeriksaan tambahan pada flag yang digunakan untuk mengkompilasi kernel, sebagai opsi, membandingkan pointer ke fungsi dengan yang dikenal.

struct sysent {         /* system call table */
        sy_call_t       *sy_call;       /* implementing function */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* system call arguments munger for 32-bit process */
#endif
        int32_t         sy_return_type; /* system call return types */
        int16_t         sy_narg;        /* number of args */
        uint16_t        sy_arg_bytes;   /* Total size of arguments in bytes for
                                         * 32-bit system calls
                                         */
};

Untungnya, dalam versi macOS modern, Apple menyediakan API baru untuk bekerja dengan proses. API Keamanan Endpoint memungkinkan klien untuk mengotorisasi banyak permintaan ke proses lain. Jadi, Anda dapat memblokir sinyal apa pun untuk proses, termasuk sinyal SIGKILL menggunakan API tersebut.

#include <bsm/libbsm.h>
#include <EndpointSecurity/EndpointSecurity.h>
#include <unistd.h>

int main(int argc, const char * argv[]) {
    es_client_t* cli = nullptr;
    {
        auto res = es_new_client(&cli, ^(es_client_t * client, const es_message_t * message) {
            switch (message->event_type) {
                case ES_EVENT_TYPE_AUTH_SIGNAL:
                {
                    auto& msg = message->event.signal;
                    auto target = msg.target;
                    auto& token = target->audit_token;
                    auto pid = audit_token_to_pid(token);
                    printf("signal '%d' sent to pid '%d'\n", msg.sig, pid);
                    es_respond_auth_result(client, message, pid == getpid() ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW, false);
                }
                    break;
                default:
                    break;
            }
        });
    }

    {
        es_event_type_t evs[] = { ES_EVENT_TYPE_AUTH_SIGNAL };
        es_subscribe(cli, evs, sizeof(evs) / sizeof(*evs));
    }

    printf("%d\n", getpid());
    sleep(60); // could be replaced with other waiting primitive

    es_unsubscribe_all(cli);
    es_delete_client(cli);

    return 0;
}

Demikian pula, Anda dapat mendaftarkan Kebijakan MAC di kernel, yang menyediakan metode perlindungan sinyal (kebijakan proc_check_signal), tetapi API tidak didukung secara resmi.

Perlindungan Ekstensi Kernel


Selain melindungi proses dalam sistem, perlindungan terhadap ekstensi kernel itu sendiri (kext) juga diperlukan. macOS menyediakan kerangka kerja bagi pengembang untuk mengembangkan driver perangkat IOKit dengan mudah. Selain menyediakan alat untuk bekerja dengan perangkat, IOKit menyediakan metode penumpukan driver menggunakan instance dari kelas C ++. Aplikasi di userspace akan dapat "menemukan" instance kelas terdaftar untuk membangun koneksi kernel-userspace.

Untuk mendeteksi jumlah instance kelas dalam sistem, utilitas ioclasscount ada.

my_kext_ioservice = 1
my_kext_iouserclient = 1

Setiap ekstensi kernel yang ingin didaftarkan pada tumpukan driver harus mendeklarasikan kelas yang diwarisi dari IOService, misalnya, my_kext_ioservice dalam kasus ini. Menghubungkan aplikasi pengguna akan membuat instance baru dari kelas yang diwarisi dari IOUserClient, dalam contoh my_kext_iouserclient.

Ketika mencoba untuk membongkar driver dari sistem (perintah kextunload), fungsi virtual "bool terminate (opsi IOOptionBits)" disebut. Sudah cukup untuk mengembalikan false pada panggilan ke fungsi terminate ketika mencoba membongkar untuk menonaktifkan kextunload.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Unload is not allowed, returning false
    return false;
  }

  return super::terminate(options);
}


Bendera IsUnloadAllowed dapat diatur oleh IOUserClient saat boot. Ketika memuat terbatas, perintah kextunload akan mengembalikan output berikut:

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Password:
(kernel) Can't remove kext my.kext.test; services failed to terminate - 0xe00002c7.
Failed to unload my.kext.test - (iokit/common) unsupported function.

Perlindungan serupa harus dilakukan untuk IOUserClient. Instance kelas dapat dibongkar menggunakan fungsi userspace IOKitLib “IOCatalogueTerminate (mach_port_t, flag uint32_t, io_name_t description);”. Anda dapat mengembalikan false pada panggilan ke perintah "terminasi" sampai userspace aplikasi mati, yaitu, tidak ada panggilan ke fungsi clientDied.

Perlindungan file


Untuk melindungi file, cukup menggunakan API Kauth, yang memungkinkan Anda membatasi akses ke file. Apple memberikan pemberitahuan kepada pengembang tentang berbagai peristiwa dalam ruang lingkup, operasi KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA dan KAUTH_VNODE_DELETE_CHILD penting bagi kami. Membatasi akses ke file paling mudah di sepanjang jalan - kami menggunakan API "vn_getpath" untuk mendapatkan path ke file dan membandingkan awalan path. Perhatikan bahwa untuk mengoptimalkan penggantian nama jalur folder dengan file, sistem tidak mengotorisasi akses ke setiap file, tetapi hanya ke folder itu sendiri, yang diganti namanya. Kita perlu membandingkan jalur induk dan membatasi KAUTH_VNODE_DELETE untuk itu.

gambar

Kerugian dari pendekatan ini mungkin kinerja rendah dengan meningkatnya jumlah awalan. Agar perbandingannya tidak sama dengan O (awalan * panjang), di mana awalan adalah jumlah awalan, panjang adalah panjang string, Anda dapat menggunakan mesin keadaan terbatas deterministik (DFA) yang ditentukan oleh awalan.

Pertimbangkan cara untuk membangun DFA untuk serangkaian awalan yang diberikan. Kami menginisialisasi kursor di awal setiap awalan. Jika semua kursor menunjuk ke karakter yang sama, maka kami menambah setiap kursor dengan satu karakter dan ingat bahwa panjang garis yang sama lebih dari satu. Jika ada dua kursor dengan simbol yang berbeda di bawahnya, kami membagi kursor ke dalam kelompok dengan simbol yang mereka arahkan dan ulangi algoritma untuk setiap grup.

Dalam kasus pertama (semua karakter di bawah kursor adalah sama), kita mendapatkan status DFA, yang hanya memiliki satu transisi pada baris yang sama. Dalam kasus kedua, kita mendapatkan tabel transisi ukuran 256 (jumlah karakter dan jumlah grup maksimum) di negara-negara berikutnya yang diperoleh dengan memanggil fungsi secara rekursif.

Pertimbangkan sebuah contoh. Untuk satu set awalan ("/ foo / bar / tmp /", "/ var / db / foo /", "/ foo / bar / aba /", "foo / bar / aac /") Anda bisa mendapatkan DFA berikut. Angka tersebut hanya menunjukkan transisi yang mengarah ke negara lain, transisi lain tidak akan bersifat final.

gambar

Saat melewati negara bagian DKA, mungkin ada 3 kasus.

  1. Status akhir tercapai - jalur dilindungi, kami membatasi operasi KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA dan KAUTH_VNODE_DELETE_CHILD
  2. , “” ( -) — , KAUTH_VNODE_DELETE. , vnode , ‘/’, “/foor/bar/t”, .
  3. , . , .


Tujuan dari solusi keamanan yang dikembangkan adalah untuk meningkatkan tingkat keamanan pengguna dan datanya. Di satu sisi, tujuan ini dipastikan oleh pengembangan produk perangkat lunak Acronis yang mencakup kerentanan di mana sistem operasi itu sendiri “lemah”. Di sisi lain, kita tidak boleh mengabaikan peningkatan aspek-aspek keamanan yang dapat ditingkatkan di sisi OS, terutama karena penutupan kerentanan tersebut meningkatkan stabilitas kita sendiri sebagai sebuah produk. Kerentanan ini dilaporkan oleh Tim Keamanan Produk Apple dan diperbaiki di macOS 10.14.5 (https://support.apple.com/en-gb/HT210119).

gambar

Semua ini dapat dilakukan hanya jika utilitas Anda telah diinstal secara resmi di kernel. Artinya, tidak ada celah seperti itu untuk perangkat lunak eksternal dan yang tidak diinginkan. Namun, seperti yang Anda lihat, bahkan untuk melindungi program yang sah seperti antivirus dan sistem cadangan, Anda harus bekerja keras. Tetapi sekarang, produk Acronis baru untuk macOS akan memiliki perlindungan tambahan terhadap pembongkaran dari sistem.

All Articles