Jangan kabur, teruskan proses: Menghindari eksekusi .NET Fork&Run dengan InlineExecute-Assembly

Seorang pria melihat layar komputer saat bekerja larut malam menulis kode

Beberapa dari Anda menyukainya dan beberapa dari Anda membencinya, tetapi pada titik ini seharusnya tidak mengherankan bahwa teknik .NET akan tetap digunakan, sedikit lebih lama dari yang diperkirakan. Kerangka kerja .NET adalah bagian integral dari sistem operasi Microsoft dengan rilis terbaru .NET menjadi .NET core. Core adalah penerus lintas platform untuk kerangka kerja yang membawa .NET ke Linux dan juga macOS. Ini menjadikan .NET lebih populer dari sebelumnya untuk teknik pasca eksploitasi di antara penyerang dan tim keamanan. Blog ini akan menyelami Beacon Object File (BOF) baru yang memungkinkan operator untuk mengeksekusi rakitan .NET dalam proses melalui Cobalt Strike versus modul execute-assembly built-in tradisional, yang menggunakan teknik fork and run.

Latar Belakang

Cobalt Strike, perangkat lunak simulasi penyerang yang populer, mengenali tren tim keamanan menjauh dari tooling PowerShell dan lebih bercondong ke C# karena peningkatan kemampuan deteksi untuk PowerShell, dan pada tahun 2018 dengan Cobalt Strike versi 3.11 memperkenalkan modul execute-assembly. Ini memungkinkan operator untuk memanfaatkan kekuatan rakitan .NET pasca-eksploitasi dengan mengeksekusinya dalam memori tanpa memiliki risiko tambahan menjatuhkan alat-alat tersebut ke disk. Sementara kemampuan memuat rakitan .NET dalam memori melalui kode yang tidak dikelola bukanlah hal baru atau tidak diketahui pada saat rilis, saya akan mengatakan Cobalt Strike membawa kemampuan ke arus utama dan membantu terus mendorong popularitas .NET untuk teknik pasca-eksploitasi.

Modul execute-assembly Cobalt Strike menggunakan teknik fork and run, yaitu menjalankan proses pengorbanan baru, menyuntikkan kode berbahaya pasca-eksploitasi Anda ke dalam proses baru itu, menjalankan kode berbahaya Anda dan setelah selesai, mematikan proses baru. Ini memiliki manfaat maupun kekurangan. Manfaat metode fork&run adalah eksekusi terjadi di luar proses implan Beacon. Ini berarti bahwa jika sesuatu dalam tindakan pasca-eksploitasi kita salah atau tertangkap, ada peluang yang jauh lebih besar untuk implan kita tetap bertahan. Singkatnya, ini sangat membantu stabilitas implan secara keseluruhan. Namun, karena vendor keamanan menangkap perilaku fork and run ini, Cobalt Strike mengakui bahwa pola tersebut menambah biaya OPSEC.

Pada versi 4.1 yang dirilis pada Juni 2020, Cobalt Strike memperkenalkan fitur baru untuk mencoba dan membantu alamat masalah ini dengan pengenalan Beacon Object Files (BOF). BOF memungkinkan operator untuk menghindari pola eksekusi yang terkenal seperti dijelaskan di atas atau kegagalan OPSEC lainnya seperti menggunakan cmd.exe/powershell.exe dengan mengeksekusi file objek dalam memori dalam proses yang sama dengan implan beacon kami. Meskipun saya tidak akan membahas cara kerja BoF, berikut adalah beberapa postingan blog yang menurut saya berwawasan luas:

Jika Anda membaca blog di atas, sekarang kita harus melihat bahwa BOF bukanlah anugerah penyelamatan yang kami harapkan dan jika Anda bermimpi menulis ulang semua alat-alat .NET yang luar biasa itu dan mengubahnya menjadi BOF, impian itu sekarang telah hancur. Maaf. Namun harapan tidak hilang karena, menurut saya, ada beberapa hal hebat yang dapat ditawarkan oleh BoF, dan baru-baru ini saya menikmati proses (dan beberapa frustrasi juga) dalam mendorong batas-batas apa yang dapat dilakukan menggunakannya. Pertama, adalah dengan membuat CredBandit yang melakukan dump memori lengkap dari proses seperti LSASS dan mengirimkannya kembali melalui saluran komunikasi Beacon Anda yang ada. Hari ini saya merilis InlineExecute-Assembly, yang dapat digunakan untuk mengeksekusi rakitan .NET di dalam proses beacon Anda tanpa modifikasi pada tooling .NET favorit Anda. Mari selami mengapa saya menulis BOF, beberapa fitur utamanya, batasan, dan bagaimana hal itu bisa berguna saat melakukan simulasi serangan/tim keamanan.

Berita teknologi terbaru, didukung oleh insight dari pakar

Tetap terinformasi tentang tren industri yang paling penting—dan menarik—tentang AI, otomatisasi, data, dan di luarnya dengan buletin Think. Lihat Pernyataan Privasi IBM®.

Terima kasih! Anda telah berlangganan.

Langganan Anda akan disediakan dalam bahasa Inggris. Anda akan menemukan tautan berhenti berlangganan di setiap buletin. Anda dapat mengelola langganan atau berhenti berlangganan di sini. Lihat Pernyataan Privasi IBM® kami untuk informasi lebih lanjut.

Mengapa menggunakan InlineExecute-Assembly?

Alasan di balik perlunya InlineExecute-Assembly cukup sederhana. Saya menginginkan cara bagi tim simulasi serangan kami untuk mengeksekusi rakitan .NET dalam proses untuk menghindari beberapa jebakan OPSEC yang dibicarakan di atas ketika menggunakan Cobalt Strike untuk beroperasi di lingkungan yang matang. Saya juga membutuhkan alat ini untuk tidak membebani tim kami dengan waktu pengembangan ekstra dengan harus membuat modifikasi pada sebagian besar alat .NET kami saat ini. Itu juga harus stabil. Sestabil mungkin yang bisa dicapai BOF yang kompleks. Karena hal terakhir yang kita inginkan adalah hilangnya salah satu dari beberapa Beacon kita ke lingkungan. Pada dasarnya, ini harus berjalan selancar mungkin bagi operator, setara seperti modul execute-assembly Cobalt Strike.

Mixture of Experts | 12 Desember, episode 85

Decoding AI: Rangkuman Berita Mingguan

Bergabunglah dengan panel insinyur, peneliti, pemimpin produk, dan sosok kelas dunia lainnya selagi mereka mengupas tuntas tentang AI untuk menghadirkan berita dan insight terbaru seputar AI.

Fitur Utama

Memuat Common Language Runtime(CLR)

Saya tahu, itu terlihat jelas. Kita tidak akan bisa melangkah terlalu jauh tanpanya. Seluk-beluk cara kerja CLR dan apa yang terjadi secara mendalam bisa menjadi postingan blog itu sendiri, jadi kami akan meninjau apa yang digunakan BOF pada tingkat yang sangat tinggi saat memuat CLR melalui kode yang tidak dikelola.

Tangkapan layar Memuat CLR

Memuat CLR

Seperti yang ditunjukkan pada tangkapan layar yang disederhanakan di atas, langkah-langkah utama yang akan diambil BOF untuk memuat CLR adalah sebagai berikut:

  1. Membuat panggilan ke CLRCreateInstance yang akan digunakan untuk mengambil antarmuka ICLRMetaHost kami.
  2. ICLRMetaHost ->GetRuntime kemudian digunakan untuk mendapatkan informasi waktu proses untuk versi .NET yang kita minta. Jika rakitan Anda dibangun dengan.NET versi 3.5 atau di bawahnya, kami akan ingin meminta v2.0.50727, dan jika rakitan Anda dibangun dengan .NET 4.0 dan di atasnya, kami akan ingin meminta v4.0.30319. Sebenarnya ada fungsi di BOF yang akan membantu kita mengetahui versi apa yang digunakan rakitan .NET kita secara otomatis tetapi kita akan membicarakannya nanti.
  3. Setelah kita memiliki info waktu proses, kita menggunakan ICLRRuntimeInfo->IsLoadable untuk memeriksa apakah waktu proses kami dapat dimuat ke dalam proses. Ini juga akan memperhitungkan jika waktu proses lain mungkin sudah dimuat dan akan menetapkan nilai BOOL fLoadable kami ke 1 (true) jika waktu proses kami dapat dimuat dalam proses.
  4. Jika semua itu berhasil, kita kemudian akan menjalankan ICLRRuntimeInfo->GetInterface untuk memuat CLR ke dalam proses kita dan mengambil antarmuka ke iCorRuntimeHost.
  5. Terakhir, kita akan memanggil ICorRuntimeHost->Start, yang akan memulai CLR.

Jadi sekarang CLR diinisialisasi tetapi masih ada sedikit lagi yang perlu terjadi sebelum kita benar-benar menjalankan rakitan .NET favorit kita. Kita perlu membuat instance AppDomain kita yang dijelaskan oleh Microsoft sebagai “lingkungan terisolasi tempat aplikasi dijalankan”. Dengan kata lain, ini akan digunakan untuk memuat dan menjalankan rakitan .NET pasca eksploitasi kita.

Tangkapan layar: AppDomain sedang dibuat dan perakitan sedang dimuat/dieksekusi

AppDomain sedang dibuat dan perakitan sedang dimuat/dieksekusi

Seperti yang ditunjukkan pada tangkapan layar yang disederhanakan di atas, langkah-langkah utama yang akan diambil BOF untuk memuat dan memanggil perakitan.NET kami adalah sebagai berikut:

  1. Gunakan ICorRuntimeHost->CreateDomain untuk membuat AppDomain unik kita
  2. Gunakan IUnknown->QueryInterface (pAppDomainThunk) untuk mendapatkan pointer ke antarmuka AppDomain
  3. Buat SafeArray kita dan salin byte perakitan .NET ke sana
  4. Muat rakitan kita lewat AppDomain->Load_3
  5. Dapatkan titik masuk di rakitan kita melalui Assembly->EntryPoint
  6. Terakhir, panggil perakitan kami melalui MethodInfo->Invoke_3

Semoga Anda sekarang memiliki pemahaman tingkat tinggi tentang eksekusi .NET melalui kode yang tidak dikelola, tetapi ini masih belum dekat untuk memiliki alat yang sehat secara operasional, jadi kami akan melihat beberapa fitur yang diterapkan di BOF untuk membuatnya benar-benar layak digunakan.

Mengalihkan Konsol STDOUT ke Pipa Bernama atau Mail Slot: Menghindari Modifikasi Alat

Anda mungkin bertanya-tanya mengapa ini penting. Nah, jika Anda seperti saya dan menghargai waktu, Anda tidak ingin menghabiskannya untuk memodifikasi hampir setiap rakitan .NET di luar sana sehingga titik masuknya mengembalikan string kembali dengan semua data Anda yang biasanya hanya disalurkan ke output standar konsol, bukan? Sudah kuduga. Untuk menghindarinya, yang perlu kita lakukan adalah mengarahkan output standar kita ke pipa bernama atau mail slot, membaca output setelah ditulis, dan kemudian mengembalikannya kembali ke keadaan semula. Dengan cara ini kita dapat menjalankan rakitan yang tidak dimodifikasi seperti yang kita lakukan dari cmd.exe atau powershell.exe. Nah, sebelum kita membahas kode apa pun, saya perlu mengucapkan terima kasih kepada @N4k3dTurtl3 dan postinganblog mereka tentang rakitan eksekusi dalam proses dan mail slot. Ini awalnya yang membuat saya menerapkan teknik ini ke implan C pribadi saya sendiri ketika pertama kali keluar dan berbulan-bulan kemudian saya mem-porting fungsi yang sama ke BOF. Oke, ucapan terima kasih telah diberikan, sekarang mari kita lihat contoh yang disederhanakan tentang bagaimana ini akan dicapai dengan mengarahkan stdout ke pipa bernama di bawah ini:

Tangkapan layar: Mengalihkan output standar konsol ke pipa bernama dan mengembalikannya

Mengalihkan output standar konsol ke pipa bernama dan mengembalikan

Tentukan versi .NET dari rakitan tersebut.

Ingat ketika memuat CLR melalui ICLRMetaHost->GetRuntime kita harus menentukan versi kerangka kerja apa yang kita butuhkan? Ingat bagaimana itu tergantung pada versi apa rakitan .NET kita dikompilasi? Tidak akan menyenangkan untuk secara manual menentukan versi mana yang dibutuhkan setiap kali, bukan? Untungnya, @b4rtik telah mengimplementasikan fungsi keren untuk menangani hal ini dalam modul execute-assembly mereka untuk frameworkMetasploit yang dapat dengan mudah diimplementasikan ke dalam alat kita sendiri seperti yang ditunjukkan di bawah ini:

Tangkapan layar: Fungsi yang membaca rakitan .NET kami dan membantu menentukan versi .NET apa yang kami butuhkan saat memuat CLR

Fungsi yang membaca rakitan .NET kami dan membantu menentukan versi .NET apa yang kami butuhkan saat memuat CLR

Pada dasarnya apa yang dilakukan fungsi ini adalah ketika melewati byte perakitan kami, itu akan membaca byte tersebut dan mencari nilai hex 76 34 2E 30 2E 33 33 31 39, yang ketika dikonversi ke ASCII adalah v4.0.30319. Mudah-mudahan itu terlihat familiar. Jika nilai itu ditemukan saat membaca rakitan, fungsi mengembalikan 1 atau true, dan jika tidak ditemukan mengembalikan 0 atau salah. Kita dapat menggunakan ini untuk dengan mudah menentukan versi apa yang akan dimuat dengan apakah 1/true atau 0/false kembali seperti yang ditunjukkan pada contoh kode di bawah ini:

Tangkapan layar: pernyataan if/else untuk mengatur variabel versi .NET

Pernyataan if/else untuk mengatur variabel versi .NET

Patching Antarmuka Pemindaian Antimalware (AMSI)

Kita tentu saja tidak dapat berbicara tentang teknik ofensif .NET dan tidak membicarakan AMSI. Meskipun kami tidak akan membahas secara mendalam tentang apa itu AMSI dan semua cara untuk melewatinya karena ini telah dibahas berkali-kali, kami akan berbicara sedikit tentang mengapa melakukan patch pada AMSI mungkin diperlukan tergantung pada apa yang Anda putuskan untuk dieksekusi melalui BOF. Sebagai contoh, jika Anda memutuskan untuk menjalankan Seatbelt tanpa ada gangguan, Anda akan segera menyadari bahwa tidak ada output apa pun dan beacon Anda mati. Ya, benar-benar mati. Ini karena AMSI menangkap rakitan Anda, memutuskan itu berbahaya, dan menghentikannya, seperti pesta rumah yang membuat terlalu banyak kebisingan. Tidak ideal, kan? Sekarang kita memiliki dua opsi bagus di sini terkait AMSI, kita dapat mengaburkan alat .NET kita melalui sesuatu seperti ConfuSerX atau Invisibility Cloak atau kita dapat menonaktifkan AMSI menggunakan berbagai teknik. Dalam kasus kami, kami akan menggunakan teknik dari RastaMouse, yaitu untuk melakukan patch pada amsi.dll dalam memori sehingga mengembalikan E_INVALIDARG dan membuat hasil pemindaian 0. Yang seperti yang ditunjukkan dalam postingan blog mereka, biasanya ditafsirkan sebagai AMSI_RESULT_CLEAN. Mari kita lihat versi kode yang disederhanakan untuk proses x64 di bawah ini:

Tangkapan layar: Patching dalam memori AmsiScanBuffer

Penambalan memori pada AmsiScanBuffer

Seperti yang Anda lihat di tangkapan layar di atas, kami cukup melakukan hal berikut:

  1. Muat amsi.dll dan dapatkan pointer ke AmsisCanBuffer
  2. Mengubah perlindungan memori
  3. Patch ke byte amsiPatch[] kita
  4. Kembalikan perlindungan memori ke keadaan semula

Dengan menerapkan ini ke dalam alat kita, sekarang kita harus dapat menjalankan versi default Seatbelt.exe menggunakan flag -amsi untuk melewati deteksi AMSI seperti yang ditunjukkan di bawah ini:

Tangkapan layar: Contoh melewati InlineExecute-Assemby AMSI

Contoh bypass InlineExecute-Assemby AMSI

Patching Pelacakan Peristiwa untuk Windows (ETW)

Untungnya bagi para pihak pertahanan, ada lebih dari sekadar AMSI untuk membantu dalam menangkap teknik .NET berbahaya dengan menggunakan ETW. Sayangnya, seperti AMSI, ini juga cukup mudah untuk dilewati oleh musuh dan @xpn melakukan  riset mendalam tentang bagaimana hal ini bisa dilakukan. Mari kita lihat contoh sederhana tentang bagaimana Anda dapat melakukan patch pada ETW untuk menonaktifkannya sepenuhnya di bawah ini:

Tangkapan layar: Patching dalam memori EtwEventWrite

Patching dalam memori EtwEventWrite

Seperti yang Anda lihat dari tangkapan layar di atas, langkah-langkahnya cukup identik dengan cara kami menambal AMSI, jadi saya tidak akan membahas langkah-langkah untuk yang satu ini. Anda dapat melihat tangkapan layar sebelum dan sesudah menjalankan flag –etw di bawah ini:

Tangkapan layar: Menggunakan Process Hacker untuk melihat properti PowerShell.exe sebelum menjalankan inlineExecute-Assembly dengan flag –etw

Menggunakan Process Hacker untuk melihat properti PowerShell.exe sebelum menjalankan inlineExecute-assembly dengan flag –etw

Tangkapan layar: Menjalankan inlineExecute-Assembly menggunakan flag –etw

Menjalankan inline-Execute-Assembly menggunakan flag –etw

Tangkapan layar: Menggunakan Process Hacker untuk melihat properti PowerShell.exe yang sama setelah menjalankan inlineExecute-Assembly

Menggunakan Process Hacker untuk melihat properti PowerShell.exe yang sama setelah menjalankan inlineExecute-Assembly

AppDomain Unik, Pipa Bernama, Mail Slot

Secara default, AppDomain, Named Pipe atau Slot Mail yang dibuat menggunakan nilai default “totesLegit”. Nilai-nilai ini dapat diubah agar lebih berbaur dalam lingkungan yang Anda uji dengan mengubahnya dalam skrip agresor yang disediakan atau melalui flag baris perintah dengan cepat. Contoh mengubahnya melalui baris perintah dapat ditunjukkan di bawah ini:

Tangkapan layar terminal menunjukkan eksekusi perintah inlineExecute-Assembly --dotnetassembly /root/Desktop/MessageBoxCS.exe di Beacon. Output termasuk pesan status: menjalankan inlineExecute-Assembly, host bernama home (dikirim 16319 byte), menerima output 'Halo From .NET! ' , dan pesan penyelesaian 'inlineExecute-Assembly Finished'.

Contoh InlineExecute-Assembly menggunakan AppDomain Name yang unik dan nama pipa bernama unik

Tangkapan layar: Contoh nama AppDomain unik ChangedMe

Contoh nama AppDomain unik ChangedMe

Tangkapan layar: Contoh pipa bernama unik LookatMe

Contoh named pipe unik LookAtMe

Tangkapan layar: Contoh AppDomain dihapus setelah eksekusi berhasil selesai

Contoh AppDomain dihapus setelah eksekusi berhasil selesai

Tangkapan layar: Contoh pipa bernama dihapus setelah eksekusi berhasil selesai

Contoh pipa bernama dihapus setelah eksekusi berhasil selesai

Batasan

Bagian ini akan menjadi pengulangan dari apa yang saya sebutkan di repositori GitHub, tetapi saya merasa penting untuk mengulangi beberapa hal yang perlu Anda ingat saat menggunakan alat ini:

  1. Meskipun saya telah mencoba membuatnya sestabil mungkin, tidak ada jaminan bahwa semuanya tidak akan pernah mengalami kerusakan dan beacon tidak akan mati. Kami tidak memiliki kemewahan tambahan seperti fork and run di mana jika terjadi kesalahan, beacon kami akan tetap hidup. Ini adalah pertukarannya dengan BoF. Dengan itu, saya tidak bisa menekankan betapa pentingnya Anda menguji rakitan Anda sebelumnya untuk memastikannya akan berfungsi dengan baik.
  2. Karena BOF dieksekusi dalam proses dan mengambil alih beacon Anda saat berjalan, ini harus diperhitungkan sebelum digunakan untuk rakitan yang berjalan lama. Jika Anda memilih untuk menjalankan sesuatu yang akan memakan waktu lama untuk mendapatkan hasil, beacon Anda tidak akan aktif untuk menjalankan perintah lain hingga hasil kembali dan perakitan Anda selesai berjalan. Ini juga tidak sesuai dengan set tidur. Misalnya, jika waktu tidur Anda diatur pada 10 menit dan Anda menjalankan BOF, Anda akan mendapatkan hasil kembali segera setelah BOF selesai dieksekusi.
  3. Kecuali jika dilakukan modifikasi pada alat yang memuat PE ke dalam memori (misalnya, SafetyKatz), hal ini kemungkinan besar akan mematikan beacon Anda. Banyak dari alat bantu ini bekerja dengan baik dengan execute assembly karena itu dapat mengirimkan output konsol dari proses pengorbanan sebelum keluar. Ketika keluar melalui BOF dalam proses kami, itu akan membunuh proses kami, yang membunuh beacon kami. Ini dapat dimodifikasi agar berfungsi, tetapi saya akan menyarankan untuk menjalankan jenis rakitan ini melalui execute assembly karena hal-hal lain yang tidak ramah OPSEC dapat dimuat ke dalam proses Anda yang tidak dihapus.
  4. Jika perakitan Anda menggunakan Environment.Exit ini perlu dihapus karena akan mematikan proses dan beacon.
  5. Pipa bernama dan mail slot harus unik. Jika Anda tidak menerima data kembali dan beacon Anda masih hidup, masalahnya adalah kemungkinan besar Anda perlu memilih nama pipa atau nama mail slot yang berbeda.

Pertimbangan Defensif

Di bawah ini adalah beberapa pertimbangan defensif:

  1. Ini menggunakan PAGE_EXECUTE_READWRITE saat melakukan patching memori AMSI dan ETW. Ini dilakukan dengan sengaja dan harus menjadi tanda peringatan karena sangat sedikit program yang memiliki rentang memori dengan perlindungan memori PAGE_EXECUTE_READWRITE.
  2. Nama default pipa bernama yang dibuat adalah “totesLegit”. Ini dilakukan dengan sengaja dan deteksi tanda tangan dapat digunakan untuk menandai ini.
  3. Nama default mail slot yang dibuat adalah “totesLegit”. Ini dilakukan dengan sengaja dan deteksi tanda tangan dapat digunakan untuk menandai ini.
  4. Nama default AppDomain yang dimuat adalah “totesLegit”. Ini dilakukan dengan sengaja dan deteksi tanda tangan dapat digunakan untuk menandai ini.
  5. Kiat bagus untuk mendeteksi penggunaan berbahaya dari.NET (oleh @bohops) di sini, (oleh F-Secure) di sini, dan di sini
  6. Mencari pemuatan .NET CLR ke dalam proses yang mencurigakan, seperti proses yang tidak dikelola yang seharusnya tidak pernah memuat CLR.
  7. Selengkapnya tentang Pelacakan Peristiwa.
  8. Mencari IOC Cobalt Strike Beacon atau IOC egress/komunikasi C2 lainnya yang dikenal.