Table of Contents
Woohoo! It (finally) happened!
I'm really proud to announce the release of mona v3!
This release has been a long time in the making. While it still builds on some of the solid foundations of earlier versions, substantial parts of the codebase have been completely reworked, refactored, and modernized.
The result is a cleaner and broader version of mona, with improved compatibility and a more consistent runtime across supported environments.
This new version is not only faster and leaner, it also brings a broader and more flexible runtime:
Additionally, mona v3:
The vast majority of existing mona commands remain available, and - as suggested in the list above - many have been improved in terms of behavior, stability, compatibility and/or performance.
The overall goal remains unchanged: to deliver high-quality, actionable output out of the box, while still allowing detailed control over search scope and behavior.
In addition, mona v3 includes a number of new features and commands. Some of those will be covered in more detail in follow-up posts.
So, that's the good news.
There is also some less encouraging news to share.
When I started developing the first version of mona.py back in 2011, the primary goal was simple: make the tool easy to use and capable of producing actionable results out of the box. It was designed to run on a default installation of Python 2 together with Immunity Debugger — no additional libraries required.
Later, when adding support for WinDBG, that principle had to be relaxed with the introduction of the pykd library.
Today, as part of the ongoing effort to bring more functionality into 64-bit debugging sessions, I ran into another limitation: WinDBG(X) does not natively support assembling 64-bit mnemonics.
Crazy, right? 🤷🏼♂️
Since pykd depends on WinDBG and doesn't have its own built-in assembler, this created a new challenge: how to provide reliable 64-bit assembly capabilities without building and maintaining a custom assembler myself?
An initial attempt involved building a cache of commonly used instructions. However, while working on improvements to the findwild wildcard search command, it quickly became clear that this approach would not scale in a practical or maintainable way.
For now, the solution is to rely on the keystone-engine library. To get the most out of mona.py on 64-bit targets, you will need to install keystone-engine via pip. When installed, windbglib will automatically import and use it. If not, it will fall back to the limited instruction cache and whatever WinDBG provides. Even on 32-bit targets, windbglib will prefer Keystone if it is present before falling back to WinDBG’s native assembler.
That said, it appears the Keystone project is no longer actively maintained. Still, that didn’t stop us from using pykd either.
If you are aware of viable alternatives — ideally a lightweight, actively maintained assembler library with solid 32-bit and 64-bit support and easy installation via pip on Python 3.9+ — feel free to reach out.
Before we dive into the practical side, I want to take a moment to give credit where it’s due.
A huge thank you to @apl3b for his dedication, persistence, and high-quality work.
He rewrote significant parts of the core routines, improved performance across multiple areas, and introduced several new features and commands.
This release would not be what it is without his contribution.
I would also like to thank angelo5d0 for his help testing the various features and commands in the updated mona/windbglib codebase.
Before installing mona v3, it’s worth making sure your environment is clean and ready.
If you have older versions of mona lying around, now is a good time to remove them.
Starting clean helps avoid subtle issues later on.
The mona3 github repo contains the full setup instructions.
The main prerequisites are:
You can have other Python 3 versions installed on the system, but please note that mona won't run in a Python version that doesn't have the pykd library.
The CorelanTraining github repository contains a CorelanPyKDInstall.ps1 powershell script that will install all of the aforementioned components. I recommend using the script and carefully checking the output for errors. The script has been tested on a handful of virtual machines, Windows 10 and Windows 11. Make sure your machines are up-to-date and everything should run smoothly.
Of course, you can also install all required components by hand. In that case, I'd like to refer you to the corresponding procedure as documented in this blogpost on WinDBG Automation and Scripting. (Check out the section on PyKD.)
Note: That procedure does not cover how to install keystone-engine. That means you'll have to add the following 2 steps to the procedure:
py -3.9-32 -m pip install keystone-engine
and
py -3.9-64 -m pip install keystone-engine
A few days after publishing my blog post on WinDBG automation, I received a message on X from @gerhart_x. He explaineded that he managed to compile pykd against Python 3.14. You can find the release here. So in theory, it means you may be able to install Python 3.14 and load the corresponding pykd.dll
With mona3, you still have the option to put both mona.py and windbglib.py inside your WinDBG program folder.
However, we're proposing a different approach now.
The mona3 GitHub repository has instructions on performing a centralized installation, which involves creating a local folder.
We're using C:\Tools\mona3 in the documentation, but you can obviously put it anywhere you'd like.
Both mona.py and windbglib.py should reside in the same folder.
Of course, you can also simply perform a git clone https://github.com/corelan/mona3.git into C:\Tools if you'd like. That will also create the mona3 folder and put all the files from the repository inside that folder. (which is a bit overkill really, we only need the 2 python files) Whatever you do, just take note of the full path of mona.py and make sure windbglib.py sits in the same folder. We'll need the full path to run mona in WinDBG.
A centralized setup simplifies maintance. Whenever you decide to run !mona up, it will update the scripts for all debuggers using it.
In WinDBG and WinDBGX, the mona.py script is referenced via its full path. We'll have the ability to use WinDBG aliases to provide us with the ability to just run !mona (instead of having to type the full path every time). We'll talk about this in a moment.
In Immunity Debuger, a symbolic link can be used to make mona.py appear in the PyCommands folder. That way, we can invoke mona just like before, by running !mona.
From an admin command prompt:
C:\>mklink "c:\Program Files (x86)\Immunity Inc\Immunity Debugger\PyCommands\mona.py" C:\Tools\mona3\mona.py symbolic link created for c:\Program Files (x86)\Immunity Inc\Immunity Debugger\PyCommands\mona.py <<===>> C:\Tools\mona3\mona.py
Mona uses a mona.ini file to store certain configurations. If you already had a mona.ini file present on your system and discovered by mona3, it will be moved over into the folder that contains mona.py. In other words, everything will be centralized in one place.
Once installed, running mona should feel familiar.
In WinDBG / WinDBGX, load the pykd bootstrapper:
!load pykd
(unlike what we did with previous versions of mona, we're not loading pykd.pyd! We're loading pykd.dll instead.
Next, execute mona using Python 3.9:
!py -3.9 C:\Tools\mona3\mona.py
If multiple Python versions are installed, specifying 3.9 avoids ambiguity. The debugger will automatically use the correct architecture.
As indicated above, we can make our lives easier by creating the following alias:
as !mona !py -3.9 C:\Tools\mona3\mona.py
This now allows us to run
!mona
As explained in the documentation, you can have WinDBG execute the required commands to load pykd and set up the alias automatically.
From there, all your usual commands are available — only now faster, more consistent, and working across more environments.
First and foremost, the Mona3 Github repository has a wiki which has up-to-date documentation for every available command and global search options.
Nevertheless, the table below provides a current (and possibly outdated) overview of the available mona.py commands at the moment of release. As commands may be removed, changed or added, please check the actual list of commands on your system to see what options you have. Just like before, when running !mona without arguments, you'll see the Global Options, followed by a table of available commands. The commands are filtered by architecture and debugger type.
Run !mona without arguments to see the commands that are available on your debugger / architecture combination.
As you can see, we have already made a decent number of commands compatible with x64. We'll keep working on offering even more logic for both 32bit and 64bit in the future.
Comands are executed using
!mona <command> [arguments]
For example, if you'd like to run the "assemble" routine and you'd like to get the opcode for the "jmp eax" instruction, you can run
!mona asm -s "jmp eax"
It's a good idea to tell mona to write its output into specific folders instead of writing them inside the WinDBG Program Folder. You can do so by setting a configuration variable workingfolder to a path. We recommend adding a %p subfolder to that path, telling mona to organize the output into subfodlers that have the process' imagename.
0:000> !mona config -set workingfolder c:\logs\%p Hold on... [ -- START -- ] Mona command started on 2026-04-19 11:44:18 (v3.0, rev 3000) 64bit [ -- START -- ] Python: 3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)]) [ -- START -- ] PyKD: 0.3.4.15 [+] Command used: !py c:\Tools\mona3\mona.py config -set workingfolder c:\logs\%p [+] Saving new value for parameter 'workingfolder' Config file: c:\Tools\mona3\mona.ini Old value of parameter workingfolder = c:\logs\%p New value: [+] Saving config file, modified parameter workingfolder mona.ini saved under c:\Tools\mona3 Parameter New value ------------- ----------- workingfolder c:\logs\%p [ -- END -- ] This mona.py action took 0:00:00.069025 [ -- END -- ] Current date/time: 2026-04-19 11:44:18
Also, it would be great to adopt the habit of checking for updates on a regular basis. We're actively fixing bugs, adding features and making changes overall.
Just like before, checking for updates and updating itself in-place is as easy as running !mona up (Make sure you have an active internet connection).
As explained before, if you're using a centralized setup, all of your debuggers will be using the updated version automatically.
The Global Options are a set of arguments and criteria that allow you to steer and control the searches that mona performs. You'll get to see them when running !mona without arguments:
Global options : ---------------- You can use one or more of the following global options on any command that will perform a search in one or more modules, returning a list of pointers : Global options affecting selection of modules: -n : Skip modules that start with a null byte. If this is too broad, use option -cp nonull instead -o : Ignore OS modules -m <module,module,...> : only query the given modules. Be sure what you are doing ! You can specify multiple modules (comma separated) Tip : you can use -m * to include all modules. All other module criteria will be ignored Other wildcards : *blah.dll = ends with blah.dll, blah* = starts with blah, blah or *blah* = contains blah -cm <crit,crit,...> : Apply some additional criteria to the modules to query. You can use one or more of the following criteria : aslr,safeseh,rebase,nx,cfg,os You can enable or disable a certain criterium by setting it to true or false Example : -cm aslr=true,safeseh=false Suppose you want to search for p/p/r in aslr enabled modules, you could call !mona seh -cm aslr -cmp <regex> : Only include modules whose full path matches the given regex (case-insensitive) Example : -cmp kernel32 -cmp "C:\\Windows" -cmp "\.dll$" Global options affecting addresses: -p <nr> : Stop search after pointers. -cp <crit,crit,...> : Apply some criteria to the pointers to return Available options are : unicode,ascii,asciiprint,upper,lower,uppernum,lowernum, numeric,alphanum,nonull,startswithnull,unicoderev Note : Multiple criteria will be evaluated using 'AND', except if you are looking for unicode + one crit -cpb '\x00\x01' : Provide list with bad chars, applies to pointers You can use .. to indicate a range of bytes (in between 2 bad chars) -x <access> : Specify desired access level of the returning pointers. If not specified, only executable pointers will be returned. Access levels can be one of the following values : R,W,X,RW,RX,WX,RWX or * Other global options: -h : Show help / usage for the selected command -debug : Enable debug routines in mona/windbglib. Don't use this option unless you've been asked to do so
There are 2 major types:
By default, almost all mona searches will exclude any module that has been REBASED or is ASLR enabled.
If you're looking for helpful pointers in relation with an exception handler overwrite, then it will exclude SAFESEH modules as well.
Within the remaining modules, mona will limit itself to pages that are marked as executable. Technically, if your target does not have DEP enabled, then you may get away by selecting a pointer from a page that is not executable...
The exceptions to the non-rebase, non-aslr rule are:
You can overrule mona's default search scope behaviour at any time. You can tell mona.py to search in specific modules regardless of their mitigations. (use options -m or -cmp) Or you can overrule the non-aslr and/or non-rebase filter generically using something like: -cm aslr=True,rebase=True You can also overrule the decision to limit itself to executable pages with the -x flag. For instance, search in all readable pages: -x R
In any case, all of this implies you understand what you're doing. The fact that mona.py was not able to find something by default is very likely caused by a relevant mitigation or access level, and is usually not a mistake. be careful when you overrule the default scope and criteria.
But sometimes is does make sense to overrule. In full ASLR applications, after obtaining an information leak, we'd have to tell mona to search in the module that we leaked. As indicated, Mona's -m flag allows you to specify the module you want to search in, overruling its default filters.
The mona output will now show the active filters and default choices it has made, reminding you of the importance and effect of those choices.
For example:
0:000> !mona jmp -r esp Hold on... [ -- START -- ] Mona command started on 2026-04-16 14:59:37 (v3.0, rev 3000) 32bit [ -- START -- ] Python: 3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:24:45) [MSC v.1929 32 bit (Intel)]) [ -- START -- ] PyKD: 0.3.4.15 [+] Command used: !py C:\Tools\mona3\mona.py jmp -r esp [+] Processing arguments and criteria - Pointer access level : X [+] Generating module info table, hang on... - Processing modules - Done. Let's rock 'n roll. [+] Criteria: ASLR = False | REBASE = False [+] Querying 1 modules - Querying module blah - Search complete, processing results [+] Preparing output file 'jmp.txt' - (Re)setting output file c:\logs\blah\jmp.txt
Likewise, the list of loaded modules now indicates some selection and ordering criteria. Let's say you're connected to MS Edg (64bit) and you'd like to see all modules that contain the word "edge" in the full path:
0:015> !mona mod -cmp edge Hold on... [ -- START -- ] Mona command started on 2026-04-16 15:27:11 (v3.0, rev 3000) 64bit [ -- START -- ] Python: 3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)]) [ -- START -- ] PyKD: 0.3.4.15 [+] Command used: !py C:\Tools\mona3\mona.py mod -cmp edge [+] Processing arguments and criteria - Pointer access level : X - Filtering modules by path matching : edge [+] Generating module info table, hang on... - Processing modules - Done. Let's rock 'n roll. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Total nr of modules loaded: 20 | Nr of modules displayed after filters: 4 | PEB order: InLoadOrder ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Module filter applied: cmp = edge ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Base | Top | Size | Rebase | ASLR | CFG | NXCompat | OS Dll | Version, ImageName & Path, DLLCharacteristics ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 0x00007ff774ec0000 | 0x00007ff7753a3000 | 0x00000000004e3000 | True | True | True | True | False | 147.0.3912.60 [msedge.exe] (C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe) 0xc160 0x00007ffdfe1e0000 | 0x00007ffdfe696000 | 0x00000000004b6000 | True | True | True | True | False | 147.0.3912.60 [msedge_elf.dll] (C:\Program Files (x86)\Microsoft\Edge\Application\147.0.3912.60\msedge_elf.dll) 0x4160 0x00007ffdc1710000 | 0x00007ffdd4849000 | 0x0000000013139000 | True | True | True | True | False | 147.0.3912.60 [msedge.dll] (C:\Program Files (x86)\Microsoft\Edge\Application\147.0.3912.60\msedge.dll) 0x4160 0x00007ffdfdd70000 | 0x00007ffdfe1df000 | 0x000000000046f000 | True | True | True | True | False | 0.0.0.0 [ffmpeg.dll] (C:\Program Files (x86)\Microsoft\Edge\Application\147.0.3912.60\ffmpeg.dll) 0x4160 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [+] Preparing output file 'modules.txt' - (Re)setting output file c:\logs\msedge\modules.txt [ -- END -- ] This mona.py action took 0:00:00.368993 [ -- END -- ] Current date/time: 2026-04-16 15:27:11
When specifying module criteria and filters, you'll see them appear in the header of the module table.
Unsure what arguments are certain command takes? Simply run the command with the -h flag, or run !mona help command (where "command" is the name of the command you're interested in)
!mona asm -h You've asked for help about the 'assemble' command. Here is the requested information: Basic command: -------------- !py mona assemble !py mona asm Usage: ------ Convert instructions to opcode. Separate multiple instructions with #. Mandatory argument : -s <instructions> : the sequence of instructions to assemble to opcode
You can interrupt (most) potentially long-running routines in mona.py by creating a file named stop in the same folder as mona.py
echo "stop" > c:\tools\mona3\stop
mona.py will check for the file on a recurring basis (but it may not be instant though) and will hard-terminate the script if the file was found. You obviously won't get results, but at least you don't have to kill your debugging session in case you'd like to try something else.
Some mona.py commands will show clickable links in WinDBG(X), facilitating easy navigation. By default, mona.py assumes that you've created a WinDBG alias, allowing you to run !mona at the WinDBG Command prompt. (In fact, mona.py will query the WinDBG aliases to find the one that you're using, but it will only work if you have only one alias defined that points to the mona.py script)
The clickable links will have that alias name hardcoded in their commands (either the actual alias name, or just !mona if no unique alias was found.
However, you can avoid doing the lookup and set your alias name as a mona configuration variable. Suppose you have decided to name your WinDBG alias !mona3 instead of !mona and you don't want to rely on mona.py to extracting it from your WinDBG aliases, you can tell mona.py to use the !mona3 alias in its clickable links by setting the alias config variable:
!mona3 config -set alias #mona3
(In order to avoid alias substitution when using the config command, please a # instead of !. mona.py will replace it back to !)
From this point forward, the clickable links will use !mona3.
Perhaps you don't even want to use a WinDBG alias. In that case, you can set the alias config value to the actual command you're using. Again, don't forget to replace the ! with a #. Once set, mona will use whatever value you've set in it clickable links.
!py -3.9 c:\Tools\mona3\mona.py config -set alias "#py -3.9 c:\Tools\mona3\mona.py"
Note: if a mona.py config value alias is set, it will use that one instead of doing an active lookup in your aliases.
Feel free to drop by our Discord server and ask questions in the #mona channel. We'll be more than happy to help
Please open an issue on Github and provide the steps to reproduce the problem. For more sophisticated issues, we may ask you to run the command again with the -debug flag and to provide us the output. (On WinDBG(X), the -debug flag will write the output to a logfile automatically using WinDBGs .logopen command.
This release is just the beginning.
We’ll explore some of the new features and improvements in more detail in future posts, and we continue to work on adding more to mona v3.
Of course, we also encourage and invite anyone to write code, to add more cool features to mona and submit PRs. Please check the CONTRIBUTING.md file for more info.
Stay tuned.
If you want to be the first to hear about new releases, deep dives, and upcoming content:
👉 If you haven't done so already, subscribe to the blog - you'll receive an update in your mailbox as soon as I publish a new post. 👉 Follow on social media. If you see something you like, please share it with others.
More is coming.
© Corelan Consulting BV. All rights reserved. The contents of this page may not be reproduced, redistributed, or republished, in whole or in part, for commercial or non-commercial purposes without prior written permission from Corelan Consulting bv. See our Terms of Use & Privacy Policy (https://www.corelan.be/index.php/legal) for more details.
Subscribe to get the latest posts sent to your email.
Type your email…
Subscribe
Peter Van Eeckhoutte is the founder of Corelan and a globally recognized expert in exploit development and vulnerability research. With over two decades in IT security, he built Corelan into a respected platform for deep technical research, hands-on training, and knowledge sharing. Known for his influential exploit development tutorials, tools, and real-world training, Peter combines a strong research mindset with a passion for education—helping security professionals understand not just how exploits work, but why.
Tags:
Your email address will not be published. Required fields are marked *
Comment *
Name *
Email *
Website
Notify me of new posts by email.
Post Comment
Δ
This site uses Akismet to reduce spam. Learn how your comment data is processed.