Metasm VS Challenge T2'08

Wed 15 October 2008 by alex

La conférence T2 se déroule à Helsinki, en Finlande, et propose chaque année un défi de reverse-engineering. Nous allons l'étudier à l'aide de Metasm et je profiterai de ce billet pour présenter une nouvelle fonctionnalité de l'outil, développée pour l'occasion.

Pour rappel les protections implémentées les années précédentes étaient :

  • 2006 : une machine virtuelle très académique, code très propre, relativement facile à analyser.
  • 2007 : une machine virtuelle largement protégée, nous avons présenté notre démarche l'an dernier à SSTIC

L'objectif récurrent du challenge est de retrouver une adresse email camouflée dans le binaire.

Ceci étant dit, concentrons-nous sur l'édition de cette année, sensiblement différente des précédentes. Les auteurs ont souhaité apporter un coté plus empreint de la réalité, et en particulier des ''malware in the wild", ce point a d'ailleurs été discuté dans les commentaires qui ont faits suite à la publication du challenge. En pratique cette orientation se matérialise par :

  • Un binaire "fonctionnel" : il n'est plus une simple boite noire recevant en entrée un mot de passe et testant sa validité ; nous avons droit à un jeu complet : un Tetris modifié pour l'occasion. Cela a des conséquences directes importantes : une des difficultés majeures consiste à retrouver les portions réellement intéressantes dans ce volume de code
  • L'utilisation de packers : un examen rapide rapide révèle en effet que l'exe principal est packé avec PeCompact et la dll avec...Themida. Va pour PeCompact, pour résumer sauvagement cela s'apparente à UPX avec un SEH; en revanche Themida est plus problématique. Ce point a d'ailleurs très vite été soulevé sur le site du défi et la réponse des auteurs a été immédiate : “This is most definitely *NOT* an unpacking challenge, but rather, a reverse engineering challenge!” Et effectivement, nous verrons que l'unpacking de la dll est superflu. Themida nous imposera toutefois de camoufler la présence d'un éventuel debugger afin de ne pas être détecté par la protection.

PeCompact se supprime en quelques secondes pour laisser le champ libre à un autre problème autrement plus conséquent : certaines fonctions de l'exe sont massivement obfusquées. Ce point m'intéressant, j'ai donc, pour commencer, totalement laissé de coté la dll pour me concentrer sur l'exe et l'obfuscateur utilisé. Après une étude assez rapide, il apparait que l'obfuscateur est relativement basique; nous retrouvons la plupart des notions que nous avions developpées l'an dernier : sauts conditionnels biaisés, insertion de faux appels de fonctions (avec un nombre de faux paramètres variable), etc.. De manière pragmatique la majorité du code que nous avions déjà développé l'an dernier à savoir un CPU filtrant utilisé sur le T2'07 ainsi que le post-traitement réalisé sur le graphe de contrôle développé pour pouet (pour rappel, nos scripts sont disponibles ici), peut tout à fait être réutilisé, avec toutefois une mise à jour nécessaire des patterns.

Cet outil bien que bancal (c'est un euphémisme), donnait des résultats assez satisfaisants. Toutefois, un écueil majeur subsistait : un temps de désassemblage outrageusement élevé même sur des fonctions a priori assez réduites. Un type de patterns mettait en échec le désassembleur de Metasm :

4126c7h  jz loc_4126D3
4126c9h  nop
4126cah  jnz loc_4126D3

Cette construction à base de deux sauts conditionnels aux conditions inversées est équivalente à un saut inconditionnel. Ces deux sauts peuvent être séparés par un ou plusieurs autres patterns. Le constat est simple, en revanche s'il est très aisé de détecter ce genre de construction par un parcours de graphe, le désassembleur n'en a pas encore conscience et va à ce moment là partir sur une fausse piste d'où une durée du désassemblage excessive et inutile.

Suite à une discussion avec Yoann (développeur de Metasm), la solution la plus simple et la plus générique consiste en une nouvelle fonctionnalité totalement inédite de Metasm : il est maintenant possible d'enregistrer un callback dans le désassembleur. Ce callback sera appelé à chaque fois que le désassembleur découvre une nouvelle adresse à explorer. Voici son prototype :

dasm.callback_newaddr = proc { |target, orig|
    next target if not di = dasm.decoded[orig]

    # do your own stuff

    # return value
    target
}

target est la nouvelle adresse à explorer, orig l'adresse de l'instruction amenant cette nouvelle adresse : un saut (jmp, jxx), un appel (call), etc. Si le callback renvoie une valeur nulle pour target, alors cette dernière ne sera pas explorée. Pour illustrer cela, prenons maintenant l'exemple suivant :

404b74h xor esi, esi
404b76h jz loc_404b83h

Ce petit pattern, présent dans le code obfusqué, est très simple à détecter et à neutraliser. L'instruction xor met à jour les flags, en particulier le flag z à zéro, le saut conditionnel est en fait inconditionnel. La fonction suivante utilise donc callback_newaddr, et le cas échéant supprime le saut conditionnel malicieux.

def immunize_disasm(dasm)
    discard_junk = proc { |addr, newaddr|
        addr = dasm.normalize(addr)
        newaddr = dasm.normalize(newaddr)

        dasm.split_block(dasm.decoded[addr].block, addr)
        dasm.decoded[addr].block.each_from_normal { |fm|
            tn = dasm.decoded[fm].block.to_normal
            tn.delete_if { |t| dasm.normalize(t) == addr }
            tn << newaddr
            dasm.addrs_todo << [newaddr, fm]
        }
        dasm.addrs_todo.each { |at|
            next if dasm.normalize(at[0]) != addr
            at[0] = newaddr
            if tdi = dasm.decoded[at[1]]
                tdi.block.to_normal.delete_if { |t| dasm.normalize(t) == addr }
                tdi.block.to_normal << newaddr
            end
        }
    }
    dasm.callback_newaddr = proc { |target, orig|
        next target if not di = dasm.decoded[orig]
        case di.instruction.opname
        when 'jz'
            if  di.block.list.length >= 3 and ppdi = di.block.list[-2] and
                pdi = di.block.list[-1] and ppdi.instruction.opname == 'xor' and
                ppdi.instruction.args.first.to_s == ppdi.instruction.args.last.to_s
                    discard_junk[ppdi.address, di.instruction.args.first]
                    target = nil
            end
        end
        target
    }
end

Le callback teste donc la présence d'une instruction jz précédée par une instruction xor op, op, enfin discard_junk sert simplement à mettre à jour l'état interne au désassembleur du graphe de contrôle. En appliquant ce même principe pour les autres patterns et en complément des méthodes développées précédemment, nous sommes en mesure d'obtenir un code très proche de l'original.

Disposant alors d'un aperçu du binaire en clair, sans packer ni obfuscation, je suis revenu vers le défi lui même. Il ne restait plus qu'à jouer un peu pour obtenir une piste :

T2.png

Après un temps variable de jeu, apparait une pièce particulière et ce fameux message : “Catch me if you can”. Comment et surtout pourquoi ce message est-il affiché ?

  • Tout d'abord comment ?

Un rapide coup d'oeil sur les imports de l'exe et l'api TextOutA s'impose à nous. En s'attachant à l'aide d'un debugger au défi et en traçant les appels à cette api, nous retrouvons le rafraîchissement de toutes les chaines de caractères présentes dans l'interface et en particulier l'affichage de la chaine magique. Problème : le code affiché dans le debugger est obfusqué, nous retournons donc vers le code nettoyé à l'aide de Metasm.

  • Pourquoi ?

En remontant un peu le code depuis la mise à jour de l'affichage, nous trouvons ceci :

graphrand.png

Un rand modulo 0xFFF sert d'index dans un tableau afin d'obtenir le numéro du type de pièce à afficher. Une brève analyse nous permet de faire la correspondance entre l'indice et le type de pièce, 7 étant celui de la pièce magique. Or, si nous allons voir ce tableau nous nous apercevons qu'il est modifié dynamiquement : des 7 sont inscrits dedans... Sans plus attendre, nous posons un point d'arrêt en écriture sur tout le tableau et sous nos yeux ébahis:

mailbreakshort.png

Au vu de l'adresse, nous ne sommes pas dans le code de l'exe principal mais dans un buffer alloué dynamiquement, très sûrement par la dll et donc Themida. Au passage, ce point illustre une faiblesse intrinsèque des packers : à un moment ou à un autre le code est présent en clair en mémoire. Le petit bout de code ci-dessus est responsable de l'écriture des 7 dans la table, mais ce qui nous intéresse le plus est bien évidement l'adresse email (46ce...@t2.fi) présente juste sous la dernière instruction ret : l’affaire est pliée.

La démarche présentée ici est partielle, de nombreuses zones d'ombres subsistent. Pour une étude plus poussée, il serait intéressant de faire un dump du buffer en notant bien son adresse de base, ensuite à le chargeant à cette même adresse dans un désassembleur; nous obtiendrions alors de nouvelles informations. Il s'avère que cette portion de code est chiffrée/déchiffrée à la volée, à l'aide d'un algorithme de chiffrement symétrique Blowfish, la clé étant en dur dans le code, l'adresse email est elle-même dérivée par une fonction de hachage SHA512.

Au final le défi de cette année m'a laissé un sentiment mitigé. En premier lieu, de la déception : il faut dire que nous nous étions particulièrement amusés sur le défi de l'an dernier et celui-ci parait quelque peu fade si l'on tente de les comparer, retrouver l'adresse ne représente pas un challenge en soi. Ensuite, avec un peu plus de recul, j'en suis tout de même venu à apprécier le puzzle proposé. La démarche est sans doute plus subtile et ludique, évitant au passage une course à l'armement sans fin. Le défi est intelligemment conçu avec ce qu'il faut d'indice pour le rendre accessible. Enfin, tout comme l'an dernier, ce défi aura été l’occasion de faire progresser Metasm, pour la plus grande joie de la grande communauté de ses utilisateurs. Un grand merci aux auteurs de ces challenges de qualité. Je ne conclurai pas sans saluer Florent (premier à avoir résolu le défi), Fabrice (prix de l'élégance), ainsi que le staff de la conférence et en particulier Tomi.