Runbook Crytox

Choix de la cible#

Groupe actif depuis 2020, toujours actif en 2025 (rapport Talos IR Q1 2025, rapport Halcyon mi/fin 2025 sur un nouvel encrypteur maison qui n'utilise plus HRSword pour désactiver l'EDR). Deux rapports publics documentent l'implémentation crypto :

  • Zscaler ThreatLabz, 2022-09-21, "Technical Analysis of Crytox Ransomware" (Romain Dumont) : affirme que la clé AES par fichier provient d'un générateur pseudo-aléatoire maison, seedé par GetTickCount() (32 bits) combine à une constante config_seed figée dans la configuration du malware. Citation directe du rapport : "Using a 32-bit integer as the seed is not sufficient with today's computational power." Neuf hashs fournis, un seul présent sur MalwareBazaar (voir Acquisition).
  • Halcyon, 2025, "Crytox Consistently Evades Endpoint Security via PowerShell" : confirme l'activité récente (mi-2025, dizaines de victimes), décrit un nouvel encrypteur maison (RSA-2048 + AES-256 par fichier, extension .wait, note de rançon Help.hta, contact via uTox). Un hash cité (d3d0bbaf2108ef7cee7ae795e31450135acff39fd95b3ec3af5da9a198d90f97) mais absent de MalwareBazaar (échantillon d'engagement IR, non soumis publiquement) : impossible à récupérer par le canal habituel. Le rapport ne confirme ni n'infirme si le générateur seedé par GetTickCount de la version 2022 est toujours en place dans ce nouvel encrypteur.

Question ouverte à trancher (même forme que Gunra/Rhysida déjà traités dans ce dépôt) : le défaut de seed 32 bits documenté par Zscaler en 2022 est-il toujours présent dans les échantillons qu'on peut effectivement obtenir, et jusqu'à quelle date ? On ne peut pas répondre pour la version 2025 précise du rapport Halcyon faute d'échantillon public, mais on peut confirmer/détailler le mécanisme exact sur les échantillons 2020-2021 disponibles, ce qui n'a pas été fait au niveau code (Zscaler ne montre pas le détail du générateur, juste sa conclusion).

Acquisition#

Recherche par tag MalwareBazaar/ThreatFox infructueuse (Crytox n'existe pas comme tag exploitable, illegal_tag sur les variantes avec underscore). Les 9 hashs cités par le rapport Zscaler et le hash cité par Halcyon vérifiés un par un via get_info : un seul présent sur MalwareBazaar.

# Hash directement cite par Zscaler/ThreatLabz comme echantillon analyse
curl -s -X POST https://mb-api.abuse.ch/api/v1/ \
  -H "Auth-Key: $(cat ../.mb_api_key)" \
  -d "query=get_file&sha256_hash=32eef267a1192a9a739ccaaae0266bc66707bb64768a764541ecb039a50cba67" \
  -o binaries/32eef267.zip
7z x -pinfected -y -obinaries binaries/32eef267.zip

# Pivot par imphash (365b1d12b684a96b167a74679ec9e4e3, meme builder) :
# get_imphash renvoie exactement 2 resultats, tous les deux 2020-2021
curl -s -X POST https://mb-api.abuse.ch/api/v1/ \
  -H "Auth-Key: $(cat ../.mb_api_key)" \
  -d "query=get_file&sha256_hash=d60dc6965f6d68a3e7c82d42e90bfda7ad3c5874d2c59a66df6212aef027b455" \
  -o binaries/d60dc696.zip
7z x -pinfected -y -obinaries binaries/d60dc696.zip

Deux échantillons obtenus, même taille (1 287 168 octets), même imphash :

  • 32eef267a1192a9a739ccaaae0266bc66707bb64768a764541ecb039a50cba67.exe (first_seen MalwareBazaar 2021-04-08, échantillon directement analysé par Zscaler)
  • d60dc6965f6d68a3e7c82d42e90bfda7ad3c5874d2c59a66df6212aef027b455.exe (nom original worker 5 -t.exe, first_seen 2020-04-18, même famille)

Depackaging#

file binaries/*.exe
# PE32+ ... 3 sections -> sections nommees UPX0/UPX1/.rsrc : UPX confirme
# (coherent avec la note Zscaler "packed with UPX")

upx -d -o binaries_unpacked/32eef267....unpacked.exe binaries/32eef267....exe
upx -d -o binaries_unpacked/d60dc696....unpacked.exe binaries/d60dc696....exe
# 1 287 168 -> 3 070 464 octets (ratio 41.92%), UPX standard, depackaging propre

Point d'attention : le point d'entrée de l'exécutable depackage (0x2e8000, très proche de la fin de l'image) et une table d'imports statique réduite à 10 entrées (GlobalAlloc, GlobalFree, CreateFileA, WriteFile, CloseHandle, ExitProcess, GetModuleFileNameA, GetLastError, RegCloseKey, MessageBoxA) suggèrent que le payload résout lui-même le reste de ses API (dont très probablement GetTickCount et les fonctions Crypt*) dynamiquement à l'exécution (GetProcAddress sur des noms construits/lus en mémoire), plutôt que par table d'imports statique -- motif d'évasion courant, cohérent avec Halcyon qui décrit explicitement des versions récentes conçues pour échapper aux outils de sécurité. Confirme par strings : GetTickCount apparaît comme chaîne littérale dans le binaire depackage, mais pas dans la table d'imports résolue par pefile :

strings -n6 binaries_unpacked/32eef267*.exe | grep -iE "GetTickCount|Crypt"
# -> GetTickCount (chaine seule, pas d'autre match Crypt*)

Chaîne complète de désassemblage : dropper -> PE embarqué -> shellcode injecteur -> payload final#

Plutôt que de suivre la piste 1 telle quelle (xrefs Ghidra sur la chaîne "GetTickCount"), la chaîne complète a été demontée couche par couche, combinant lecture statique et émulation Unicorn ciblée (tools/ emulate_shellcode.py, tools/emulate_stage3_keygen.py, tools/crack_api_hash.py). Quatre niveaux d'indirection en tout :

  1. Dropper depackage UPX (entry() à 0x1402e8000) : écrit un PE embarque sur disque (WriteFile), gère une clé de registre (RegOpenKeyExA/RegQueryValueExA, tailles attendues 0x114/0x500), puis énumère les processus en cours (Process32First/Next) pour injecter un shellcode (FUN_1402e8547) dans un processus cible via VirtualAllocEx/WriteProcessMemory/CreateRemoteThread.
  2. PE embarqué (extrait à l'offset RVA 0x1000 du dropper, 3 043 328 octets, PE32 i386, sha256 0a563967a7d48e702adaa6f21f22a80b7db6aae7b7149ede51b0fde583e37283) : confirme être le client uTox complet (chaînes "uTox", "utox.io", version "0.14.0"), cohérent avec le protocole de contact par messagerie uTox documenté publiquement (Halcyon, K7). Utilise ici uniquement comme "processus légitime" pour l'injection, pas comme charge utile active.
  3. Shellcode injecte (0x1142 = 4418 octets, extrait de 0x1402e889e dans le dropper) : résout ses appels d'API par hash (ROR32 7 bits sur le nom de module UTF-16 majuscule+NUL, ROR32 11 bits sur le nom de fonction ASCII+NUL, somme, cf tools/ crack_api_hash.py -- algorithme rétro-concu empiriquement, confirmé sur plus de 70 hash cassés avec succès au total sur ce dépôt). Rouvre le FICHIER DROPPER ORIGINAL sur disque (chemin reçu en paramètre), mappe le fichier (CreateFileA/CreateFileMappingA/ MapViewOfFile), extrait un blob de 0x4400 (17408) octets à l'offset fixe 0x135ca4, zeroise puis supprime le fichier original (autodestruction anti-forensique confirmée dynamiquement : rep stos sur toute la vue mappée avant DeleteFileA). Déchiffré un petit blob de configuration embarquée (0xc0 octets, AES-NI, clé dérivée des tables S-box) révélant des noms de fichiers (pghdn.txt, rwjfk.bat), écrit et exécuté réellement via ShellExecuteA un script de sabotage anti-récupération : for /F "tokens=*" %1 in ('wevtutil.exe el') DO wevtutil.exe cl "%1" vssadmin.exe Delete Shadows /All /Quiet diskshadow.exe /s ../pghdn.txt (purge des journaux d'événements Windows + suppression de tous les clichés instantanés, capture intégrale dynamique, cf tools/emulate_shellcode.py). Énumère ensuite les processus à la recherche de EXPLORER.EXE (hash 0xdcf164cd) ou, à défaut, SVCHOST.EXE (0x561f1820), pour y injecter (mêmes primitives VirtualAllocEx/WriteProcessMemory/CreateRemoteThread) le blob de 0x4400 octets déchiffré (même routine AES-NI, clé dérivée des tables S-box à [r12+0x428]) : sortie 20 480 octets, capturée intégralement via le hook WriteProcessMemory.

Deux bugs réels du HARNAIS D'ÉMULATION (pas du malware) trouvés et corrigés pendant cette étape, tous deux des inversions de paramètres dans les stubs d'API simulées en Python (à chaque fois confirmés par comparaison avec la convention d'appel x64 réelle observée au désassemblage) : - memcpy(dest,src,n) simulé avec n/src/dest sur les mauvais registres (RCX pris pour la longueur au lieu de la destination) : la copie du blob de 0x4400 octets vers le paramètre du thread suivant échouait silencieusement (vérification de bornes toujours fausse), laissant le blob a zero. - CryptAcquireContextA simulé avec le pointeur de sortie sur R9 (en réalité dwProvType, un entier) au lieu de RCX (le vrai phProv) : ecriture mémoire a l'adresse littérale 0x1, plantage immédiat au tout premier appel API du payload final.

  1. Payload final (les 20 480 octets déchiffrés et injectés, position-indépendant, même mécanisme de résolution d'API par hash) : surface d'API cassée via crack_api_hash.py (44 constantes de hash extraites du binaire, la grande majorité résolues) confirmant sans ambiguïté qu'il s'agit du vrai module de chiffrement de fichiers : CryptAcquireContextA/CryptGenKey/CryptExportKey/CryptImportKey/ CryptEncrypt/CryptDestroyKey (CryptoAPI, génération et export de la paire de clés RSA de session -- cohérent avec la description Halcyon "génère une paire de clés RSA-2048 de session... chiffre la clé privée de session cinq fois avec la clé publique de l'attaquant"), GetLogicalDrives+boucle sur les 26 lettres (spawn d'un thread CreateThread par lecteur local détecté), WNetOpenEnumW/ WNetEnumResourceW/WNetCloseEnum (énumération des lecteurs réseau, confirme la portée "lecteurs locaux ET réseau" documentée publiquement), FindFirstFileW/FindNextFileW/FindClose (parcours récursif), CreateFileW/ReadFile/WriteFile/SetEndOfFile/ SetFilePointerEx/SetFileAttributesW/MoveFileW (E/S de chiffrement par fichier + renommage), AdjustTokenPrivileges/ LookupPrivilegeValueA/OpenProcessToken (SeDebugPrivilege ou similaire), RegOpenKeyExA/RegQueryValueExA/RegSetValueExA/ RegCloseKey (persistance d'un identifiant victime).

Le générateur pseudo-aléatoire : reconstruction complète et exacte#

Localise par lecture directe dans le payload final (offset 0x1396 du blob de 20 480 octets, immédiatement precede d'un appel GetTickCount dont le hash 0xe2b00ace est résolu). Code source reconstitué (variables nommées d'après leur rôle, traduction directe du désassemblage x86-64, aucune conjecture) :

// r12 : structure de configuration partagee (dechiffree au demarrage du
//       payload par AES-NI depuis un blob de 0x2f30 octets embarque).
// [r12+0x10] : etat 64 bits PERSISTANT du generateur, partage entre TOUS
//              les appels de cette fonction sur tout le run (jamais remis
//              a zero -- confirme par le site d'appel unique de cette
//              fonction dans tout le payload, place juste apres
//              SetFilePointerEx/ReadFile sur le fichier courant : appelee
//              UNE FOIS PAR FICHIER, coherent avec Zscaler "each file is
//              encrypted with a unique AES-256 key generated at runtime").
// [r12+0x58] : diviseur/modulus (valeur issue de la configuration
//              dechiffree, pas une constante figee dans le CODE lui-meme
//              -- seuls le multiplicateur et l'incrementateur ci-dessous
//              sont des constantes de code en dur).
//
// rax porte le "fil" de calcul d'une iteration a l'autre : au tout debut
// du tout premier appel de cette fonction dans tout le run, rax vaut
// GetTickCount() (appele a l'entree de CHAQUE appel, mais seule sa toute
// premiere valeur compte vraiment : ensuite rax est entierement regi par
// la recurrence ci-dessous). A l'interieur d'un tour, rax porte le
// QUOTIENT de la division precedente vers l'iteration suivante ; entre
// deux tours, rax porte le MOT DE SORTIE du tour precedent (pas remis a
// GetTickCount()).
void generer_cle_32_octets(uint64_t *sortie /* rdi, 4 x 8 octets */) {
    uint64_t rax = GetTickCount();             // SEULE injection d'entropie fraiche
    for (int tour = 0; tour < 4; tour++) {     // 4 tours -> 32 octets
        uint64_t acc = 0;
        for (int i = 0; i < 64; i++) {         // 64 iterations par tour
            rax = rax + r12->accumulateur;             // 0x13b7
            rax = rax * 0x7D6C3ULL + 0x159EC7ULL;      // 0x13bc-0x13c3, LCG
            r12->accumulateur = rax;                    // 0x13c9, PERSISTE
            uint64_t haut = rax >> 16;                  // 0x13ce
            uint64_t cube = haut * haut * haut;         // 0x13d8/0x13dc, deux imul
            uint64_t quotient = cube / r12->modulus;    // 0x13e0
            uint64_t reste    = cube % r12->modulus;
            rax = quotient;               // reinjecte dans l'iteration SUIVANTE
            acc = (acc >> 1) | ((reste & 1) << 63);   // 0x13e5, shrd : 1 bit/iteration
        }
        *sortie++ = acc;   // 8 octets de sortie par tour, 4 tours = 32 octets
        rax = acc;         // reinjecte au tour SUIVANT (pas remis a GetTickCount())
    }
}

Réimplémentation Python complète et testée : tools/crytox_lcg_poc.py (fonction derive_key/derive_key_chain, cf docstring pour le détail).

Constat : l'intégralité de l'entropie de la clé AES-256 (32 octets = 256 bits) dérivé d'un unique appel GetTickCount (nombre de millisecondes depuis le démarrage de Windows, 32 bits, résolution typique de 10 à 16 ms sur la plupart des systèmes). Le reste du mécanisme (multiplicateur/incrémentateur LCG fixés dans le code, élévation au cube, division modulaire, extraction bit a bit via shrd) est de la mise en forme déterministe et publique : aucune information supplémentaire n'est nécessaire pour reproduire intégralement la clé au-delà de la valeur de GetTickCount au moment de l'infection (bornée par la durée de fonctionnement du système en millisecondes, un espace de recherche dérisoire face à un keyspace AES-256 réel). Confirmé et détaillé au niveau assembleur ce que Zscaler avait seulement pu affirmer par sa conclusion ("32-bit integer... not sufficient") sans montrer le mécanisme. [r12+0x58] (le "modulus") n'a pas été pin été à une valeur numérique précise (dépend du contenu de la configuration déchiffrée, elle-même chiffrée par une clé non extraite ici) mais cela ne change rien au constat : quel que soit ce modulus, la chaîne entière est une fonction PUREMENT DÉTERMINISTE d'un seul entier 32 bits public/devinable (GetTickCount).

Vérification dynamique partielle : l'émulation Unicorn du payload final (tools/emulate_stage3_keygen.py) confirmé dynamiquement l'intégralité de la chaîne jusqu'au spawn du thread de chiffrement par lecteur (bug de paramètre CryptAcquireContextA trouvé et corrige au passage, cf ci-dessus), et jusqu'au tout début de ce thread (un appel d'API supplémentaire non résolu, hash 0xada80e4c, non identifié faute de temps). L'appel réel à generer_cle_32_octets n'a pas été atteint dynamiquement (nécessiterait de résoudre ce dernier hash et de poursuivre la simulation du parcours de fichiers), mais la lecture statique du code ci-dessus est sans aucune ambiguïté (structure de boucles, constantes, séquence d'instructions parfaitement idiomatique d'un LCG) : le niveau de confiance sur ce point ne dépend pas de cette dernière étape d'émulation.

Exploitabilité : preuve de concept de récupération de clé#

Confirmation directe du site d'appel : generer_cle_32_octets (offset 0x1396) n'est appelée qu'une seule fois dans tout le binaire, juste après SetFilePointerEx/ReadFile sur le descripteur du fichier en cours de traitement (offset 0xc0b, vérifié par recherche de tous les call rel32 ciblant 0x1396) : confirme un appel par fichier, cohérent avec Zscaler ("each file is encrypted with a unique AES-256 key generated at runtime"). L'accumulateur [r12+0x10] n'est en revanche jamais réinitialisé entre deux fichiers : la clé du fichier N dépend de l'état laissé par le fichier N-1, pas seulement d'un nouvel appel GetTickCount isole.

Conséquence directe pour la récupération : retrouver la valeur GetTickCount() utilisée pour le premier fichier chiffre d'un run suffit, en rejouant exactement la même récurrence, à reconstruire les clés de tous les fichiers suivants du même run (l'accumulateur est entièrement déterministe une fois amorce). Le coût de la récupération complète d'une victime se ramène donc, au pire, au coût d'un seul brute-force sur un espace de 2^32 valeurs (GetTickCount est un compteur 32 bits, résolution ~10-16 ms, boucle tous les ~49,7 jours), pas un brute-forcé par fichier.

Extraction réelle du modulus, de la configuration, et validation bit-à-bit#

Suite directe de la section précédente : les trois manques identifiés ("modulus non extrait", "format IV/footer inconnu", "aucun fichier de test") ont été attaques un par un par émulation Unicorn poussée (tools/emulate_stage3_keygen.py, plusieurs passes successives réutilisant la même instance mémoire pour rejouer les threads que Unicorn, mono-thread, ne peut pas exécuter nativement).

Le modulus, obtenu en exécutant réellement le déchiffrement de la config#

Hook placé juste après le retour de la routine AES-NI de déchiffrement de configuration (offset 0x1292, l'epilogue commun aux deux branches de FUN_0047xxxx-équivalent) : dump direct de la mémoire à [r12+0x58] après déchiffrement réel (pas de réimplémentation manuelle de l'AES nécessaire, Unicorn/QEMU émulé nativement aesenc/aesenclast).

Modulus réel obtenu : 0xa77efce7 (2 810 117 351), pour l'échantillon 32eef267a1192a9a739ccaaae0266bc66707bb64768a764541ecb039a50cba67.

Bonus inattendu : la configuration déchiffrée (12 080 octets) contient bien plus que le modulus. Structure identifiée par inspection directe des octets décryptés :

  • Octets 0-3 : le modulus lui-même (e7 fc 7e a7, LE) -- confirme que [r12+0x58] pointe simplement vers le tout début du buffer de sortie du déchiffrement, pas un champ spécial.
  • Octets 4 à ~11460 : la note de rançon complète au format HTA (<html><head>...<HTA:APPLICATION...), interprétable directement (police, styles CSS, image en base64, script VBScript lançant %windir%\utox.exe). Contenu de la note : "YOUR FILES ARE ENCRYPTED", délai de 5 jours avant suppression de la clé privée, contact prioritaire via uTox (ID d'opérateur 204E91D375BADE81DC528EFCC105A5D046DB92FCC4B75F08E151053DCD8D5025DA55D689165D), email de secours Colin_Kaan@aol.com, limite de 5 Mo pour les fichiers de test envoyés par email.
  • Chaînes suivantes (extraites proprement, mélange de chaînes larges UTF-16 et étroites ASCII selon le champ) :
  • windows, ReadMe.hta : nom du fichier de note déployé
  • \\?\C:\*.* : motif d'énumération récursive (chemin long, tous fichiers)
  • .waiting : extension de fichier chiffré, confirme le format documente publiquement par Zscaler
  • SOFTWARE\Microsoft\Windows\CurrentVersion\Run\ : clé de persistance
  • SeBackupPrivilege, SeRestorePrivilege : privilèges ajustés (cohérent avec AdjustTokenPrivileges/LookupPrivilegeValueA déjà relevés dans la surface d'API)
  • C:\Windows\System32\mshta.exe "C:\ReadMe.hta" : commande de persistance/affichage de la note au démarrage
  • .waiting\shell\open\command\, open : association d'extension (double-clic sur un fichier chiffre relance la note)
  • MM/dd/yyyy : format de date (probablement pour le compte à rebours de la note, dont le template contient un 01/01/2000 factice non substitue dans cet échantillon)
  • Clé publique RSA-2048 de l'attaquant, format Microsoft CryptoAPI PUBLICKEYBLOB standard (magic RSA1 à l'offset 0x2de0 de la config, bType=0x06, bVersion=0x02, aiKeyAlg=CALG_RSA_KEYX, bitlen=2048, exposant 65537, module extrait et vérifié valide sur 2048 bits). Clé spécifique à ce build/cette victime (comme documenté par le mécanisme de personnalisation par build déjà observé sur d'autres familles de ce dépôt, ex. Akira), pas une constante Crytox universelle.

Toutes ces valeurs sont propres à CET échantillon/build précis : un autre échantillon Crytox aurait très probablement un modulus, une clé RSA et un contact différents (même mécanisme de personnalisation par configuration chiffrée que les autres familles de ce dépôt), à re-extraire de la même façon sur un autre sample.

Validation bit-à-bit de l'algorithme LCG reconstruit#

En poussant l'émulation en trois passes successives (bootstrap -> thread par lecteur 0x7c7 -> fonction renommer+chiffrer 0xa73, capturée et rejouée directement une fois son adresse et son paramètre identifiés via CreateThread), l'appel réel à generer_cle_32_octets (0x1396) a été atteint et son exécution RÉELLE tracée pas a pas (hooks sur 0x13c9 et 0x13e0 dans tools/emulate_stage3_keygen.py), avec GetTickCount figé à une valeur connue.

Point à ne pas rater, visible en comparant l'état interne itération par itération entre la sortie réelle et tools/crytox_lcg_poc.py : l'instruction réelle est div DWORD PTR [r12+0x58], une division 32 BITS (EDX:EAX avec EDX=0 confirmé par traçage, donc en pratique EAX / diviseur), et non une division 64 bits sur le cube entier. Seuls les 32 bits bas du cube comptent, et le quotient qui en ressort (réinjecte à l'itération suivante) tient sur 32 bits (RAX zéro-étendu par l'instruction DIV elle-même). tools/crytox_lcg_poc.py suit cette convention.

Validation stricte : derive_key(tick, 0xa77efce7) en Python reproduit exactement, octet pour octet, la sortie de la fonction réelle exécutée dans Unicorn, vérifie sur deux valeurs GetTickCount de test indépendantes (0x123456 et 0x7777777). Le brute-forceur par plaintext connu (bruteforce_known_plaintext) a également été revalidé avec ce modulus réel sur un cas synthétique : tick=12345 retrouvé en 0,9 seconde.

Ce qui reste ouvert#

  1. Le format exact de l'IV et du footer du fichier chiffré n'a toujours pas été tracé jusqu'au bout : la fonction 0xa73 (renommer + chiffrer un fichier) a été localisée et son début confirmé dynamiquement (renommage via MoveFileW avant chiffrement, extension .waiting ajoutée, CreateFileW sur le fichier renommé), mais la boucle de lecture/chiffrement/écriture elle-même (au-delà de l'appel au générateur de clé) n'a pas été rejouée jusqu'à la construction de l'IV ni jusqu'au format exact du footer contenant la clé enveloppée en RSA.
  2. Aucun fichier réellement chiffré par Crytox n'est disponible pour valider l'oracle contre du ciphertext authentique plutôt qu'un test synthétique -- aucun trouve publiquement (voir §Acquisition), et une exécution réelle du malware (même en VM isolée jetable, protocole déjà utilise sur Akira dans ce dépôt, cf annexes/preparation-vm-dynamique.md) n'a pas été tentée ici : décision à prendre à tête reposée avant de l'entreprendre, vu la nature du risque (exécution réelle d'un ransomware), même isolé.
  3. Question ouverte non tranchable avec les échantillons actuels (2020/ 2021) : comparer avec la description Halcyon 2025 (nouvel encrypteur maison) pour savoir si ce même défaut persiste -- nécessiterait un échantillon 2024/2025 réel, non trouvé sur MalwareBazaar/ThreatFox à ce jour (2026-07-31).

Environnement#

Identique aux autres analyses de ce dépôt (Ghidra headless via PyGhidra, venv ~/.venvs/ctf-reverse sur disque local, PAS sur la clé USB exFAT -- voir la mémoire dédiée à ce piège). Outils génériques copiés depuis gunra-time-seed-rng/tools/ (list_functions.py, decompile_by_name.py, find_api_callers.py, decompile_addr.py, find_string_refs.py, find_constant_refs.py).