Dump d'un processus avec Metasm

Tue 22 April 2008 by alex

Nous allons prendre l'excuse de vouloir coder un petit unpacker dynamique pour UPX afin de voir comment implémenter très facilement le cœur d'un débugger à l'aide de Metasm.

Metasm est un framework open source de manipulation de code machine. Pour développer notre debugger, nous utilisons l'interface fournie par Metasm sur l'API de debug Windows. Le code d'initialisation qui suit charge le binaire en mémoire, créé l'objet PE, recherche le point d'entrée original (OEP) et enfin, entre dans la boucle de débuggage.

class  Unpacker < WinDbg
    def initialize(file, oep=nil)
        pe = PE.decode_file(file)
        @oep = oep || find_oep(pe)
        @baseaddr = pe.optheader.image_base
        super(file)
        debugloop
    end
end

La version d'UPX utilisée pour cet exemple (compression de notepad.exe) insère un saut vers le véritable point d'entrée (OEP) à la fin du code de décompression. Ce saut est en direction de la section UPX0 alors que le stub de décompression est situé dans la section UPX1. Cette caractéristique nous permet de le retrouver simplement :

def find_oep(pe)
    # Desassemble le loader UPX
    dasm = pe.disassemble('entrypoint')
    # Recherche du saut cross section
    cross_section = dasm.decoded.values.find{|disasmInstr|
        disasmInstr.instruction.opname == 'jmp' and
        s = dasm.get_section_at(disasmInstr.instruction.args.first) and
        s != dasm.get_section_at(disasmInstr.address)
    }

    # Renvoie l'argument de l'instruction, c'est-à-dire la cible du saut
    dasm.normalize(cross_section.instruction.args.first)
end

Maintenant l'OEP connu, comment poser un point d'arrêt dessus (breakpoint) ? Tout d'abord, nous interceptons l'événement de debug lié à la création du thread principal en surchargeant le handler par défaut. Il est alors très facile de jouer avec le contexte du processus débuggé afin de mettre en place un point d'arrêt matériel à l'aide des debug registers :

def handler_newthread(pid, tid, info)
    super\
    ctx = get_context(pid, tid)
    ctx[:dr0] = @oep
    ctx[:dr6] = 0
    ctx[:dr7] = 1
    WinAPI::DBG_CONTINUE
end

Comment recevoir les événements liés au point d'arrêt nouvellement installé ? Lorsqu'il est atteint, un point d'arrêt matériel lève une exception int1, nous rajoutons donc le handler suivant:

def handler_exception(pid, tid, info)
    case info.code
    when WinAPI::STATUS_SINGLE_STEP
        if get_context(pid, tid)[:eip] == @oep
            dump_process(pid)
            WinAPI::TerminateProcess(@hprocess[pid], 0)
        end
    end
    super
    WinAPI::DBG_CONTINUE
end

Le dumping du processus est très simple.

Nous avons toutefois une légère modification à accomplir : il faut marquer la section qui contient le code original comme exécutable.En effet, dans le programme compressé celle-ci est marquée simplement read/write, ce qui rendrait impossible l'exécution du binaire obtenu sur une machine ou le DEP serait activé par exemple.

def dump_process(pid)
    pe_dmp = LoadedPE.memdump(@mem[pid], @baseaddr, @oep)
    pe_dmp.sections.first.characteristics = %w[MEM_READ MEM_WRITE MEM_EXECUTE]
    pe_dmp.encode_file('dumped.exe')
end

Pour conclure, ce petit exemple illustre quelques unes des possibilités d'interactions à différents niveaux d'abstraction du binaire.

Le script complet est accessible dans le répertoire samples/dump_upx.rb de Metasm.

file = 'NOTEPAD.EXE'

require 'metasm'
include Metasm
include WinAPI

class  Unpacker < WinDbg

    attr_accessor :oep

    def initialize(file, oep=nil)
        pe = PE.decode_file(file)
        @oep = oep || find_oep(pe)
        @baseaddr = pe.optheader.image_base
        super(file)
        debugloop
    end

    def find_oep(pe)
        dasm = pe.disassemble 'entrypoint'
        cross_section = dasm.decoded.values.find{|disasmInstr|
            disasmInstr.instruction.opname == 'jmp' and
            s = dasm.get_section_at(disasmInstr.instruction.args[0]) and
            s != dasm.get_section_at(disasmInstr.address)
        }
        dasm.normalize(cross_section.instruction.args[0])
    end

    def handler_newthread(pid, tid, info)
        super
        ctx = get_context(pid, tid)
        ctx[:dr0] = @oep
        ctx[:dr6] = 0
        ctx[:dr7] = 1
        WinAPI::DBG_CONTINUE
    end

    def handler_exception(pid, tid, info)
        case info.code
        when WinAPI::STATUS_SINGLE_STEP
            if get_context(pid, tid)[:eip] == @oep
                dump_process(pid)
                WinAPI::TerminateProcess(@hprocess[pid], 0)
            end
        end
        super
        WinAPI::DBG_CONTINUE
    end

    def dump_process(pid)
        pe_dmp = LoadedPE.memdump(@mem[pid], @baseaddr, @oep)
        pe_dmp.sections.first.characteristics = %w[MEM_READ MEM_WRITE MEM_EXECUTE]
        pe_dmp.encode_file('dumped.exe')
    end

end
dbg = Unpacker::new(file)\r