Sudah lama saya tidak membahas mengenai security dan reverse engineering yang sifatnya tingkat lanjut, kali ini saya ingin membahas fitur seccomp (secure computing) di kernel Linux dan kegunaannya/penyalahgunaannya untuk mengintersepsi syscall. Karena Android memakai kernel Linux, ini juga berlaku untuk Android.

Tulisan ini saya buat karena berhubungan dengan sebuah malware yang saya analisis. Meski tidak bisa bercerita detail karena NDA, malwarenya memakai teknik serupa dengan yang dipakai SnowBlind (https://promon.io/app-threat-reports/snowblind), yaitu membypass RASP (Runtime Application Self-Protection) menggunakan fitur seccomp di Linux/Android.
Contents
seccomp
Seccomp berguna untuk membatasi syscall yang bisa dilakukan oleh sebuah proses. Syscall dipakai oleh sebuah proses untuk memanggil fungsi kernel, misalnya membuka file, membaca file, menulis file, menutup file, mengeksekusi program baru, dsb. Dengan membatasi syscall, kita bisa mengamankan program kita.
Ada banyak artikel yang membahas penggunaan seccomp secara high level. Di tulisan ini saya hanya membahas low level, jika ingin tahu tentang high level, silakan baca artikel di sumber lain misalnya Improving Linux container security with seccomp.
Sebagai catatan: di Linux ada banyak fitur security lain, mislanya SELinux, Capabilities, dsb. Fokus seccomp adalah pada syscall filtering.
Strict Mode
Penggunaan seccomp paling sederhana adalah mode strict: membatasi proses agar hanya bisa melakukan read dan write pada file descriptor yang terbuka sebelum pemanggilan seccomp dan melakukan exit. Contoh kasus pengunaannya adalah untuk memproses data dari user: sub process yang dibatasi ini hanya akan bisa membaca dan menuliskan hasilnya.
Ketika seccomp mode strict aktif, andaikan ada bug, misalnya terjadi buffer overflow, dan attacker berhasil menginjeksi shellcode, maka shellcodenya amat sangat terbatas, tidak bisa mengeksekusi shell, tidak bisa menghapus/rename file atau membuat file baru, dsb. Mungkin yang bisa dilakukan shellcode hanya sekedar menulis data sangat banyak (memenuhi disk) menggunakan loop, tapi ini pun bisa dibatasi dengan setrlimit(RLIMIT_FSIZE, &file_size).
int fd = open("input.txt", O_RDONLY);
int fdout = open("output.txt", O_WRONLY|O_CREAT);
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
//di sini file input bisa dibaca, diproses dan ditulis ke output
int fdx = open("test", O_RDONLY); // ini akan crash
Beberapa aplikasi kompleks butuh lebih dari sekedar read dan write, dann mode strict kurang cocok. Dalam kasus ini kita bisa mengeset filter dalam bentuk bytecode BPF. Setelah filter diset, tidak bisa diubah lagi.
BPF (Berkeley Packet Filter)
Berkeley Packet Filter (BPF) adalah teknologi filtering network yang menjalankan instruksi (bytecode) dengan virtual machine. Teknologi ini tadinya hanya dipakai di komponen networking Linux untuk filtering paket jaringan, tapi sekarang ini sudah dikembangkan (extended BPF/eBPF) agar bisa dipakai di berbagai komponen Linux lain, termasuk juga untuk seccomp ini.
Ada banyak cara menuliskan kode BPF ini:
- memakai compiler C yang bisa menghasilkan kode BPF
- memakai assembler yang bisa menghasilkan BPF
- memakai library (libseccomp) untuk menghasilkan BPF pada runtime
- memakai makro C untuk filter sederhana
- memakai konstanta numerik langsung
Dalam contoh di sini, saya akan menggunakan pendekatan makro C, karena tidak butuh tool dan library eksternal dan untuk keperluan syscall filtering biasanya ini sudah cukup. Perhatikan bahwa seccomp (saat ini) hanya mendukung BPF classic (bukan eBPF), jadi tool yang menghasilkan instruksi eBPF tidak bisa dipakai)
syscall
Saya pernah menulis mengenai pemanggilan syscall di blog Cinta Programming, di situ saya sebutkan bahwa nomor syscall berbeda untuk tiap arsitektur (misalnya ARM vs X86). Saya ingin mencontohkan kode BPF yang akan mengecek arsitektur saat ini, untuk memastikan bahwa kita meload kode BPF yang benar untuk tiap arsitektur (misalnya supaya nomor syscallnya benar).
Berikut ini contoh pemakaian macro C BPF untuk mengkonstruksi bytecode:
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, arch))),
// Check architecture (x86_64)
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
Semua BPF_XXX adalah makro, penjelasan singkatnya:
- STMT: statement, LD (load) W (word) ABS (absolute), lalu diikuti offset variable “arch (arsitektur) di data seccomp
- JUMP: untuk jumping, JMP (jump), JEQ (Jump if Equal), K (konstanta), AUDIT_ARCH_X86_64 (intel 64 bit). Jika equal, maka skip 1 instruksi ke depan (lompati instruksi berikut), jika tidak equal maka skipnya adalah 0, alias pergi ke instruks berikut
- RET (return), RET_KILL (kill): langsung kill program
Jadi intinya adalah: jika arsitekturnya bukan intel 64 bit, maka exit, selain itu, lanjut ke kode berikutnya.
RASP dan syscall
Banyak program sekarang memiliki proteksi runtime, istilahnya RASP (Runtime Application Self-Protection). Komponen RASP akan mengecek validitas program ketika program dijalankan agar tidak mudah dicrack/dimodifikasi. Salah satu pengecekan yang umum dilakukan adalah membaca isi program itu sendiri, dan membandingkan nilainya dengan pattern, signature, atau dengan database online untuk memastikan program tersebut masih valid.
Contoh yang sering dilihat adalah pada APK yang diproteksi dengan RASP. Biasanya RASP akan membaca APK dan langsung membaca signature (file .RSA) di base.apk, dan mengecek apakah signaturenya sesuai yang diharapkan.
Untuk membaca isi file, program perlu membuka file dengan fungsi library C (open atau fopen) atau dengan syscall. Fungsi di library C mudah dihook, jadi biasanya komponen RASP akan menggunakan syscall langsung.
Kenapa fungsi library lebih gampang dihook? secara sederhana: ketika program dicompile dinamik menggunakan library tertentu, maka akan ada call ke sebuah pointer. Di format ELF pointer ini memakai yang namanya PLT (procedure linkage table). Ketika program diload, pointer itu akan diisi dengan alamat fungsi di library. Untuk melakukan hooking, maka kita cukup mengoverwrite pointernya saja. Pointer ini ada di data section dan bisa ditulisi dengan mudah.
Patching syscall
Untuk melakukan intercept syscall secara generik (tidak tergantung sistem operasi apapun), kita bisa memakai teknik patching binary code. Intinya: kita timpa instruksi syscall dengan jump ke lokasi lain.
Masalah dengan cara ini adalah: tidak selalu bisa digunakan karena instruksi syscall biasanya sangat singkat, sedangkan jump ke suatu lokasi memori butuh byte opcode yang lebih banyak. Di AMD64/Intel 64 bit, SYSCALL hanya memakan 2 byte. Di ARM64, syscall (seperti instruksi lainnya) memakan 4 byte. Untuk melakukan JUMP ke area memori yang jauh dibutuhkan lebih dari 2 byte di Intel, dan lebih dari 4 byte di ARM64.
Ada beberapa cara program bisa mendeteksi jika syscall dipatch, misalnya dengan membaca memori dan melihat apakah pattern syscall masih ada, atau dengan instruksi assembly yang melakukan jump ke instruksi setelah syscall.
Dalam kasus ini: jika syscall dipatch (dan patchnya menimpa instruksi berikutnya), maka jump ke instruksi setelah syscall akan menjadi instruksi tidak valid.
mov rbx, offset next
jmp after_syscall
next:
mov rbx, offset normal
;; isi parameter syscall di sini
syscall
after_syscall:
jmp rbx
normal:
Menggunakan syscall seccomp untuk intersepsi syscall
Seperti telah disebutkan sebelumnya, seccomp bisa digunakan untuk filtering syscall dengan BPF. Kita bisa melakukan operasi komparasi pada syscal dan parameternya, dan aksi yang bisa dilakukan adalah:
- Allow (ijinkan syscall)
- Kill Process/Thread jika syscall dilakukan
- Kembalikan nilai error, dan jangan jalankan syscall
- Hasilkan signal SIGSYS di proses saat ini
- Notifikasi ptrace (untuk debugger)
Ketika kita ingin mengakali RASP, yang bisa kita lakukan adalah:
- Menginstall filter seccomp
- Menginstall handler untuk signal SIGSYS dengan sigaction
- Pada handler: jika berusaha membuka file yang sudah dimodifikasi, maka redirect ke file asli
Berikut ini contoh kode yang akan mengintercept syscall openat di ARM64, jika file “a.txt” dibuka, maka diakan di-intercept dan diganti dengan “b.txt”.
Jika hook tidak diaktifkan, maka program akan membuka file file “a.txt”, membuka isinya, lalu print ke layar.
Namun jika hook diaktifkan, flownya seperti ini
- filter seccomp dipasang dengan prctl dan handler SIGSYS dipasang dengan sigaction
- Fungsi libc open pada akhirnya akan memanggil syscall “openat” (di ARM64, di AMD64 akan memanggil “open”)
- seccomp filter (berupa bytecode BPF) akan dijalankan, isinya adalah jika syscall openat dipanggil, maka trigger SIGSYS
- handler SIGSYS akan dipanggil, dan di sini dibandingkan apakah file a.txt dibuka, jika iya, ganti dengan b.txt
- Masalahnya: jika kita panggil syscall open lagi, maka akan rekursif memanggil handler lagi, jadi kali ini kita tambahkan parameter SECMAGIC. Perhatikan bahwa di BPF ada pengecekan: jika
openat
dipanggil, tapi parameter kelima adalah SECMAGIC , maka jangan memberikan signal SIGSYS, teruskan langsung ke kernel (parameter kelima ini aslinya tidak dipakai, hanya ada 4 parameteropenat
). - Cara lain tanpa memakai magic adalah dengan menggunakan field instruction pointer pada seccomp data. Jika syscall dipanggil dari alamat memori tertentu, maka ijinkan ini (jangan hasilkan TRAP).
Demonya bisa dilihat di: https://asciinema.org/a/DbjDWQ2Var5aNVZlBIJF89Q0L
Deteksi dan mitigasi malware dengan seccomp
Jika kita ingin membuat RASP sendiri, ada beberapa cara untuk mendeteksi seccomp. Namun perlu diingat attacker juga bisa cerdas menyembunyikan penggunaan seccomp ini, jadi teknik-teknik pengecekan lain tetap dibutuhkan.
- Coba timpa signal handler untuk SIGSYS. Attacker mungkin mengintercept sigaction, dan mengabaikan signal handler yang kita pasang. Kita bisa memaksa signal handler kita dipanggil dengan kill(getpid(), SIGSYS), dan mengecek apakah beneran signal handler kita terpasang.
- Coba timpa seccomp dengan filter kita sendiri, ini akan gagal karena seccomp sudah terpasang. Tentunya ini bisa dibuat seolah-olah pemanggilan sukses. Kita bisa menginstall handler syscall tertentu (misalnya sethostname, diset selalu mengembalikan errno tertentu), lalu mencoba memanggil sethostname, error codenya seharusnya seperti yang kita set.
Penutup
Demikian pembahasan singkat mengenai seccomp ini. Produk RASP sudah ada belasan tahun, namun selalu ada teknik baru dari attacker yang bisa membypass dengan teknik terbaru. Sebagai researcher, kita harus selalu berusaha mengikuti perkembangan yang ada.