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
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.
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) -> Emucargar un estado guardado previamente conemu.dump_to_file().pymwemu.load_from_minidump(filename: str) -> Emucargar un estado guardado conemu.dump_to_minidump().pymwemu.deserialize(data: bytes) -> Emureconstruir un emulador desde los bytes devueltos poremu.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 | Nonedetener después de esta cantidad de instrucciones.emu.timeout_secs: float | Nonedetener la emulación después de esta cantidad de segundos.emu.max_faults: int | Nonedetener después de este número de fallos de memoria.emu.exit_position: intposición de instrucción donde la emulación debe detenerse.emu.dump_on_exit: bool/emu.dump_filename: str | Nonevolcar el estado a un archivo cuando la emulación termine.emu.trace_filename: str | None/emu.trace_start: int/emu.trace_calls: boolescribir un rastro de ejecución.emu.module_name: str/emu.exe_name: strnombre presentado a la muestra emulada.emu.user_name: str/emu.host_name: stridentidad reportada por la winapi.emu.temp_path: str/emu.cwd_path: str/emu.windows_directory: str/emu.system_directory: strrutas de sistema de archivos simuladas.emu.emulate_winapi: boolemular la winapi o simplemente omitir las llamadas.emu.short_circuit_sleep: boolhacer que Sleep() regrese inmediatamente.emu.heap_alloc_min_size: int/emu.heap_free_soft: boolajuste del asignador de heap.emu.ssdt_use_ldr_initialize_thunk: boolusar 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) -> intla 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) -> intenlaza 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) -> intproporciona el nombre del registro para verlo.set_reg(reg: str, value: int) -> intmodifica un registro.get_xmm(reg: str) -> u128también puedes ver los registros xmm.set_xmm(reg: str, value: u128) -> u128y puedes modificar los registros xmm.set_rip(addr: int) -> boolpara cambiar rip activando toda la lógica de cambio de flujo, es decir, llamadas winapi, etc.set_eip(addr: int) -> boolpara 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) -> boolwrite_dword(addr: int, value: int) -> boolwrite_word(addr: int, value: u16) -> boolwrite_byte(addr: int, value: u8) -> boolwrite_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) -> u128read_128bits_le(addr: int) -> u128read_qword(addr: int) -> intread_dword(addr: int) -> intread_word(addr: int) -> u16read_byte(addr: int) -> u8read_buffer(from: int, sz: int) -> Vecread_bytes(addr: int, sz: int) -> bytesread_string_of_bytes(addr: int, sz: int) -> strread_string(addr: int) -> strread_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) -> intsearch_spaced_bytes_from_bw(saddr: int, sbs: str) -> intsearch_spaced_bytes(sbs: str, map_name: str) -> Vecsearch_spaced_bytes_in_all(sbs: str) -> Vecsearch_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) -> intproporciona la dirección para finalizar la emulación o None para emular lo más lejos posible.run_to(position: int) -> intemula un número específico de instrucciones.call32(address: int, params: list[int]) -> intllama a una función usando la convención de llamada de 32 bits de Microsoft.call64(address: int, params: list[int]) -> intllama a una función usando la convención de llamada de 64 bits de Microsoft.linux_call64(address: int, params: list[int]) -> intllama 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() -> intrun_until_apicall() -> (int, str)step() -> boolpuedes 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() -> intobtiene la posición actual de la instrucciónemu.set_verbose(level:int)establece la verbosidad (0=silencioso, 1=API, 2=asm, 3=todo)emu.spawn_console()abre una consola interactivaemu.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) -> listbusca 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) -> boolstack_push64(value: int) -> boolstack_pop32() -> intstack_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() -> Vecbp_set_inst(ins: int)bp_get_inst() -> Vecbp_set_mem_read(addr: int)bp_get_mem_read() -> Vecbp_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() -> bytesserializa 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) -> Emureconstruye desde los bytes producidos porserialize().pymwemu.load_from_file(filename: str) -> Emucarga un estado guardado condump_to_file().pymwemu.load_from_minidump(filename: str) -> Emucarga un estado guardado condump_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() -> strobtiene la versión actual de pymwemu.get_prev_mnemonic() -> strobtiene el último mnemónico emulado.reset_pos()restablece el contador de instrucciones emuladas a cero.is_64bits() -> booldetecta en qué modo está el emulador.is_32bits() -> booldetecta en qué modo está el emulador.get_position() -> intobtiene la posición actual (cantidad de instrucciones emuladas).disassemble(addr: int, amount: int) -> strdesensambla 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) -> intobtiene el inicio de un mapa.is_mapped(addr: int) -> boolverifica si una dirección está asignada.get_addr_name(addr: int) -> strobtiene 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() -> intmuestra la memoria total asignada.memory_overlaps(addr: int, sz: int) -> boolverifica si un bloque de memoria se superpone con un mapa existente.show_allocs()imprime todas las asignaciones.mem_test() -> boolhace una prueba automática de memoria para buscar superposiciones de mapas.api_addr_to_name(addr: int) -> strproporciona una dirección que apunta a una API y obtendrá el nombre de la API.api_name_to_addr(name: str) -> intobtiene 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 usapymwemu.init_aarch64()).is_64bits() -> bool/is_32bits() -> boolverifica 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_win32emu.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) -> u64la 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_mapemu.maps.create_map(name: &str, base: u64, size: u64, permission: Permissioncrear 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) -> u64enlazar 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) -> Stringemu.maps.read_string(addr: u64) -> Stringemu.maps.read_wide_string_nocrash(addr: u64) -> Stringemu.maps.read_wide_string(addr: u64) -> Stringemu.maps.read_wide_string_n(addr: u64, max_chars: usize) -> String
escritura de memoria
emu.maps.write_byte(addr: u64, value: u8) -> boolemu.maps.write_qword(addr: u64, value: u64) -> boolemu.maps.write_dword(addr: u64, value: u32) -> boolemu.maps.write_word(addr: u64, value: u16) -> boolemu.maps.write_bytes(addr: u64, data: Vec<u8>) -> boolemu.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) -> boolemu.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) -> u64emu.maps.search_spaced_bytes_from_bw(spaced_bytes: &str, start_address: u64) -> u64emu.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() -> boolemu.step_single_threaded() -> boolemu.step_multi_threaded() -> bool
Métodos Útiles Adicionales
Otras cosas útiles para el control de la emulación:
emu.poseste 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 = truehabilitar 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) -> boolstack_push64(value: u53) -> boolstack_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_64bitsdetectar en qué modo está el emulador.emu.disassemble(addr: u64, amount: u32) -> Stringdesensamblar 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) -> boolverificar si una dirección está asignada.emu.maps.get_addr_name(addr: u64) -> Stringobtener 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() -> usizemostrar la memoria total asignada.emu.maps.overlaps(addr: u64, sz: u64) -> boolverificar 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() -> boolhacer una prueba de memoria automática para buscar superposiciones de maps.emu.api_addr_to_name(addr: u64) -> Stringproporcionar una dirección que apunte a una API y obtendrá el nombre de la API.emu.api_name_to_addr(name: &str) -> u64obtener 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.