Initiation au langage d'assemblage

(Vous allez être déçu)

L'auteur de ce sujet a trouvé une solution à son problème.
Auteur du sujet

Bonjour,

Suite à une discussion sur IRC avec Xia, je vous propose d'écrire votre premier programme (ou presque) en langage d'assemblage x86.

En fait nous allons faire mieux : nous allons désassembler un programme écrit en C, puis le ré-assembler juste pour le fun.

Je pars du principe que vous avez un environnement Linux.

Ouvrez votre éditeur favoris et rédigez le code suivant :

1
2
3
4
5
6
7
8
 % cat main.c 
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
    printf("Hello, world!\n");
    return EXIT_SUCCESS;
}

Vous l'aurez deviné : c'est du C. :-)

Compilez le code source au moyen de la ligne suivante :

1
% gcc -o main main.c -m32

Le drapeau -m32 spécifie que le code généré doit être exécutable sur une architecture 32-bit. Nous voulons désassembler et réassembler du code Intel 32-bit pour rester un maximum accessible (peut-être que des vieux de la vieille ont encore un processeur intel x86 ? :D )

Ceci fait, récupérons quelques informations sur notre programme :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
% file main
main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0xbb18920a5be16ccd8f707db06e5abf964f9bcf7c, not stripped

 % readelf -h main
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048320
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1964 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         8
  Size of section headers:           40 (bytes)
  Number of section headers:         31
  Section header string table index: 28

Le binaire main est structuré au format ELF. Si cet acronyme vous est étrangé et que vous souhaitiez en savoir plus, je vois renvoie sur l'introduction à la rétro-ingénierie des binaires.

Notre objectif : désassembler le fichier, puis le réassembler.

Il faut comprendre que notre fichier main n'est en fait qu'une collection de structures destinées à décrire le fonctionnement du fichier (propriétés des segments mémoire à cartographier, ce qu'ils contiennent, des informations sur les symboles de débogage, etc).

Pour cela… On va écrire un désassembleur naïf en python. Et quand je dis naïf, je ne joue pas sur les mots !

En effet, celui-ci va prendre en entrée un fichier binaire et va générer en sortie un fichier source en langage d'assemblage, au format .asm.

Pour cela, on va utiliser python3 (coucou nohar ! coucou !) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 % cat naive_disassembler.py 
#!/usr/bin/env python3
import sys

if len(sys.argv) < 2:
    print("Usage: %s <binary>" % (sys.argv[0]))
    sys.exit(-1)

with open(sys.argv[1], "rb") as file_handle:
    binary_content = file_handle.read()
    file_handle.close()

    with open("%s.asm" % (sys.argv[1]), "w") as output_asm_file:
        output_asm_file.write("bits 32\n\n")
        for c in binary_content:
            output_asm_file.write("db 0x%02X\n" % (c & 0xff))
        output_asm_file.close()
        print("Written %d bytes into output file!" % len(binary_content))

Démonstration :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ge0@samaritan ~/python/retrodisass
 % ./naive_disassembler.py              
Usage: ./naive_disassembler.py <binary>
ge0@samaritan ~/python/retrodisass
 % ./naive_disassembler.py ./main
Written 4868 bytes into output file!
ge0@samaritan ~/python/retrodisass
 % head main.asm 
bits 32

db 0x7F
db 0x45
db 0x4C
db 0x46
db 0x01
db 0x01
db 0x01
db 0x00

C'est là que le "(Vous allez être déçu)" du sous-titre prend tout son sens ! On s'est contenté d'écrire une série débile d'instructions "db", qui signifient en fait data byte. En résumé, on déclare les octets un par un, sans écrire de mnémonique (exemple : mov eax, 0xdeadbeef).

La première directive, "bits 32" au début du fichier, sert à indiquer que celui-ci contient en fait du code pour processeur Intel 32-bit. Cette directive n'est réellement utile que si nous avions écrit des instructions à l'aide de mnémoniques.

Pourquoi je m'arrête là ? Pour que vous puissiez pratiquer vous-mêmes sans trop d'effort. On ré-assemble le fichier ? Pour ce faire, on va utiliser nasm, un désassembleur puissant pour architectures intel ! (http://www.nasm.us)

1
% nasm -f bin main.asm -o main_assembled

L'option -f permet de spécifier que le format de sortie sera du binaire pur. En effet, dans notre fichier .asm, nous avons plus ou moins fait un "dump" de notre binaire, en incluant ses en-têtes, ses symboles, son code… Le tout dans une joyeuse série idiote de "db". Mais ça fonctionne au réassemblage !

1
2
3
4
% md5sum main_assembled             
d63e1c7dee081a3a1f5c210481ef928f  main_assembled
 % md5sum main                   
d63e1c7dee081a3a1f5c210481ef928f  main

Quel est l'intérêt, me demanderez-vous ? On pourrait imaginer un désassembleur plus intelligent, générant un fichier source qui décrirait de manière claire les en-têtes, les données, le code… Pour ensuite le modifier à souhait.

Paradoxal de faire ça quand on a entre les mains un logiciel "privateur", vous ne trouvez pas ? ;) Et pourtant…

Bonus : un hello world en intel x86, avec commandes de fabrication fournies :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
% cat helloworld.asm 
bits 32

extern printf

section .data ; section des données (format ELF)
helloWorld db "Hello world", 10, 0 ; 10 = Line Feed, 0 = Null-Byte

section .text ; section du code (format ELF)

global main ; pour le linker

main:
    push ebp ; prologue
    mov ebp, esp
    sub esp, 4 ; on alloue de l'espace mémoire au sein de la pile
    mov dword [esp], helloWorld ; dépose l'argument sur la pile
    call printf
    xor eax, eax ; return 0
    leave
    ret ; épilogue / fin de la fonction

Assemblons et lions (il n'y a pas de compilation à proprement parler :P ) :

1
2
3
4
5
 % nasm -f elf32 helloworld.asm -o helloworld.o      
ge0@samaritan ~/python/retrodisass
 % gcc -o helloworld helloworld.o -m32
% ./helloworld
Hello world

C'est tout. Pour le moment.

Édité par anonyme

+8 -0
Auteur du sujet

Hop !

Après le post inutile de Dominus Carnufex, j'ai apporté une amélioration mineure à mon désassembleur "naïf".

Je me suis servi de la (superbe) bibliothèque python "pyelftools" ( https://github.com/eliben/pyelftools ) pour gagner du temps sur le parsing du binaire fourni à mon script.

Grâce à cette bibliothèque, on peut récupérer des informations sur notre ELF, notamment l'en-tête du fichier (elf header dans le manuel) ou les en-têtes de segment (ou program headers dans le manuel).

Dans les en-têtes du fichier, le champ e_entry contient l'adresse virtuelle du point d'entrée de notre ELF. C'est à cette adresse mémoire que le code sera exécuté une fois que le binaire sera cartographié en mémoire !

L'idée, c'est de faire la correspondance adresse virtuelle pointant sur le code à exécuter <–> offset dans le fichier pointant sur le code à exécuter. En effet, le code est stocké quelque part dans le fichier avant d'être positionné en mémoire.

C'est grâce aux en-têtes de segments que nous pouvons obtenir ces informations. Chaque en-tête de segments contient trois champs qui nous intéressent :

  • le champ p_vaddr qui désigne l'adresse virtuelle à laquelle sera cartographié ledit segment ;
  • le champ p_offset qui désigne l'offset, dans le fichier, auquel sont stockées les données du segment (ça peut-être du code, des données, des pointeurs de fonction, des symboles, …) ;
  • le champ p_filesz qui désigne la taille occupée dans le fichier par notre segment.

A partir de ces informations, il faut déterminer où pointe notre point d'entrée. On va dérouler l'algorithme suivant :

1
2
3
4
Pour chaque segment:
    si le point d'entrée est supérieur ou égal à l'adresse de base du segment ET
    inférieur à l'adresse de base + la taille du segment alors :
         offset_point_d_entree = offset_base_segment + (adresse_point_d_entree - adresse_base_segment)

A partir de là, on a l'offset à partir duquel débute le code exécuté.

La seule modification pertinente que j'ai faite à ce niveau dans mon script, c'est d'afficher un label _start: à partir duquel commence le code exécuté par le système d'exploitation une fois le binaire complètement chargé en mémoire.

Mesdames messieurs, voici naive_disassembler2.py en python3 ! :D

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/usr/bin/env python3
import sys
import logging
from elftools.elf.elffile import ELFFile
from elftools.elf.descriptions import describe_p_type

if len(sys.argv) < 2:
    print("Usage: %s <binary>" % (sys.argv[0]))
    sys.exit(-1)

with open(sys.argv[1], "rb") as file_handle:
    binary_content = file_handle.read()
    elf_file       = ELFFile(file_handle)
    entry_point    = elf_file['e_entry']
    #print(entry_point)
    print("Entry point: %08X\n" % (entry_point))
    print("Retrieving appropriated offset...")
    start_offset = -1
    for segment in elf_file.iter_segments():

        start = segment['p_vaddr']
        size  = segment['p_filesz']

        print("Segment '%s' starting at %08X with size of %d bytes" %
            (describe_p_type(segment['p_type']), segment['p_vaddr'], segment['p_filesz']))

        if(entry_point >= start and entry_point < (start+size)):
            print("\t*** Entry point is located here! ***")
            start_offset = segment['p_offset'] + (entry_point - start)

    if start_offset >= 0:
        print("Start offset computed at %08X" % (start_offset))

    with open("%s.asm" % (sys.argv[1]), "w") as output_asm_file:
        output_asm_file.write("bits 32\n\n")
        i = 0
        for c in binary_content:
            if i == start_offset:
                output_asm_file.write("_start:\n")
            output_asm_file.write("\tdb 0x%02X\n" % (c & 0xff))
            i += 1
        output_asm_file.close()
        print("Written %d bytes into output file!" % len(binary_content))

    file_handle.close()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ge0@samaritan ~/python/retrodisass
 % ./naive_disassembler2.py 
Usage: ./naive_disassembler2.py <binary>
ge0@samaritan ~/python/retrodisass
 % ./naive_disassembler2.py ./main
Entry point: 08048320

Retrieving appropriated offset...
Segment 'PHDR' starting at 08048034 with size of 256 bytes
Segment 'INTERP' starting at 08048134 with size of 19 bytes
Segment 'LOAD' starting at 08048000 with size of 1356 bytes
        *** Entry point is located here! ***
Segment 'LOAD' starting at 0804954C with size of 288 bytes
Segment 'DYNAMIC' starting at 08049558 with size of 240 bytes
Segment 'NOTE' starting at 08048148 with size of 68 bytes
Segment 'GNU_EH_FRAME' starting at 080484D0 with size of 28 bytes
Segment 'GNU_STACK' starting at 00000000 with size of 0 bytes
Start offset computed at 00000320
Written 4868 bytes into output file!
ge0@samaritan ~/python/retrodisass
 % nasm -f bin main.asm -o main_assembled
ge0@samaritan ~/python/retrodisass
 % md5sum ./main ./main_assembled 
d63e1c7dee081a3a1f5c210481ef928f  ./main
d63e1c7dee081a3a1f5c210481ef928f  ./main_assembled

On réassemble bien ce qu'on a désassemblé.

Prochaine étape ? Désassembler au sens propre du terme une petite portion de code. (Je m'aiderai de capstone, je pense)

Édité par anonyme

+3 -4
Auteur du sujet

Hello :)

Dans mon élan de motivation, j'en suis à la troisième version de mon PoC.

J'ai commencé à éclater mon script en classes, ayant plus ou moins imaginé une chaîne de fonctions à dérouler pour obtenir au résultat souhaité.

L'idée est de produire un jeu de fichiers .asm à partir du binaire désassemblé, que nous pourrions a priori réassembler derrière.

Mon PoC ne génère qu'un gros fichier. Mais sans plus attendre, voici comment je conçois les choses :

Première étape : le "Code Coverage".

Cette étape consiste à établir une couverture de code. Elle va énumérer, pour chaque "région" du binaire (j'entends, par exemple, de l'offset 0 à l'offset 200, de l'offset 201 à l'offset 300), le type de données qu'elles renferme.

J'ai deux types pour l'instant (et un troisième en cours) :

  • le type "INCONNU". On ne sait pas ce que la région contient. Dans ce cas, comme pour le premier exemple, on désassemble en une série de db octet ;
  • le type "CODE". Là, on est certain que c'est du code machine. On va donc le désassembler et écrire dans notre fichier les mnémoniques correspondants ! :D

Chaque région possède :

  • Un libellé qui la distingue des autres ;
  • Une taille ;
  • Une adresse virtuelle de base (pour ne pas oublier à quelle adresse nous nous situons à l'exécution)

En résumé, on a ça :

1
code_coverage = create_code_coverage(elf_file)

Seconde étape : le désassemblage.

Une fois la couverture de code établie, nous pouvons fournir celle-ci à notre désassembleur. D'un point de vue externe, le désassembleur n'aura besoin que de savoir où se situe les informations cartographiées par la "code coverage". Ce sera plus simple pour nous d'écrire notre/nos fichier(s) final(aux) :

Ce qui nous donne grossièrement :

1
asm_file = disassemble(code_coverage, elf_file)

J'ai maintenant trois fichiers :

  • code_coverage.py : contient les classes qui gèrent la couverture de code ;
  • binary_disassembler.py : contient les classes qui gèrent le désassemblage du binaire en fonction de la couverture de code ;
  • naive_disassembler3.py : script de test qui va établir le lien entre toutes mes classes, donc construire une couverture de code minimaliste, et la faire désassembler.

Mon test ne se résume qu'à établir trois régions pour le moment :

  • before_start : tout le code avant le point d'entrée. Je ne me suis pas attardé sur le désassemblage de l'en-tête du fichier ELF et de ses autres méta-données mais c'est pour la suite ;
  • start : une partie du code réellement exécuté à partir du point d'entrée et désassemblé jusqu'à trouver une instruction "de fin" (NDLR: les amoureux du C auront une petite surprise à ce propos) ;
  • after_start : le code qui se situe après le code réellement exécuté après start.

Petite précision d'importance : le code exécutable désassemblé trouvera dans start sera maigre. J'ai en effet utilisé la même méthode de désassemblage que gdb ou autre désassembleur naïve, à savoir dérouler les instructions sans prendre en compte les branchements, les appels de routines… Cet algorithme s'appelle le Linear Sweep. Je tendrai vers un algorithme plus évolué de type Recursive Traversal où une exécution symbolique sera effectuée pour effectuer une meilleure couverture de code.

Passé les explications, voici mes sources Python 3 :

code_coverage.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class CodeCoverage(object):
    def __init__(self, disassembler, regions, base_address=0x0):
        """ Init a CodeCoverage class, which aims at "covering" a whole binary by
        locating/differentiating code from (relevant) data, and so on."""
        self._base_address = base_address
        self._disassembler = disassembler
        if regions is None:
            self._regions = []
        else:
            self._regions = regions

    @property
    def base_address():
        """ Retrieve the base memory address used by the code coverage """
        return self._base_address

    @property
    def regions():
        """ Retrieve the regions handled by the code coverage """
        return self._regions

    @property
    def disassembler(self):
        """ Get the current disassembler instance """
        return self._disassembler

    def disassemble(self, data):
        self._disassembler.set_org(self._base_address)
        offset = 0
        for region in self._regions:
            region.get_disassembled(self._disassembler, data[offset:offset+region.size])
            offset += region.size

class BinaryRegion(object):
    def __init__(self, label, size, base_address):
        self._size = size
        self._label = label
        self._base_address = base_address

    @property
    def size(self):
        """ Retrieve the size of the region """
        return self._size

    @property
    def label(self):
        """ Retrieve the label of the region """
        return self._label

    @property
    def base_address(self):
        """ Retrieve the base address of the region """
        return self._base_address

    def get_disassembled(self, disassembler, data):
        pass

class UnknownRegion(BinaryRegion):
    def get_disassembled(self, disassembler, data):
        disassembler.disassemble_unknown_region(self, data)

class CodeRegion(BinaryRegion):
    def get_disassembled(self, disassembler, data):
        disassembler.disassemble_code_region(self, data)

class DataRegion(BinaryRegion):
    def get_disassembled(self, disassembler, data):
        disassembler.disassemble_data_region(self, data)

Les amoureux de la conception orientée-objet auront potentiellement repéré un patron de conception de type Visiteur : ici, une région accepte un désassembleur et le désassembleur possède son propre traitement selon la nature de la région.

binary_disassembler.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import capstone

class BinaryDisassembler(object):
    def set_org(self, org):
        pass
    def disassemble_unknown_region(self, region):
        pass
    def disassemble_code_region(self, region):
        pass
    def disassemble_data_region(self, region):
        pass

class NaiveBinaryDisassembler(BinaryDisassembler):
    def __init__(self, output_file):
        self._handle = open("%s_naive_disass.asm" % (output_file), "w")
        self._handle.write("bits 32\n") # For the beauty of the PoC

    def _output_region_comments(self, region, comment="REGION"):
        self._handle.write("; ---------------- %s ----------------\n"
        "; Name: %s\n"
        "; Base Address: %08X\n"
        "; Size: %d bytes\n" % (comment, region.label, region.base_address, region.size))

    def set_org(self, org):
        self._handle.write("ORG 0x%08X\n" % (org))

    def disassemble_unknown_region(self, region, data):
        self._output_region_comments(region, "UNKNOWN REGION")
        self._handle.write("%s:\n" % (region.label))
        self._handle.write("db 0x%02X" % (data[0]))
        i = 1
        while i < len(data):
            if(i % 16 == 0):
                self._handle.write("\ndb 0x%02X" % (data[i]))
            else:
                self._handle.write(", 0x%02X" % (data[i]))
            i += 1
        self._handle.write("\n")

        self._handle.write("; ------------------------------------------------\n\n")

    def disassemble_code_region(self, region, data):
        self._output_region_comments(region, "EXECUTABLE CODE")
        # TODO: check the target architecture.
        md  = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
        for instruction in md.disasm(data, region.base_address):
            self._handle.write("\t%s %s\n" % (instruction.mnemonic, instruction.op_str))
        #self.disassemble_unknown_region(region, data)

    def disassemble_data_region(region):
        # TODO
        self.disassemble_unknown_region(region, data)

Comme promis, j'ai utilisé capstone. On peut voir des TODO çà et là. Pas grand-chose à expliquer, si ce n'est que j'ai créé une classe concrète à titre d'exemple, nommée NaiveDisassembler.

Et enfin le code de naive_disassembler3.py :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/usr/bin/env python3
import sys
import logging
from elftools.elf.elffile import ELFFile
from elftools.elf.descriptions import describe_p_type
import capstone
import code_coverage
import binary_disassembler

def linear_sweep_disassemble(base_addr, code, arch, mode):
    # Dumb strategy, disass until:
    #   - end of code reached OR
    #   - ret or hlt instruction is met
    disassembler = capstone.Cs(arch, mode)
    for current_instruction in disassembler.disasm(code, base_addr):
        print("%s\t%s" % (current_instruction.mnemonic, current_instruction.op_str))

def create_start_proc_region(content, base_address):
    region = code_coverage.CodeRegion("start", 0, base_address)
    disassembler = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
    for instruction in disassembler.disasm(content, 0x00000000):
        region._size += instruction.size
        if instruction.mnemonic == "ret" or instruction.mnemonic == "hlt":
            break
    return region

if len(sys.argv) < 2:
    print("Usage: %s <binary>" % (sys.argv[0]))
    sys.exit(-1)

def get_elf_information(elf_file):
    base_address = 0
    start_offset = 0
    for segment in elf_file.iter_segments():
        if(segment['p_type'] == 'PT_LOAD' and segment['p_offset'] == 0):
            base_address = segment['p_vaddr']
        if(elf_file['e_entry'] >= segment['p_vaddr'] and elf_file['e_entry'] < (segment['p_vaddr'] + segment['p_filesz'])):
            start_offset = segment['p_offset'] + (elf_file['e_entry'] - base_address)
    return (base_address, start_offset)

def elf_gen_code_coverage(elf_file):
    elf_base_address, start_offset = get_elf_information(elf_file)

    disasm = binary_disassembler.NaiveBinaryDisassembler(sys.argv[1])
    regions = [ code_coverage.UnknownRegion("before_start", size=start_offset, base_address=elf_base_address) ]

    # start code until ret/hlt
    regions.append(create_start_proc_region(binary_content[start_offset:], elf_file['e_entry']))

    # rest of the code
    regions.append(code_coverage.UnknownRegion("after_start", len(binary_content) - (start_offset + regions[-1].size), elf_file['e_entry'] + (start_offset +regions[-1].size)))

    cover = code_coverage.CodeCoverage(disasm, regions, base_address=elf_base_address)
    return cover


with open(sys.argv[1], "rb") as file_handle:
    binary_content = file_handle.read()
    elf_file       = ELFFile(file_handle)

    print("Creating code coverage...")
    cover = elf_gen_code_coverage(elf_file)
    print("Disassembling the file...")
    cover.disassemble(binary_content)
    print("Done.")
    file_handle.close()

C'est un peu le cafouilli mais ça marche sur notre binaire d'exemple main :

1
2
3
4
5
6
7
8
ge0@samaritan ~/python/retrodisass
 % ./naive_disassembler3.py
Usage: ./naive_disassembler3.py <binary>
ge0@samaritan ~/python/retrodisass
 % ./naive_disassembler3.py ./main
Creating code coverage...
Disassembling the file...
Done.

Le fichier généré est main_naive_disass.asm. Vous pourrez retrouver son contenu sur Pastebin : http://pastebin.com/9BZy0vQJ

La partie qui nous intéresse de loin est celle-là :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
_start:
        xor ebp, ebp
        pop esi
        mov ecx, esp
        and esp, 0xfffffff0
        push eax
        push esp
        push edx
        push 0x8048430
        push 0x8048440
        push ecx
        push esi
        push 0x804840c
        call 0x8048310
        hlt

Il s'agit en fait du code qui va mettre en place les arguments de la fonction main sur la pile, plus précisément argc, argv et envp pour les soldats du C.

On remarque que capstone désassemble proprement. Au final on a des adresses en dur, type 0x8048430 mais rien ne nous empêchera de remplacer ça par des labels pointant sur du code ou d'autres données.

Si on essaie d'assembler le fichier source généré, on remarque que…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ge0@samaritan ~/python/retrodisass
 % nasm -f bin main_naive_disass.asm -o main_naive_disass
ge0@samaritan ~/python/retrodisass
 % chmod +x ./main_naive_disass
ge0@samaritan ~/python/retrodisass
 % ./main_naive_disass
Hello, world!
ge0@samaritan ~/python/retrodisass
 % md5sum ./main_naive_disass ./main
d63e1c7dee081a3a1f5c210481ef928f  ./main_naive_disass
d63e1c7dee081a3a1f5c210481ef928f  ./main

… Les fichiers sont identiques ! :D

J'attire votre attention sur une chose : on a réussi à désassembler du code, mais celui-ci est encore trop "rigide" pour être modifié ou étendu. Il se pourrait qu'un octet soit de trop ou de moins pour que la structure de notre binaire ELF soit altérée et que notre binaire ne soit plus valide. Si vous voulez tenter de modifier le binaire, il vous faudra donc aller taper dans les octets des "régions inconnues" puisque le script est encore trop peu mature.

Prochaine étape ? Essayer de désassembler les structures du format ELF pour que celles-ci soient clairement identifiables dans notre listing de sortie.

Au fait, si vous avez des questions ou des remarques (sur la structure du code et des classes par exemple), n'hésitez surtout pas. De même si vous souhaitez étendre le sujet et travailler dessus avec moi. :)

Édité par anonyme

+2 -0

Au fait, si vous avez des questions ou des remarques (sur la structure du code et des classes par exemple), n'hésitez surtout pas.

J'en profite alors, car ça fait un moment que je te lis, et je regarde tes codes python… À part la partie syntaxe, où j'ai cru voir une méthode close inutile avec l'instruction with open, je n'ai pas grand chose à dire, par contre,

La partie concrète de ton exercice je ne là comprend pas ! Je veux dire que, c'est une suggestion, mais ne peut-on pas partir d'un cas concret, un problème que tu as déjà rencontré où ton travail permettrait de le résoudre ?

Je dis ça parce-que de plus en plus, on choisit une technologie selon un besoin concret, et malgré une technique ultra performante de ta part concernant l'assembleur, ton premier post m'a interpellé, quand tu dis ceci !

En fait nous allons faire mieux : nous allons désassembler un programme écrit en C, puis le ré-assembler juste pour le fun.

Ok, mais n'étant pas masochiste, je souhaiterais avoir un autre objectif pour te suivre… Aussi j'aurais souhaité des explications sur le code assembleur, qui présenté comme cela fait décrocher le lecteur et même si tu comptes sur la recherche de tes lecteurs, tu n'auras plus que les 10% restants pour la suite de la lecture.

Bref tout ça est très bien, et au contraire je t'encourage pour cet exercice, mais le concret c'est ce qui attire, l'abstrait de moins en moins, et crois moi, je sais de quoi je parle ;)

Édité par fred1599

+0 -0
Auteur du sujet

Merci pour ton message, fred1599.

Je prends bonne note de ta coquille repérée sur mon code python…

Ok, mais n'étant pas masochiste, je souhaiterais avoir un autre objectif pour te suivre… Aussi j'aurais souhaité des explications sur le code assembleur, qui présenté comme cela fait décrocher le lecteur et même si tu comptes sur la recherche de tes lecteurs, tu n'auras plus que les 10% restants pour la suite de la lecture.

Je ne suis pas masochiste non plus. ^^

Tu souhaites des explications sur le code du dernier message en particulier ? Car il n'y a pas grand chose à dire sur la suite de db.

Bref tout ça est très bien, et au contraire je t'encourage pour cet exercice, mais le concret c'est ce qui attire, l'abstrait de moins en moins, et crois moi, je sais de quoi je parle ;)

Ce sont des motivations personnelles avant tout que je n'étaierai pas ici. Mais n'hésite pas à me poser la question par MP si tu tiens vraiment à savoir… :)

+0 -1

Je ne suis pas masochiste non plus.

Pour ton cas ça serait plutôt sadique :)

Tu souhaites des explications sur le code du dernier message en particulier ? Car il n'y a pas grand chose à dire sur la suite de db.

Je ne veux rien, je suis capable de faire une recherche, je dis juste qu'avoir un objectif se rapportant à un problème concret aurait permis d'intéresser plus de membres du forum.

Ce sont des motivations personnelles avant tout que je n'étaierai pas ici. Mais n'hésite pas à me poser la question par MP si tu tiens vraiment à savoir…

Avoir des motivations personnelles, c'est la meilleure manière d'apprendre mais aussi d'enseigner ;)

+0 -0
Auteur du sujet

Pour ton cas ça serait plutôt sadique :)

C'est plutôt semblable au masochisme. Je ne recherche nullement la souffrance mais c'est quelque chose que tu peines à comprendre car tu n'appréhendes peut-être pas ce domaine de la même manière que je l'appréhende.

Je profite de ce post pour poster un hello world en ELF x86 de but en blanc.

Tout d'abord le fichier de définitions elf.asm qui va nous permettre de travailler avec des defines plutôt qu'avec des valeurs en dures, parce que quand même, on reste des programmeurs fainéants :

defines/elf.asm

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
; These constants are for the segment types stored in the image headers
%define PT_NULL    0
%define PT_LOAD    1
%define PT_DYNAMIC 2
%define PT_INTERP  3
%define PT_NOTE    4
%define PT_SHLIB   5
%define PT_PHDR    6
%define PT_TLS     7        /* Thread local storage segment */
%define PT_LOOS    0x60000000   /* OS-specific */
%define PT_HIOS    0x6fffffff   /* OS-specific */
%define PT_LOPROC  0x70000000
%define PT_HIPROC  0x7fffffff
%define PT_GNU_EH_FRAME     0x6474e550

%define PT_GNU_STACK    (PT_LOOS + 0x474e551)

; These constants define the different elf file types
%define ET_NONE   0
%define ET_REL    1
%define ET_EXEC   2
%define ET_DYN    3
%define ET_CORE   4
%define ET_LOPROC 0xff00
%define ET_HIPROC 0xffff

; These constants define the various ELF target machines
%define EM_NONE         0
%define EM_M32          1
%define EM_SPARC        2
%define EM_386          3
%define EM_68K          4
%define EM_88K          5
%define EM_860          6
%define EM_MIPS         7
%define EM_PARISC       8
%define EM_SPARC32PLUS  9
%define EM_PPC          10
%define EM_PPC64        11
%define EM_S390         12
%define EM_ARM          13
%define EM_SH           14
%define EM_SPARCV9      15
%define EM_IA_64        16
%define EM_X86_64       17
%define EM_VAX          18

%define EI_NIDENT   16

struc Elf32_Ehdr
    .e_ident:        resb    16
    .e_type:         resw    1
    .e_machine:      resw    1
    .e_version:      resd    1
    .e_entry:        resd    1
    .e_phoff:        resd    1
    .e_shoff:        resd    1
    .e_flags:        resd    1
    .e_ehsize:       resw    1
    .e_phentsize:    resw    1
    .e_phnum:        resw    1
    .e_shentsize:    resw    1
    .e_shnum:        resw    1
    .e_shstrndx:     resw    1

    .size:
endstruc

; These constants define the permissions on sections in the program
; header, p_flags.
%define PF_R        0x4
%define PF_W        0x2
%define PF_X        0x1

struc Elf32_Phdr
    .p_type:         resd    1
    .p_offset:       resd    1
    .p_vaddr:        resd    1
    .p_paddr:        resd    1
    .p_filesz:       resd    1
    .p_memsz:        resd    1
    .p_flags:        resd    1
    .p_align:        resd    1

    .size:
endstruc

%define EI_MAG0     0   ; e_ident[] indexes
%define EI_MAG1     1
%define EI_MAG2     2
%define EI_MAG3     3
%define EI_CLASS    4
%define EI_DATA     5
%define EI_VERSION  6
%define EI_OSABI    7
%define EI_PAD      8

%define ELFMAG0     0x7f    ; EI_MAG
%define ELFMAG1     'E'
%define ELFMAG2     'L'
%define ELFMAG3     'F'
%define ELFMAG      `\177ELF`
%define SELFMAG     4

%define ELFCLASSNONE    0   ; EI_CLASS
%define ELFCLASS32  1
%define ELFCLASS64  2
%define ELFCLASSNUM 3

%define ELFDATANONE 0   ; e_ident[EI_DATA]
%define ELFDATA2LSB 1
%define ELFDATA2MSB 2

%define EV_NONE     0   ; e_version, EI_VERSION
%define EV_CURRENT  1
%define EV_NUM      2

%define ELFOSABI_NONE   0
%define ELFOSABI_LINUX  3

Et le code qui décrit les en-têtes ELF minimalistes ainsi que du code exécutable :

test_elf.asm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
bits 32
ORG 0x08048000
header:
    istruc Elf32_Ehdr
        at Elf32_Ehdr.e_ident,      db `\x7FELF`, ELFCLASS32, ELFDATA2LSB, EV_CURRENT, ELFOSABI_NONE, 0, 0, 0, 0
        at Elf32_Ehdr.e_type,       dw ET_EXEC
        at Elf32_Ehdr.e_machine,    dw EM_386
        at Elf32_Ehdr.e_version,    dd EV_CURRENT
        at Elf32_Ehdr.e_entry,      dd _start
        at Elf32_Ehdr.e_phoff,      dd (program_section_headers - header)
        at Elf32_Ehdr.e_shoff,      dd 0
        at Elf32_Ehdr.e_flags,      dd 0
        at Elf32_Ehdr.e_ehsize,     dw Elf32_Ehdr.size
        at Elf32_Ehdr.e_phentsize,  dw Elf32_Phdr.size
        at Elf32_Ehdr.e_phnum,      dw 1
        at Elf32_Ehdr.e_shentsize,  dw 0
        at Elf32_Ehdr.e_shnum,      dw 0
        at Elf32_Ehdr.e_shstrndx,   dw 0
    iend

program_section_headers:
    istruc Elf32_Phdr
        at Elf32_Phdr.p_type,   db PT_LOAD
        at Elf32_Phdr.p_offset, dd 0
        at Elf32_Phdr.p_vaddr,  dd header
        at Elf32_Phdr.p_paddr,  dd header
        at Elf32_Phdr.p_filesz, dd (end - header)
        at Elf32_Phdr.p_memsz,  dd (end - header)
        at Elf32_Phdr.p_flags,  dd PF_X | PF_R
        at Elf32_Phdr.p_align,  dd 0x1000
    iend


data:
    HelloWorld  db "Hello world!", 10
_start:
    mov eax, 4 ; sys_write
    mov ebx, 1 ; stdout
    mov ecx, HelloWorld
    mov edx, 13
    int 0x80

    mov eax, 1 ; sys_exit
    xor ebx, ebx
    int 0x80

end:

Assemblage et exécution :

1
2
3
4
5
6
% nasm -f bin ./test_elf.asm -o test_elf
% chmod +x ./test_elf
% ./test_elf
Hello world
% echo $?
0

Édité par anonyme

+0 -0
Auteur du sujet

Tu envisages de faire un tutoriel avec ça ?

Vayel

L'idée est tentante, mais contrairement à Dominus Carnufex je n'aurai pas la patience pour faire ce genre de chose.

Pour programmer en assembleur il faut tout un tas de pré-requis que je ne suis pas sûr de posséder/pouvoir énumérer.

Pour le moment, comme tu peux le voir, je me contente d'une série d'articles puisqu'il s'agit de travaux ponctuels et peu chronophages. J'ai l'idée de regrouper ça en une série de tutoriels mais cela requiert, hélas, davantage de discipline. :P

+0 -3
Auteur du sujet

Reste à savoir jusqu'où tu pousses le vice.

Au passage, si tu connais un désassembleur capable de produire des sources réassemblables sans trop de heurt voire zéro modification, je suis preneur.

(IDA n'est pas une bonne réponse :P )

+0 -0

Plus je lis ce topic, plus je me dis que tu as du bouffer de gros paquets de doc pour arriver à ce niveau de compétence sur x86. Je ne m'attends pas à ce niveau de connaissance du système chez beaucoup d'informaticiens. C'est surement incompréhensible pour le néophyte, mais je trouve ça super intéressant (mais pas au point de développer sur x86, il ne faut pas déconner non plus)

Édité par Natalya

+2 -0
Auteur du sujet

:D !!

super intéressant (mais pas au point de développer sur x86, il ne faut pas déconner non plus)

L'idée, c'est que tu puisses re-développer sur x86 derrière. Plus ou moins. Les scripts ne doivent pas nécessairement se limiter au désassemblage d'un ELF. Tu pourrais très bien adapter la couverture de code à un bootloader ou bien à un firmware d'une autre architecture. :P

+1 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte