mwemu-mcp

emulação binária, x86 e várias funções de SO (API do Windows, API de driver Linux, espaço de usuário Linux)

Documentação

Introdução

Bem-vindo à documentação oficial do MWEMU. Você pode rolar a página ou usar os links diretos à esquerda.

Repositório no Github: https://github.com/sha0coder/mwemu

O que é o MWEMU?

O MWEMU é um emulador de hardware e simulador de processos Windows escrito em Rust puro, do zero. Foi criado por @sha0coder e é open source. Os contribuidores deste software melhoraram muito a qualidade do projeto. Clique aqui para conhecê-los.

O hardware implementado é x86/x64. Diferente de outros emuladores, também implementa partes do SO (principalmente Windows), porque o código assembly, mais cedo ou mais tarde, fará chamadas de sistema (WinAPI, syscalls Linux, etc.).

Ele se mostrou extremamente útil para desofuscação de malware, mas isso não substitui a engenharia reversa. É necessário um trabalho prévio de reversão para preparar bem o estado inicial da emulação, e emular apenas um pequeno grupo de funções, como descriptografia, keygen, etc. Em alguns casos específicos, o mwemu pode fazer emulação completa, com packers simples, shellcodes codificados, etc.

A emulação e a simulação são implementadas do zero, mas a excelente biblioteca Rust iced-x86 é usada para a tradução de um conjunto de bytes em objetos de instrução. Implementei mais de 300 instruções x86, flags, exceções, alguns loaders PE32/PE64/ELF64/shellcode32/shellcode64 e muitas outras coisas.

O MWEMU é extremamente rápido e também seguro em relação à memória, graças ao incrível compilador Rust.

Além de binários em modo usuário e shellcodes, o mwemu também pode carregar e emular módulos de kernel Linux (.ko) para caçar bugs de segurança de memória em drivers sem um kernel real — veja Modo kernel: drivers Linux.

As 4 formas de usar o MWEMU

1. A primeira forma é via linha de comando. Veja linha de comando para mais detalhes. Isso serve para tentar emulação completa.

2. A segunda forma é criar scripts Python com o módulo pymwemu, que pode ser instalado via pip ou git.

pypi: https://pypi.org/project/pymwemu/

Se você precisar de recursos ou correções de bugs implementados recentemente, use o git. Será necessário compilar o projeto completo e depois os bindings Python com maturin. Mais detalhes nesta seção: instalação do pymwemu

3. A terceira forma é usar o crate Rust publicado no crates.io https://crates.io/crates/libmwemu a partir de uma aplicação Rust.

4. A quarta forma é o mwemu-mcp, um servidor Model Context Protocol que expõe a libmwemu para clientes MCP como o Claude. Funciona como dirigir o pymwemu manualmente, mas via MCP: você abre uma sessão para uma arquitetura, configura-a, prepara a memória (allocs, escrita de registradores e memória), então emula passo a passo e inspeciona o resultado, tudo por meio de ferramentas discretas. Isso permite que um agente de IA dirija o emulador para analisar um binário. Mais detalhes: github.com/sha0coder/mwemu/crates/mwemu-mcp

Arquiteturas

Você pode executar o MWEMU no Windows, Linux e Mac (x86 e também m1).

Mas apenas código x86 (32 bits e 64 bits) pode ser emulado, principalmente para Windows. Há suporte a shellcode Linux, e syscalls são implementados, mas em relação a ELF apenas 64 bits, somente compilação estática e suporte bastante básico por enquanto. Sem problemas com shellcodes.

A libc pode ser emulada bem, apesar de estar cheia de instruções ymm, mas o linker não pode ser totalmente emulado até agora. Meu plano é emular completamente o linker; nesse caso, não preciso implementar todo o processo de linking (criação de .got e .plt, etc.).

Um pouco dos internals

Por enquanto, apenas uma visão geral básica dos internals.

No passado, nomeei o projeto de SCEMU, e ele estava armazenado em 3 repositórios separados: mwemu (a linha de comando), pymwemu (módulo Python) e libmwemu (o motor onde tudo é implementado e que também é o módulo Rust crate no crates.io).

Depois foi renomeado para MWEMU, porque SCEMU é mais específico para shellcode e porque é uma palavra feia em italiano.

Então, agora é um único repositório com uma pasta crates/ contendo os 3 crates.

Os testes são implementados em crates/libmwemu/src/tests/ e são descritos mais adiante.

A maioria dos arquivos foi dividida em arquivos menores.

Shell

~/s/mwemu ❯❯❯ ls crates/libmwemu/src/
banzai.rs       emu/               kuser_shared.rs
breakpoint.rs   emu_context.rs     lib.rs   
colors.rs       engine/            macros.rs
config.rs       err.rs             maps/    
console.rs      exception.rs       ntapi/   
constants.rs    exception_type.rs  pe/      
context/        flags.rs           peb/     
crit_state.rs   fpu/               regs64.rs    
definitions.rs  fpu.rs             script.rs
eflags.rs       global_locks.rs    serialization/
elf/            hooks.rs           structures/
syscall/        tests/             thread_context.rs
threading.rs    tools/             tracing.rs
utils.rs w      inapi/

emu/ contém métodos e subobjetos de emulação envolvidos na própria emulação.

Em engine/ estão as implementações de todas as instruções.

winapi/ contém as implementações da WinAPI divididas em winapi32/ e winapi64/.

Há outras coisas como contants.rs, structures/, etc.

Modo syscall (SSDT)

Além de emular a WinAPI de alto nível, o mwemu pode rodar em um modo de syscall de nível mais baixo (também chamado de modo SSDT), habilitado com a flag --ssdt (alias --syscall-mode) na linha de comando (ou a configuração ssdt_use_ldr_initialize_thunk na API).

Nesse modo, o mwemu não ignora o userland: ele carrega um ntdll.dll genuíno e deixa a amostra ir até a instrução syscall, exatamente como em um Windows real. Isso é mais próximo da realidade e muito útil para estudar malware que chama syscalls diretamente para evadir hooks no userland.

Para despachar um syscall, o mwemu precisa mapear um System Service Number (SSN) para a rotina de kernel que ele representa. Esses números não são fixos: eles mudam entre builds do Windows. Então, em vez de codificar uma tabela fixa, o mwemu os resolve a partir do próprio ntdll.dll carregado, percorrendo sua Export Address Table (EAT) para encontrar os stubs Nt* / Zw* e lendo o SSN codificado dentro de cada stub (o mov eax, ssn logo antes do syscall). Assim, o SSDT é sempre consistente com a versão exata do Windows em uso (veja o recurso load_maps_from_winver() para buscar um build específico).

O diagrama a seguir mostra esse layout de resolução da EAT, como os SSNs são extraídos da tabela de exportação do ntdll para construir a tabela de despacho de syscalls:

Sistema de Testes

Para acionar localmente, use make tests. Isso baixa alguns binários e inicia o sistema de testes cargo test.

Não use --release; sempre é mais conveniente fazer os testes sem aplicar otimizações, que podem ignorar alguns tipos de erro. Na verdade, o CI do Github está configurado para executar cargo test e também cargo test --release para verificar ambos os modos.

Todo git push ou pull-request acionará o CI no Github para executar todos os testes no Linux, Windows e Mac. No caso de PR, é obrigatório; no caso de git push, é apenas informativo.

PR também aciona uma análise de cobertura dos testes, que atualmente é de apenas 32%.

Contribuidores do Projeto

Brandon Ros

Archeron2302

ElCapor

Há também outras pessoas que sugeriram ideias interessantes e otimizações.

Sobre wit00, é uma falha do github para git push com configuração ruim no git config. (O bug foi reportado ao github.)

Eu sou @sha0coder e criei este software para potencializar meus trabalhos de engenharia reversa. Estou compartilhando porque acho útil para alguns casos.

Alguns gráficos: https://github.com/sha0coder/mwemu/graphs/contributors

Licença

Na verdade, há várias licenças: o código-fonte é GPLv3, mas o módulo Rust do crates.io e o módulo Python do pypi são MIT, para ter menos restrições na distribuição de software que usa libmwemu ou pymwemu.

https://github.com/sha0coder/mwemu/blob/main/LICENSE

Não hesite em me contatar para criar tecnologias baseadas neste software.

email: sha0 at badchecksum dot net

Ferramenta de linha de comando do MWEMU

A linha de comando é uma forma rápida de usar o mwemu, e há muitos recursos como rastreamento de registradores/memória/chamadas/strings ou captura de momentos da emulação.

Se o packer for simples, provavelmente pode ser totalmente emulado usando a ferramenta de linha de comando, mas se você precisar de mais controle, use pymwemu e, para controle total, libmwemu.

Em Rust, você pode compilar e executar junto com cargo run. Use o modo --release para execução mais rápida. Exemplo:

Shell

❯❯❯ cargo run --release -- -6 -f file -vv -c 100

Isso é equivalente a fazer:

Shell

❯❯❯ cargo build --release
❯❯❯ target/release/mwemu -6 -f file -vv -c 100

Instalação do MWEMU

1. Primeiro, você precisa instalar Rust e Cargo. A melhor forma é usando rustup.

https://rustup.rs/

Por exemplo, no Linux ou Mac:

Shell

❯❯❯ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Na configuração padrão, os binários estão em ~/.cargo/bin/, mas você precisa adicionar isso ao PATH, idealmente na última seção do PATH.

Shell

❯❯❯ export PATH="$PATH:/home/username/.cargo/bin/"

O instalador diz como colocar no bashrc ou em outros shells rc.

2. Depois, há 2 opções para instalar:

  • Instalar do crates.io (a segunda opção é mais conveniente) Shell
    ❯❯❯ cargo install mwemu
    
  • A forma recomendada é clonar o repositório do Github e compilar a partir dele. Com Cargo, é simples. Shell
    git clone https://github.com/sha0coder/mwemu.git
    cargo build --release
    make tests
    

--help

Use a opção de ajuda para ver as opções da linha de comando. Note que antes do "--" estão as flags do cargo e depois do "--" estão os parâmetros do programa, neste caso a linha de comando do mwemu.

Shell

~/s/mwemu ❯❯❯ cargo run --release -- --help                                                                                                        Finished \`release\` profile [optimized] target(s) in 0.06s
Running \`target/release/mwemu --help\`
MWEMU emulator for malware 0.7.12
@sha0coder

USAGE:
    mwemu [FLAGS] [OPTIONS]

FLAGS:
    -6, --64bits             enable 64bits architecture emulation
        --banzai             skip unimplemented instructions, and keep up emulating what can be emulated
        --call               enable call tracer
        --entropy            display changes in the entropy
        --flags              trace the flags hex value in every instruction.
    -F, --fpu                trace the fpu states.
    -h, --handle             handle Ctrl+C to spawn console
        --help               Prints help information
    -l, --loops              show loop interations, it is slow.
        --multithread        enable multithread emulation
    -n, --nocolors           print without colors for redirecting to a file >out
    -p, --stack_trace        trace stack on push/pop
    -t, --test               test mode
    -m, --trace_memory       trace all the memory accesses read and write.
    -r, --trace_registers    print the register values in every step.
        --version            Prints version information
    -v, --verbose            -vv for view the assembly, -v only messages, without verbose only see the api calls and
                             goes faster

OPTIONS:
    -A, --args                              provide arguments to the EXE like: --args '"aa" "bb"'
        --cmd                            launch a console command
    -b, --base <ADDRESS>                          set base address for code
    -c, --console                         select in which moment will spawn the console to inspect.
    -C, --console_addr <ADDRESS>                  spawn console on first eip = address
    -d, --dump                              load from dump.
    -a, --entry <ADDRESS>                         entry point of the shellcode, by default starts from the beginning.
    -e, --exit                          exit position of the shellcode
    -f, --filename                          set the shellcode binary file.
    -i, --inspect                      monitor memory like: -i 'dword ptr [ebp + 0x24]
    -L, --log <LOG_FILENAME>                      log output to file
    -M, --maps                              select the memory maps folder
        --mxcsr                            set mxcsr register
        --r10                                set r10 register
        --r11                                set r11 register
        --r12                                set r12 register
        --r13                                set r13 register
        --r14                                set r14 register
        --r15                                set r15 register
        --r8                                  set r8 register
        --r9                                  set r9 register
        --rax                                set rax register
        --rbp                                set rbp register
        --rbx                                set rbx register
        --rcx                                set rcx register
        --rdi                                set rdi register
        --rdx                                set rdx register
        --rflags                          set rflags register
        --rip                                set rip register
        --rsi                                set rsi register
        --rsp                                set rsp register
    -x, --script <SCRIPT>                         launch an emulation script, see scripts_examples folder
        --stack_address <ADDRESS>                 set stack address
    -s, --string <ADDRESS>                        monitor string on a specific address
    -T, --trace_filename          output trace to specified file
    -R, --trace_register     trace a specific register in every step, value and content
    -S, --trace_start                start trace at specified position
    -V, --verbose_at 
            start displaying assembly at specific position (is like -vv enabled in specific moment)

Emulação Completa

As principais opções são:

  • -6 para modo 64 bits (caso contrário, seria 32 bits)
  • -f para selecionar o caminho do arquivo.
  • -vv para visualizar o assembly emulado. (A emulação é mais rápida sem verbosidade; nesse caso, apenas mostra as chamadas de API)

Shell

❯❯❯ cargo run --release -- -6 -f test/elf64lin_cpu_arithmetics1.bin -vv

Capturando um momento

O MWEMU sempre exibe o número de instruções emuladas, e isso é um identificador único de um momento.

O momento 1 é a primeira instrução assembly. Se você adicionar a flag -c 1, o emulador parará antes de emular a instrução 1.

Shell

~/s/mwemu ❯❯❯ cargo run --release -- -6 -f test/elf64lin_cpu_arithmetics1.bin -vv -c 1
    Finished \`release\` profile [optimized] target(s) in 0.07s
     Running \`target/release/mwemu -6 -f test/elf64lin_cpu_arithmetics1.bin -vv -c 1\`
static elf64 detected.
Entry point pointing to .text 0x401000
-------
1 0x401000: inc   al
--- console ---
=>

A instrução 1 não será colorida, o que significa que essa instrução será emulada no próximo passo.

Isso abre o console nesse estado do emulador, e você pode pressionar Enter para emular passos ou o comando "h" para ver as opções.

Se o modo verboso não estiver definido, apenas WinAPI, syscalls etc. serão exibidos, mas também haverá um número que representa as instruções emuladas até aquele estado.

Há a opção de parar o emulador em um endereço específico com "-C addr" em maiúsculas. Mas note que o endereço pode ser acionado várias vezes e não é um identificador único como o momento -c.

Verbosidade

Há 4 níveis de verbosidade:

  • 0: Não use nenhum -v para verbosidade mínima; apenas chamadas WinAPI e syscalls serão impressas.
  • 1: Use -v para ver também algumas mensagens, como "código polimórfico detectado", etc.
  • 2: Use -vv para ver também o código assembly. O mwemu imprimirá cada instrução assembly, o que torna a emulação mais lenta.
  • 3: Use -vvv para ver também cada interação "rep". Em instruções com prefixo rep, como "rep movzx", será impressa uma linha para cada passo do loop rep.

Use -V ou --verbose_at para habilitar o modo verboso em um ponto específico.

O modo verboso é ativado automaticamente 100 instruções antes do momento -c configurado para parar.

Logging

É possível redirecionar a saída para um arquivo, por exemplo:

Shell

❯❯❯ cargo run --release -- -6 -f test/elf64lin_cpu_arithmetics1.bin -vv -c 1 > /tmp/log

Mas note que as cores são bytes de escape de terminal e serão registrados, dificultando o parsing. Se você fizer cat /tmp/log, verá as cores, mas se usar um editor, verá esses bytes.

É mais conveniente usar a opção --log para logs limpos.

Shell

❯❯❯ cargo run --release -- -6 -f test/elf64lin_cpu_arithmetics1.bin -vv -c 1 --log /tmp/log

Inicializando registradores

Há alguns casos, como emular DLLs ou trechos de código, que precisam de alguns valores iniciais nos registradores.

A ferramenta de linha de comando permite definir registradores usando estas opções:

Shell

--mxcsr <MXCSR>                           set mxcsr register
--r10 <R10>                               set r10 register
--r11 <R11>                               set r11 register
--r12 <R12>                               set r12 register
--r13 <R13>                               set r13 register
--r14 <R14>                               set r14 register
--r15 <R15>                               set r15 register
--r8 <R8>                                 set r8 register
--r9 <R9>                                 set r9 register
--rax <RAX>                               set rax register
--rbp <RBP>                               set rbp register
--rbx <RBX>                               set rbx register
--rcx <RCX>                               set rcx register
--rdi <RDI>                               set rdi register
--rdx <RDX>                               set rdx register
--rflags <RFLAGS>                         set rflags register
--rip <RIP>                               set rip register
--rsi <RSI>                               set rsi register
--rsp <RSP>                               set rsp register

Mas note que as cores são bytes de escape de terminal e serão registrados, dificultando o parsing. Se você fizer cat /tmp/log, verá as cores, mas se usar um editor, verá esses bytes.

É mais conveniente usar a opção --log para logs limpos.

Shell

❯❯❯ cargo run --release -- -6 -f test/elf64lin_cpu_arithmetics1.bin -c 1 --rax 0x123 --rbx 0x1337
Finished \`release\` profile [optimized] target(s) in 0.08s
Running \`target/release/mwemu -6 -f test/elf64lin_cpu_arithmetics1.bin -c 1 --rax 0x123 --rbx 0x1337\`
static elf64 detected.
Entry point pointing to .text 0x401000
--- console ---
=>r rax
rax: 0x123 291
=>r rbx
rbx: 0x1337 4919

Rastreando registradores

Use a opção -R <registers to trace> para rastrear alguns registradores.

Shell

❯❯❯ cargo run --release -- -6 -f test/elf64lin_cpu_arithmetics1.bin -vv -R rax,rsp
...
57 0x4010a2: rcr   al,1
58 rax: 0x82f61001 2197164033
58 rsp: 0x7fffffffe270
58 0x4010a4: rcr   ax,1
59 rax: 0x82f60800 2197161984
59 rsp: 0x7fffffffe270
59 0x4010a7: rcr   eax,1
60 rax: 0xc17b0400 3246064640
60 rsp: 0x7fffffffe270
60 0x4010a9: rcr   rax,1
61 rax: 0x60bd8200 1623032320
61 rsp: 0x7fffffffe270
61 0x4010ac: rcr   al,cl
62 rax: 0x60bd8200 1623032320
62 rsp: 0x7fffffffe270
62 0x4010ae: rcr   ax,cl
63 rax: 0x60bd8200 1623032320
63 rsp: 0x7fffffffe270
63 0x4010b1: rcr   eax,cl
64 rax: 0x60bd8200 1623032320
64 rsp: 0x7fffffffe270
64 0x4010b3: rcr   rax,cl
65 rax: 0x60bd8200 1623032320
65 rsp: 0x7fffffffe270
65 0x4010b6: mov   eax,90909090h ; 0x90909090
66 rax: 0x90909090 2425393296
66 rsp: 0x7fffffffe270
66 0x4010bb: mov   edx,90909090h ; 0x90909090
67 rax: 0x90909090 2425393296
67 rsp: 0x7fffffffe270
67 0x4010c0: mov   ecx,0 ; 0x0
68 rax: 0x90909090 2425393296
68 rsp: 0x7fffffffe270
68 0x4010c5: shrd  eax,edx,cl
69 rax: 0x90909090 2425393296
69 rsp: 0x7fffffffe270
69 0x4010c8: ret ; ret-addr: 0x0 ret-value: 0x90909090

Você pode rastrear um ou vários registradores ao mesmo tempo com a opção -R, mas sem espaços entre os registradores.

Rastreando memória

Há 2 formas de rastrear memória:

  • -m para rastrear todas as leituras e escritas de memória. (opcionalmente -S momento habilita o rastreador a partir de um momento específico)
  • -i 'dword ptr [eax + 0x8]' o modo de inspeção permite muitas expressões, mas não todas as combinações.

Shell

❯❯❯ cargo run --release -- -6 -f test/sc32win_donut.bin -vv -m -c 494253 -S 490000

Shell

❯❯❯ cargo run --release -- -6 -f test/sc32win_donut.bin -vv -m -i 'qword ptr [rsp + 0x8]'

Rastrear String

Tendo o endereço da string a ser rastreada, use -s <addr> para rastreá-la

Shell

❯❯❯ cargo run --release -- -6 -f test/sc64lin_strgen.bin -vv  -s 0x329ec8

Fazer verbose completo de milhões de instruções é mais lento que o modo não verbose, então vamos habilitar o modo verbose apenas quando for necessário.

Shell

❯❯❯ cargo run --release -- -6 -f test/sc64lin_strgen.bin -s 0x329ec8 -V 191

Outra opção é usar rastreadores sem modo verbose.

Shell

❯❯❯ cargo run --release -- -6 -f test/sc64lin_strgen.bin -s 0x329ec8

Rastrear Chamadas de Função

Seguir os caminhos de chamada pode ser útil ao combinar emulação com análise estática para ver de onde estamos vindo.

Shell

❯❯❯ cargo run --release -- -6 -f test/exe64win_enigma.bin --call

Neste caso, é mais conveniente não usar o modo verbose.

Console Interativo

Com a opção -c <num> o mwemu para a emulação quando atingir esse número de instruções emuladas, então abre um console.

Por exemplo, não queremos emular 102063765 instruções em modo verbose, é mais rápido não usar o modo verbose. A opção -c habilitará o modo verbose e rastreadores 100 instruções antes de atingir esse número, então quando o console for aberto teremos algum contexto anterior.

Shell

❯❯❯ cargo run --release -- -6 -f test/exe64win_enigma.bin -c 102063765
dll_path: maps/maps64//ntdll.dll dll: ntdll.dll
PE64 header detected.
loading memory maps
dll_path: maps/maps64//kernel32.dll dll: kernel32.dll
dll_path: maps/maps64//kernelbase.dll dll: kernelbase.dll
dll_path: maps/maps64//iphlpapi.dll dll: iphlpapi.dll
dll_path: maps/maps64//ws2_32.dll dll: ws2_32.dll
dll_path: maps/maps64//advapi32.dll dll: advapi32.dll
dll_path: maps/maps64//comctl32.dll dll: comctl32.dll
dll_path: maps/maps64//winhttp.dll dll: winhttp.dll
dll_path: maps/maps64//wininet.dll dll: wininet.dll
dll_path: maps/maps64//dnsapi.dll dll: dnsapi.dll
dll_path: maps/maps64//shell32.dll dll: shell32.dll
dll_path: maps/maps64//shlwapi.dll dll: shlwapi.dll
dll_path: maps/maps64//kernel32.dll dll: kernel32.dll
dll_path: maps/maps64//user32.dll dll: user32.dll
dll_path: maps/maps64//advapi32.dll dll: advapi32.dll
dll_path: maps/maps64//oleaut32.dll dll: oleaut32.dll
dll_path: maps/maps64//gdi32.dll dll: gdi32.dll
dll_path: maps/maps64//shell32.dll dll: shell32.dll
dll_path: maps/maps64//version.dll dll: version.dll
dll_path: maps/maps64//ole32.dll dll: ole32.dll
IAT Bound.
Delay load binding started ...
delay load bound!
entry point at 0x14072ce04
base: 0x140000000
102063740 0x14072d2ac: jne   000000014072D2A4h taken
102063741 0x14072d2a4: xor   [rax],dl
mem_trace: pos = 102063741 rip = 14072d2a4 op = read bits = 8 address = 0x14072d4cb value = 0x59 name = 'exe64win_enigma593000'
mem_trace: pos = 102063741 rip = 14072d2a4 op = write bits = 8 address = 0x14072d4cb value = 0x54 name = 'exe64win_enigma593000'
102063742 0x14072d2a6: inc   rax
102063743 0x14072d2a9: dec   rcx
102063744 0x14072d2ac: jne   000000014072D2A4h taken
102063745 0x14072d2a4: xor   [rax],dl
mem_trace: pos = 102063745 rip = 14072d2a4 op = read bits = 8 address = 0x14072d4cc value = 0x44 name = 'exe64win_enigma593000'
mem_trace: pos = 102063745 rip = 14072d2a4 op = write bits = 8 address = 0x14072d4cc value = 0x49 name = 'exe64win_enigma593000'
102063746 0x14072d2a6: inc   rax
102063747 0x14072d2a9: dec   rcx
102063748 0x14072d2ac: jne   000000014072D2A4h taken
102063749 0x14072d2a4: xor   [rax],dl
mem_trace: pos = 102063749 rip = 14072d2a4 op = read bits = 8 address = 0x14072d4cd value = 0x42 name = 'exe64win_enigma593000'
mem_trace: pos = 102063749 rip = 14072d2a4 op = write bits = 8 address = 0x14072d4cd value = 0x4f name = 'exe64win_enigma593000'
102063750 0x14072d2a6: inc   rax
102063751 0x14072d2a9: dec   rcx
102063752 0x14072d2ac: jne   000000014072D2A4h taken
102063753 0x14072d2a4: xor   [rax],dl
mem_trace: pos = 102063753 rip = 14072d2a4 op = read bits = 8 address = 0x14072d4ce value = 0x43 name = 'exe64win_enigma593000'
mem_trace: pos = 102063753 rip = 14072d2a4 op = write bits = 8 address = 0x14072d4ce value = 0x4e name = 'exe64win_enigma593000'
102063754 0x14072d2a6: inc   rax
102063755 0x14072d2a9: dec   rcx
102063756 0x14072d2ac: jne   000000014072D2A4h not taken
102063757 0x14072d2b2: jmp   000000014072D2BBh
mem_trace: pos = 102063758 rip = 14072d2bb op = read bits = 32 address = 0x14000303c value = 0x80 name = 'exe64win_enigma3000'
102063758 0x14072d2bb: mov   edi,[rsi+3Ch] ; 0x80
mem_trace: pos = 102063759 rip = 14072d2be op = read bits = 32 address = 0x140003110 value = 0x571000 name = 'exe64win_enigma3000'
102063759 0x14072d2be: mov   edi,[rdi+rsi+90h] ; 0x571000
102063760 0x14072d2c5: add   rdi,rsi
102063761 0x14072d2c8: cmp   dword ptr [rdi+0Ch],0
mem_trace: pos = 102063761 rip = 14072d2c8 op = read bits = 32 address = 0x14057400c value = 0x5722ac name = 'exe64win_enigma3000'
cmp: 0x5722ac > 0x0
102063762 0x14072d2cc: je    000000014072D370h not taken
mem_trace: pos = 102063763 rip = 14072d2d2 op = read bits = 32 address = 0x14057400c value = 0x5722ac name = 'exe64win_enigma3000'
102063763 0x14072d2d2: mov   ecx,[rdi+0Ch] ; 0x5722ac
102063764 0x14072d2d5: add   rcx,rsi
-------
102063765 0x14072d2d8: call  qword ptr [rbp+5960B4h]
--- console ---
=>

Comando de Ajuda

pressione h para ver os comandos disponíveis:

console mwemu

=>h
--- help ---
q ...................... quit
cls .................... clear screen
h ...................... help
s ...................... stack
v ...................... vars
sv ..................... set verbose level 0, 1 or 2
r ...................... register show all
r reg .................. show reg
rc ..................... register change
f ...................... show all flags
fc ..................... clear all flags
fz ..................... toggle flag zero
fs ..................... toggle flag sign
c ...................... continue
b ...................... breakpoint list
ba ..................... breakpoint on address
bi ..................... breakpoint on instruction number
bmr .................... breakpoint on read memory
bmw .................... breakpoint on write memory
bmx .................... breakpoint on execute memory
bcmp ................... break on next cmp or test
bc ..................... clear breakpoint
n ...................... next instruction
eip .................... change eip
rip .................... change rip
push ................... push dword to the stack
pop .................... pop dword from stack
fpu .................... fpu view
md5 .................... check the md5 of a memory map
seh .................... view SEH
veh .................... view vectored execption pointer
m ...................... memory maps
ms ..................... memory filtered by keyword string
ma ..................... memory allocs
mc ..................... memory create map
mn ..................... memory name of an address
ml ..................... memory load file content to map
mr ..................... memory read, speficy ie: dword ptr [esi]
mw ..................... memory write, speficy ie: dword ptr [esi]  and then: 1af
mwb .................... memory write bytes, input spaced bytes
md ..................... memory dump
mrd .................... memory read dwords
mrq .................... memory read qwords
mds .................... memory dump string
mdw .................... memory dump wide string
mdd .................... memory dump to disk
mdda ................... memory dump all allocations to disk
mt ..................... memory test
r2 [addr] .............. spawn radare2 console if it's isntalled
ss ..................... search string
sb ..................... search bytes
sba .................... search bytes in all the maps
ssa .................... search string in all the maps
ll ..................... linked list walk
d ...................... dissasemble
dt ..................... dump structure
pos .................... print current position
enter .................. step into
tr ..................... trace reg
trc .................... trace regs clear
ldr .................... show ldr linked list
iat .................... find api name in all iat's
iatx ................... addr to api name
iatd ................... dump the iat of specific module
dump ................... dump current state to disk
---
=>

Comandos de Registradores

Use r para visualizar todos os registradores, ou r [reg] para ver um registrador específico.

console mwemu

=>r rsp
    rsp: 0x329f40
=>r
    rax: 0x0 0
    rbx: 0x0 0
    rcx: 0x0 0
    rdx: 0x2f 47
    rsi: 0x0 0
    rdi: 0x0 0
    rsp: 0x329f40
    rbp: 0x329ff8
    rip: 0x3c006e
    r8 : 0x0 0
    r9 : 0x0 0
    r10: 0x0 0
    r11: 0x0 0
    r12: 0x0 0
    r13: 0x0 0
    r14: 0x0 0
    r15: 0x0 0

console mwemu

=>rc rax
command not found, type h
=>rc
register name=>rax
value=>0x1337
=>r rax
    rax: 0x1337 4919
=>

você pode usar apenas registradores de 64 bits e 32 bits, 16/8 bits não são permitidos por enquanto ("r ax" ou "r al")

Comando de Mapas

pressione m para listar todos os mapas de memória e endereços.

console mwemu

=>m
--- maps ---
oleaut32.data       0x7ff001dc7000 - 0x7ff001dca000 (12288)
ldr                 0x7ff000000000 - 0x7ff0000000b4 (180)
ntdll.pe            0x7ff000003000 - 0x7ff000004000 (4096)
kernelbase.text     0x7ff0002b6000 - 0x7ff0003c8000 (1122304)
ws2_32.didat        0x7ff00060f000 - 0x7ff000610000 (4096)
advapi32.pe         0x7ff000623000 - 0x7ff000624000 (4096)
winhttp.text        0x7ff000960000 - 0x7ff000a0f000 (716800)
iphlpapi.text       0x7ff00057f000 - 0x7ff0005a8000 (167936)
version.rsrc        0x7ff001e0c000 - 0x7ff001e0d000 (4096)
dnsapi.rdata        0x7ff000d6d000 - 0x7ff000d8f000 (139264)
kernelbase.rsrc     0x7ff000555000 - 0x7ff000556000 (4096)
comctl32.dll.ldr    0x7fe00000a000 - 0x7fe00000a940 (2368)
shell32.rsrc        0x7ff0012d8000 - 0x7ff001b1d000 (8671232)
advapi32.reloc      0x7ff0006cb000 - 0x7ff0006cd000 (8192)
advapi32.rdata      0x7ff00068a000 - 0x7ff0006bf000 (217088)
wininet.rsrc        0x7ff000cbe000 - 0x7ff000cd7000 (102400)
wininet.didat       0x7ff000cbd000 - 0x7ff000cbe000 (4096)
dnsapi.didat        0x7ff000d99000 - 0x7ff000d9a000 (4096)
ole32.pdata         0x7ff001f38000 - 0x7ff001f47000 (61440)
exe64win_enigma3000 0x140003000 - 0x140593000 (5832704)
user32.rsrc         0x7ff001c30000 - 0x7ff001d12000 (925696)
comctl32.pdata      0x7ff0008f9000 - 0x7ff00090f000 (90112)
oleaut32.text       0x7ff001d14000 - 0x7ff001da1000 (577536)
kernelbase.data     0x7ff000540000 - 0x7ff000545000 (20480)
winhttp.pdata       0x7ff000a3c000 - 0x7ff000a46000 (40960)
ws2_32.rdata        0x7ff0005fd000 - 0x7ff00060a000 (53248)
...

Podemos ver o nome do mapa, endereço inicial, endereço final e tamanho em bytes.

Outros comandos relacionados à memória:

console mwemu

m ...................... memory maps
ms ..................... memory filtered by keyword string
ma ..................... memory allocs
mc ..................... memory create map
mn ..................... memory name of an address
ml ..................... memory load file content to map
mr ..................... memory read, speficy ie: dword ptr [esi]
mw ..................... memory write, speficy ie: dword ptr [esi]  and then: 1af
mwb .................... memory write bytes, input spaced bytes
md ..................... memory dump
mrd .................... memory read dwords
mrq .................... memory read qwords
mds .................... memory dump string
mdw .................... memory dump wide string
mdd .................... memory dump to disk
mdda ................... memory dump all allocations to disk
mt ..................... memory test

Obter Detalhes do Mapa a partir do Endereço

se o código estiver usando um endereço e você quiser mais detalhes, use o comando mn.

console mwemu

=>mn
address=>0x140000008
map: exe64win_enigma.pe 0x140000000-0x140001000 (4096)
=>

Observe que os comandos do mwemu não aceitam parâmetros diretamente, primeiro digite o comando + enter e então o parâmetro será solicitado.

exceto para o comando r2 que precisa de um endereço para abrir o radare2, ou seja: r2 0x140000008

Comandos de Busca

Existem quatro comandos para buscar.

  • use o comando ss para buscar uma string em um mapa específico.
  • use o comando sb para buscar uma sequência de bytes espaçados em um mapa específico.
  • use o comando ssa para buscar uma string em todos os mapas.
  • use o comando sba para buscar uma sequência de bytes espaçados em todos os mapas.

console mwemu

=>ssa  (search string in all the maps)
string=>http://something.com/
found at 0x329ec8 'http://something.com/'
found at 0x329fc8 'http://something.com/'
map not found

=>mds (display string on an address)
address=>0x329ec8
0x329ec8: 'http://something.com/'
=>mds
address=>0x329fc8
0x329fc8: 'http://something.com/'
=> 

=>mn (which map is that address?)
address=>0x329ec8
map: stack 0x22a000-0x32c000 (1056768)
=>mn
address=>0x329fc8
map: stack 0x22a000-0x32c000 (1056768)
=>

=>ss (search string on specific address)
map name=>stack
string=>http://something.com/
found 0x329ec8 'http://something.com/'
found 0x329fc8 'http://something.com/'

=>sb (search spaced bytes on specific address, ie searching hexlified "http://")
map name=>stack
spaced bytes=>68 74 74 70 3a 2f 2f
found at 0x329ec8
found at 0x329fc8
=>

Comandos de Breakpoint

Existem quatro tipos de breakpoints, mas apenas um breakpoint pode ser definido para cada tipo por vez.

  • break no endereço, na próxima vez que este endereço for alcançado a emulação irá parar ali.
  • break na instrução, quando o emulador atingir esse número de instruções emuladas no total, irá parar ali.
  • break na leitura de memória, na próxima vez que este endereço for lido por qualquer instrução de assembly (não api ou syscall) o emulador irá parar ali.
  • break na escrita de memória, a próxima escrita neste endereço (não importa se é uma escrita de 1 byte ou qualquer quantidade) irá parar a emulação.
  • break na próxima instrução cmp ou test, isso irá parar o emulador na próxima instrução cmp ou test.

Use o comando "b" para ver o estado dos 4 tipos de breakpoints. Existem quatro comandos para buscar.

console mwemu

=>b
break on address: []
break on instruction: []
break on memory read: []
break on memory write: []

Use estes comandos para definir os breakpoints:

console mwemu

b ...................... breakpoint list
ba ..................... breakpoint on address
bi ..................... breakpoint on instruction number
bmr .................... breakpoint on read memory
bmw .................... breakpoint on write memory
bmx .................... breakpoint on execute memory
bcmp ................... break on next cmp or test instruction
bc ..................... clear breakpoint

Alguns exemplos:

console mwemu

--- console ---
=>bi
instruction number=>100
=>c
18 0x3c006a: jne   short 00000000003C004Ah taken
19 0x3c004a: mov   rax,[rbp-8] ; 0x329f10
20 0x3c004e: movzx edx,byte ptr [rax]
21 0x3c0051: mov   rax,[rbp-10h] ; 0x329e10
22 0x3c0055: mov   [rax],dl ; 0x68
23 0x3c0057: add   qword ptr [rbp-8],1
24 0x3c005c: add   qword ptr [rbp-10h],1
25 0x3c0061: mov   rax,[rbp-8] ; 0x329f11
...
96 0x3c0065: movzx eax,byte ptr [rax]
97 0x3c0068: test  al,al
98 0x3c006a: jne   short 00000000003C004Ah taken
99 0x3c004a: mov   rax,[rbp-8] ; 0x329f18
-------
100 0x3c004e: movzx edx,byte ptr [rax]   (this instruction is the next to be emulated, it was not emulated yet)
--- console ---
=>

Alterar Verbosidade

Se você digitar o comando "sv", o mwemu perguntará o novo número do nível de verbosidade, estes são os possíveis níveis de verbose:

  • 0: É como não usar nenhum -v para verbosidade mínima, apenas chamadas WinAPI e syscalls serão impressas.
  • 1: É como usar -v para visualizar também algumas mensagens como, código polimórfico detectado etc.
  • 2: É como usar -vv para visualizar também o código assembly, o mwemu imprimirá cada instrução de assembly, isso torna a emulação mais lenta. 2: Use -vv para visualizar também o código assembly, o mwemu imprimirá cada instrução de assembly, isso torna a emulação mais lenta.
  • 3: É como usar -vvv para também visualizar cada interação "rep", em instruções com prefixo rep como "rep movzx" imprimirá uma linha para cada passo do loop rep.

Por exemplo, queremos emular rapidamente as primeiras 200 instruções, e então habilitar a verbosidade, isso poderia ser feito com -V, mas vamos fazer isso a partir do console com o comando "sv":

console mwemu

~/s/mwemu ❯❯❯ cargo run --release -- -6 -f test/sc64win_strgen.bin -c 200
...
200 0x3c004e: movzx edx,byte ptr [rax]    (instruction 200 is not emulated yet, it's the next instruction to be emulated)
--- console ---
=>sv
verbose level=>3
=> [enter for emulating instruction 200]
=> c  (continue emulating now with maximum verbosity) 
...

Visualizar LDR

O LDR é uma lista triplamente circular encadeada que contém todos os módulos vinculados (não apenas DLL, também EXE)

O MWEMU fornece vários comandos para visualizar e consultar o LDR.

  • o comando "ldr" é a forma de visualizar o conteúdo do LDR.
  • o comando "iat" permite encontrar um nome de api especificado em todos os IATs de cada módulo vinculado.
  • "iatx" Se tivermos um endereço e quisermos saber qual nome de API é, este comando faz a consulta de endereço para nome.
  • "iatd" comando despeja o IAT completo de um módulo especificado.

console mwemu

--- console ---
=>ldr
0x7fe000000000 loader.exe flink:7fe000004000 blink:7fe00000f000 base:7ff001b7e000 pe_hdr:f8 5045
0x7fe000004000 ntdll.dll flink:7fe000005000 blink:7fe000000000 base:7ff000003000 pe_hdr:e8 5045
0x7fe000005000 kernel32.dll flink:7fe000006000 blink:7fe000004000 base:7ff0001f8000 pe_hdr:f0 5045
0x7fe000006000 kernelbase.dll flink:7fe000007000 blink:7fe000005000 base:7ff0002b5000 pe_hdr:f0 5045
0x7fe000007000 iphlpapi.dll flink:7fe000008000 blink:7fe000006000 base:7ff00057e000 pe_hdr:f8 5045
0x7fe000008000 ws2_32.dll flink:7fe000009000 blink:7fe000007000 base:7ff0005b8000 pe_hdr:f0 5045
0x7fe000009000 advapi32.dll flink:7fe00000a000 blink:7fe000008000 base:7ff000623000 pe_hdr:100 5045
0x7fe00000a000 comctl32.dll flink:7fe00000b000 blink:7fe000009000 base:7ff0006cd000 pe_hdr:f0 5045
0x7fe00000b000 winhttp.dll flink:7fe00000c000 blink:7fe00000a000 base:7ff00095f000 pe_hdr:f8 5045
0x7fe00000c000 wininet.dll flink:7fe00000d000 blink:7fe00000b000 base:7ff000a4f000 pe_hdr:f0 5045
0x7fe00000d000 dnsapi.dll flink:7fe00000e000 blink:7fe00000c000 base:7ff000cd9000 pe_hdr:f8 5045
0x7fe00000e000 shell32.dll flink:7fe00000f000 blink:7fe00000d000 base:7ff000da4000 pe_hdr:f0 5045
0x7fe00000f000 shlwapi.dll flink:7fe000000000 blink:7fe00000e000 base:7ff001b2c000 pe_hdr:f0 5045

Visualizar Estruturas

O depurador de windows windbg tem um recurso único que é o comando dt para ver informações sobre estruturas, é bastante útil e único.

O MWEMU implementa um comando dt semelhante, mas para estruturas específicas, que pode ser útil em algumas situações.

Vamos usar dt para inspecionar a estrutura PEB.

console mwemu

=>dt
structure=>peb
address=>0x7ffdf000
PEB {
    reserved1: [
        0x0,
        0x0,
    ],
    being_debugged: 0x0,
    reserved2: 0x0,
    reserved3: [
        0xffffffff,
        0x400000,
    ],
    ldr: 0x77647880,
    process_parameters: 0x2c1118,
    reserved4: [
        0x0,
        0x2c0000,
        0x77647380,
    ],
    alt_thunk_list_ptr: 0x0,
    reserved5: 0x0,
    reserved6: 0x6,
    reserved7: 0x773cd568,
    reserved8: 0x0,
    alt_thunk_list_ptr_32: 0x0,
    reserved9: [
        0x0,
...

Vamos usar dt para inspecionar a estrutura PEB_LDR_DATA.

console mwemu

=>dt
structure=>PEB_LDR_DATA
address=>0x77647880
PebLdrData {
    length: 0x30,
    initializated: 0x1,
    sshandle: 0x0,
    in_load_order_module_list: ListEntry {
        flink: 0x2c18b8,
        blink: 0x2cff48,
    },
    in_memory_order_module_list: ListEntry {
        flink: 0x2c18c0,
        blink: 0x2cff50,
    },
    in_initialization_order_module_list: ListEntry {
        flink: 0x2c1958,
        blink: 0x2d00d0,
    },
    entry_in_progress: ListEntry {
        flink: 0x0,
        blink: 0x0,
    },
}
=>

Vamos usar dt para inspecionar a estrutura LDR_DATA_TABLE_ENTRY, que representa uma entrada LDR na lista vinculada de um módulo vinculado específico.

console mwemu

=>dt
structure=>LDR_DATA_TABLE_ENTRY
address=>0x2c18c0
LdrDataTableEntry {
    reserved1: [
        0x2c1950,
        0x77647894,
    ],
    in_memory_order_module_links: ListEntry {
        flink: 0x0,
        blink: 0x0,
    },
    reserved2: [
        0x0,
        0x400000,
    ],
    dll_base: 0x4014e0,
    entry_point: 0x1d000,
    reserved3: 0x40003e,
    full_dll_name: 0x2c1716,
    reserved4: [
        0x0,
        0x0,
        0x0,
        0x0,
        0x0,
        0x0,
        0x0,
        0x0,
    ],
    reserved5: [
        0x17440012,
        0x4000002c,
        0xffff0000,
    ],
    checksum: 0x1d6cffff,
    reserved6: 0xa640002c,
    time_date_stamp: 0xcdf27764,
}
=>

Exemplo: um malware está escondendo algo em uma exceção.

console mwemu

3307726 0x4f9673: push  ebp
3307727 0x4f9674: push  edx
3307728 0x4f9675: push  eax
3307729 0x4f9676: push  ecx
3307730 0x4f9677: push  ecx
3307731 0x4f9678: push  4F96F4h
3307732 0x4f967d: push  dword ptr fs:[0]
Reading SEH 0x0
-------
3307733 0x4f9684: mov   eax,[51068Ch]
--- console ---
=>

Vamos inspecionar estruturas de exceção:

console mwemu

--- console ---
=>r esp
        esp: 0x22de98
=>dt
structure=>cppeh_record
address=>0x22de98
CppEhRecord {
    old_esp: 0x0,
    exc_ptr: 0x4f96f4,
    next: 0xfffffffe,
    exception_handler: 0xfffffffe,
    scope_table: PScopeTableEntry {
        enclosing_level: 0x278,
        filter_func: 0x51068c,
        handler_func: 0x288,
    },
    try_level: 0x288,
}
=>

E aqui temos a rotina de erro 0x4f96f4 e o filtro 0x51068c.

Visualizar Dados

Existem vários comandos para visualizar dados, mas atualmente estou usando o comando "r2 addr" que é melhor tanto para código quanto para dados. Observe que o comando r2 executa o radare2 e transfere o mapa de memória do endereço selecionado, e sincroniza o radare2 com o mwemu, mas este comando precisa ter o radare2 instalado no path, por exemplo do git. Mais detalhes no capítulo radare2. Vale a pena instalar o radare2.

Comandos para exibir informações:

console mwemu

mr ..................... memory read, speficy ie: dword ptr [esi]
mw ..................... memory write, speficy ie: dword ptr [esi]  and then: 1af
mwb .................... memory write bytes, input spaced bytes
md ..................... memory dump
mrd .................... memory read dwords
mrq .................... memory read qwords
mds .................... memory dump string
mdw .................... memory dump wide string
mdd .................... memory dump to disk
mdda ................... memory dump all allocations to disk
mt ..................... memory test

Exemplo com o comando md:

console mwemu

--- console ---
=>md
address=>0x329ec8
0x329ec8: 68 74 74 70 3a 2f 2f 73 6f 6d 65 74 68 69 6e 67     http://something
0x329ed8: 2e 63 6f 6d 2f 00 00 00 00 00 00 00 00 00 00 00     .com/...........
0x329ee8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00     ................
0x329ef8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00     ................
0x329f08: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00     ................
0x329f18: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00     ................
0x329f28: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00     ................
0x329f38: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00     ................
=>

Comando Radare2

Este comando dá muito poder ao MWEMU porque podemos usar o radare2 de dentro de um momento de emulação do MWEMU.

Observe que isso executa outro programa (também software livre) chamado radare2 que precisa estar instalado e acessível pelo path.

A instalação é simples, explicarei mais tarde.

Abrindo o radare sobre um endereço de dados:

Usando q para retornar do prompt do r2 para o prompt do mwemu e abrir o radare novamente com um endereço de código:

O radare está reconhecendo funções e descompilando-as com os múltiplos descompiladores, até podemos usar decai para descompilação baseada em IA em python etc.

Mais informações específicas sobre comandos do radare consulte o r2book: https://book.rada.re/

Sobre a instalação do radare2:

console mwemu

❯❯❯ git clone https://github.com/radareorg/radare2.git
❯❯❯ cd radare2
❯❯❯ sys/install.sh

O script install.sh faz toda a instalação, ele solicitará sudo para copiar binários para pastas que estão no path.

Se o radare2 estiver no path, seria possível acioná-lo a partir do comando r2 do mwemu.

MWEMU a partir de scripts python

Este é provavelmente o caso de uso mais prático do MWEMU, usando o módulo python pymwemu.

Instalação do pymwemu

A maneira mais fácil de instalar isso é usando pip, o pacote está publicado no pypi https://pypi.org/project/pymwemu/

Para usar a versão mais recente use git, mas é mais complicado de instalar, porque você precisa de rust, cargo e maturin.

Está pré-compilado para linux 64 bits, então no linux em teoria você não precisa instalar rust primeiro.

No linux você apenas faz:

console mwemu

❯❯❯ pip3 install pymwemu --break-system-packages

No mac, windows ou se o pip exigir, instale primeiro o rust.

Instale o rust do rustup, certifique-se de que o cargo está no path, e faça pip ou pip3:

console mwemu

❯❯❯ pip install --upgrade pip
❯❯❯ pip3 install --upgrade pip
❯❯❯ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
❯❯❯ pip install pymwemu
❯❯❯ pip3 install pymwemu

No mac, se houver um problema com !tapi-tbd a solução é:

Shell

❯❯❯ sudo xcode-select --switch /Library/Developer/CommandLineTools

Se houver o erro: Caused by: feature edition2024\ is required. então atualize seu rust: rustup update

Para verificar a instalação podemos importar o módulo no console python.

Shell

❯❯❯ python3
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pymwemu
Initialized logging
>>> emu = pymwemu.init64()
>>> emu.version()
            '0.11.6'

construir pymwemu (apenas para desenvolvedores)

shell

sudo apt install python3.13-venv
python3 -m venv .venv
source .venv/bin/activate.fish
pip install -U pip maturin
maturin develop --release
python
import pymwemu
Initialized logging  # loaded ok
^D
MATURIN_PYPI_TOKEN="..." maturin publish

lembre-se de atualizar a versão tanto em Crates.toml quanto em pyproject.toml

Criar o objeto de emulação

Primeiro de tudo, importe o módulo e inicialize o motor para a arquitetura que você precisa: 32 bits, 64 bits ou aarch64 (ARM64).

Console Python

>>> import pymwemu
Initialized logging
>>> emu = pymwemu.init32()       # x86 32bits
            or
>>> emu = pymwemu.init64()       # x86_64 64bits
            or
>>> emu = pymwemu.init_aarch64() # ARM64 / aarch64

Você também pode restaurar um estado de emulador previamente salvo em vez de criar um novo, veja a seção serialização:

  • pymwemu.load_from_file(filename: str) -> Emu carregar um estado previamente salvo com emu.dump_to_file().
  • pymwemu.load_from_minidump(filename: str) -> Emu carregar um estado salvo com emu.dump_to_minidump().
  • pymwemu.deserialize(data: bytes) -> Emu reconstruir um emulador a partir dos bytes retornados por emu.serialize().

Configurar o Emulador

Então há algumas configurações iniciais que você pode fazer.

Console Python

>>> emu.set_verbose(3)     # to show all the assembly, normally this is not needed. By default is 0 verbose.
>>> emu.enable_console()   # by default is disabled.
>>> emu.spawn_console_at_pos(6) # useful for debugging the script.
loading memory maps
>>>
>>>

Todas as configurações disponíveis:

  • set_verbose(n: int) 0: apenas exibe apicalls, 1: também mensagens, 2: também assembly, 3: também reps desenrolados.
  • enable_console() isso abre o console de comandos do mwemu.
  • disable_console()
  • spawn_console_at_pos(n: int) abre o console quando a posição n for alcançada (n instruções emuladas)
  • spawn_console_at_addr(addr: int) abre o console quando addr for alcançado.
  • disable_colors()
  • enable_colors()
  • enable_trace_mem()
  • disable_trace_mem()
  • enable_trace_regs()
  • disable_trace_regs()
  • enable_trace_reg(regs_list: list(str)) fornecer lista de strings com nomes de registradores a serem rastreados.
  • disable_trace_reg()
  • update_ldr_entry_base(libname: str, base: int)
  • set_stack_base(addr: int)
  • enable_count_loops()
  • disable_count_loops()
  • enable_trace_string(addr: int)
  • disable_trace_string()
  • enable_inspect_sequence(seq: str)
  • disable_inspect_sequence()
  • enable_shellcode_mode() forçar modo shellcode em vez de autodetectar o formato.
  • set_entry_point(addr: int)
  • set_base_address(addr: int)
  • enable_stack_trace()
  • disable_stack_trace()
  • enable_test_mode()
  • disable_test_mode()
  • enable_banzai_mode()
  • disable_banzai_mode()
  • allow_empty_code_blocks() se encontrar múltiplos zeros também emulá-los.
  • banzai_add(apiname: str, nparams: int) banzai continuar emulando em uma API não implementada.
  • enable_ctrlc() / disable_ctrlc() lidar com Control-C para abrir o console.
  • enable_threading(enable: bool) habilitar/desabilitar o agendador de threads.

Propriedades de configuração

Além dos toggles acima, o emulador expõe toda a configuração como propriedades python simples que você pode ler e atribuir diretamente (estas espelham os flags de linha de comando do binário mwemu):

  • emu.max_instructions: int | None parar após este número de instruções.
  • emu.timeout_secs: float | None parar a emulação após este número de segundos.
  • emu.max_faults: int | None parar após este número de falhas de memória.
  • emu.exit_position: int posição de instrução onde a emulação deve parar.
  • emu.dump_on_exit: bool / emu.dump_filename: str | None despejar o estado em um arquivo quando a emulação terminar.
  • emu.trace_filename: str | None / emu.trace_start: int / emu.trace_calls: bool escrever um rastro de execução.
  • emu.module_name: str / emu.exe_name: str nome apresentado à amostra emulada.
  • emu.user_name: str / emu.host_name: str identidade relatada pela winapi.
  • emu.temp_path: str / emu.cwd_path: str / emu.windows_directory: str / emu.system_directory: str caminhos simulados do sistema de arquivos.
  • emu.emulate_winapi: bool emular a winapi ou apenas pular as chamadas.
  • emu.short_circuit_sleep: bool fazer Sleep() retornar imediatamente.
  • emu.heap_alloc_min_size: int / emu.heap_free_soft: bool ajuste do alocador de heap.
  • emu.ssdt_use_ldr_initialize_thunk: bool usar o caminho real do ntdll LdrInitializeThunk no modo syscall/ssdt.

Console Python

>>> emu.max_instructions = 5_000_000
>>> emu.timeout_secs = 30
>>> emu.short_circuit_sleep = True
>>> emu.user_name = "sha0"

Carregando Mapas

Na maioria dos casos você precisará carregar toda a parte de simulação do SO Windows, para ter toda a infraestrutura de PEB+TEB+LDR linkedlist e WinAPI. Para fazer isso, você pode usar emu.load_maps(folder:str)

Se você for emular assembly puro, sem chamadas de API e sem acesso a estruturas do Windows, não precisa carregar os mapas.

O mesmo vale se você estiver emulando linux elf64 ou shellcodes (lembre-se, elf64 não é bem suportado por enquanto).

Exemplo:

Console Python

>>> emu.load_maps('/home/sha0/src/mwemu/maps/maps64/')
loading memory maps
dll_path: /home/sha0/src/mwemu/maps/maps64//ntdll.dll dll: ntdll.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//kernel32.dll dll: kernel32.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//kernelbase.dll dll: kernelbase.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//iphlpapi.dll dll: iphlpapi.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//ws2_32.dll dll: ws2_32.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//advapi32.dll dll: advapi32.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//comctl32.dll dll: comctl32.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//winhttp.dll dll: winhttp.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//wininet.dll dll: wininet.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//dnsapi.dll dll: dnsapi.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//shell32.dll dll: shell32.dll
dll_path: /home/sha0/src/mwemu/maps/maps64//shlwapi.dll dll: shlwapi.dll
>>>

Observe que isso precisa ter os mapas, então clone o repositório:

Shell

❯❯❯ git clone https://github.com/sha0coder/mwemu.git

Buscando mapas de uma versão do Windows (sem mapas locais necessários)

Desde a versão 0.11, você não precisa mais de uma cópia local dos mapas. Com emu.load_maps_from_winver(version: str), o mwemu baixa as DLLs genuínas do sistema Windows diretamente do servidor de símbolos da Microsoft e as usa como pasta de mapas.

O argumento version pode ser um nome amigável ("win11", "win10", "win2019") ou um número de build exato ("26100.7920"). Ele também fixa o build, então qualquer DLL extra que o loader precisar depois é buscada automaticamente sob demanda. Requer acesso à rede no primeiro uso e armazena tudo em cache em maps/winver/.

Console Python

>>> emu = pymwemu.init64()
>>> emu.load_maps_from_winver("win11")
>>> emu.load_binary("sample.exe")

Carregando ELF, PE ou shellcodes

  • emu.load_binary(filename:str) carrega a amostra a ser emulada.
  • emu.load_code_bytes(opcodes:bytes) se você já tiver os bytes em python bytes() / bytearray()

Exemplo:

Console Python

>>> emu.load_binary('shellcodes32/shikata.bin')
>>>

Ele detecta se é ELF, PE e, caso contrário, é um shellcode.

No caso de PE ou shellcode, a simulação win32 é inicializada automaticamente.

No caso de ELF, a simulação linux64 é inicializada automaticamente.

Mas se por qualquer motivo você não usar load_binary() e precisar do simulador Windows com peb/ldr/dlls carregados na memória, você precisa chamar init_win32 (mesmo para 64 bits, ele detectará a arquitetura).

E se você não usar load_binary() mas precisar do simulador Linux com libc etc carregados, use init_linux64. Observe que a implementação Linux é muito básica e apenas para 64 bits estáticos.

Desde a versão 0.11, os mapas estão incluídos, então init_win32() funciona imediatamente; você só precisa de load_maps() ou load_maps_from_winver() se quiser uma pasta de mapas específica ou um build concreto do Windows.

Console Python

>>> emu.init_win32()
            or
>>> emu.init_linux64(is_dynamic)

Criando Buffers

Você pode carregar seções do disco para a memória virtual do emulador e também pode alocar buffers.

  • alloc(name: str, size: int) -> int maneira mais simples de alocar um buffer, retorna o endereço.
  • alloc_at(name: str, addr: int, size: int) o nome é um id único do mapa alocado.
  • load_map(name: str, filename: str, base_addr: int) carrega payloads extras do disco, seções despejadas etc.
  • link_library(filepath: str) -> int vincula uma DLL personalizada no LDR.
  • free(name: str) libera uma alocação.

Exemplos:

Console Python

>>> addr = emu.alloc("mybuffer", 1024)
>>> emu.alloc_at("mybuffer", 0x1000, 1024)
>>> emu.load_map("mybuffer", "something.dll", 0x1000)

Registradores

Você pode visualizar os valores dos registradores e modificá-los a qualquer momento.

Por exemplo, para preparar o contexto antes da emulação, preparando um estado de emulação.

  • get_reg(reg: str) -> int forneça o nome do registrador para visualizá-lo.
  • set_reg(reg: str, value: int) -> int modifica um registrador.
  • get_xmm(reg: str) -> u128 também pode visualizar registradores xmm.
  • set_xmm(reg: str, value: u128) -> u128 e pode modificar registradores xmm.
  • set_rip(addr: int) -> bool para alterar o rip acionando toda a lógica de mudança de fluxo, ou seja, chamadas winapi etc.
  • set_eip(addr: int) -> bool para alterar o eip acionando toda a lógica de mudança de fluxo, ou seja, chamadas winapi etc.

Exemplos:

Console Python

>>> rax = emu.get_reg('rax')
>>> emu.set_reg('rax', rax+1)

Operações de memória

Pode ser útil para definir o estado da emulação antes de iniciá-la, mas outra opção é verificar a memória após a emulação ou alterá-la durante a emulação.

Observe que esta leitura/escrita está dentro da memória virtual da emulação.

escrita de memória

  • write_qword(addr: int, value: int) -> bool
  • write_dword(addr: int, value: int) -> bool
  • write_word(addr: int, value: u16) -> bool
  • write_byte(addr: int, value: u8) -> bool
  • write_bytes(to: int, from: bytes)
  • write_string(to: int, from: str)
  • write_wide_string(to: int, from: str)
  • write_buffer(to: int, from: bytes)
  • write_spaced_bytes(addr: int, spaced_hex_bytes: str) -> bool

operações de leitura

  • read_128bits_be(addr: int) -> u128
  • read_128bits_le(addr: int) -> u128
  • read_qword(addr: int) -> int
  • read_dword(addr: int) -> int
  • read_word(addr: int) -> u16
  • read_byte(addr: int) -> u8
  • read_buffer(from: int, sz: int) -> Vec
  • read_bytes(addr: int, sz: int) -> bytes
  • read_string_of_bytes(addr: int, sz: int) -> str
  • read_string(addr: int) -> str
  • read_wide_string(addr: int) -> str

operações de memória estilo libc:

  • memset(addr: int, byte: int, amount: int)
  • sizeof_wide(unicode_str_ptr: int) -> int

métodos de busca:

  • search_spaced_bytes_from(saddr: int, sbs: str) -> int
  • search_spaced_bytes_from_bw(saddr: int, sbs: str) -> int
  • search_spaced_bytes(sbs: str, map_name: str) -> Vec
  • search_spaced_bytes_in_all(sbs: str) -> Vec
  • search_string(kw: str, map_name: str) -> list[int] busca uma string em um mapa específico.
  • search_string_in_all(kw: str) busca uma string em todos os mapas de memória, não muito útil porque não retorna o resultado, apenas imprime. Será melhorado.
  • search_bytes(bkw: bytes, map_name: str) -> list[int] busca bytes em um mapa de memória específico, o resultado é uma lista de endereços.

Exemplos de preparação de contexto antes de iniciar a emulação:

Console Python

>>> some_data_structure_needed = open('blob.bin','rw').read()
>>> addr = emu.alloc("struct1", len(some_data_structure_needed))
>>> emu.write_bytes(addr, some_data_structure_needed)
>>> emu.set_reg('rdi', addr)
>>> rax = emu.call32(0x40323, [addr, 0])

Outro exemplo:

Script Python

import pymwemu

emu = pymwemu.init64()
buffer_addr = emu.alloc("mybuffer", 1024)

# Write a string
emu.write_string(buffer_addr, "Hello MWEMU!")

# Read it back
text = emu.read_string(buffer_addr)
print(f"Read: {text}")

# Write dword
emu.write_dword(buffer_addr + 0x100, 0x12345678)
value = emu.read_dword(buffer_addr + 0x100)
print(f"DWORD: {hex(value)}")

# Write raw bytes
data = b"\x90\x90\x90\xc3"  # nop nop nop ret
emu.write_buffer(buffer_addr + 0x200, data)

Iniciar Emulação

Uma vez que tudo está configurado, você pode iniciar a emulação.

  • run(end_addr) -> int forneça o endereço para terminar a emulação ou None para emular o máximo possível.
  • run_to(position: int) -> int emula um número específico de instruções.
  • call32(address: int, params: list[int]) -> int chama uma função usando a convenção de chamada microsoft 32 bits.
  • call64(address: int, params: list[int]) -> int chama uma função usando a convenção de chamada microsoft 64 bits.
  • linux_call64(address: int, params: list[int]) -> int chama uma função usando a convenção de chamada linux 64 bits.
  • stop() para a emulação, nunca usei esta chamada.
  • run_until_return() -> int
  • run_until_apicall() -> (int, str)
  • step() -> bool você pode usar um loop while step(): e controlar a situação passo a passo. Isso é lento.
  • handle_winapi(addr: int)

Script Python - basic_run.py

import pymwemu

emu = pymwemu.init64()
emu.load_binary('shellcode.bin')

rip = emu.run(None)

print("Emulation finished")
print(f"Final RAX: {hex(emu.get_reg('rax'))}")
print(f"Instructions executed: {emu.get_position()}")

Chamando Funções Diretamente

Você pode chamar funções em endereços específicos com call32/call64:

Script Python - call_function.py

import pymwemu

# Example: call a decryption function
emu = pymwemu.init64()
emu.load_maps('/path/to/maps/maps64/')
emu.load_binary('malware.exe')

# Allocate buffer for output
output_buffer = emu.alloc("output", 1024)

# Call function at 0x401234 with arguments
# Function signature: decrypt(char* input, char* output, int size)
encrypted_str = 0x404000  # address of encrypted data
result = emu.call64(0x401234, [encrypted_str, output_buffer, 100])

# Read decrypted result
decrypted = emu.read_string(output_buffer)
print(f"Decrypted: {decrypted}")
print(f"Return value: {hex(result)}")

Script Python - call32_example.py

import pymwemu

# Example with 32-bit malware
emu = pymwemu.init32()
emu.load_binary('malware32.exe')

# Call a string decryption routine
# Function: char* decrypt_string(int index)
decrypt_func = 0x00401200

for i in range(10):
    # Call function with index
    result_addr = emu.call32(decrypt_func, [i])
    
    # Read decrypted string
    if result_addr != 0:
        decrypted = emu.read_string(result_addr)
        print(f"String {i}: {decrypted}")

Emulação Passo a Passo

Para depuração ou controle detalhado, execute instrução por instrução:

Script Python - step_by_step.py

import pymwemu

emu = pymwemu.init64()
emu.set_verbose(2)  # show assembly
emu.load_binary('shellcode.bin')

# Execute 10 instructions one by one
for i in range(10):
    emu.step()
    rip = emu.get_reg('rip')
    rax = emu.get_reg('rax')
    print(f"Step {i+1}: RIP={hex(rip)} RAX={hex(rax)}")

print(f"Total instructions: {emu.get_position()}")

Métodos Úteis Adicionais

Outros métodos úteis para controle de emulação:

  • emu.get_position() -> int obtém a posição atual da instrução
  • emu.set_verbose(level:int) define a verbosidade (0=silencioso, 1=API, 2=asm, 3=tudo)
  • emu.spawn_console() inicia console interativo
  • emu.print_maps() imprime a lista completa de mapas.
  • emu.print_maps_by_keyword(kw: str) imprime os mapas que correspondem a essa palavra-chave.
  • emu.search_bytes(pattern: bytes, map_name: str) -> list busca um padrão de bytes em um mapa de memória específico.

Console Python

>>> # Run and then spawn console for inspection
>>> emu.run(1000)
>>> emu.spawn_console()  # interactive debugging
>>> 
>>> 
>>> # Search for strings (search_bytes needs a map name, or use *_in_all)
>>> addresses = emu.search_bytes(b"http://", "code")
>>> for addr in addresses:
...     print(f"Found at: {hex(addr)}")
>>>

Exemplo Prático: Descriptografia de Strings

Um caso de uso típico é descriptografar strings de malware:

Script Python - string_decrypt.py

#!/usr/bin/env python3
import pymwemu

# Initialize
emu = pymwemu.init32()  # 32-bit malware
emu.load_binary('malware32.exe')

# Find encrypted strings in .data section
encrypted_strings = [
    0x00401000,
    0x00401100,
    0x00401200,
]

# Decrypt function address (from IDA/Ghidra)
decrypt_func = 0x00402340

decrypted = []

for enc_addr in encrypted_strings:
    # Set parameters
    emu.set_reg('eax', enc_addr)  # pointer to encrypted string
    emu.set_eip(decrypt_func)  # start at decrypt function

    # Run until return
    emu.run(10000)

    # Read decrypted result (assume in EAX)
    result_addr = emu.get_reg('eax')
    decrypted_str = emu.read_string(result_addr)
    
    print(f"Encrypted {hex(enc_addr)} -> {decrypted_str}")
    decrypted.append(decrypted_str)

# Save results
with open('decrypted_strings.txt', 'w') as f:
    for s in decrypted:
        f.write(s + '\n')

Operações de pilha

Existem métodos para alterar a pilha; observe que você também pode fazer emu.write_qword(emu.get_reg('rsp'), 123)

Mas com esses métodos você aciona toda a lógica da pilha, também incrementando rsp/esp.

  • stack_push32(value: int) -> bool
  • stack_push64(value: int) -> bool
  • stack_pop32() -> int
  • stack_pop64() -> int

Este exemplo simula uma convenção de chamada baseada em pilha, mas observe que você também pode usar os métodos call32() e call64() e linux_call64().

Console Python

>>> emu.stack_push32(ret_addr)
>>> emu.stack_push32(param1)
>>> emu.stack_push32(param2)
>>> emu.set_reg('rip', 0x40123)
>>> emu.run(None)

Breakpoints

Os breakpoints do mwemu são simples, e na maioria das vezes há maneiras melhores de acionar a parada da emulação.

Você pode definir bp em um endereço (apenas um por vez...) ou instrução, também leitura/escrita de memória

  • bp_show()
  • bp_clear_all()
  • bp_set_addr(addr: int)
  • bp_get_addr() -> Vec
  • bp_set_inst(ins: int)
  • bp_get_inst() -> Vec
  • bp_set_mem_read(addr: int)
  • bp_get_mem_read() -> Vec
  • bp_set_mem_write(addr: int)
  • bp_get_mem_write() -> Vec

Exemplo

Console Python

>>> emu.bp_set_addr(0x11223344)
>>> emu.run(None)

Despejo de memória

Existem várias maneiras de fazer um despejo de memória

  • save_all_allocs(path: str) apenas salva no disco alocações feitas pelo código emulado.
  • save(addr: int, size: int, filename: str) despeja um blob específico no disco.

Exemplo

Console Python

>>> emu.print_maps()
...
>>> emu.save(0x40324234, 1024, "/tmp/blob.bin")

Serializar / snapshots

Todo o estado do emulador (mapas de memória, registradores, configuração, módulos carregados...) pode ser serializado para que você possa salvar um snapshot e retomá-lo depois, compartilhá-lo ou ramificar a análise a partir de um ponto conhecido.

  • serialize() -> bytes serializa todo o estado para um objeto bytes que você pode manter na memória ou armazenar você mesmo.
  • dump_to_file(filename: str) serializa o estado diretamente para um arquivo.
  • dump_to_minidump(filename: str) serializa o estado para um arquivo minidump do Windows (carregável por outras ferramentas).
  • save_all_allocs(path: str) salva no disco cada bloco de memória alocado durante a emulação.
  • save(addr: int, size: int, filename: str) despeja um pedaço específico de memória no disco.

E para restaurar um estado, estas são funções de nível de módulo que retornam um novo objeto Emu:

  • pymwemu.deserialize(data: bytes) -> Emu reconstrói a partir dos bytes produzidos por serialize().
  • pymwemu.load_from_file(filename: str) -> Emu carrega um estado salvo com dump_to_file().
  • pymwemu.load_from_minidump(filename: str) -> Emu carrega um estado salvo com dump_to_minidump().

Exemplo: tire um snapshot, continue emulando e retome do snapshot depois.

Script Python - snapshot.py

import pymwemu

emu = pymwemu.init64()
emu.load_binary('sample.exe')

# run up to an interesting point and snapshot it
emu.run_to(100000)
emu.dump_to_file('snapshot.bin')

# ... keep emulating, maybe ruining the state ...
emu.run(None)

# later, or in another script, resume exactly from the snapshot
emu2 = pymwemu.load_from_file('snapshot.bin')
print(f"resumed at {hex(emu2.get_pc())}")
emu2.run(None)

Ver informações

Existem alguns métodos menos usados para buscar diferentes tipos de informação.

  • version() -> str obtém a versão atual do pymwemu.
  • get_prev_mnemonic() -> str obtém o último mnemônico emulado.
  • reset_pos() redefine a contagem de instruções emuladas para zero.
  • is_64bits() -> bool detecta em qual modo o emulador está.
  • is_32bits() -> bool detecta em qual modo o emulador está.
  • get_position() -> int obtém a posição atual (quantidade de instruções emuladas).
  • disassemble(addr: int, amount: int) -> str desmonta bytes.
  • print_maps() imprime todos os mapas (alocações, DLLs vinculadas, etc)
  • print_maps_by_keyword(kw: str) imprime os mapas que contêm uma palavra-chave.
  • get_addr_base(addr: int) -> int obtém o início de um mapa.
  • is_mapped(addr: int) -> bool verifica se um endereço está alocado.
  • get_addr_name(addr: int) -> str obtém em qual nome de mapa o endereço está.
  • dump_memory(addr: int) imprime bytes.
  • dump_n(addr: int, amount: int) imprime n bytes.
  • dump_qwords(addr: int, n: int) imprime uma lista de qwords.
  • dump_dwords(addr: int, n: int) imprime uma lista de dwords.
  • allocated_size() -> int mostra a memória total alocada.
  • memory_overlaps(addr: int, sz: int) -> bool verifica se um bloco de memória se sobrepõe a um mapa existente.
  • show_allocs() imprime todas as alocações.
  • mem_test() -> bool faz um teste automático de memória para procurar sobreposições de mapas.
  • api_addr_to_name(addr: int) -> str forneça um endereço apontando para uma API e buscará o nome da API.
  • api_name_to_addr(name: str) -> int obtém o endereço de um nome de API.

Exemplo

Console Python

>>> name = api_addr_to_name(0x11223344)
>>> print( name )
        MessageBoxA

Alterar bits da arquitetura.

Existem métodos para mudar de 32 bits para 64 bits e vice-versa, ou para aarch64.

  • set_64bits()
  • set_32bits()
  • set_aarch64() alterna para o modo ARM64 (ou melhor, use pymwemu.init_aarch64()).
  • is_64bits() -> bool / is_32bits() -> bool verifica o modo atual.
  • inspect_seq(s: str)

Para permanecer agnóstico em relação à arquitetura (útil quando você também tem como alvo aarch64), use os acessadores genéricos de contador de programa e ponteiro de pilha em vez dos nomes de registradores x86:

  • get_pc() -> int / set_pc(addr: int) funciona em x86, x64 e aarch64.
  • get_sp() -> int / set_sp(addr: int) ponteiro de pilha para qualquer arquitetura.

Mas é melhor não alterar a arquitetura em tempo real; reinstancie o objeto de emulação assim:

Exemplo

Console Python

>>> import pymwemu
>>> emu = pymwemu.init32()
>>> ... 
>>> emu = pymwemu.init64()

Exemplos de casos reais.

Encontre alguns exemplos aqui:

https://github.com/sha0coder/mwemu/tree/main/crates/pymwemu/examples/scripts

E alguns notebooks jupyter aqui:

https://github.com/sha0coder/mwemu/tree/main/crates/pymwemu/examples

MWEMU para aplicativos Rust

O núcleo e as ligações são tudo implementado em Rust, e toda a lógica está dentro da libmwemu, então um programa Rust pode ter ainda mais controle do emulador do que o pymwemu.

Dito isso, a partir do Rust você pode usar todo o poder do MWEMU e acessar a maioria dos objetos, pois a maioria deles são públicos, então você pode ter um bom controle do emulador.

https://docs.rs/libmwemu/0.23.5/libmwemu/

https://crates.io/crates/libmwemu

Iniciar um projeto

Primeiro de tudo, você precisa do compilador rustc e da ferramenta cargo; a melhor maneira de instalar isso é rustup.rs

Shell

❯❯❯ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Então você precisa criar um projeto Rust.

Shell

❯❯❯ cargo init --bin myproject
❯❯❯ cd myproject

Isso cria uma pasta com src/main.rs e também Cargo.toml

Adicione a dependência ao Cargo.toml manualmente ou, melhor, automaticamente usando também o cargo.

Shell

❯❯❯ cargo add libmwemu

❯❯❯ cat Cargo.toml
[package]
name = "myproject"
version = "0.1.0"
edition = "2024"

[dependencies]
libmwemu = "0.23.5"

Então você pode adicionar código ao src/main.rs

construir projeto

Se você precisar executar o projeto, lembre-se de usar o modo release (adicionando --release)

No modo release, o emulador é extremamente rápido.

shell

❯❯❯ cargo run --release

A compilação será mais lenta na primeira vez porque precisa baixar a libmwemu e compilá-la automaticamente.

O que cargo run faz internamente é compilar (como cargo build --release) e depois executar o binário. Você pode passar parâmetros para o cargo e também para o seu código desta forma:

cargo run [cargo args] -- [you program args] exemplo:

shell

❯❯❯ cargo run --release -- samples/binary.exe

Criar o objeto de emulação

Primeiro de tudo, use o objeto emu32 ou emu64 dependendo do que você precisa.

Se você for fazer alocações de memória, você também precisará usar o objeto Permission.

main.rs

use libmwemu::emu32;
use libmwemu::maps::mem64::Permission;

Note que mesmo em 32 bits, a implementação do objeto Permission é feita em mem64, na verdade internamente tudo é 64 bits.

Então crie o objeto de emulação para a arquitetura escolhida.

main.rs

let mut emu = emu32();  // or also emu64()

Configurar o Emulador

Então, opcionalmente, você pode usar modificadores para configurar o emulador, até mesmo acessar diretamente o objeto Config.

Todas as configurações disponíveis:

  • emu.set_verbose(n: u32) alterar a verbosidade, 0: apenas mostrar chamadas winapi, 1: também mensagens, 2: também todas as instruções asm, 3: também cada iteração de rep.
  • emu.set_stack_address(addr: u64)
  • emu.set_base_address(addr: u64) definir o endereço base do código, o blob emulável inicial, por padrão o mwemu obtém isso das estruturas PE/ELF.
  • emu.disable_ctrlc()
  • emu.disable_console()
  • emu.disable_banzai()
  • emu.disable_color()
  • emu.disable_interrupt()
  • emu.disable_exception()
  • emu.disable_memory_read()
  • emu.disable_memory_write()
  • emu.disable_pre_instruction()
  • emu.disable_post_instruction()
  • emu.disable_winapi_call()
  • emu.spawn_console()
  • emu.spawn_console_at(exp: u64)
  • emu.spawn_console_at_addr(addr: u64)
  • emu.update_ldr_entry_base(libname: &str, base: u64)

A partir do rust, você também pode modificar diretamente a estrutura Config, exemplo:

main.rs

emu.cfg.arguments = "--exe_sample_some_arg".to_string();
emu.cfg.shellcode = true;

Encontre aqui todas as configurações disponíveis no objeto Config.

Mas as principais configurações têm wrappers de função no objeto base principal Emu.

Você também pode criar um objeto Config e defini-lo no emulador com emu.set_config(cfg: Config), mas normalmente é mais conveniente acessar emul.cfg como visto acima.

Configurando a Pasta de Maps

Na maioria dos casos, você precisará carregar toda a parte de simulação do SO Windows, para ter toda a lista encadeada PEB+TEB+LDR fazendo a Simulação de Processos do Windows.

Para fazer isso, você pode usar emu.set_maps_folder(folder:str)

Se você for emular assembly puro, sem chamadas de API e sem acesso a estruturas do Windows, você não precisa carregar os maps.

Exemplo:

main.rs

let mut emu = emu64();
emu.set_maps_folder("/home/sha0/src/mwemu/maps/maps64/");
emu.init_logger();

Note que isso precisa ter os maps, então clone o repositório com git:

Shell

❯❯❯ git clone https://github.com/sha0coder/mwemu.git

Esta chamada configura o caminho, mas não o carrega por enquanto!

Mais tarde, usaremos um destes 3 para acionar o simulador win32:

  • emu.load_code(file: &str) se for PE ou shellcode, chamará internamente init_win32
  • emu.init_win32(clear_registers: bool, clear_flags: bool) você pode acionar diretamente a simulação win32, mas não a chame duas vezes.

Então, se você precisar dos maps e das coisas do win32, mas não fizer load_sample, então você precisará chamar diretamente o init_win32

Carregando ELF, PE ou shellcodes

Na maioria dos casos, você precisará carregar uma amostra, de arquivo ou de vetor.

Carregue sua amostra principal com um destes métodos, e apenas uma vez.

Para carregar maps adicionais, use os métodos de load/alocação que são explicados mais adiante.

  • emu.load_code(filename: &str) carrega a amostra a ser emulada.
  • emu.load_code_bytes(opcodes: &[u8]) se você já tiver os bytes em uma variável.

Exemplo1:

main.rs

emu.load_code("/samples/sample.bin");

Exemplo2:

main.rs

let mut opcodes: Vec<u8> = Vec::new();
... // fill the opcodes
let mut emu = emu32();
emu.set_maps_folder("/home/sha0/src/mwemu/maps/maps64/");
emu.load_code_bytes(&opcodes);
emu.init_win32(false, false);

Exemplo3:

main.rs

let mut emu = emu32();
emu.load_code_bytes(&opcodes);
emu.init_linux64();  // load_code("sample") would trigger init_win32 or init_linux64 depending on file format. 
                     // but with no load_code we must trigger manually the init if we need it.

Ele detecta se é ELF, PE e, caso contrário, é um shellcode.

No caso de PE ou shellcode, a simulação win32 é inicializada automaticamente.

No caso de ELF, a simulação linux64 é inicializada automaticamente.

Mas se por qualquer motivo você não usar load_code() e precisar do simulador do Windows com peb/ldr/dlls carregados na memória, você precisa chamar init_win32 (mesmo para 64 bits, ele detectará a arquitetura).

E se você não usar load_binary() mas precisar do simulador linux com libc etc carregados, use init_linux64. Note que a implementação linux é muito básica e apenas para 64 bits estáticos.

Note que chamar init_win32() requer que você tenha feito load_maps anteriormente para definir a pasta de maps.

Criando Buffers

Você pode carregar seções do disco para a memória virtual do emulador, e também pode alocar buffers.

  • emu.alloc(map_name: &str, size: u64, permission: Permission) -> u64 a maneira mais simples de alocar um buffer, retorna o endereço.
  • emu.maps.alloc(sz: u64) -> Option<u64> isso é usado pelas syscalls winapi e linux, isso apenas encontra um bloco livre de tamanho sz, mas não o aloca, você precisa então fazer emu.maps.create_map
  • emu.maps.create_map(name: &str, base: u64, size: u64, permission: Permission cria um map vazio em um local específico.
  • emu.free(map_name: &str); desaloca pelo nome do map.
  • emu.maps.dealloc(addr: u64); desaloca pelo endereço.
  • emu.link_library(filepath: &smp;str) -> u64 vincula dinamicamente uma dll personalizada à estrutura LDR.

Exemplo1:

main.rs

use libmwemu::maps::mem64::Permission;
use libmwemu::emu32;
...
let ptr_encoded = emu.alloc("enc", 1024, Permission::READ_WRITE_EXECUTE);
let ptr_decoded = emu.alloc("dec", 1024, Permission::READ_WRITE_EXECUTE);
emu.maps.write_bytes(ptr_encoded, &data);
let rax = emu.call64(decoder_func_addr, &[ptr_encoded, ptr_decoded]);

Exemplo2:

main.rs

let addr = emu.maps.alloc(1024);
let map = emu
            .maps
            .create_map("section_memorydump", addr, 1024, Permission::READ_WRITE_EXECUTE)
            .expect("load_map out of memory");
map.load("dumped_section.bin");

map é um objeto Mem64, e tem vários métodos. Mas para a maioria dos casos, os métodos emu e emu.maps são suficientes.

Registradores

Você pode visualizar os valores dos registradores e modificá-los a qualquer momento.

Por exemplo, para preparar o contexto antes da emulação, preparando um estado de emulação.

Ler um registrador de 64 bits ou maior é muito direto

let rax = emu.regs().rax;

let rip = emu.regs().rip;

let xmm15 = emu.regs().xmm15;

Definir um registrador de 64 bits ou maior é muito direto.

emu.regs_mut().rax = 0x123;

emu.regs_mut().rip = 0x4012312;

emu.regs_mut().xmm15 = 0x11223344_11223344_11223344_11223344u128;

Getters para não-64 bits (sempre retornam tipo u64)

let eax = emu.regs().get_eax() as u32;

let eip = emu.regs().get_eip() as u32;

let al = emu.regs().get_al() as u8;

Setters para não-64 bits (sempre forneça tipo u64)

emu.regs_mut().set_eax(123u64);

emu.regs_mut().set_eip(0x123123);

emu.regs_mut().set_al(0xff);

emu.regs_mut().set_sil(0xff);

emu.regs_mut().set_dil(0xff);

r1 a r15

let r15 = emu.regs().r15;

let n = emu.regs().get_r12w();

emu.regs_mut().set_r8h(0x00);

emu.regs_mut().set_r8l(0xff);

emu.regs_mut().r8 = 0;

let lower_32bits = emu.regs().get_r8d();

let upper_32bits = emu.regs().get_r8u();

Regs64 é completo, também existem alguns registradores incomuns como ymm, cr, gs, fs, tr e msr.

métodos auxiliares

emu.regs_mut().clear::<u8>(); limpa o byte inferior de todos os registradores.

emu.regs_mut().rand(); randomiza os valores dos registradores.

emu.regs().print::<u32>(); imprime os principais registradores.

E há muito mais coisas, para mais verifique o objeto Regs64.

Note que você pode alterar emu.regs_mut().set_eip(addr);, mas na maioria das vezes o que você realmente precisa é emu.set_eip(addr);, que emula a lógica de jmp acionando WinAPI se o endereço for uma lib.

Operações de memória

Pode ser útil para identificar o estado da emulação antes de iniciá-la, mas outra opção é verificar a memória após a emulação ou alterá-la durante a emulação.

Note que estas leituras/escritas estão dentro da memória virtual da emulação.

leitura de memória

  • emu.maps.read_byte(addr: u64) -> Option<u8>
  • emu.maps.read_f64(addr: u64) -> Option<f64>
  • emu.maps.read_f32(addr: u64) -> Option<f32>
  • emu.maps.read_128bits_be(addr: u64) -> Option<u128>
  • emu.maps.read_128bits_le(addr: u64) -> Option<u128>
  • emu.maps.read_qword(addr: u64) -> Option<u64>
  • emu.maps.read_dword(addr: u64) -> Option<u32>
  • emu.maps.read_word(addr: u64) -> Option<u16>
  • emu.maps.read_buffer(from: u64, sz: usize) -> Vec<u8>
  • emu.maps.read_bytes(addr: u64, sz: usize) -> &[u8]
  • emu.maps.read_bytes_option(addr: u64, sz: usize) -> Option<&[u8]>
  • emu.maps.read_string_of_bytes(addr: u64, sz: usize) -> String
  • emu.maps.read_string(addr: u64) -> String
  • emu.maps.read_wide_string_nocrash(addr: u64) -> String
  • emu.maps.read_wide_string(addr: u64) -> String
  • emu.maps.read_wide_string_n(addr: u64, max_chars: usize) -> String

escrita de memória

  • emu.maps.write_byte(addr: u64, value: u8) -> bool
  • emu.maps.write_qword(addr: u64, value: u64) -> bool
  • emu.maps.write_dword(addr: u64, value: u32) -> bool
  • emu.maps.write_word(addr: u64, value: u16) -> bool
  • emu.maps.write_bytes(addr: u64, data: Vec<u8>) -> bool
  • emu.maps.write_string(to: u64, from: &str)
  • emu.maps.write_wide_string(to: u64, from: &str)
  • emu.maps.write_buffer(to: u64, from: &[u8])
  • emu.maps.write_spaced_bytes(addr: u64, sbs: &str) -> bool

operações de memória estilo libc:

  • emu.maps.memset(addr: u64, b: u8, amount: usize)
  • emu.maps.memcpy(to: u64, from: u64, size: usize) -> bool
  • emu.maps.sizeof_wide(unicode_str_ptr: u64) -> usize

métodos de busca:

  • emu.maps.search_string(kw: &str, map_name: &str) -> Option<Vec<u64>>
  • emu.maps.search_spaced_bytes_from(sbs: &str, saddr: u64) -> u64
  • emu.maps.search_spaced_bytes_from_bw(spaced_bytes: &str, start_address: u64) -> u64
  • emu.maps.search_spaced_bytes(sbs: &str, map_name: &str) -> Vec<u64>
  • emu.maps.search_spaced_bytes_in_all(sbs: &str) -> Vec<u64>
  • emu.maps.search_string_in_all(kw: String)
  • emu.maps.search_bytes(bkw: Vec<u8>, map_name: &str) -> Vec<u64>

Exemplos de preparação de contexto antes de iniciar a emulação:

main.rs

let mut some_data_structure_needed: Vec = Vec::new();
load_blob_from_disk("blob.bin", &mut some_data_structure_needed);

let mut emu = libmwemu::emu32();
let addr = emu.alloc("struct1", some_data_structure_needed.len());
emu.maps.write_bytes(addr, some_data_structure_needed);
let rax = emu.call32(0x40323, &[addr, some_data_structure_needed.len()]);

Outro exemplo:

main.rs

let mut emu = libmwemu::emu32();
let addr = emu.alloc("buff1", 100);
emu.maps.write_string(addr, "Hello MWEMU!");
let text = emu.maps.read_string(addr, 12);
println!("{}", text);

// Write dword
let buffer_addr = emu.alloc("buff2", 4);
emu.maps.write_dword(buffer_addr + 0x100, 0x12345678);
let value = emu.maps.read_dword(buffer_addr + 0x100);
println!("dword: 0x{:x}", value);

// emulate some opcodes
let buff = emu.alloc("code_blob", 1024);
let opcodes = vec![0x90, 0x90, 0x90, 0xc3]; // nop nop nop ret
emu.maps.write_buffer(buff, opcodes);
emu.regs_mut().set_eip(buff);
emu.run(Some(buff+3));
println!("eip: 0x{:x}", emu.regs().get_eip());
// just an example, probalby more convinient using emu.load_code_bytes(&opcodes) for emulating shellcode that is in a variable.

Iniciar Emulação

Uma vez que tudo está configurado, você pode iniciar a emulação.

Você pode iniciar a emulação usando funções run

  • emu.run(end_addr: Option<u64>) -> Result<u64, MwemuError>
  • emu.run_until_ret() -> Result<u64, MwemuError>
  • emu.run_to(end_pos: u64) -> Result<u64, MwemuError>
  • emu.run_multi_threaded(end_addr: Option<u64>) -> Result<u64, MwemuError>
  • emu.run_single_threaded(end_addr: Option<u64>) -> Result<u64, MwemuError>
  • emu.stop()

Mas estas formas são muito mais confortáveis porque imitam convenções de chamada.

  • emu.call32(addr: u64, args: &[u32]) -> Result<u32, MwemuError> convenção de chamada microsoft 32 bits.
  • emu.call64(addr: u64, args: &[u64]) -> Result<u64, MwemuError> convenção de chamada microsoft 64 bits.
  • emu.linux_call64(addr: u64, args: &[u64]) -> Result<u64, MwemuError> convenção de chamada ABI SysV AMD64 usada no espaço de usuário linux64.

Para mais controle, mas de forma lenta, você pode usar step, mas note que também pode usar hooks.

  • emu.step() -> bool
  • emu.step_single_threaded() -> bool
  • emu.step_multi_threaded() -> bool

Métodos Úteis Adicionais

Outras coisas úteis para controle de emulação:

  • emu.pos este é o contador de instruções emuladas, você pode acessá-lo e até modificá-lo.
  • emu.set_verbose(n: u32) defina verbose 0 para ir mais rápido a um ponto específico e depois defina 2 para ver o asm apenas nesses intervalos de posição.
  • emu.cfg.console_enabled = true habilita o autospawn do console.
  • emu.spawn_console() em caso específico, abra o console para inspecionar manualmente a situação.

emu.rs

fn main() {
    let mut emu = limwemu::emu64();
    emu.init_logger();
    emu.load_code("/bin/ls.static");
    emu.cfg.console_enabled = true;
    emu.run(None).unwrap();
    emu.spawn_console();
}

emu.rs

let mut emu = libmwemu::emu64();
let vm_buff = emu.alloc("buff", 1024, Permission::READ_WRITE_EXECUTE);
let vm: Vec = vec![
    0x17, 0xC6, 0x04, 0x4D, 0xFF,
    0x10, 0x5C, 0x3D, 0x86, 0x09,
    0xC9, 0x30, 0xAC, 0x42, 0x05,
    0x59, 0x8C, 0x21, 0x07, 0xD2,
];

emu.load_code_bytes(&shellcode);
let decoder = 0x3c000;
emu.call64(decoder, &[&vm_buff]);

emu.rs

fn main() {
    let mut emu = limwemu::emu64();
    emu.init_logger();
    emu.load_code("/bin/ls.static");
    emu.cfg.console_enabled = true;
    emu.run(None).unwrap();
    emu.spawn_console();
}

Operações de pilha

Existem métodos para alterar a pilha, note que você também poderia fazer com escritas emu.maps.

Mas com estes métodos você aciona toda a lógica da pilha, também incrementando rsp/esp.

  • stack_push32(value: u32) -> bool
  • stack_push64(value: u53) -> bool
  • stack_pop32() -> Option<u32>
  • stack_pop64() -> Option<u64>

Este exemplo simula uma convenção de chamada baseada em pilha, mas note que você também pode usar os métodos call32() e call64() e linux_call64().

emu.rs

loop {
    emu.maps.memset(str_buff, 0, 1024);
    emu.maps.memset(str_decoded, 0, 1024);
    emu.regs_mut().set_eip(decrypt_strings_func);
    emu.stack_push32(i);
    emu.stack_push32(str_buff as u32);
    emu.stack_push32(thread_ctx as u32);
    emu.stack_push32(ret_addr as u32);
    emu.run(Some(ret_addr)).unwrap();
    ...
}

Breakpoints

Como um depurador, você pode configurar diferentes tipos de breakpoints, mas a implementação é simples, mas existem outras maneiras de acionar a parada da emulação.

  • emu.bp.show()
  • emu.bp.clear_bp()
  • emu.bp.add_bp(addr: u64)
  • emu.bp.addr.clone() obtém um vetor de breakpoints baseados em endereço.
  • emu.bp.add_bp_instruction(ins: u64)
  • emu.bp.instruction.clone()
  • emu.bp.add_bp_mem_read(addr: u64)
  • emu.bp.mem_read_addr.clone()
  • emu.bp.add_bp_mem_write(addr: u64)
  • emu.bp.mem_write_addr.clone()

Para mais informações, verifique a referência do objeto Breakpoint.

Despejo de memória

existem várias maneiras de fazer um despejo de memória

  • emu.maps.save_all_allocs(path: &str) apenas salva em disco as alocações feitas pelo código emulado.
  • emu.maps.save(addr: u64, size: u64, filename: String) despeja um blob específico em disco.

Exemplo

main.rs

emu.maps.print_maps()
emu.maps.save(0x40324234, 1024, "/tmp/blob.bin")

Ver informações

Existem alguns métodos menos usados para buscar diferentes tipos de informação.

  • let mut output = String::new(); self.emu .formatter .format(&self.emu.instruction.unwrap(), &mut output); obtém o último mnemônico emulado.
  • emu.pos = 0; redefine o contador de instruções emuladas para zero.
  • emu.cfg.is_64bits detecta em qual modo o emulador está.
  • emu.disassemble(addr: u64, amount: u32) -> String desmonta bytes.
  • emu.maps.print_maps() imprime todos os maps (alocações, dlls vinculadas, etc)
  • emu.maps.print_maps_keyword(kw: &str) imprime os maps que contêm uma palavra-chave.
  • let base = match emu.maps.get_addr_base(addr) { Some(v) => Ok(v), None => ..., };
  • emu.maps.is_mapped(addr: u64) -> bool verifica se um endereço está alocado.
  • emu.maps.get_addr_name(addr: u64) -> String obtém em qual nome de map o endereço está.
  • emu.maps.dump(addr: u64) imprime bytes.
  • emu.maps.dump_n(addr: u64, amount: u64) imprime n bytes.
  • emu.maps.dump_qwords(addr: u64, n: u64) imprime uma lista de qwords.
  • emu.maps.dump_dwords(addr: u64, n: u64) imprime uma lista de dwords.
  • emu.maps.size() -> usize mostra a memória total alocada.
  • emu.maps.overlaps(addr: u64, sz: u64) -> bool verifica se um bloco de memória se sobrepõe a um map existente.
  • emu.maps.show_allocs() imprime todas as alocações feitas pelo código emulado.
  • emu.maps.mem_test() -> bool faz um teste automático de memória para procurar sobreposições de map.
  • emu.api_addr_to_name(addr: u64) -> String fornece um endereço apontando para uma API e buscará o nome da API.
  • emu.api_name_to_addr(name: &str) -> u64 obtém o endereço de um nome de api.

Exemplo

main.rs

let name = emu.api_addr_to_name(0x11223344)
println!("{}", name)
   MessageBoxA

Alternar bits de arquitetura.

Existem métodos para mudar de 32 bits para 64 bits e vice-versa.

  • set_64bits()
  • set_32bits()
  • inspect_seq(s: str)

Mas é melhor não mudar a arquitetura em tempo de execução, re-instanciar o objeto de emulação como:

Exemplo

main.rs

let mut emu = libmwemu::emu64();
...
let mut emu = libmwemu::emu32();

Ou até mesmo criar ambos os objetos.

Hooks

O objeto Hooks permite estender o emulador sem bloquear a emulação.

Habilitar um hook

  • emu.hooks.on_memory_read(trace_memory_read); o hook é acionado após cada leitura.
  • emu.hooks.on_memory_write(trace_memory_write); o hook é acionado antes de cada escrita, e o hook pode alterar o valor a ser escrito retornando-o.
  • emu.hooks.on_interrupt(trace_interrupt); o hook é acionado antes de processá-lo, o hook pode decidir com o valor de retorno se o mwemu faz o processamento ou não.
  • emu.hooks.on_exception(trace_exceptions); o hook é acionado antes de processá-lo, o hook pode decidir com o valor de retorno se o mwemu faz o processamento ou não.
  • emu.hooks.on_pre_instruction(trace_pre_instruction); o hook é acionado antes de emular a instrução.
  • emu.hooks.on_post_instruction(trace_post_instruction); o hook é acionado após emular a instrução.
  • emu.hooks.on_winapi_call(trace_winapi_call); você pode implementar uma API do Windows.

Desabilitar um hook

  • emu.hooks.disable_memory_read();
  • emu.hooks.disable_memory_write();
  • emu.hooks.disable_interrupt();
  • emu.hooks.disable_exception();
  • emu.hooks.disable_pre_instruction();
  • emu.hooks.disable_post_instruction();
  • emu.hooks.disable_winapi_call();

Exemplo main.rs

use libmwemu::emu32;

//need iced_x86 crate only for instruction hooks, to get the
//instruction object, so cargo add iced-x86
use iced_x86::{Instruction};  

fn trace_memory_read(emu:&mut libmwemu::emu::Emu, ip_addr:u64, 
                     mem_addr:u64, sz:u8) {
    log::info!("0x{:x}: reading {} at 0x{:x}", ip_addr, sz, mem_addr);
    if mem_addr == 0x22dff0 {
        emu.stop();
    }
}

fn trace_memory_write(emu:&mut libmwemu::emu::Emu, ip_addr:u64, 
                      mem_addr:u64, sz:u8, value:u128) -> u128 {
    log::info!("0x{:x}: writing {} '0x{:x}' at 0x{:x}", ip_addr, sz, 
             value, mem_addr);
    value   // I could change the value to write
}

fn trace_interrupt(emu:&mut libmwemu::emu::Emu, ip_addr:u64, 
                   interrupt:u64) -> bool {
    log::info!("interrupt {} triggered at eip: 0x{:x}", interrupt, 
             ip_addr);
    true  // do handle interrupts
}   

fn trace_exceptions(emu:&mut libmwemu::emu::Emu, ip_addr:u64, ex_type: libmwemu::exception_type::ExceptionType) -> bool {
    log::info!("0x{:x} triggered an exception {}", ip_addr, ex_type);
    if (ex_type == libmwemu::exception_type::ExceptionType::Int3) {
        // do handle SIGTRAP for example
    }
    true // do handle exceptions
}

fn trace_pre_instruction(emu:&mut libmwemu::emu::Emu, ip_addr:u64, 
                         ins:&Instruction, sz:usize) -> bool{
                            // return false to skip the instruction
                            true
}

fn trace_post_instruction(emu:&mut libmwemu::emu::Emu, ip_addr:u64, 
                          ins:&Instruction, sz:usize, emu_ok:bool) {
}

fn trace_winapi_call(emu:&mut libmwemu::emu::Emu, ip_addr:u64, api_addr:u64) -> bool {
    return true; // handle api calls
}

fn main() {
    let mut emu = emu32();
    emu.set_maps_folder("../mwemu/maps32/"); // download the maps, ideally from mwemu git.

    emu.load_code("/home/sha0/src/mwemu/shellcodes32/mars.exe"); // load_code() sets everything up
    emu.hooks.on_memory_read(trace_memory_read);
    emu.hooks.on_memory_write(trace_memory_write);
    emu.hooks.on_interrupt(trace_interrupt);
    emu.hooks.on_exception(trace_exceptions);
    emu.hooks.on_pre_instruction(trace_pre_instruction);
    emu.hooks.on_post_instruction(trace_post_instruction);
    emu.hooks.on_winapi_call(trace_winapi_call);
    emu.run(None).unwrap();
    log::info!("end!");
}

Modo kernel: drivers Linux

Além de binários PE/ELF em modo usuário e shellcode, o mwemu pode carregar e emular módulos de kernel Linux (arquivos .ko). Ele vincula o driver a um kernel sintético que controla e modela o alocador de slab explicitamente, de modo que um bug de segurança de memória — a espécie dominante de vulnerabilidade de kernel — aparece como um relatório em vez de um pânico de kernel. Sem kernel real, sem root, nada é executado no host.

Por que a emulação de drivers é diferente

Emular um driver não é emular um programa com imports diferentes. Um driver não tem ponto de entrada, nem libc, nem loader e nem processo: um .ko é um objeto relocável ET_REL — sem cabeçalhos de programa, sem .dynamic, seções que não foram posicionadas. É o sistema operacional que posiciona essas seções em seu próprio espaço de endereçamento, as realoca e então chama de volta para o módulo. O mwemu fornece exatamente as três coisas que estão faltando:

1. Um linker. As seções .ko são posicionadas e cada realocação é aplicada no momento do carregamento.

2. Um kernel para chamar. Cada símbolo importado (kmalloc, mutex_lock, printk …) é resolvido para um endereço em uma região sintética de "texto do kernel"; uma chamada que chega lá é interceptada e roteada para uma implementação em Rust — o mesmo mecanismo que a camada WinAPI usa.

3. Um alocador com memória. Bugs de driver são bugs de tempo de vida, então o slab é modelado explicitamente: chunks são rastreados com sua proveniência, chunks liberados vão para quarentena em vez de serem reciclados, e cada acesso é verificado contra o registro.

Como funciona

O ponto 3 é a razão pela qual isso existe. Um slab real devolve memória liberada diretamente, o que é exatamente o que torna um use-after-free difícil de ver. O mwemu inverte isso: um chunk liberado não é reciclado — ele vai para quarentena, permanece mapeado, e seus bytes são sobrescritos com o veneno de liberação do SLUB (0x6b6b6b6b…). Os chunks são separados por uma redzone não mapeada, então um overflow linear no final de uma alocação causa falha em vez de corromper silenciosamente o próximo objeto. Manter o chunk mapeado-mas-envenenado é o que transforma um bug invisível em um relatório — e como ele permanece mapeado, a execução continua além da primeira dereferência obsoleta, então uma única execução pode revelar toda a cadeia em vez de parar no primeiro sintoma.

Espaço de endereçamento

As regiões são escolhidas para corresponder aos layouts reais do kernel e — mais importante — para que as distâncias entre elas permaneçam dentro do que as realocações podem codificar. Um módulo construído com -mcmodel=kernel alcança o kernel através de R_X86_64_PLT32, um deslocamento assinado de 32 bits, então o módulo e a área de stub devem estar dentro de ±2GB.

Espaço de endereçamento Linux

kernel text (stubs)   0xffffffff81000000   one interceptable slot per imported function
kernel data           0xffffffff82000000   storage for imported variables (jiffies, ...)
module image          0xffffffffc0000000   the loaded .ko
slab                  0xffff888000000000   kmalloc / kmem_cache_alloc chunks
vmalloc               0xffffc90000000000   vmalloc / page allocations
kernel stack          0xffffc90000100000

Superfície do kernel emulado

A superfície Linux é ampla, e cada variante do alocador converge para um único registro (__kmalloc, __kmalloc_noprof, kmem_cache_alloc, kvmalloc, devm_kzalloc, vmalloc, kfree, kmem_cache_free …), então não importa contra qual versão do kernel o driver foi construído. Também implementados: cópias de usuário (copy_from_user / copy_to_user), helpers de string e memória (memcpy, memmove, strscpy …), refcounts e krefs, locking, trabalho adiado (workqueues, timers, RCU), logging (printk / dev_*) e registro de dispositivos (misc_register, __register_chrdev, proc_create …).

Algumas peças são modeladas de verdade em vez de stubadas, porque o bug vive nelas. Os helpers de memória também passam pela guarda — um memcpy() em um objeto liberado é um use-after-free que nenhuma verificação em nível de instrução veria, porque a cópia é executada dentro do código do kernel, não do driver. Refcounts são reais: refcount_dec_and_test() retornando true é o que dispara uma liberação. E callbacks adiados (call_rcu, schedule_work, timers) são enfileirados, não executados inline — "desregistrar agora, liberar depois" é a forma da maioria dos UAFs de kernel, então executar o callback imediatamente fecharia exatamente a janela onde o bug vive; drene-os com kernel_run_deferred(). Locking é um no-op (emulação single-threaded não pode deadlockar). Um import sem implementação não é fatal: é relatado no momento do carregamento como unresolved, e se for chamado retorna 0, então um kernel parcialmente coberto ainda executa um driver o mais longe possível. A lista completa é retornada em tempo de execução por libmwemu::kernel::linux::SURFACE e pela ferramenta MCP mwemu_kernel_surface.

O que ele detecta

Descobertas

use-after-free (read/write)   access lands in a quarantined chunk
poisoned-pointer deref        the address itself is free poison (0x6b6b...) --
                              the pointer was loaded out of a freed object
use-after-free call           an indirect branch target came out of quarantine
double-free                   free of a chunk already in quarantine
invalid-free                  free of something that is not a chunk base
slab out-of-bounds            access past the requested size, inside the bucket
memory-leak                   still live after the module's exit path ran

Cada descoberta carrega a instrução que causou a falha, o objeto, seu cache e tanto o local de alocação quanto o de liberação. Repetições do mesmo (tipo, instrução, objeto) são colapsadas em uma contagem de ocorrências.

Carregando um .ko a partir da CLI

A CLI vincula o módulo e deixa o PC em seu init, então uma execução simples faz o que insmod faria:

Shell

❯❯❯ mwemu -f driver.ko -6 -v

Alcançar a superfície ioctl (e portanto a maioria dos bugs) precisa de structs de argumento na memória do convidado, então é dirigido a partir de Rust ou via MCP — veja abaixo.

Dirigindo um driver a partir de Rust

Rust

let mut emu = libmwemu::emu64();
emu.load_kernel_module("driver.ko")?;          // link + relocate the ET_REL object
emu.run_module_init()?;                         // this is what insmod does
emu.call_module_symbol("drv_ioctl", &[0, cmd, argp])?;
for f in emu.kernel_findings() {
    println!("{}", f.report());
}

Dirigindo um driver a partir de Python (pymwemu)

A mesma superfície de driver está disponível a partir de Python, então você pode vincular um .ko, dirigir um handler específico com um argumento elaborado e ler o registro de slab de volta — a maneira prática de fuzzing de uma função sem um kernel. Observe que a ferramenta de linha de comando apenas executa o init do módulo; a partir de pymwemu (ou Rust, ou MCP) você pode alcançar qualquer símbolo exportado com seus próprios argumentos.

Python

import pymwemu

emu = pymwemu.init64()
emu.load_kernel_module("driver.ko")          # link + relocate the ET_REL object
emu.run_module_init()                         # this is what insmod does

# drive one handler with attacker-controlled arguments
emu.call_module_symbol("drv_ioctl", [0, cmd, argp])

for f in emu.kernel_findings():               # the slab ledger's reports (list of str)
    print(f)

if emu.kernel_found_uaf():                    # quick boolean check
    print("use-after-free detected")

Dirigindo um driver via MCP

A mesma superfície é exposta através do servidor Model Context Protocol, então um agente de IA pode vincular um driver, dirigir seus ioctls conversacionalmente e ler as descobertas de volta sem nunca tocar em um kernel. As ferramentas de modo kernel são mwemu_kernel_load_module, mwemu_kernel_init, mwemu_kernel_call, mwemu_kernel_findings e mwemu_kernel_surface.

Exemplo trabalhado: um use-after-free

O alvo de referência é tlm, um driver de telemetria deliberadamente vulnerável escrito da maneira que um real é: objetos com refcount em seus próprios kmem_cache, uma lista de canais protegida por mutex, vetores de operação por objeto e uma superfície ioctl. O bug não é "liberar, depois ler duas linhas depois". O driver mantém um cache de canal quente de uma entrada para pular a caminhada da lista em escritas repetidas; o invariante é que quem remove um canal também limpa o cache. Isso é honrado no fechamento e no descarregamento, mas o autor perdeu a terceira maneira de um canal morrer: TLM_IOC_DESTROY libera o objeto enquanto o handle do arquivo permanece aberto. O cache fica pendurado, e a próxima escrita pega o caminho quente direto através dele — pulando até a verificação de magic — até uma chamada indireta:

drivers/linux/tlm/tlm.c (excerto)

if (dev->fast && dev->fast_id == req->id)
        ch = dev->fast;                       /* freed object */
...
ret = ch->ops->encode(ch, kbuf, req->len);    /* indirect call through it */

Gatilho: crie um canal, escreva uma vez (popula o cache), destrua-o, escreva novamente. Construa o driver e execute-o de ponta a ponta:

Shell

❯❯❯ make driver
❯❯❯ cargo test -p libmwemu tests::kernel -- --nocapture

O que o mwemu relata para a escrita com cache obsoleto:

Saída

BUG: KMWEMU: use-after-free (read) in tlm_channel of size 8 at addr 0xffff888000001128
  object 0xffff888000001100..0xffff888000001160 (requested 88 bytes, bucket 96), offset 40
  allocated by kmem_cache_alloc_noprof at 0xffffffffc000056c (step 86)
  freed by kmem_cache_free at 0xffffffffc00004e9 (step 407)

BUG: KMWEMU: use-after-free (poisoned pointer dereference) at addr 0x6b6b6b6b6b6b6b73

Lidas juntas, essas duas linhas são o bug inteiro. A primeira é o carregamento de ch->ops (offset 40) fora do objeto em quarentena, nomeando seu cache e ambos os locais de alocação e liberação. A segunda é a dereferência do ponteiro que esse carregamento produziu — 0x6b6b… é veneno de liberação, então sua proveniência é prova, não um palpite.

Windows e macOS

Apenas as tabelas de símbolos e os handlers diferem entre sistemas operacionais; posicionamento, interceptação, o registro e a análise são compartilhados. Windows e macOS já têm suas superfícies declaradas e seus alocadores de pool/zone implementados (ExAllocatePool2 / ExFreePool para ntoskrnl, IOMalloc / IOFree / kalloc_external para XNU). O que ainda está faltando é o loader: um .sys é um PE com um DriverEntry, então precisa do caminho PE com posicionamento em espaço de kernel em vez do caminho ET_REL; um kext é um Mach-O MH_KEXT_BUNDLE com realocações externas. No dia em que qualquer loader chegar, ele herda toda a análise de use-after-free de graça, porque a parte que encontra o bug nunca se importou para qual SO o driver era.