Decrypting Mirai configuration With radare2 (Part 2)
This is the third part of our three-part series on code emulation for malware analysis:
- Part 1 explains how to use radare2’s function emulation feature, featuring a password-cracking exercise using radare2’s Python scripting plugin,
r2pipe. - Part 2 demonstrates how to decode the configuration of the Mirai IoT botnet by writing an automation script using radare2’s Python scripting capabilities.
- Part 3 (this post) builds on the previous script by adding support for searching encrypted string addresses and generating function signatures to locate the decryption routine dynamically.
In the previous two posts, we explored how to emulate string decryption routines using radare2 macros and Python scripts. We successfully decrypted parts of the configuration, but not all of it. In this post, we will enhance our automation script to solve this. Specifically, we will find the addresses of the encrypted data dynamically and feed them into our emulator.
We will also address another interesting challenge: when testing our script against different Mirai variants, the decryption function offset changes. Even though the underlying assembly remains identical, the hardcoded address breaks. We can elegantly solve this by creating function signatures—another incredibly useful feature in radare2. Let’s get right into it!
#Searching via Data References
If you analyzed the decryption routine closely in the previous post, you might argue that we took a unnecessarily complex path. Instead of manually constructing a fake configuration structure on the stack and modifying register values, why didn’t we just pass the configuration index as an argument and let the function emulation run naturally?
That would indeed be the ideal approach if the array of configuration structures existed statically in memory. However, it doesn’t. Mirai dynamically populates this configuration array at runtime. Let’s inspect the memory at 0x08052800—the base address of the configuration structure array.
As you can see, there are numerous cross-references pointing to this location. We see DATA XREFS originating from the function sub.7_1_700 (located at 0x0804d700). Let’s investigate what that function is doing.
This block contains a recurring pattern: pushing a byte value, pushing a global memory address, and invoking a function. This looks like a dynamic initialization routine copying encrypted strings and metadata into the runtime configuration array. Let’s disassemble the helper function called at 0x0804e0f0 to see what is happening under the hood.
This nested function contains a loop copying bytes from edx + ebx to edx + esi, with edx acting as the loop counter. This is a custom string/memory copy routine.
Why didn’t the malware authors use the standard library memcpy? Because Mirai is designed to be highly portable across stripped and embedded Linux environments that might lack standard C runtime libraries (libc). Writing custom implementations of standard functions ensures the malware runs reliably on almost any target.
Now that we know the initialization function (sub.7_1_700) references our encrypted data, we can programmatically retrieve these memory addresses. In radare2, the agaj command generates a control flow graph with local and global data references in JSON format. The destination addresses are stored in the title field of the nodes. We can parse this JSON in Python and feed the extracted addresses directly into our decryption function:
1 | # Start address of the configuration initialization function |
Running this code produces the following output:
1 | +\xfb\x94l\x12l\x90\x8doxx\xfb\xbct\xf1\xbb\x12l\x10\xc0v}p(\x90\xab |
Success! We have recovered highly critical strings, such as the C2 domain (zantari.duckdns.org), virtual file paths (/proc/net/tcp, /maps), and fallback credentials (g1abc4dmo35hnp2lie0kjf).
However, we are still missing a few configuration strings that our previous heuristic detected. To get a complete extraction, we can try an alternative approach.
#Searching via Assembly Patterns
The initialization routine pushes arguments onto the stack immediately before calling the copy function. We can find every push instruction inside this routine, parse the pushed argument (the pointer to the encrypted string), and run our emulator on it.
We can search for instructions matching a specific pattern using radare2’s opcode search engine. The command /atj push searches for all push instructions and returns the results in JSON.
To avoid scanning the entire binary, we must limit our search window to the initialization function. We can restrict the search boundaries using the search.from and search.to configuration variables. Here is how we implement this in Python:
1 | # Define search boundaries |
Using this assembly search pattern, we get the following output:
1 | zantari.duckdns.org |
This is outstanding! This method extracts every single encrypted string, including critical commands (/bin/busybox ps, /bin/busybox kill -9) and protocol-specific patterns (TSource Engine Query).
Now that we can extract the complete configuration, let’s focus on portability. We want to remove all hardcoded offsets so this script works out-of-the-box on new Mirai samples.
#Creating and Searching Function Signatures
To locate functions dynamically in radare2, we can generate a unique signature. The z command family manages signatures. To create a signature for a function, seek to its starting address and run:
zaf sub.7_1_700 config_func: This generates a signature namedconfig_funcfor our configuration initialization routine.zaf fcn.decrypt decrypt_func: This generates a signature nameddecrypt_funcfor our string decryption function.
You can save these signatures to a file using zos [filename] and load them later using zo [filename]. We will use this signature database inside our Python script to find function boundaries automatically.
#Locating Functions Programmatically
Once signatures are loaded, we can scan the binary using z/. Matching locations are flagged in the sign flag space under the name sign.bytes.[signature_name].
To process these results in Python, we switch to the signature flag space using fs sign and query the flags in JSON format using fj.
Here is our updated Python routine to locate the decryption function dynamically:
1 | def find_decryption_func(): |
We do the same to locate our configuration initialization function:
1 | def find_config_func(): |
Our script is now completely modular and immune to address offsets!
#Partial Emulation Troubleshooting Tips
During this analysis, I ran into a few edge cases. Here are some key takeaways for debugging radare2’s emulation engine:
- Unresolved System Calls: If your target function nested-calls other routines that make syscalls, ESIL will halt or behave unpredictably. Keep your emulation boundaries as narrow as possible.
- Uninitialized Globals: If the target routine reads global variables that aren’t mapped or initialized in your VM, it will cause infinite loops or invalid state.
- Byte Ordering: Pay close attention to endianness when writing addresses directly to the virtual stack. A single mismatched byte will point the emulator to unmapped memory, resulting in crashes.
#Conclusion
This concludes our three-part exploration of radare2’s partial code emulation capabilities. We reversed Mirai’s decryption logic, mapped its structures, automated cipher text extraction, and made our script robust and portable using function signatures. Emulating code directly saves hours of manual static analysis and avoids the need to port complex obfuscation logic to external scripts.
You can download the full, automated script on GitHub. If you are a malware researcher and want to analyze the samples used in this post, please email me using your work or institutional email address to verify your identity.
Thanks for reading, and happy reversing!