Présentation
Cet article dĂ©crira la crĂ©ation d'un simple packer de fichiers exĂ©cutables pour linux x86_64. Il est supposĂ© que le lecteur est familiarisĂ© avec le langage de programmation C, le langage d'assemblage pour l'architecture x86_64 et les fichiers ELF du pĂ©riphĂ©rique. Afin de garantir la clartĂ©, la gestion des erreurs a Ă©tĂ© supprimĂ©e du code dans l'article et les implĂ©mentations de certaines fonctions n'ont pas Ă©tĂ© affichĂ©es, le code complet peut ĂȘtre trouvĂ© en cliquant sur les liens vers github (
chargeur ,
packer ).
L'idée est la suivante: nous transférons le fichier ELF vers le packer, et nous en obtenons un nouveau avec la structure suivante en sortie:
Pour la compression, il a été décidé d'utiliser l'algorithme de Huffman, pour le chiffrement - AES-CTR avec une clé de 256 bits, à savoir l'implémentation de kokke
tiny-AES-c . 256 octets de données aléatoires sont utilisés pour initialiser la clé AES et le vecteur d'initialisation à l'aide d'un générateur de nombres pseudo aléatoires, comme indiqué ci-dessous:
for(int i = 0; i < 32; i++) { seed = (1103515245*seed + 12345) % 256; key[i] = buf[seed]; }
Cette décision est due à la volonté de compliquer la rétro-ingénierie. à ce jour, j'ai réalisé que la complication est insignifiante, mais je n'ai pas commencé à l'enlever, car je ne voulais pas y consacrer temps et énergie.
Bootloader
Tout d'abord, le travail du chargeur de dĂ©marrage sera considĂ©rĂ©. Le chargeur ne doit pas avoir de dĂ©pendances, donc toutes les fonctions nĂ©cessaires de la bibliothĂšque C standard devront ĂȘtre Ă©crites indĂ©pendamment (l'implĂ©mentation de ces fonctions est disponible par
rĂ©fĂ©rence ). Il doit Ă©galement ĂȘtre indĂ©pendant de sa position.
Fonction _Start
Le chargeur de démarrage démarre à partir de la fonction _start, qui passe simplement argc et argv à main:
.extern main .globl _start .text _start: movq (%rsp), %rdi movq %rsp, %rsi addq $8, %rsi call main
Fonction principale
Le fichier main.c commence par définir plusieurs variables externes:
extern void* loader_end;
Tous sont déclarés comme externes afin de trouver la position des caractÚres correspondant aux variables (Elf64_Sym) dans le packer et changer leurs valeurs.
La fonction principale elle-mĂȘme est assez simple. La premiĂšre Ă©tape consiste Ă initialiser les pointeurs vers un fichier ELF compressĂ©, un tampon de 256 octets et vers le haut de la pile. Ensuite, le fichier ELF est dĂ©chiffrĂ© et dĂ©veloppĂ©, puis il est placĂ© au bon endroit dans la mĂ©moire Ă l'aide de la fonction load_elf, et enfin, la valeur du registre rsp revient Ă son Ă©tat d'origine, et un saut au point d'entrĂ©e du programme se produit:
#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;
La réinitialisation de l'état d'AES et du fichier ELF décompressé est effectuée à des fins de sécurité - de sorte que la clé et les données déchiffrées ne sont stockées en mémoire que pour la durée d'utilisation.
Ensuite, nous considérerons l'implémentation de certaines fonctions.
load_elf
J'ai pris cette fonction de l'utilisateur github avec le surnom bediger de son rĂ©fĂ©rentiel userlandexec et je l'ai finalisĂ©e, car la fonction d'origine s'est Ă©crasĂ©e sur des fichiers comme ET_DYN. L'Ă©chec est dĂ» au fait que la valeur du premier argument de l'appel systĂšme mmap a Ă©tĂ© dĂ©finie sur NULL et que l'adresse a Ă©tĂ© renvoyĂ©e assez prĂšs du programme principal, lors d'appels ultĂ©rieurs Ă mmap et de la copie de segments aux adresses renvoyĂ©es par eux, le code du programme principal a Ă©tĂ© Ă©crasĂ© et une erreur de segmentation s'est produite. Par consĂ©quent, il a Ă©tĂ© dĂ©cidĂ© d'ajouter l'adresse de dĂ©part en tant que paramĂštre Ă la fonction load_elf. La fonction elle-mĂȘme passe par tous les en-tĂȘtes de programme, alloue de la mĂ©moire (son nombre doit ĂȘtre un multiple de la taille de la page) pour les segments PT_LOAD du fichier ELF, copie leur contenu dans les zones de mĂ©moire allouĂ©es et dĂ©finit les droits de lecture, d'Ă©criture et d'exĂ©cution correspondants sur ces zones:
elf_load_addr
Cette fonction pour les fichiers ELF ET_EXEC renvoie NULL, car les fichiers de ce type doivent ĂȘtre situĂ©s aux adresses qui y sont spĂ©cifiĂ©es. Pour les fichiers ET_DYN, l'adresse Ă©gale Ă la diffĂ©rence entre l'adresse de base du programme principal (c'est-Ă -dire le chargeur de dĂ©marrage), la quantitĂ© de mĂ©moire requise pour placer l'ELF dans la mĂ©moire et 4096, 4096 - l'espace nĂ©cessaire pour ne pas placer le fichier ELF juste Ă cĂŽtĂ© du programme principal sont d'abord calculĂ©s. AprĂšs avoir calculĂ© cette adresse, il est vĂ©rifiĂ© si la zone de mĂ©moire se croise, de l'adresse donnĂ©e Ă l'adresse de base du programme principal, avec la zone du dĂ©but du fichier ELF dĂ©compressĂ© jusqu'Ă sa fin. En cas d'intersection, l'adresse est retournĂ©e Ă©gale Ă la diffĂ©rence entre l'adresse de dĂ©but de l'ELF dĂ©compressĂ© et la quantitĂ© de mĂ©moire requise pour le placer, sinon l'adresse prĂ©cĂ©demment calculĂ©e est retournĂ©e.
L'adresse de base du programme est trouvĂ©e en extrayant l'adresse des en-tĂȘtes de programme du vecteur auxiliaire (vecteur auxiliaire ELF), qui se trouve aprĂšs les pointeurs vers les variables d'environnement dans la pile, et en soustrayant la taille de l'en-tĂȘte ELF:
--------------------------------------------------------------------------- -> [ 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 ---------------------------------------------------------------------------
La structure par laquelle chaque élément du vecteur auxiliaire est décrit a la forme:
typedef struct { uint64_t a_type;
Une des valeurs a_type valides est AT_PHDR, a_val pointera alors vers les en-tĂȘtes du programme. Voici le code de la fonction 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);
Description du script de l'éditeur de liens
Il est nĂ©cessaire de dĂ©finir les caractĂšres des variables externes dĂ©crites ci-dessus et de s'assurer Ă©galement que le code et les donnĂ©es du chargeur aprĂšs compilation sont dans la mĂȘme section .text. Cela est nĂ©cessaire pour extraire facilement le code machine du chargeur en coupant simplement le contenu de cette section du fichier. Pour atteindre ces objectifs, le script de l'Ă©diteur de liens suivant a Ă©tĂ© Ă©crit:
ENTRY(_start) SECTIONS { . = 0; .text :{ *(.text) *(.text.startup) *(.data) *(.rodata) payload_size = .; QUAD(0) key_seed = .; QUAD(0) iv_seed = .; QUAD(0) loader_end = .; } }
Il vaut la peine d'expliquer que QUAD (0) place 8 octets de zéros, au lieu desquels le packer substituera des valeurs spécifiques. Pour découper le code machine, un petit utilitaire a été écrit qui écrit également au début du code machine le décalage du point d'entrée vers le chargeur de démarrage depuis le début du chargeur de démarrage, le décalage des valeurs des caractÚres payload_size, key_seed et iv_seed depuis le début du chargeur de démarrage. Le code de cet utilitaire est disponible
ici . Ceci termine la description du chargeur de démarrage.
Emballeur direct
Considérez la fonction principale du packer. Il utilise deux arguments de ligne de commande: le nom du fichier d'entrée est argv [1] et le nom du fichier de sortie est argv [2]. Tout d'abord, le fichier d'entrée est affiché en mémoire et vérifié pour la compatibilité avec le packer. Le packer fonctionne avec seulement deux types de fichiers ELF: ET_EXEC et ET_DYN, et uniquement avec ceux compilés statiquement. La raison de l'introduction de cette restriction était le fait que différents systÚmes Linux ont différentes versions de bibliothÚques partagées, c'est-à -dire la probabilité qu'un programme compilé dynamiquement ne démarre pas sur un systÚme autre que le systÚme parent est assez élevée. Le code correspondant dans la fonction principale:
size_t mapped_size; void *mapped = map_file(argv[1], &mapped_size); if(check_elf(mapped) < 0) return 1;
AprÚs cela, si le fichier d'entrée réussit le contrÎle de compatibilité, il est compressé:
size_t comp_size; uint8_t *comp_buf = huffman_encode(mapped, &comp_size);
Ensuite, l'état AES est généré et le fichier ELF compressé est chiffré. L'état d'AES est déterminé par la structure suivante:
#define AES_ENTROPY_BUFSIZE 256 typedef struct { uint8_t entropy_buf[AES_ENTROPY_BUFSIZE];
Code correspondant en principal:
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);
AprÚs cela, la structure qui stocke les informations sur le chargeur de démarrage est initialisée, les valeurs payload_size, key_seed et iv_seed dans le chargeur de démarrage sont remplacées par celles générées à l'étape précédente, aprÚs quoi l'état AES est réinitialisé. Les informations sur le chargeur de démarrage sont stockées dans la structure suivante:
typedef struct { char *loader_begin;
Code correspondant en principal:
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));
Dans la derniĂšre partie, nous crĂ©ons un fichier de sortie, Ă©crivons un en-tĂȘte ELF, un en-tĂȘte de programme, un code de chargeur, un fichier ELF compressĂ© et chiffrĂ© et un tampon de 256 octets:
int out_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0755);
Le code principal du packer se termine ici, puis les fonctions suivantes seront considĂ©rĂ©es: la fonction d'initialisation des informations sur le chargeur de dĂ©marrage, la fonction d'Ă©criture de l'en-tĂȘte ELF et la fonction d'Ă©criture de l'en-tĂȘte du programme.
Initialisation des informations du chargeur de démarrage
Le code machine du chargeur est intégré dans l'exécutable du packer à l'aide du code simple ci-dessous:
.data .globl _loader_begin .globl _loader_end _loader_begin: .incbin "loader" _loader_end:
Afin de déterminer son adresse en mémoire, les variables suivantes sont déclarées dans le fichier main.c:
extern void* _loader_begin; extern void* _loader_end;
Ensuite, considérez la fonction init_loader. Tout d'abord, les valeurs suivantes y sont lues de maniÚre séquentielle: le décalage du point d'entrée depuis le début du chargeur de démarrage (entry_offset), le décalage de la taille du fichier ELF compressé depuis le début du chargeur de démarrage (payload_size_patch_offset), le décalage de la valeur initiale du générateur pour la clé depuis le début du chargeur de démarrage (key_seed_patch le vecteur initial du décalage) initialisation depuis le début du chargeur de démarrage (iv_seed_patch_offset). Ensuite, l'adresse du chargeur est ajoutée aux trois derniÚres valeurs, donc lorsque vous déréférencer des pointeurs et leur attribuer des valeurs, nous remplacerons les zéros attribués au stade de la mise en page (QUAD (0)) par les valeurs dont nous avons besoin.
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) {
Ici, l'initialisation standard de l'en-tĂȘte ELF a lieu et son enregistrement ultĂ©rieur dans un fichier, la seule chose Ă laquelle vous devez faire attention est le fait que dans les fichiers ETF ELF, le segment dĂ©crit par le premier en-tĂȘte de programme comprend non seulement le code exĂ©cutable, mais Ă©galement l'en-tĂȘte ELF et tous les en-tĂȘtes programmes. Par consĂ©quent, son dĂ©calage depuis le dĂ©but doit ĂȘtre Ă©gal Ă zĂ©ro, la taille doit ĂȘtre la somme de la taille de l'en-tĂȘte ELF, de tous les en-tĂȘtes de programme et du code exĂ©cutable, et le point d'entrĂ©e est dĂ©terminĂ© comme la somme de la taille de l'en-tĂȘte ELF, de la taille de tous les en-tĂȘtes de programme et du dĂ©calage par rapport au dĂ©but du code exĂ©cutable.
write_elf_phdr
void write_elf_phdr(int fd, loader_t *loader, size_t payload_size) {
Ici, l'en-tĂȘte du programme est initialisĂ© puis Ă©crit dans un fichier. Vous devez faire attention au dĂ©calage par rapport au dĂ©but du fichier et Ă la taille du segment dĂ©crit par l'en-tĂȘte du programme. Comme dĂ©crit dans le paragraphe prĂ©cĂ©dent, le segment dĂ©crit par cet en-tĂȘte comprend non seulement le code exĂ©cutable, mais Ă©galement l'en-tĂȘte ELF et l'en-tĂȘte de programme. Nous rendons Ă©galement le segment avec du code exĂ©cutable accessible pour l'Ă©criture, cela est dĂ» au fait que l'implĂ©mentation AES utilisĂ©e dans le chargeur de dĂ©marrage crypte et dĂ©crypte les donnĂ©es «en place».
Quelques faits sur le travail de l'emballeur
Lors des tests, il a été remarqué que les programmes compilés statiquement avec glibc vont en segfault au démarrage, sur cette instruction:
movq% fs: 0x28,% rax
Je n'ai pas pu comprendre pourquoi cela se produit, je serai heureux si vous partagez des informations Ă ce sujet. Au lieu de glibc, vous pouvez utiliser musl-libc, tout fonctionne sans problĂšme. En outre, le packer a Ă©tĂ© testĂ© avec des programmes golang compilĂ©s statiquement, par exemple, un serveur http. Pour les plantages statiques complets des programmes golang, les indicateurs suivants doivent ĂȘtre utilisĂ©s:
CGO_ENABLED = 0 go build -a -ldflags '-extldflags "-static"'.
La derniĂšre chose avec laquelle le packer a Ă©tĂ© testĂ© Ă©tait les fichiers EL_ ET_DYN sans Ă©diteur de liens dynamique. Certes, lorsque vous travaillez avec ces fichiers, la fonction elf_load_addr peut Ă©chouer. En pratique, il peut ĂȘtre coupĂ© du chargeur de dĂ©marrage et utiliser une adresse fixe, par exemple 0x10000.
Conclusion
Ce packer, évidemment, n'a pas de sens à utiliser pour son usage prévu, car les fichiers protégés par lui sont assez facilement décryptés. L'objectif de ce projet était de mieux maßtriser l'utilisation des fichiers ELF, la pratique de leur génération, ainsi que la préparation de la création d'un packer plus complet.