Membuat file packer ELF x86_64 untuk linux

Pendahuluan


Posting ini akan menjelaskan pembuatan file packer sederhana yang dapat dieksekusi untuk linux x86_64. Diasumsikan bahwa pembaca sudah mengenal bahasa pemrograman C, bahasa assembly untuk arsitektur x86_64, dan file perangkat ELF. Untuk memastikan kejelasan, penanganan kesalahan dihapus dari kode di artikel dan implementasi beberapa fungsi tidak ditampilkan, kode lengkap dapat ditemukan dengan mengklik tautan ke github ( loader , packer ).

Idenya adalah ini: kami mentransfer file ELF ke packer, dan kami mendapatkan yang baru dengan struktur berikut pada output:
Header ELF
Judul program
Segmen KodePengunduh File ELF yang Dikemas
File ELF yang Dikemas
256 byte data acak

Untuk kompresi, diputuskan untuk menggunakan algoritma Huffman, untuk enkripsi - AES-CTR dengan kunci 256-bit, yaitu implementasi dari kokke tiny-AES-c . 256 byte data acak digunakan untuk menginisialisasi kunci AES dan vektor inisialisasi menggunakan generator angka acak semu, seperti yang ditunjukkan di bawah ini:

for(int i = 0; i < 32; i++) { seed = (1103515245*seed + 12345) % 256; key[i] = buf[seed]; } 

Keputusan ini disebabkan oleh keinginan untuk menyulitkan reverse engineering. Sampai saat ini, saya menyadari bahwa komplikasi itu tidak signifikan, tetapi saya tidak mulai menghilangkannya, karena saya tidak ingin menghabiskan waktu dan energi untuk itu.

Bootloader


Pertama, boot loader akan ditinjau. Loader tidak boleh memiliki dependensi, jadi semua fungsi yang diperlukan dari pustaka C standar harus ditulis secara independen (implementasi fungsi-fungsi ini tersedia dengan referensi ). Itu juga harus mandiri secara posisi.

_Mulailah fungsi


Bootloader dimulai dari fungsi _start, yang hanya meneruskan argc dan argv ke main:

 .extern main .globl _start .text _start: movq (%rsp), %rdi movq %rsp, %rsi addq $8, %rsi call main 

Fungsi utama


File main.c dimulai dengan mendefinisikan beberapa variabel eksternal:

 extern void* loader_end; //    , .   //  ELF . extern size_t payload_size; //   ELF  extern size_t key_seed; //     // -   . extern size_t iv_seed; //     // -     

Semua dari mereka dinyatakan sebagai eksternal untuk menemukan posisi karakter yang sesuai dengan variabel (Elf64_Sym) dalam paket dan mengubah nilainya.

Fungsi utamanya sendiri cukup sederhana. Langkah pertama adalah menginisialisasi pointer ke file ELF yang dikemas, buffer 256-byte, dan ke atas tumpukan. Kemudian file ELF didekripsi dan diperluas, kemudian ditempatkan di tempat yang tepat di memori menggunakan fungsi load_elf, dan akhirnya, nilai register rsp kembali ke keadaan semula, dan lompatan ke titik entri program terjadi:

 #define SET_STACK(sp) __asm__ __volatile__ ("movq %0, %%rsp"::"r"(sp)) #define JMP(addr) __asm__ __volatile__ ("jmp *%0"::"r"(addr)) int main(int argc, char **argv) { uint8_t *payload = (uint8_t*)&loader_end; //    // ELF  uint8_t *entropy_buf = payload + payload_size; //   256- //  void *rsp = argv-1; //     struct AES_ctx ctx; AES_init_ctx_iv(&ctx, entropy_buf, key_seed, iv_seed); //  AES AES_CTR_xcrypt_buffer(&ctx, payload, payload_size); //  ELF memset(&ctx, 0, sizeof(ctx)); //   AES size_t decoded_payload_size; //  ELF char *decoded_payload = huffman_decode((char*)payload, payload_size, &decoded_payload_size); //     ELF  , //   ET_EXEC  NULL. void *load_addr = elf_load_addr(rsp, decoded_payload, decoded_payload_size); load_addr = load_elf(load_addr, decoded_payload); //  ELF  , //    //  . memset(decoded_payload, 0, decoded_payload_size); //   ELF munmap(decoded_payload, decoded_payload_size); //   //    //  ELF     AES AES_init_ctx_iv(&ctx, entropy_buf, key_seed, iv_seed); AES_CTR_xcrypt_buffer(&ctx, payload, payload_size); memset(&ctx, 0, sizeof(ctx)); SET_STACK(rsp); //    JMP(load_addr); //       } 

Menyetel ulang status AES dan file ELF yang dikompresi dilakukan untuk tujuan keamanan - sehingga data kunci dan dekripsi disimpan dalam memori hanya selama durasi penggunaan.

Selanjutnya, kami akan mempertimbangkan implementasi beberapa fungsi.

load_elf


Saya mengambil fungsi ini dari pengguna github dengan nickname bediger dari repositori userlandexec- nya dan menyelesaikannya, karena fungsi aslinya macet pada file seperti ET_DYN. Kegagalan terjadi karena fakta bahwa nilai argumen pertama dari panggilan sistem mmap diatur ke NULL, dan alamat dikembalikan cukup dekat dengan program utama, selama panggilan berikutnya ke mmap dan menyalin segmen ke alamat yang dikembalikan oleh mereka, kode program utama ditimpa, dan segfault terjadi. Oleh karena itu, diputuskan untuk menambahkan alamat awal sebagai parameter ke fungsi load_elf. Fungsi itu sendiri berjalan melalui semua tajuk program, mengalokasikan memori (jumlahnya harus merupakan kelipatan dari ukuran halaman) untuk segmen PT_LOAD dari file ELF, menyalin kontennya ke area memori yang dialokasikan dan menetapkan hak baca, tulis, eksekusi yang sesuai untuk area ini:

 //      #define PAGEUP(x) (((unsigned long)x + 4095)&(~4095)) //      #define PAGEDOWN(x) ((unsigned long)x&(~4095)) void* load_elf(void *load_addr, void *mapped) { Elf64_Ehdr *ehdr = mapped; Elf64_Phdr *phdr = mapped + ehdr->e_phoff; void *text_segment = NULL; unsigned long initial_vaddr = 0; unsigned long brk_addr = 0; for(size_t i = 0; i < ehdr->e_phnum; i++, phdr++) { unsigned long rounded_len, k; void *segment; //   PT_LOAD,    if(phdr->p_type != PT_LOAD) continue; if(text_segment != 0 && ehdr->e_type == ET_DYN) { //  ET_DYN phdr->p_vaddr    , //        //    ,      //     load_addr = text_segment + phdr->p_vaddr - initial_vaddr; load_addr = (void*)PAGEDOWN(load_addr); } else if(ehdr->e_type == ET_EXEC) { //  ET_EXEC phdr->p_vaddr     load_addr = (void*)PAGEDOWN(phdr->p_vaddr); } //        rounded_len = phdr->p_memsz + (phdr->p_vaddr % 4096); rounded_len = PAGEUP(rounded_len); //        segment = mmap(load_addr, rounded_len, PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); if(ehdr->e_type == ET_EXEC) load_addr = (void*)phdr->p_vaddr; else load_addr = segment + (phdr->p_vaddr % 4096); //         memcpy(load_addr, mapped + phdr->p_offset, phdr->p_filesz); if(!text_segment) { text_segment = segment; initial_vaddr = phdr->p_vaddr; } unsigned int protflags = 0; if(phdr->p_flags & PF_R) protflags |= PROT_READ; if(phdr->p_flags & PF_W) protflags |= PROT_WRITE; if(phdr->p_flags & PF_X) protflags |= PROT_EXEC; mprotect(segment, rounded_len, protflags); //   // , ,  k = phdr->p_vaddr + phdr->p_memsz; if(k > brk_addr) brk_addr = k; } if (ehdr->e_type == ET_EXEC) { brk(PAGEUP(brk_addr)); //  ET_EXEC ehdr->e_entry     load_addr = (void*)ehdr->e_entry; } else { //  ET_DYN ehdr->e_entry    , //           load_addr = (void*)ehdr + ehdr->e_entry; } return load_addr; //       } 

elf_load_addr


Fungsi ini untuk file ET_EXEC ELF mengembalikan NULL, karena file jenis ini harus ditempatkan di alamat yang ditentukan di dalamnya. Untuk file ET_DYN, alamat sama dengan perbedaan antara alamat dasar program utama (mis., Bootloader), jumlah memori yang diperlukan untuk menempatkan ELF dalam memori, dan 4096, 4096 - jarak yang diperlukan agar tidak menempatkan file ELF tepat di sebelah program utama dihitung pertama kali. Setelah menghitung alamat ini, diperiksa apakah area memori berpotongan, dari alamat yang diberikan ke alamat dasar dari program utama, dengan area dari awal file ELF yang belum dibongkar sampai akhir. Dalam hal persimpangan, alamat dikembalikan sama dengan perbedaan antara alamat awal ELF yang dibongkar dan jumlah memori yang diperlukan untuk meletakkannya, jika tidak, alamat yang dihitung sebelumnya dikembalikan.

Alamat dasar program ditemukan dengan mengekstraksi alamat header program dari vektor bantu (vektor tambahan ELF), yang terletak setelah pointer ke variabel lingkungan di stack, dan mengurangi ukuran header ELF dari itu:

       ---------------------------------------------------------------------------    -> [ argc ] 8 [ argv[0] ] 8 [ argv[1] ] 8 [ argv[..] ] 8 * x [ argv[n – 1] ] 8 [ argv[n] ] 8 (= NULL) [ envp[0] ] 8 [ envp[1] ] 8 [ envp[..] ] 8 [ envp[term] ] 8 (= NULL) [ auxv[0] (Elf64_auxv_t) ] 16 [ auxv[1] (Elf64_auxv_t) ] 16 [ auxv[..] (Elf64_auxv_t) ] 16 [ auxv[term] (Elf64_auxv_t) ] 16 (= AT_NULL) [  ] 0 - 16 [    ] >= 0 [   ] >= 0 [   ] 8 (= NULL) <    > 0 --------------------------------------------------------------------------- 

Struktur dimana setiap elemen dari vektor bantu dijelaskan memiliki bentuk:

 typedef struct { uint64_t a_type; //   union { uint64_t a_val; //  } a_un; } Elf64_auxv_t; 

Salah satu nilai a_type yang valid adalah AT_PHDR, a_val kemudian akan mengarah ke header program. Berikut ini adalah kode untuk fungsi elf_load_addr:

 void* elf_base_addr(void *rsp) { void *base_addr = NULL; unsigned long argc = *(unsigned long*)rsp; char **envp = rsp + (argc+2)*sizeof(unsigned long); //    //   while(*envp++); //        Elf64_auxv_t *aux = (Elf64_auxv_t*)envp; //    //  for(; aux->a_type != AT_NULL; aux++) { //        if(aux->a_type == AT_PHDR) { //   ELF ,     //      base_addr = (void*)(aux->a_un.a_val – sizeof(Elf64_Ehdr)); break; } } return base_addr; } size_t elf_memory_size(void *mapped) { Elf64_Ehdr *ehdr = mapped; Elf64_Phdr *phdr = mapped + ehdr->e_phoff; size_t mem_size = 0, segment_len; for(size_t i = 0; i < ehdr->e_phnum; i++, phdr++) { if(phdr->p_type != PT_LOAD) continue; segment_len = phdr->p_memsz + (phdr->p_vaddr % 4096); mem_size += PAGEUP(segment_len); } return mem_size; } void* elf_load_addr(void *rsp, void *mapped, size_t mapped_size) { Elf64_Ehdr *ehdr = mapped; if(ehdr->e_type == ET_EXEC) return NULL; size_t mem_size = elf_memory_size(mapped) + 0x1000; void *load_addr = elf_base_addr(rsp); if(mapped < load_addr && mapped + mapped_size > load_addr - mem_size) load_addr = mapped; return load_addr - mem_size; } 

Deskripsi Skrip Linker


Kita perlu mendefinisikan karakter untuk variabel eksternal yang dijelaskan di atas, dan juga memastikan bahwa kode dan data loader setelah kompilasi berada di bagian .text yang sama. Ini diperlukan untuk mengekstrak kode mesin loader dengan mudah hanya dengan memotong konten bagian ini dari file. Untuk mencapai tujuan ini, skrip tautan berikut ditulis:

 ENTRY(_start) SECTIONS { . = 0; .text :{ *(.text) *(.text.startup) *(.data) *(.rodata) payload_size = .; QUAD(0) key_seed = .; QUAD(0) iv_seed = .; QUAD(0) loader_end = .; } } 

Perlu dijelaskan bahwa QUAD (0) menempatkan 8 byte nol, alih-alih pengepak akan mengganti nilai-nilai tertentu. Untuk memotong kode mesin, sebuah utilitas kecil ditulis yang juga menulis ke permulaan kode mesin pergeseran titik masuk ke bootloader dari awal bootloader, offset nilai nilai payload_size, key_seed dan karakter iv_seed dari awal bootloader. Kode untuk utilitas ini tersedia di sini . Ini mengakhiri deskripsi bootloader.

Pengepakan langsung


Pertimbangkan fungsi utama pengepak. Ia menggunakan dua argumen baris perintah: nama file input adalah argv [1] dan nama file output adalah argv [2]. Pertama, file input ditampilkan dalam memori dan diperiksa kompatibilitasnya dengan pengepak. Packer hanya berfungsi dengan dua jenis file ELF: ET_EXEC dan ET_DYN, dan hanya dengan yang dikompilasi secara statis. Alasan untuk memperkenalkan pembatasan ini adalah kenyataan bahwa sistem linux yang berbeda memiliki versi yang berbeda dari pustaka bersama, yaitu kemungkinan program yang dikompilasi secara dinamis tidak akan mulai pada sistem selain sistem induknya cukup tinggi. Kode yang sesuai dalam fungsi utama:

 size_t mapped_size; void *mapped = map_file(argv[1], &mapped_size); if(check_elf(mapped) < 0) return 1; 

Setelah itu, jika file input melewati pemeriksaan kompatibilitas, ia dikompres:

 size_t comp_size; uint8_t *comp_buf = huffman_encode(mapped, &comp_size); 

Kemudian keadaan AES dihasilkan, dan file ELF terkompresi dienkripsi. Keadaan AES ditentukan oleh struktur berikut:

 #define AES_ENTROPY_BUFSIZE 256 typedef struct { uint8_t entropy_buf[AES_ENTROPY_BUFSIZE]; // 256-  size_t key_seed; //      size_t iv_seed; //       struct AES_ctx ctx; //  AES-CTR } AES_state_t; 

Kode yang sesuai di utama:

 AES_state_t aes_st; for(int i = 0; i < AES_ENTROPY_BUFSIZE; i++) state.entropy_buf[i] = rand() % 256; state.key_seed = rand(); state.iv_seed = rand(); AES_init_ctx_iv(&state.ctx, state.entropy_buf, state.key_seed, state.iv_seed); AES_CTR_xcrypt_buffer(&aes_st.ctx, comp_buf, comp_size); 

Setelah itu, struktur yang menyimpan informasi tentang bootloader diinisialisasi, nilai payload_size, key_seed, dan iv_seed di bootloader diubah menjadi yang dihasilkan pada langkah sebelumnya, setelah itu keadaan AES diatur ulang. Informasi tentang bootloader disimpan dalam struktur berikut:

 typedef struct { char *loader_begin; //      size_t entry_offset; //       size_t *payload_size_patch_offset; //     // ELF    size_t *key_seed_pacth_offset; //     //       size_t *iv_seed_patch_offset; //     //     //    size_t loader_size; //     } loader_t; 

Kode yang sesuai di utama:

 loader_t loader; init_loader(&loader); *loader.payload_size_patch_offset = comp_size; *loader.key_seed_pacth_offset = aes_st.key_seed; *loader.iv_seed_patch_offset = aes_st.iv_seed; memset(&aes_st.ctx, 0, sizeof(aes_st.ctx)); 

Pada bagian terakhir, kita membuat file output, menulis header ELF, satu header program, kode loader, file ELF terkompresi dan terenkripsi, dan buffer 256-byte ke dalamnya:

 int out_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0755); //  //   write_elf_ehdr(out_fd, &loader); //  ELF  write_elf_phdr(out_fd, &loader, comp_size); //    write(out_fd, loader.loader_begin, loader.loader_size); //   write(out_fd, comp_buf, comp_size); //     ELF write(out_fd, aes_st.entropy_buf, AES_ENTROPY_BUFSIZE); //  // 256-  

Kode utama dari pengepak berakhir di sini, maka fungsi-fungsi berikut akan dipertimbangkan: fungsi menginisialisasi informasi tentang bootloader, fungsi penulisan header ELF dan fungsi penulisan header program.

Menginisialisasi informasi bootloader


Kode mesin loader tertanam dalam paket yang dapat dieksekusi menggunakan kode sederhana di bawah ini:

 .data .globl _loader_begin .globl _loader_end _loader_begin: .incbin "loader" _loader_end: 

Untuk menentukan alamatnya dalam memori, variabel berikut dideklarasikan dalam file main.c:

 extern void* _loader_begin; extern void* _loader_end; 

Selanjutnya, pertimbangkan fungsi init_loader. Pertama, secara berurutan membaca nilai berikut: offset masukan dari awal titik bootloader (entry_offset), nilai dimensi offset dikemas berkas ELF dari awal loader (payload_size_patch_offset), perpindahan nilai pembangkit utama untuk kunci dari awal loader (key_seed_patch_offset), perpindahan nilai pembangkit utama untuk vektor inisialisasi dari awal bootloader (iv_seed_patch_offset). Kemudian, alamat pemuat ditambahkan ke tiga nilai terakhir, jadi ketika menunjuk referensi dan menetapkan nilai padanya, kami akan mengganti nol yang ditetapkan pada tahap tata letak (QUAD (0)) dengan nilai yang kami butuhkan.

 void init_loader(loader_t *l) { void *loader_begin = (void*)&_loader_begin; l->entry_offset = *(size_t*)loader_begin; loader_begin += sizeof(size_t); l->payload_size_patch_offset = *(void**)loader_begin; loader_begin += sizeof(void*); l->key_seed_pacth_offset = *(void**)loader_begin; loader_begin += sizeof(void*); l->iv_seed_patch_offset = *(void**)loader_begin; loader_begin += sizeof(void*); l->payload_size_patch_offset = (size_t)l->payload_size_patch_offset + loader_begin; l->key_seed_pacth_offset = (size_t)l->key_seed_pacth_offset + loader_begin; l->iv_seed_patch_offset = (size_t)l->iv_seed_patch_offset + loader_begin; l->loader_begin = loader_begin; l->loader_size = (void*)&_loader_end - loader_begin; } 


write_elf_ehdr


 void write_elf_ehdr(int fd, loader_t *loader) { //  ELF  Elf64_Ehdr ehdr; memset(ehdr.e_ident, 0, sizeof(ehdr.e_ident)); memcpy(ehdr.e_ident, ELFMAG, SELFMAG); ehdr.e_ident[EI_CLASS] = ELFCLASS64; ehdr.e_ident[EI_DATA] = ELFDATA2LSB; ehdr.e_ident[EI_VERSION] = EV_CURRENT; ehdr.e_ident[EI_OSABI] = ELFOSABI_NONE; ehdr.e_type = ET_DYN; ehdr.e_machine = EM_X86_64; ehdr.e_version = EV_CURRENT; ehdr.e_entry = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) + loader->entry_offset; ehdr.e_phoff = sizeof(Elf64_Ehdr); ehdr.e_shoff = 0; ehdr.e_flags = 0; ehdr.e_ehsize = sizeof(Elf64_Ehdr); ehdr.e_phentsize = sizeof(Elf64_Phdr); ehdr.e_phnum = 1; ehdr.e_shentsize = sizeof(Elf64_Shdr); ehdr.e_shnum = 0; ehdr.e_shstrndx = 0; write(fd, &ehdr, sizeof(ehdr)); //     return 0; } 

Di sini inisialisasi standar header ELF terjadi dan penulisan berikutnya ke file, satu-satunya hal yang perlu diperhatikan adalah kenyataan bahwa dalam file ET_DYN ELF segmen yang dijelaskan oleh header program pertama tidak hanya mencakup kode yang dapat dieksekusi, tetapi juga header ELF dan semua header. program. Oleh karena itu, offsetnya dari awal harus sama dengan nol, ukurannya haruslah jumlah dari ukuran header ELF, semua header program dan kode yang dapat dieksekusi, dan titik masuk ditentukan sebagai jumlah dari ukuran header ELF, ukuran semua header program dan offset dari awal kode yang dapat dieksekusi.

write_elf_phdr


 void write_elf_phdr(int fd, loader_t *loader, size_t payload_size) { //    Elf64_Phdr phdr; phdr.p_type = PT_LOAD; phdr.p_offset = 0; phdr.p_vaddr = 0; phdr.p_paddr = 0; phdr.p_filesz = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) + loader->loader_size + payload_size + AES_ENTROPY_BUFSIZE; phdr.p_memsz = phdr.p_filesz; phdr.p_flags = PF_R | PF_W | PF_X; phdr.p_align = 0x1000; write(fd, &phdr, sizeof(phdr)); //      } 

Di sini, header program diinisialisasi dan kemudian ditulis ke file. Anda harus memperhatikan offset relatif terhadap awal file dan ukuran segmen yang dijelaskan oleh header program. Seperti dijelaskan dalam paragraf sebelumnya, segmen yang dijelaskan oleh tajuk ini tidak hanya mencakup kode yang dapat dieksekusi, tetapi juga tajuk ELF dan tajuk program. Kami juga membuat segmen dengan kode yang dapat dieksekusi dapat diakses untuk ditulis, ini disebabkan oleh fakta bahwa implementasi AES yang digunakan dalam bootloader mengenkripsi dan mendekripsi data β€œdi tempat”.

Beberapa fakta tentang hasil kerja pengepak


Selama pengujian, diketahui bahwa program yang dikompilasi secara statis dengan glibc pergi ke segfault saat memulai, dengan instruksi ini:

  movq% fs: 0x28,% rax 

Saya tidak dapat menemukan mengapa hal ini terjadi, saya akan senang jika Anda berbagi informasi tentang hal ini. Alih-alih glibc, Anda dapat menggunakan musl-libc, semuanya bekerja dengannya tanpa gagal. Juga, pengepak itu diuji dengan program golang yang dikompilasi secara statis, misalnya, server http. Untuk crash statis penuh dari program golang, bendera berikut harus digunakan:

  CGO_ENABLED = 0 buat build -a -ldflags '-extldflags "-static"'. 

Hal terakhir yang diuji oleh packer adalah file ET_DYN ELF tanpa linker dinamis. Benar, ketika bekerja dengan file-file ini, fungsi elf_load_addr mungkin gagal. Dalam praktiknya, ini dapat dipotong dari bootloader dan menggunakan alamat tetap, misalnya 0x10000.

Kesimpulan


Packer ini, jelas, tidak masuk akal untuk digunakan untuk tujuan yang dimaksudkan, karena file yang dilindungi olehnya cukup mudah didekripsi. Tujuan dari proyek ini adalah untuk menjadi master yang lebih baik dalam bekerja dengan file ELF, praktik pembuatannya, serta mempersiapkan penciptaan paket yang lebih lengkap.

Source: https://habr.com/ru/post/id483368/


All Articles