mwemu-mcp

emulación binaria, x86 y varias funciones de sistema operativo (API de Windows, API de controladores de Linux, espacio de usuario de Linux)

Documentación

Introducción

Bienvenido a la documentación oficial de MWEMU, puedes desplazarte hacia abajo o usar los enlaces directos que están a la izquierda.

Repositorio de Github: https://github.com/sha0coder/mwemu

¿Qué es MWEMU?

MWEMU es un emulador de hardware y simulador de procesos de Windows escrito en Rust puro desde cero, fue creado por @sha0coder y es de código abierto. Los contribuidores de este software han mejorado mucho la calidad del proyecto, haz clic aquí para conocerlos.

El hardware implementado es x86/x64, a diferencia de otros emuladores también implementa algunas partes del sistema operativo (principalmente Windows) porque el código ensamblador tarde o temprano va a hacer llamadas al sistema (WinAPI, syscalls de Linux, etc.).

Ha resultado extremadamente útil para la desofuscación de malware, pero esto no reemplaza la ingeniería inversa, se necesita una ingeniería inversa previa para preparar bien el estado inicial de la emulación, y emular solo un pequeño grupo de funciones, como descifrado, keygen, etc. En algunos casos específicos mwemu puede hacer emulación completa, con empaquetadores simples, shellcodes codificados, etc.

La emulación y simulación está implementada desde cero, pero la increíble librería de Rust iced-x86 se usa para la traducción de un montón de bytes a objetos de instrucción. He implementado más de 300 instrucciones x86, flags, excepciones, algunos cargadores PE32/PE64/ELF64/shellcode32/shellcode64 y muchas otras cosas.

MWEMU es extremadamente rápido y también es seguro en cuanto a memoria gracias al mágico compilador de Rust.

Además de binarios en modo usuario y shellcode, mwemu también puede cargar y emular módulos de kernel de Linux (.ko) para buscar errores de seguridad de memoria en drivers sin un kernel real — ver Modo kernel: drivers de Linux.

Las 4 formas de usar MWEMU

1. La primera forma es la línea de comandos, ver línea de comandos para más detalles, esto es para probar la emulación completa.

2. La segunda forma es crear scripts de Python con el módulo pymwemu, que se puede instalar con pip o git.

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

Si necesitas funciones o correcciones de errores implementadas recientemente, usa el git, necesitarás compilar el proyecto completo y luego los enlaces de Python con maturin, más detalles en esta sección instalación de pymwemu

3. La tercera forma es usar el crate de Rust publicado en crates.io https://crates.io/crates/libmwemu desde una aplicación de Rust.

4. La cuarta forma es mwemu-mcp, un servidor de Model Context Protocol que expone libmwemu a clientes MCP como Claude. Funciona como manejar pymwemu a mano pero sobre MCP: abres una sesión para una arquitectura, la configuras, preparas la memoria (asignaciones, escrituras de registros y memoria), luego emulas paso a paso e inspeccionas el resultado, todo a través de herramientas discretas. Esto permite que un agente de IA maneje el emulador para analizar un binario. Más detalles: github.com/sha0coder/mwemu/crates/mwemu-mcp

Arquitecturas

Puedes ejecutar MWEMU desde Windows, Linux y Mac (x86 y también m1)

Pero solo se puede emular código x86 (32 bits y 64 bits) principalmente para Windows, hay soporte de shellcode de Linux e incluso syscalls están implementadas, pero en cuanto a ELF solo 64 bits, solo compilado estático y soporte bastante básico por ahora, pero no hay problema con shellcodes.

libc se puede emular bien a pesar de que está lleno de instrucciones ymm, pero el enlazador no se puede emular totalmente por ahora, mi plan es emular completamente el enlazador, en ese caso no necesito implementar todo el proceso de enlazado (creación de .got y .plt, etc.)

Un poco de internals

Por ahora, solo una visión general básica de los internals.

En el pasado nombré al proyecto SCEMU, y estaba almacenado en 3 repositorios separados para mwemu (la línea de comandos), pymwemu (módulo de Python) y (libmwemu) el motor donde todo está implementado y también es el módulo de Rust en crates.io.

Luego fue renombrado a MWEMU, porque SCEMU es más específico para shellcode y porque es una mala palabra en italiano.

Entonces, ahora es un solo repositorio con una carpeta crates/ con los 3 crates.

Las pruebas están implementadas en crates/libmwemu/src/tests/ y se describen más adelante.

La mayoría de los archivos fueron divididos en archivos pequeños.

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/ contiene métodos de emulación y subobjetos que están involucrados en la emulación misma.

En engine/ están las implementaciones de todas las instrucciones.

winapi/ contiene las implementaciones de WinAPI divididas en winapi32/ y winapi64/

hay otras cosas como contants.rs, structures/, etc.

Modo syscall (SSDT)

Además de emular la WinAPI de alto nivel, mwemu puede ejecutarse en un modo de syscall de nivel más bajo (también llamado modo SSDT), habilitado con el flag --ssdt (alias --syscall-mode) en la línea de comandos (o la configuración ssdt_use_ldr_initialize_thunk desde la API).

En este modo mwemu no ataja el espacio de usuario: carga un ntdll.dll genuino y deja que la muestra baje hasta la instrucción syscall, exactamente como en un Windows real. Esto está más cerca de lo real y es muy útil para estudiar malware que llama syscalls directamente para evadir hooks de espacio de usuario.

Para despachar una syscall, mwemu necesita mapear un Número de Servicio del Sistema (SSN) a la rutina del kernel que representa. Esos números no son fijos: cambian entre versiones de Windows. Así que en lugar de hardcodear una tabla, mwemu los resuelve desde el mismo ntdll.dll que se carga, recorriendo su Tabla de Direcciones de Exportación (EAT) para encontrar los stubs Nt* / Zw* y leyendo el SSN codificado dentro de cada stub (el mov eax, ssn justo antes del syscall). De esta manera el SSDT siempre es consistente con la versión exacta de Windows en uso (ver la función load_maps_from_winver() para obtener una compilación concreta).

El siguiente diagrama muestra ese diseño de resolución EAT, cómo se extraen los SSN de la tabla de exportación de ntdll para construir la tabla de despacho de syscalls:

Sistema de pruebas

Para activar localmente usa make tests esto descarga algunos binarios y lanza el sistema de pruebas cargo test

No uses --release, siempre es más conveniente hacer las pruebas sin aplicar las optimizaciones, que podrían ignorar algunos tipos de errores. Actualmente el CI de GitHub está configurado para hacer cargo test y también cargo test --release para verificar ambos modos.

Cada push o pull-request activará el CI en GitHub para realizar todas las pruebas en Linux, Windows y Mac. En el caso de un PR es obligatorio, en el caso de un push es solo informativo.

El PR también activa un análisis de cobertura de las pruebas, que actualmente es solo del 32%

Contribuidores del proyecto

Brandon Ros

Archeron2302

ElCapor

También hay otras personas que sugirieron ideas interesantes y optimizaciones.

En cuanto a wit00, es una falla de GitHub por hacer push con una mala configuración en git config. (el error fue reportado a GitHub)

Soy @sha0coder y creé este software para potenciar mis trabajos de ingeniería inversa, y lo comparto porque creo que es útil para algunos casos.

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

Licencia

Actualmente hay varias licencias, el código fuente es GPLv3, pero el módulo de Rust en crates.io y el módulo de Python en pypi son MIT para tener menos restricciones al distribuir software que use libmwemu o pymwemu.

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

No dudes en contactarme para crear tecnologías basadas en este software.

email: sha0 at badchecksum dot net

Herramienta de línea de comandos de MWEMU

La línea de comandos es una forma rápida de usar mwemu, y hay muchas funciones como rastreo de registros/memoria/llamadas/cadenas o captura de momentos de emulación.

Si el empaquetador es simple, probablemente se pueda emular completamente usando la herramienta de línea de comandos, pero si necesitas más control usa pymwemu y para control total libmwemu.

En Rust puedes compilar y ejecutar junto con cargo run, usa el modo --release para una ejecución más rápida, ejemplo:

Shell

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

Esto es equivalente a hacer:

Shell

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

Instalación de MWEMU

1. Primero necesitas instalar Rust y Cargo, y la mejor manera es usando rustup.

https://rustup.rs/

Por ejemplo en Linux o Mac:

Shell

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

En la configuración predeterminada los binarios están en ~/.cargo/bin/ pero necesitas agregar esto a la ruta, idealmente en la última sección de la ruta.

Shell

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

El instalador dice cómo ponerlo en el bashrc u otros archivos rc de shells.

2. Luego hay 2 opciones para instalar esto

  • Instalar desde crates.io (es más conveniente la segunda opción) Shell
    ❯❯❯ cargo install mwemu
    
  • La forma recomendada es clonar el repositorio de GitHub y compilar desde ahí, con Cargo es simple. Shell
    git clone https://github.com/sha0coder/mwemu.git
    cargo build --release
    make tests
    

--help

Usa la opción de ayuda para ver las opciones de la línea de comandos, nota que antes del "--" hay flags de cargo y después del "--" están los parámetros del programa, en este caso la línea de comandos de 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)

Emulación completa

Las opciones principales son:

  • -6 para modo de 64 bits (de lo contrario sería de 32 bits)
  • -f para seleccionar la ruta del archivo.
  • -vv para ver el ensamblador emulado. (la emulación es más rápida sin verbosidad, en este caso solo muestra las llamadas a la API)

Shell

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

Capturando un momento

MWEMU siempre muestra el número de instrucciones emuladas, y este es un identificador único de un momento.

El momento 1 es la primera instrucción de ensamblador, si agregas el flag -c 1 el emulador se detendrá antes de emular la instrucción 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 ---
=>

La instrucción 1 no estará coloreada, esto significa que esa instrucción se va a emular en el siguiente paso.

Esto abre la consola en ese estado del emulador, y puedes presionar enter para emular pasos o el comando "h" para ver opciones.

Si el modo verboso no está configurado, solo se mostrarán WinAPI, syscalls, etc., pero también habrá un número que representa las instrucciones emuladas hasta ese estado.

Existe la opción de detener el emulador en una dirección específica con "-C addr" en mayúsculas, pero nota que la dirección puede activarse múltiples veces y no es un identificador único como el momento -c

Verbosidad

Hay 4 niveles de verbosidad:

  • 0: No uses ningún -v para verbosidad mínima, solo se imprimirán llamadas a WinAPI y syscalls.
  • 1: Usa -v para ver también algunos mensajes como, código polimórfico detectado, etc.
  • 2: Usa -vv para ver también el código ensamblador, mwemu imprimirá cada instrucción de ensamblador, esto hace más lenta la emulación.
  • 3: Usa -vvv para ver también cada interacción "rep", en instrucciones con prefijo rep como "rep movzx" se imprimirá una línea por cada paso del bucle rep.

Usa -V o --verbose_at para habilitar el modo verboso en un punto específico.

El modo verboso se activa automáticamente 100 instrucciones antes del momento -c configurado para detener.

Registro de logs

Es posible redirigir la salida a un archivo, es decir:

Shell

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

Pero nota que los colores son bytes de escape de terminal y se registrarán haciendo el análisis más difícil, si haces cat /tmp/log verás los colores pero si usas un editor verás esos bytes

Es más conveniente usar la opción --log para logs limpios.

Shell

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

Inicializar registros

Hay algunos casos como emular dlls o fragmentos de código que necesitan algunos valores iniciales en los registros.

La herramienta de línea de comandos permite configurar registros usando estas opciones:

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

Pero nota que los colores son bytes de escape de terminal y se registrarán haciendo el análisis más difícil, si haces cat /tmp/log verás los colores pero si usas un editor verás esos bytes

Es más conveniente usar la opción --log para logs limpios.

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

Rastrear registros

Usa la opción -R <registers to trace> para rastrear algunos registros

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

Puedes rastrear uno o múltiples registros al mismo tiempo con la opción -R pero sin espacios entre registros.

Rastrear memoria

Hay 2 formas de rastrear memoria:

  • -m para rastrear todas las lecturas y escrituras de memoria. (opcionalmente -S momento para habilitar el rastreador desde un momento específico)
  • -i 'dword ptr [eax + 0x8]' el modo de inspección permite muchas expresiones pero no todas las combinaciones.

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 Cadena

Teniendo la dirección de la cadena a rastrear, usa -s <addr> para rastrearla

Shell

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

Hacer un verbose completo de millones de instrucciones es más lento que el modo no verbose, así que habilitemos el modo verbose solo cuando sea necesario.

Shell

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

Otra opción es usar rastreadores sin modo verbose.

Shell

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

Rastrear Llamadas a Funciones

Seguir las rutas de llamadas puede ser útil al combinar emulación con análisis estático para ver de dónde venimos.

Shell

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

En este caso, es más conveniente no usar el modo verbose.

Consola Interactiva

Con la opción -c <num> mwemu detiene la emulación cuando se alcanza ese número de instrucciones emuladas, y luego abre una consola.

Por ejemplo, no queremos emular 102063765 instrucciones en modo verbose, es más rápido no usar el modo verbose. La opción -c habilitará el modo verbose y los rastreadores 100 instrucciones antes de alcanzar ese número, así cuando se abre la consola tenemos algo de contexto previo.

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 Ayuda

presiona h para ver los comandos disponibles:

consola 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 Registros

Usa r para ver todos los registros, o r [reg] para ver un registro específico.

consola 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

consola mwemu

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

puedes usar solo registros de 64 bits y 32 bits, 16/8 bits no está permitido por ahora ("r ax" o "r al")

Comando de Mapas

presiona m para listar todos los mapas de memoria y direcciones.

consola 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 el nombre del mapa, dirección de inicio, dirección de fin y tamaño en bytes.

Otros comandos relacionados con memoria:

consola 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

Obtener Detalles del Mapa desde una Dirección

si el código está usando una dirección y quieres más detalles, usa el comando mn.

consola mwemu

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

Nota que los comandos de mwemu no aceptan parámetros directamente, primero escribe el comando + enter y luego se solicitará el parámetro.

excepto para el comando r2 que necesita una dirección para abrir radare2, es decir: r2 0x140000008

Comandos de Búsqueda

Hay cuatro comandos para buscar.

  • usa el comando ss para buscar una cadena en un mapa específico.
  • usa el comando sb para buscar una secuencia de bytes espaciados en un mapa específico.
  • usa el comando ssa para buscar una cadena en todos los mapas.
  • usa el comando sba para buscar una secuencia de bytes espaciados en todos los mapas.

consola 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 Puntos de Interrupción

Hay cuatro tipos de puntos de interrupción, pero solo se puede establecer un punto de interrupción por cada tipo a la vez.

  • interrupción en dirección, la próxima vez que se alcance esta dirección la emulación se detendrá allí.
  • interrupción en instrucción, cuando el emulador alcance ese número de instrucciones emuladas en total, se detendrá allí.
  • interrupción en lectura de memoria, la próxima vez que esta dirección sea leída por cualquier instrucción de ensamblador (no api o syscall) el emulador se detendrá allí.
  • interrupción en escritura de memoria, la próxima escritura a esta dirección (no importa si es una escritura de 1 byte o cualquier cantidad) detendrá la emulación.
  • interrupción en la próxima instrucción cmp o test, esto detendrá el emulador en la próxima instrucción cmp o test.

Usa el comando "b" para ver el estado de los 4 tipos de puntos de interrupción. Hay cuatro comandos para buscar.

consola mwemu

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

Usa estos comandos para establecer los puntos de interrupción:

consola 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

Algunos ejemplos:

consola 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 ---
=>

Cambiar Verbosidad

Si escribes el comando "sv", mwemu te pedirá el nuevo número de nivel de verbosidad, estos son los posibles niveles de verbose:

  • 0: Es como no usar ningún -v para verbosidad mínima, solo se imprimirán llamadas WinAPI y syscalls.
  • 1: Es como usar -v para ver también algunos mensajes como, código polimórfico detectado etc.
  • 2: Es como usar -vv para ver también el código ensamblador, mwemu imprimirá cada instrucción de ensamblador, esto hace más lenta la emulación.2: Usa -vv para ver también el código ensamblador, mwemu imprimirá cada instrucción de ensamblador, esto hace más lenta la emulación.
  • 3: Es como usar -vvv para ver también cada interacción "rep", en instrucciones con prefijo rep como "rep movzx" se imprimirá una línea por cada paso del bucle rep.

Por ejemplo, queremos emular rápidamente las primeras 200 instrucciones, y luego habilitar la verbosidad, esto se podría hacer con -V, pero hagámoslo desde la consola con el comando "sv":

consola 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) 
...

Ver LDR

El LDR es una triple lista circular enlazada que contiene todos los módulos enlazados (no solo DLL, también EXE)

MWEMU proporciona varios comandos para ver y consultar el LDR.

  • el comando "ldr" es la forma de ver el contenido del LDR.
  • el comando "iat" permite encontrar un nombre de api específico en todos los IAT de cada módulo enlazado.
  • "iatx" Si tenemos una dirección y queremos saber qué nombre de API es, este comando hace la consulta de dirección a nombre.
  • "iatd" el comando vuelca el IAT completo de un módulo especificado.

consola 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

Ver Estructuras

El depurador de windows windbg tiene una característica única que es el comando dt para ver información sobre estructuras, es bastante útil y único.

MWEMU implementa un comando dt similar pero para estructuras específicas, que podría ser útil en algunas situaciones.

Usemos dt para inspeccionar la estructura PEB.

consola 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,
...

Usemos dt para inspeccionar la estructura PEB_LDR_DATA.

consola 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,
    },
}
=>

Usemos dt para inspeccionar la estructura LDR_DATA_TABLE_ENTRY, que representa una entrada LDR en la lista enlazada de un módulo enlazado específico.

consola 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,
}
=>

Ejemplo: un malware está ocultando algo en una excepción.

consola 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 ---
=>

Inspeccionemos las estructuras de excepción:

consola 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,
}
=>

Y aquí tenemos la rutina de error 0x4f96f4 y el filtro 0x51068c.

Ver Datos

Hay múltiples comandos para ver datos, pero actualmente estoy usando el comando "r2 addr" que es mejor tanto para código como para datos. Nota que el comando r2 ejecuta radare2 y transfiere el mapa de memoria de la dirección seleccionada, y sincroniza radare2 con mwemu, pero este comando necesita tener radare2 instalado en el path, por ejemplo desde el git. Más detalles en el capítulo radare2. Vale la pena instalar radare2.

Comandos para mostrar información:

consola 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

Ejemplo con el comando md:

consola 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 da mucho poder a MWEMU porque podemos usar radare2 desde dentro de un momento de emulación de MWEMU.

Nota que esto ejecuta otro programa (también software libre) llamado radare2 que tiene que estar instalado y accesible desde el path.

La instalación es simple, lo explicaré más adelante.

Abriendo radare sobre una dirección de datos:

Usando q para volver del prompt de r2 al prompt de mwemu y abrir radare de nuevo con una dirección de código:

Radare está reconociendo funciones y decompilándolas con los múltiples decompiladores, incluso podemos usar decai para decompilación basada en IA en python etc.

Más información específica de los comandos de radare consulta el r2book: https://book.rada.re/

Respecto a la instalación de radare2:

consola mwemu

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

El script install.sh hace toda la instalación, pedirá sudo para copiar binarios a carpetas que están en el path.

Si radare2 está en el path, sería posible activarlo desde el comando r2 de mwemu.

MWEMU desde scripts de python

Este es probablemente el caso de uso más práctico de MWEMU, usando el módulo de python pymwemu.

Instalación de pymwemu

La forma más fácil de instalar esto es usando pip, el paquete está publicado en pypi https://pypi.org/project/pymwemu/

Para usar la última versión usa git, pero es más complicado de instalar, porque necesitas rust, cargo y maturin.

Está precompilado para linux 64 bits, así que en linux en teoría no necesitas instalar rust primero.

En linux solo haces:

consola mwemu

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

En mac, windows o si pip lo requiere, instala primero rust.

Instala rust desde rustup, asegúrate de que cargo esté en el path, y haz pip o pip3:

consola 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

En mac, si hay un problema con !tapi-tbd la solución es:

Shell

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

Si hay el error: Caused by: feature edition2024\ is required. entonces actualiza tu rust: rustup update

Para verificar la instalación podemos importar el módulo en la consola de 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 (solo para desarrolladores)

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

recuerda actualizar la versión tanto en Crates.toml como en pyproject.toml

Crear el objeto de emulación

Primero de todo, importa el módulo e inicializa el motor para la arquitectura que necesites: 32 bits, 64 bits o aarch64 (ARM64).

Consola de Python

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

También puedes restaurar un estado de emulador guardado previamente en lugar de crear uno nuevo, consulta la sección de serialización:

  • pymwemu.load_from_file(filename: str) -> Emu cargar un estado guardado previamente con emu.dump_to_file().
  • pymwemu.load_from_minidump(filename: str) -> Emu cargar un estado guardado con emu.dump_to_minidump().
  • pymwemu.deserialize(data: bytes) -> Emu reconstruir un emulador desde los bytes devueltos por emu.serialize().

Configurar el Emulador

Luego hay algunas configuraciones iniciales que puedes hacer.

Consola de 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 las configuraciones disponibles:

  • set_verbose(n: int) 0: solo mostrar apicalls, 1: también mensajes, 2: también ensamblador, 3: también reps desenrollados.
  • enable_console() esto abre la consola de comandos de mwemu.
  • disable_console()
  • spawn_console_at_pos(n: int) abre la consola cuando se alcance la posición n (n instrucciones emuladas)
  • spawn_console_at_addr(addr: int) abre la consola cuando se alcance la dirección addr.
  • disable_colors()
  • enable_colors()
  • enable_trace_mem()
  • disable_trace_mem()
  • enable_trace_regs()
  • disable_trace_regs()
  • enable_trace_reg(regs_list: list(str)) proporciona lista de cadenas con nombres de registros a rastrear.
  • 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() forzar modo shellcode en lugar de autodetectar el 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() si encuentra múltiples ceros también emularlos.
  • banzai_add(apiname: str, nparams: int) banzai seguir emulando en una API no implementada.
  • enable_ctrlc() / disable_ctrlc() manejar Control-C para abrir la consola.
  • enable_threading(enable: bool) habilitar/deshabilitar el planificador de hilos.

Propiedades de configuración

Además de los interruptores anteriores, el emulador expone toda la configuración como propiedades de python simples que puedes leer y asignar directamente (estas reflejan las banderas de línea de comandos del binario mwemu):

  • emu.max_instructions: int | None detener después de esta cantidad de instrucciones.
  • emu.timeout_secs: float | None detener la emulación después de esta cantidad de segundos.
  • emu.max_faults: int | None detener después de este número de fallos de memoria.
  • emu.exit_position: int posición de instrucción donde la emulación debe detenerse.
  • emu.dump_on_exit: bool / emu.dump_filename: str | None volcar el estado a un archivo cuando la emulación termine.
  • emu.trace_filename: str | None / emu.trace_start: int / emu.trace_calls: bool escribir un rastro de ejecución.
  • emu.module_name: str / emu.exe_name: str nombre presentado a la muestra emulada.
  • emu.user_name: str / emu.host_name: str identidad reportada por la winapi.
  • emu.temp_path: str / emu.cwd_path: str / emu.windows_directory: str / emu.system_directory: str rutas de sistema de archivos simuladas.
  • emu.emulate_winapi: bool emular la winapi o simplemente omitir las llamadas.
  • emu.short_circuit_sleep: bool hacer que Sleep() regrese inmediatamente.
  • emu.heap_alloc_min_size: int / emu.heap_free_soft: bool ajuste del asignador de heap.
  • emu.ssdt_use_ldr_initialize_thunk: bool usar la ruta real de ntdll LdrInitializeThunk en modo syscall/ssdt.

Consola de Python

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

Cargar Mapas

En la mayoría de los casos necesitarás cargar toda la parte de simulación del sistema operativo Windows, para tener toda la lista enlazada PEB+TEB+LDR y la infraestructura WinAPI.

Para hacer esto puedes usar emu.load_maps(folder:str)

Si vas a emular ensamblador puro, sin llamadas a API y sin acceso a estructuras de Windows, no necesitas cargar los mapas.

Igual si estás emulando linux elf64 o shellcodes (recuerda, elf64 no está bien soportado por ahora).

Ejemplo:

Consola de 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
>>>

Ten en cuenta que esto necesita tener los mapas, así que clona el repositorio:

Shell

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

Obteniendo mapas de una versión de Windows (sin mapas locales necesarios)

Desde la versión 0.11 ya no necesitas una copia local de los mapas. Con emu.load_maps_from_winver(version: str) mwemu descarga las DLLs genuinas del sistema de Windows directamente del servidor de símbolos de Microsoft y las usa como carpeta de mapas.

El argumento version puede ser un nombre amigable ("win11", "win10", "win2019") o un número de compilación exacto ("26100.7920"). También fija la compilación, por lo que cualquier DLL adicional que el cargador necesite después se obtiene automáticamente bajo demanda. Requiere acceso a la red en el primer uso y guarda todo en caché bajo maps/winver/.

Consola de Python

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

Cargando ELF, PE o shellcodes

  • emu.load_binary(filename:str) carga la muestra a emular.
  • emu.load_code_bytes(opcodes:bytes) si ya tienes los bytes en python bytes() / bytearray()

Ejemplo:

Consola de Python

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

Detecta si es ELF, PE y de lo contrario es un shellcode.

En el caso de PE o shellcode, la simulación win32 se inicializa automáticamente.

En el caso de ELF, la simulación linux64 se inicializa automáticamente.

Pero si por alguna razón no usas load_binary() y necesitas el simulador de Windows con peb/ldr/dlls cargados en la memoria, necesitas llamar a init_win32 (incluso para 64 bits, detectará la arquitectura).

Y si no usas load_binary() pero necesitas el simulador de Linux con libc etc. cargado, usa init_linux64. Ten en cuenta que la implementación de Linux es muy básica y solo para 64 bits estáticos.

Desde la versión 0.11 los mapas están incluidos, por lo que init_win32() funciona de inmediato; solo necesitas load_maps() o load_maps_from_winver() si quieres una carpeta de mapas específica o una compilación concreta de Windows.

Consola de Python

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

Creando buffers

Puedes cargar secciones desde el disco a la memoria virtual del emulador, y también puedes asignar buffers.

  • alloc(name: str, size: int) -> int la forma más sencilla de asignar un buffer, devuelve la dirección.
  • alloc_at(name: str, addr: int, size: int) el nombre es un identificador único del mapa asignado.
  • load_map(name: str, filename: str, base_addr: int) carga cargas útiles adicionales desde el disco, secciones volcadas, etc.
  • link_library(filepath: str) -> int enlaza una DLL personalizada en el LDR.
  • free(name: str) libera una asignación.

Ejemplos:

Consola de Python

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

Registros

Puedes ver los valores de los registros y modificarlos en cualquier momento.

Por ejemplo, para preparar el contexto antes de la emulación, preparando un estado de emulación.

  • get_reg(reg: str) -> int proporciona el nombre del registro para verlo.
  • set_reg(reg: str, value: int) -> int modifica un registro.
  • get_xmm(reg: str) -> u128 también puedes ver los registros xmm.
  • set_xmm(reg: str, value: u128) -> u128 y puedes modificar los registros xmm.
  • set_rip(addr: int) -> bool para cambiar rip activando toda la lógica de cambio de flujo, es decir, llamadas winapi, etc.
  • set_eip(addr: int) -> bool para cambiar eip activando toda la lógica de cambio de flujo, es decir, llamadas winapi, etc.

Ejemplos:

Consola de Python

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

Operaciones de memoria

Puede ser útil para fijar el estado de emulación antes de comenzar la emulación, pero otra opción es verificar la memoria después de la emulación o alterarla durante la emulación.

Ten en cuenta que esta lectura/escritura está dentro de la memoria virtual de emulación.

escritura de memoria

  • 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

operaciones de lectura

  • 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

operaciones de memoria estilo libc:

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

métodos de búsqueda:

  • 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 una cadena en un mapa específico.
  • search_string_in_all(kw: str) busca una cadena en todos los mapas de memoria, no muy útil porque no devuelve el resultado, solo lo imprime. Se mejorará.
  • search_bytes(bkw: bytes, map_name: str) -> list[int] busca bytes en un mapa de memoria específico, el resultado es una lista de direcciones.

Ejemplos de preparación del contexto antes de comenzar la emulación:

Consola de 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])

Otro ejemplo:

Script de 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 emulación

Una vez que todo está configurado, puedes iniciar la emulación.

  • run(end_addr) -> int proporciona la dirección para finalizar la emulación o None para emular lo más lejos posible.
  • run_to(position: int) -> int emula un número específico de instrucciones.
  • call32(address: int, params: list[int]) -> int llama a una función usando la convención de llamada de 32 bits de Microsoft.
  • call64(address: int, params: list[int]) -> int llama a una función usando la convención de llamada de 64 bits de Microsoft.
  • linux_call64(address: int, params: list[int]) -> int llama a una función usando la convención de llamada de 64 bits de Linux.
  • stop() detiene la emulación, nunca usé esta llamada.
  • run_until_return() -> int
  • run_until_apicall() -> (int, str)
  • step() -> bool puedes usar un bucle while step(): y controlar la situación paso a paso. Esto es lento.
  • handle_winapi(addr: int)

Script de 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()}")

Llamando funciones directamente

Puedes llamar funciones en direcciones específicas con call32/call64:

Script de 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 de 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}")

Emulación paso a paso

Para depuración o control detallado, ejecuta instrucción por instrucción:

Script de 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 útiles adicionales

Otros métodos útiles para el control de la emulación:

  • emu.get_position() -> int obtiene la posición actual de la instrucción
  • emu.set_verbose(level:int) establece la verbosidad (0=silencioso, 1=API, 2=asm, 3=todo)
  • emu.spawn_console() abre una consola interactiva
  • emu.print_maps() imprime la lista completa de mapas.
  • emu.print_maps_by_keyword(kw: str) imprime los mapas que coinciden con esa palabra clave.
  • emu.search_bytes(pattern: bytes, map_name: str) -> list busca un patrón de bytes en un mapa de memoria específico.

Consola de 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)}")
>>>

Ejemplo práctico: Descifrado de cadenas

Un caso de uso típico es descifrar cadenas de malware:

Script de 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')

Operaciones de pila

Hay métodos para alterar la pila, ten en cuenta que también podrías hacer emu.write_qword(emu.get_reg('rsp'), 123)

Pero con estos métodos activas toda la lógica de pila, también incrementando rsp/esp.

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

Este ejemplo simula una convención de llamada basada en pila, pero ten en cuenta que también puedes usar los métodos call32() y call64() y linux_call64().

Consola de Python

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

Puntos de interrupción

Los puntos de interrupción de mwemu son simples, y la mayoría de las veces hay mejores formas de hacer que la emulación se detenga.

Puedes poner un punto de interrupción en una dirección (solo uno a la vez...) o instrucción, también lectura/escritura de memoria

  • 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

Ejemplo

Consola de Python

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

Volcado de memoria

hay varias formas de hacer un volcado de memoria

  • save_all_allocs(path: str) solo guarda en disco las asignaciones hechas por el código emulado.
  • save(addr: int, size: int, filename: str) vuelca un blob específico al disco.

Ejemplo

Consola de Python

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

Serializar / instantáneas

Todo el estado del emulador (mapas de memoria, registros, configuración, módulos cargados...) se puede serializar para que puedas guardar una instantánea y reanudarla más tarde, compartirla o ramificar el análisis desde un punto conocido.

  • serialize() -> bytes serializa todo el estado a un objeto bytes que puedes mantener en memoria o almacenar tú mismo.
  • dump_to_file(filename: str) serializa el estado directamente a un archivo.
  • dump_to_minidump(filename: str) serializa el estado a un archivo minidump de Windows (cargable por otras herramientas).
  • save_all_allocs(path: str) guarda en disco cada bloque de memoria asignado durante la emulación.
  • save(addr: int, size: int, filename: str) vuelca un fragmento específico de memoria al disco.

Y para restaurar un estado, estas son funciones a nivel de módulo que devuelven un objeto Emu nuevo:

  • pymwemu.deserialize(data: bytes) -> Emu reconstruye desde los bytes producidos por serialize().
  • pymwemu.load_from_file(filename: str) -> Emu carga un estado guardado con dump_to_file().
  • pymwemu.load_from_minidump(filename: str) -> Emu carga un estado guardado con dump_to_minidump().

Ejemplo: toma una instantánea, continúa emulando y reanuda desde la instantánea más tarde.

Script de 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 información

Hay algunos métodos menos usados para obtener diferentes tipos de información.

  • version() -> str obtiene la versión actual de pymwemu.
  • get_prev_mnemonic() -> str obtiene el último mnemónico emulado.
  • reset_pos() restablece el contador de instrucciones emuladas a cero.
  • is_64bits() -> bool detecta en qué modo está el emulador.
  • is_32bits() -> bool detecta en qué modo está el emulador.
  • get_position() -> int obtiene la posición actual (cantidad de instrucciones emuladas).
  • disassemble(addr: int, amount: int) -> str desensambla bytes.
  • print_maps() imprime todos los mapas (asignaciones, DLLs enlazadas, etc.)
  • print_maps_by_keyword(kw: str) imprime los mapas que contienen una palabra clave.
  • get_addr_base(addr: int) -> int obtiene el inicio de un mapa.
  • is_mapped(addr: int) -> bool verifica si una dirección está asignada.
  • get_addr_name(addr: int) -> str obtiene en qué nombre de mapa está la dirección.
  • dump_memory(addr: int) imprime bytes.
  • dump_n(addr: int, amount: int) imprime n bytes.
  • dump_qwords(addr: int, n: int) imprime una lista de qwords.
  • dump_dwords(addr: int, n: int) imprime una lista de dwords.
  • allocated_size() -> int muestra la memoria total asignada.
  • memory_overlaps(addr: int, sz: int) -> bool verifica si un bloque de memoria se superpone con un mapa existente.
  • show_allocs() imprime todas las asignaciones.
  • mem_test() -> bool hace una prueba automática de memoria para buscar superposiciones de mapas.
  • api_addr_to_name(addr: int) -> str proporciona una dirección que apunta a una API y obtendrá el nombre de la API.
  • api_name_to_addr(name: str) -> int obtiene la dirección de un nombre de API.

Ejemplo

Consola de Python

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

Cambiar bits de arquitectura.

Hay métodos para cambiar de 32 bits a 64 bits y viceversa, o a aarch64.

  • set_64bits()
  • set_32bits()
  • set_aarch64() cambia al modo ARM64 (o mejor usa pymwemu.init_aarch64()).
  • is_64bits() -> bool / is_32bits() -> bool verifica el modo actual.
  • inspect_seq(s: str)

Para mantenerte agnóstico a la arquitectura (útil cuando también apuntas a aarch64) usa los accesores genéricos de contador de programa y puntero de pila en lugar de los nombres de registros x86:

  • get_pc() -> int / set_pc(addr: int) funciona en x86, x64 y aarch64.
  • get_sp() -> int / set_sp(addr: int) puntero de pila para cualquier arquitectura.

Pero mejor no cambies la arquitectura sobre la marcha, vuelve a instanciar el objeto de emulación como:

Ejemplo

Consola de Python

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

Ejemplos de casos reales.

Encuentra algunos ejemplos aquí:

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

Y algunos cuadernos de jupyter aquí:

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

MWEMU para aplicaciones Rust

El núcleo y los enlaces están implementados completamente en Rust, y toda la lógica está dentro de libmwemu, por lo que un programa Rust puede tener aún más control del emulador que pymwemu.

Dicho esto, desde Rust puedes usar todo el poder de MWEMU y acceder a la mayoría de los objetos porque la mayoría son públicos, por lo que puedes tener un buen control del emulador.

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

https://crates.io/crates/libmwemu

Iniciar un proyecto

Primero que nada necesitas el compilador rustc y la herramienta cargo, la mejor forma de instalarlos es rustup.rs

Shell

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

Luego tienes que crear un proyecto Rust.

Shell

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

Esto crea una carpeta con src/main.rs y también Cargo.toml

Agrega la dependencia a Cargo.toml manualmente o mejor automáticamente usando cargo también.

Shell

❯❯❯ cargo add libmwemu

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

[dependencies]
libmwemu = "0.23.5"

Luego puedes agregar código a src/main.rs

compilar proyecto

Si necesitas ejecutar el proyecto recuerda usar el modo release (agregando --release)

En modo release el emulador es extremadamente rápido.

shell

❯❯❯ cargo run --release

La compilación será más lenta la primera vez porque tiene que descargar libmwemu y compilarlo automáticamente.

Lo que hace cargo run internamente es compilar (como cargo build --release) y luego ejecutar el binario. Puedes pasar parámetros a cargo y también a tu código de esta manera:

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

shell

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

Crear el objeto de emulación

En primer lugar, usa el objeto emu32 o emu64 dependiendo de lo que necesites.

Si vas a hacer asignaciones de memoria, también necesitarás usar el objeto Permission.

main.rs

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

Ten en cuenta que incluso en 32 bits, la implementación del objeto Permission está implementada en mem64; internamente todo es de 64 bits.

Luego crea el objeto de emulación para la arquitectura elegida.

main.rs

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

Configurar el Emulador

Luego, opcionalmente, puedes usar modificadores para configurar el emulador, incluso acceder directamente al objeto Config.

Todas las configuraciones disponibles:

  • emu.set_verbose(n: u32) cambiar la verbosidad, 0: solo mostrar llamadas winapi, 1: también mensajes, 2: también cada instrucción asm, 3: también cada iteración de rep.
  • emu.set_stack_address(addr: u64)
  • emu.set_base_address(addr: u64) establecer la dirección base del código, el blob emulable inicial; por defecto mwemu lo obtiene de las estructuras 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)

Desde rust también puedes modificar directamente la estructura Config, ejemplo:

main.rs

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

Encuentra aquí todas las configuraciones disponibles en el objeto Config.

Pero las configuraciones principales tienen envoltorios de funciones en el objeto base principal Emu.

También puedes crear un objeto Config y asignarlo al emulador con emu.set_config(cfg: Config), pero normalmente es más conveniente acceder a emul.cfg como se ve arriba.

Configurar la Carpeta de Maps

En la mayoría de los casos necesitarás cargar toda la parte de simulación del sistema operativo Windows, para tener toda la lista enlazada PEB+TEB+LDR haciendo la Simulación de Procesos de Windows.

Para hacer esto puedes usar emu.set_maps_folder(folder:str)

Si vas a emular ensamblador puro, sin llamadas a API y sin acceso a estructuras de Windows, no necesitas cargar los maps.

Ejemplo:

main.rs

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

Ten en cuenta que esto necesita tener los maps, así que clona el repositorio:

Shell

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

Esta llamada configura la ruta, ¡pero no la carga por ahora!

Más tarde usaremos uno de estos 3 para activar el simulador win32:

  • emu.load_code(file: &str) si es PE o shellcode, llamará internamente a init_win32
  • emu.init_win32(clear_registers: bool, clear_flags: bool) puedes activar directamente la simulación win32, pero no la llames dos veces.

Entonces, si necesitas los maps y las cosas de win32 pero no haces load_sample, entonces necesitarás llamar directamente a init_win32

Cargar ELF, PE o shellcodes

En la mayoría de los casos necesitarás cargar una muestra, desde un archivo o desde un vector.

Carga tu muestra principal con uno de estos métodos, y solo una vez.

Para cargar maps adicionales, usa los métodos de carga/asignación que se explican más adelante.

  • emu.load_code(filename: &str) cargar la muestra a emular.
  • emu.load_code_bytes(opcodes: &[u8]) si ya tienes los bytes en una variable.

Ejemplo1:

main.rs

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

Ejemplo2:

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);

Ejemplo3:

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.

Detecta si es ELF, PE y de lo contrario es un shellcode.

En el caso de PE o shellcode, la simulación win32 se inicializa automáticamente.

En el caso de ELF, la simulación linux64 se inicializa automáticamente.

Pero si por cualquier razón no usas load_code() y necesitas el simulador de Windows con peb/ldr/dlls cargados en la memoria, necesitas llamar a init_win32 (incluso para 64 bits, detectará la arquitectura).

Y si no usas load_binary() pero necesitas el simulador de Linux con libc etc. cargado, usa init_linux64. Ten en cuenta que la implementación de Linux es muy básica y solo para 64 bits estáticos.

Ten en cuenta que llamar a init_win32() requiere haber hecho previamente load_maps para establecer la carpeta de maps.

Crear Buffers

Puedes cargar secciones desde el disco a la memoria virtual del emulador, y también puedes asignar buffers.

  • emu.alloc(map_name: &str, size: u64, permission: Permission) -> u64 la forma más simple de asignar un buffer, devuelve la dirección.
  • emu.maps.alloc(sz: u64) -> Option<u64> esto es usado por las syscalls de winapi y linux, esto solo encuentra un bloque libre de tamaño sz, pero no lo asigna, luego necesitas hacer emu.maps.create_map
  • emu.maps.create_map(name: &str, base: u64, size: u64, permission: Permission crear un map vacío en una ubicación específica.
  • emu.free(map_name: &str); desasignar por nombre de map.
  • emu.maps.dealloc(addr: u64); desasignar por dirección.
  • emu.link_library(filepath: &smp;str) -> u64 enlazar dinámicamente una dll personalizada a la estructura LDR.

Ejemplo1:

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]);

Ejemplo2:

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 es un objeto Mem64, y tiene múltiples métodos. Pero para la mayoría de los casos, los métodos de emu y emu.maps son suficientes.

Registros

Puedes ver los valores de los registros y modificarlos en cualquier momento.

Por ejemplo, para preparar el contexto antes de la emulación, preparando un estado de emulación.

Leer un registro de 64 bits o más grande es muy directo

let rax = emu.regs().rax;

let rip = emu.regs().rip;

let xmm15 = emu.regs().xmm15;

Establecer un registro de 64 bits o más grande es muy directo.

emu.regs_mut().rax = 0x123;

emu.regs_mut().rip = 0x4012312;

emu.regs_mut().xmm15 = 0x11223344_11223344_11223344_11223344u128;

Getters para no 64 bits (siempre devuelven 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 no 64 bits (proporciona siempre 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 está completamente equipado, también hay algunos registros poco comunes como ymm, cr, gs, fs, tr y msr.

métodos auxiliares

emu.regs_mut().clear::<u8>(); limpiar el byte inferior de todos los registros.

emu.regs_mut().rand(); aleatorizar los valores de los registros.

emu.regs().print::<u32>(); imprimir los registros principales.

Y hay muchas más cosas, para más información consulta el objeto Regs64.

Ten en cuenta que puedes cambiar emu.regs_mut().set_eip(addr); pero la mayoría de las veces lo que realmente necesitas es emu.set_eip(addr); que emula la lógica de jmp activando WinAPI si la dirección es una lib.

Operaciones de memoria

Puede ser útil para precisar el estado de emulación antes de comenzar la emulación, pero otra opción es verificar la memoria después de la emulación o alterarla durante la emulación.

Ten en cuenta que estas lecturas/escrituras están dentro de la memoria virtual de emulación.

lectura de memoria

  • 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

escritura de memoria

  • 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

operaciones de memoria 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 búsqueda:

  • 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>

Ejemplos de preparación del contexto antes de comenzar la emulación:

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()]);

Otro ejemplo:

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 la Emulación

Una vez que todo está configurado, puedes iniciar la emulación.

Puedes iniciar la emulación usando funciones 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()

Pero esta forma es mucho más cómoda porque imita las convenciones de llamada.

  • emu.call32(addr: u64, args: &[u32]) -> Result<u32, MwemuError> convención de llamada de microsoft de 32 bits.
  • emu.call64(addr: u64, args: &[u64]) -> Result<u64, MwemuError> convención de llamada de microsoft de 64 bits.
  • emu.linux_call64(addr: u64, args: &[u64]) -> Result<u64, MwemuError> convención de llamada ABI SysV AMD64 usada en el espacio de usuario de linux64.

Para más control pero de forma lenta puedes usar step, pero ten en cuenta que también puedes usar hooks.

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

Métodos Útiles Adicionales

Otras cosas útiles para el control de la emulación:

  • emu.pos este es el contador de instrucciones emuladas, puedes acceder e incluso modificarlo.
  • emu.set_verbose(n: u32) establece verbose 0 para ir más rápido a un punto específico y luego establece 2 para ver el asm solo en esos rangos de posiciones.
  • emu.cfg.console_enabled = true habilitar el autospawn de la consola.
  • emu.spawn_console() en casos específicos, abrir la consola para inspeccionar manualmente la situación.

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();
}

Operaciones de pila

Hay métodos para alterar la pila, ten en cuenta que también podrías hacerlo con escrituras emu.maps.

Pero con estos métodos activas toda la lógica de la pila, también incrementando rsp/esp.

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

Este ejemplo simula una convención de llamada basada en pila, pero ten en cuenta que también puedes usar los métodos call32() y call64() y 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();
    ...
}

Puntos de interrupción

Como un depurador, puedes configurar diferentes tipos de puntos de interrupción, pero la implementación es simple, aunque hay otras formas de hacer que la emulación se detenga.

  • emu.bp.show()
  • emu.bp.clear_bp()
  • emu.bp.add_bp(addr: u64)
  • emu.bp.addr.clone() obtener un vector de puntos de interrupción basados en direcciones.
  • 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 más información consulta la referencia del objeto Breakpoint.

Volcado de memoria

hay varias formas de hacer un volcado de memoria

  • emu.maps.save_all_allocs(path: &str) solo guardar en disco las asignaciones hechas por el código emulado.
  • emu.maps.save(addr: u64, size: u64, filename: String) volcar un blob específico al disco.

Ejemplo

main.rs

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

Ver información

Hay algunos métodos menos usados para obtener diferentes tipos de información.

  • let mut output = String::new(); self.emu .formatter .format(&self.emu.instruction.unwrap(), &mut output); obtener el último mnemónico emulado.
  • emu.pos = 0; restablecer el contador de instrucciones emuladas a cero.
  • emu.cfg.is_64bits detectar en qué modo está el emulador.
  • emu.disassemble(addr: u64, amount: u32) -> String desensamblar bytes.
  • emu.maps.print_maps() imprimir todos los maps (asignaciones, dlls enlazadas, etc.)
  • emu.maps.print_maps_keyword(kw: &str) imprimir los maps que contienen una palabra clave.
  • let base = match emu.maps.get_addr_base(addr) { Some(v) => Ok(v), None => ..., };
  • emu.maps.is_mapped(addr: u64) -> bool verificar si una dirección está asignada.
  • emu.maps.get_addr_name(addr: u64) -> String obtener en qué nombre de map está la dirección.
  • emu.maps.dump(addr: u64) imprimir bytes.
  • emu.maps.dump_n(addr: u64, amount: u64) imprimir n bytes.
  • emu.maps.dump_qwords(addr: u64, n: u64) imprimir una lista de qwords.
  • emu.maps.dump_dwords(addr: u64, n: u64) imprimir una lista de dwords.
  • emu.maps.size() -> usize mostrar la memoria total asignada.
  • emu.maps.overlaps(addr: u64, sz: u64) -> bool verificar si un bloque de memoria se superpone con un map existente.
  • emu.maps.show_allocs() imprimir todas las asignaciones hechas por el código emulado.
  • emu.maps.mem_test() -> bool hacer una prueba de memoria automática para buscar superposiciones de maps.
  • emu.api_addr_to_name(addr: u64) -> String proporcionar una dirección que apunte a una API y obtendrá el nombre de la API.
  • emu.api_name_to_addr(name: &str) -> u64 obtener la dirección de un nombre de API.

Ejemplo

main.rs

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

Cambiar los bits de arquitectura.

Hay métodos para cambiar de 32 bits a 64 bits y viceversa.

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

Pero mejor no cambies la arquitectura sobre la marcha, vuelve a instanciar el objeto de emulación así:

Ejemplo

main.rs

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

O incluso crea ambos objetos.

Hooks

El objeto Hooks permite extender el emulador sin bloquear la emulación.

Habilitar un hook

  • emu.hooks.on_memory_read(trace_memory_read); el hook se activa después de cada lectura.
  • emu.hooks.on_memory_write(trace_memory_write); el hook se activa antes de cada escritura, y el hook puede cambiar el valor a escribir devolviéndolo.
  • emu.hooks.on_interrupt(trace_interrupt); el hook se activa antes de manejarlo, el hook puede decidir con el valor de retorno si mwemu hace el manejo o no.
  • emu.hooks.on_exception(trace_exceptions); el hook se activa antes de manejarlo, el hook puede decidir con el valor de retorno si mwemu hace el manejo o no.
  • emu.hooks.on_pre_instruction(trace_pre_instruction); el hook se activa antes de emular la instrucción.
  • emu.hooks.on_post_instruction(trace_post_instruction); el hook se activa después de emular la instrucción.
  • emu.hooks.on_winapi_call(trace_winapi_call); puedes implementar una API de Windows.

Deshabilitar un 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();

Ejemplo 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: controladores de Linux

Además de binarios PE/ELF en modo usuario y shellcode, mwemu puede cargar y emular módulos de kernel de Linux (archivos .ko). Enlaza el controlador contra un kernel sintético que controla y modela el asignador de slabs explícitamente, de modo que un error de seguridad de memoria —la especie dominante de vulnerabilidad de kernel— aparece como un informe en lugar de un pánico de kernel. Sin kernel real, sin root, nada se ejecuta en el host.

Por qué la emulación de controladores es diferente

Emular un controlador no es emular un programa con diferentes importaciones. Un controlador no tiene punto de entrada, ni libc, ni cargador ni proceso: un .ko es un objeto reubicable ET_REL —sin cabeceras de programa, sin .dynamic, secciones que no han sido colocadas. Es el sistema operativo el que coloca esas secciones en su propio espacio de direcciones, las reubica y luego llama de vuelta al módulo. mwemu proporciona exactamente las tres cosas que faltan:

1. Un enlazador. Las secciones .ko se colocan y cada reubicación se aplica en tiempo de carga.

2. Un kernel al que llamar. Cada símbolo importado (kmalloc, mutex_lock, printk …) se resuelve a una dirección en una región sintética de "texto del kernel"; una llamada que aterriza allí se intercepta y se enruta a una implementación en Rust —el mismo mecanismo que usa la capa WinAPI.

3. Un asignador con memoria. Los errores de controladores son errores de ciclo de vida, por lo que el slab se modela explícitamente: los fragmentos se rastrean con su procedencia, los fragmentos liberados van a cuarentena en lugar de reciclarse, y cada acceso se verifica contra el registro.

Cómo funciona

El punto 3 es la razón por la que esto existe. Un slab real devuelve la memoria liberada directamente, que es exactamente lo que hace difícil ver un use-after-free. mwemu lo invierte: un fragmento liberado no se recicla —va a cuarentena, permanece mapeado y sus bytes se sobrescriben con el veneno de liberación de SLUB (0x6b6b6b6b…). Los fragmentos están separados por una zona roja sin mapear, por lo que un desbordamiento lineal desde el final de una asignación falla en lugar de corromper silenciosamente el siguiente objeto. Mantener el fragmento mapeado pero envenenado es lo que convierte un error invisible en un informe —y como permanece mapeado, la ejecución continúa más allá de la primera desreferencia obsoleta, de modo que una sola ejecución puede sacar a la luz toda la cadena en lugar de detenerse en el primer síntoma.

Espacio de direcciones

Las regiones se eligen para coincidir con los diseños reales del kernel y —más importante— para que las distancias entre ellas permanezcan dentro de lo que las reubicaciones pueden codificar. Un módulo construido con -mcmodel=kernel llega al kernel a través de R_X86_64_PLT32, un desplazamiento firmado de 32 bits, por lo que el módulo y el área de stub deben estar dentro de ±2GB.

Espacio de direcciones de 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

Superficie del kernel emulado

La superficie de Linux es amplia, y cada variante del asignador desemboca en un solo registro (__kmalloc, __kmalloc_noprof, kmem_cache_alloc, kvmalloc, devm_kzalloc, vmalloc, kfree, kmem_cache_free …), por lo que no importa contra qué versión del kernel se construyó el controlador. También están implementados: copias de usuario (copy_from_user / copy_to_user), ayudantes de cadenas y memoria (memcpy, memmove, strscpy …), refcounts y krefs, bloqueos, trabajo diferido (workqueues, timers, RCU), registro (printk / dev_*) y registro de dispositivos (misc_register, __register_chrdev, proc_create …).

Algunas piezas se modelan de verdad en lugar de simularse, porque el error vive en ellas. Los ayudantes de memoria también pasan por la guardia —un memcpy() hacia un objeto liberado es un use-after-free que ninguna verificación a nivel de instrucción vería, porque la copia se ejecuta dentro del código del kernel, no del controlador. Los refcounts son reales: que refcount_dec_and_test() devuelva true es lo que desencadena una liberación. Y las devoluciones de llamada diferidas (call_rcu, schedule_work, timers) están en cola, no se ejecutan en línea —"desregistrar ahora, liberar después" es la forma de la mayoría de los UAF de kernel, por lo que ejecutar la devolución de llamada inmediatamente cerraría la misma ventana en la que vive el error; vacíalas con kernel_run_deferred(). El bloqueo es un no-op (la emulación de un solo hilo no puede bloquearse). Una importación sin implementación no es fatal: se informa en tiempo de carga como unresolved, y si se llama devuelve 0, por lo que un kernel parcialmente cubierto aún ejecuta un controlador hasta donde puede llegar. La lista completa se devuelve en tiempo de ejecución mediante libmwemu::kernel::linux::SURFACE y mediante la herramienta MCP mwemu_kernel_surface.

Qué detecta

Hallazgos

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 hallazgo lleva la instrucción que falla, el objeto, su caché y tanto el sitio de asignación como el de liberación. Las repeticiones del mismo (tipo, instrucción, objeto) se colapsan en un contador de aciertos.

Cargar un .ko desde la CLI

La CLI enlaza el módulo y deja el PC en su init, por lo que una ejecución simple hace lo que haría insmod:

Shell

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

Alcanzar la superficie ioctl (y por tanto la mayoría de los errores) necesita estructuras de argumentos en la memoria invitada, por lo que se maneja desde Rust o mediante MCP —ver más abajo.

Manejar un controlador desde 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());
}

Manejar un controlador desde Python (pymwemu)

La misma superficie de controlador está disponible desde Python, por lo que puedes enlazar un .ko, manejar un handler específico con un argumento elaborado y leer el registro de slabs de vuelta —la forma práctica de fuzzear una función sin un kernel. Ten en cuenta que la herramienta de línea de comandos solo ejecuta el init del módulo; desde pymwemu (o Rust, o MCP) puedes alcanzar cualquier símbolo exportado con tus propios 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")

Manejar un controlador mediante MCP

La misma superficie se expone a través del servidor Model Context Protocol, por lo que un agente de IA puede enlazar un controlador, manejar sus ioctls conversacionalmente y leer los hallazgos sin tocar nunca un kernel. Las herramientas de modo kernel son mwemu_kernel_load_module, mwemu_kernel_init, mwemu_kernel_call, mwemu_kernel_findings y mwemu_kernel_surface.

Ejemplo trabajado: un use-after-free

El objetivo de referencia es tlm, un controlador de telemetría deliberadamente vulnerable escrito como uno real: objetos con refcount en su propio kmem_cache, una lista de canales protegida por mutex, vectores de operación por objeto y una superficie ioctl. El error no es "liberarlo y luego leerlo dos líneas después". El controlador mantiene una caché de canal caliente de una entrada para saltarse el recorrido de la lista en escrituras repetidas; el invariante es que quien elimina un canal también limpia la caché. Se respeta al cerrar y al descargar, pero el autor omitió la tercera forma en que muere un canal: TLM_IOC_DESTROY libera el objeto mientras el manejador de archivo permanece abierto. La caché queda colgando, y la siguiente escritura toma el camino caliente directamente a través de ella —saltándose incluso la verificación mágica— hasta una llamada indirecta:

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

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 */

Disparador: crea un canal, escribe una vez (rellena la caché), destrúyelo, escribe de nuevo. Construye el controlador y ejecútalo de principio a fin:

Shell

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

Lo que mwemu informa para la escritura con caché obsoleta:

Salida

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

Leídas juntas, esas dos líneas son todo el error. La primera es la carga de ch->ops (offset 40) fuera del objeto en cuarentena, nombrando su caché y tanto el sitio de asignación como el de liberación. La segunda es la desreferencia del puntero que produjo esa carga —0x6b6b… es veneno de liberación, por lo que su procedencia es prueba, no una suposición.

Windows y macOS

Solo las tablas de símbolos y los handlers difieren entre sistemas operativos; la colocación, la interceptación, el registro y el análisis son compartidos. Windows y macOS ya tienen sus superficies declaradas y sus asignadores de pool/zone implementados (ExAllocatePool2 / ExFreePool para ntoskrnl, IOMalloc / IOFree / kalloc_external para XNU). Lo que aún falta es el cargador: un .sys es un PE con un DriverEntry, por lo que necesita la ruta PE con colocación en espacio de kernel en lugar de la ruta ET_REL; un kext es un Mach-O MH_KEXT_BUNDLE con reubicaciones externas. El día que cualquiera de los cargadores aterrice, hereda todo el análisis de use-after-free gratis, porque la parte que encuentra el error nunca le importó para qué sistema operativo era el controlador.