
Génération universelle de signatures pour toute fonction système de toutes les builds Windows à l'aide de Winbindex
Signatures binaires multi-versions et offsets RVA pour les fonctions Windows PE
pe-signgen est un outil destiné aux ingénieurs en rétro-ingénierie et aux chercheurs en sécurité qui génère automatiquement :
L'idée centrale est de fournir un moyen systématique et robuste d'accéder aux fonctions non exportées sur les builds Windows 10/11. L'outil s'appuie sur :
⚠️ Prise en charge des versions Windows
pe-signgenprend en charge uniquement Windows 10 et Windows 11. Il s'agit d'un choix de conception délibéré : Winbindex ne fournit pas de données complètes pour les versions plus anciennes.
Internes Windows non exportés
Générer des signatures pour des fonctions comme LdrpInitializeTls, RtlpInsertInvertedFunctionTableEntry, etc.
Recherche sur le game hacking / l'anti-triche Générer des signatures stables qui survivent aux mises à jour des jeux
Recherche en sécurité Localiser les routines critiques pour la sécurité sur différents builds Windows
Automatisation Génération scriptable de signatures et d'offsets pour des ensembles entiers d'API internes
pe-signgen fournit trois formats de sortie distincts pour différents cas d'usage :
Données structurées pour l'automatisation, les scripts et l'intégration avec d'autres outils.```bash pe-signgen --signature ntdll!NtCreateFile -o ntcreatefile.json --output-format json
**Structure de sortie :**```json
{
"dll_name": "ntdll",
"function_name": "NtCreateFile",
"architecture": "x64",
"generated": "2024-12-11T15:30:00.123456",
"total_builds": 1247,
"unique_signatures": 3,
"signature_groups": [
{
"matched_symbol": "NtCreateFile",
"signature": "4C 8B DC 49 89 5B 08 49 89 6B 10 49 89 73 18 ...",
"length": 48,
"build_count": 845,
"versions": [
{ "major": 10240, "minor": 16384, "build": "10240.16384" },
{ "major": 10586, "minor": 0, "build": "10586.0" }
]
}
]
}
Notes :
major et minor sont dérivés de la chaîne de build en la divisant au premier ..
Exemple : "10240.16384" → major = 10240, minor = 16384.build est la chaîne de build d’origine utilisée comme clé interne.Formats binaires compacts, prêts à l’exécution, optimisés pour les systèmes embarqués et l’analyse à faible surcharge.
Ils correspondent à la disposition sur disque implémentée dans write_wsig() et write_woff().
Magic : WSO\0 (0x57 0x53 0x4F 0x00)
Version actuelle : 1
Objectif : stocker des signatures binaires avec masques génériques (wildcards) et les versions de build Windows associées
┌─────────────────────────────────────┐ │ Header (36 bytes) │ ├─────────────────────────────────────┤ │ DLL Name (variable) │ ├─────────────────────────────────────┤ │ Function Name (variable) │ ├─────────────────────────────────────┤ │ Signature / Mask / Build blobs │ ← Arbitrary order, see notes ├─────────────────────────────────────┤ ← Aligned to 4 bytes │ Groups Table (24 × N bytes) │ └─────────────────────────────────────┘
**Notes importantes sur la disposition (correspond à `write_wsig`)**
* Après l'en-tête, les noms de DLL et de fonction sont écrits sous forme d'octets UTF‑8.
* Pour chaque groupe de signatures, les octets de motif et les octets de masque sont écrits, suivis du tableau de build pour ce groupe.
* Ces régions par groupe ne sont **pas** regroupées globalement par type : les motifs, les masques et les tableaux de build peuvent être entrelacés.
* Le builder s'aligne sur **4 octets** avant chaque tableau de build et avant la table des groupes. Cela peut introduire du remplissage.
* Les consommateurs doivent **toujours** suivre les décalages dans l'en-tête et les entrées de groupe ; ne **pas** se fier au diagramme conceptuel pour la contiguïté physique.
##### Disposition de l'en-tête (36 octets)```c
// Packed as: "<4sIIIIIIII" (little-endian)
typedef struct {
char magic[4]; // "WSO\0" (WSIG_MAGIC)
uint32_t version; // FORMAT_VERSION (currently 1)
uint32_t arch; // Architecture code (1=x64, 2=ARM64, 3=WoW64)
uint32_t dll_off; // Offset to DLL name string
uint32_t dll_len; // Length of DLL name in bytes
uint32_t func_off; // Offset to function name string
uint32_t func_len; // Length of function name in bytes
uint32_t group_count;// Number of signature groups
uint32_t groups_off; // Offset to groups table
} wsig_header_t; // 36 bytes
Chaque groupe de signatures représente un motif unique qui s'applique à une ou plusieurs builds Windows.```c // Packed as: "<IIIIII" (little-endian)
typedef struct { uint32_t sig_off; // Offset to signature pattern bytes uint32_t sig_len; // Length of signature pattern (in bytes) uint32_t mask_off; // Offset to wildcard mask bytes uint32_t mask_len; // Length of wildcard mask (≈ ceil(sig_len/8)) uint32_t builds_off; // Offset to build version array uint32_t build_cnt; // Number of builds using this signature } wsig_group_t; // 24 bytes
##### Entrée de version de build (8 octets)
Chaque entrée de version de build identifie une version Windows spécifique qui utilise cette signature.```c
typedef struct {
uint32_t major; // e.g. 19041
uint32_t minor; // e.g. 1234
} wsig_build_t; // 8 bytes
major et minor proviennent de la décomposition de la chaîne de build (« A.B » → A, B). La chaîne de build d'origine n'est pas stockée dans le format binaire ; si vous en avez besoin, conservez-la en externe (elle est présente dans la sortie JSON).
Le masque est un masque de bits (bitmask) où chaque bit correspond à un octet du motif de signature :
Exemple :``` Signature: 4C 8B DC 49 89 ?? 08 49 Mask bits: 1 1 1 1 1 0 1 1 (MSB first within each byte) Mask byte: 0xBF (binary: 10111111)
Les octets de masque sont stockés et interprétés selon **l'ordre des bits little-endian** au sein de chaque octet (exactement comme dans les helpers C et `parse_signature`) :```c
uint8_t bit = (mask_bytes[byte_index >> 3] >> (byte_index & 7)) & 1u;
*_len pour déterminer la longueur ; ne lisez pas au-delà.Magic : WOF\0 (0x57 0x4F 0x46 0x00)
Version actuelle : 1
Objectif : Stocker les RVA directes et les offsets de fichier pour les fonctions à travers les builds Windows
┌─────────────────────────────────────┐ │ Header (36 bytes) │ ├─────────────────────────────────────┤ │ DLL Name (variable) │ ├─────────────────────────────────────┤ │ Function Name (variable) │ ├─────────────────────────────────────┤ │ Matched Symbol Names (variable) │ ← One UTF‑8 string per entry ├─────────────────────────────────────┤ ← Aligned to 4 bytes │ Entries Table (32 × N bytes) │ └─────────────────────────────────────┘
Détails de mise en page (correspond à `write_woff`) :
* Après l'espace réservé de l'en-tête, les noms de DLL et de fonctions sont écrits sous forme d'octets UTF‑8.
* Pour chaque build, le nom de symbole correspondant est écrit sous forme de chaîne UTF‑8 (sans terminateur). Ceux-ci forment un simple pool de chaînes.
* L'écrivain aligne ensuite sur 4 octets et écrit la table d'entrées à taille fixe.
* Chaque entrée contient des décalages (`matched_off`, `matched_len`) pointant vers ce pool de chaînes.
##### Mise en page de l'en-tête (36 octets)```c
// Packed as: "<4sIIIIIIII" (little-endian)
typedef struct {
char magic[4]; // "WOF\0" (WOFF_MAGIC)
uint32_t version; // FORMAT_VERSION (currently 1)
uint32_t arch; // Architecture code (1=x64, 2=ARM64, 3=WoW64)
uint32_t dll_off; // Offset to DLL name string
uint32_t dll_len; // Length of DLL name in bytes
uint32_t func_off; // Offset to function name string
uint32_t func_len; // Length of function name in bytes
uint32_t entry_cnt; // Number of offset entries
uint32_t entries_off;// Offset to entries table
} woff_header_t; // 36 bytes
Chaque entrée fait correspondre une build Windows à l'emplacement de la fonction dans cette build.```c // Packed as: "<IIQQII" (little-endian)
typedef struct { uint32_t major; // Windows major version (e.g., 19041) uint32_t minor; // Windows minor version (e.g., 1234) uint64_t rva; // Relative Virtual Address in the DLL uint64_t file_offset; // Raw file offset in the DLL on disk uint32_t matched_off; // Offset to matched symbol name string uint32_t matched_len; // Length of matched symbol name } woff_entry_t; // 32 bytes
##### Remarques d'utilisation
* **RVA** est le décalage mémoire lorsque la DLL est chargée à sa base préférée.
* **Décalage de fichier** est la position brute dans le fichier PE sur le disque.
* **Symbole correspondant** peut différer de la fonction demandée (p. ex., exports transférés).
La chaîne est stockée une seule fois dans le pool de chaînes ; `matched_off`/`matched_len` y font référence.
* Les entrées sont triées par version de build (majeure, puis mineure) pour une recherche efficace.
---
#### Codes d'architecture
Les deux formats binaires utilisent le même encodage d'architecture (via `ARCH_CODE_MAP`) :
| Code | Architecture | Description |
| ---- | ------------ | ---------------------------- |
| 1 | x64 | AMD64/Intel64 64 bits |
| 2 | ARM64 | ARM 64 bits (AArch64) |
| 3 | WoW64 | x86 32 bits sur Windows 64 bits |
Les chaînes d'architecture inconnues sont définies par défaut sur `1` (x64) en interne ; la CLI limite les valeurs à l'ensemble pris en charge.
---
### 3. **Format d'en-tête C**
En-têtes C prêts à compiler avec des structures à sécurité de type et des tableaux de données.
`pe-signgen` peut générer deux *types* d'en-têtes C :
* **En-têtes WSIG** – pour les données de signature et de masque (depuis `write_wsig_header`).
* **En-têtes WOFF** – pour les tables directes RVA/décalage de fichier (depuis `write_woff_header`).
L'option `--output-format cheader` sélectionne les en-têtes C ; en la combinant avec `--offsets`, on alterne entre les variantes WSIG et WOFF.
#### En-tête C WSIG```bash
pe-signgen --signature ntdll!RtlpInitializeThreadActivationContextStack \
-o rtlp_init_actx.h --output-format cheader
Structure d'en-tête générée (simplifiée, correspond à write_wsig_header) :```c
/* Auto-generated WSIG header for ntdll ! RtlpInitializeThreadActivationContextStack ! x64. */
#ifndef WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_H
#define WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_H
#include <stdint.h> #include <stddef.h>
#define WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_DLL_NAME "ntdll" #define WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_FUNCTION_NAME "RtlpInitializeThreadActivationContextStack" #define WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_ARCH "x64"
/* Per-version build identifier. */ typedef struct { uint32_t major; uint32_t minor; } WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_version_t;
/* Signature group entry. */ typedef struct { const uint8_t *pattern; const uint8_t *mask; uint32_t length; uint32_t build_count; const WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_version_t *versions; } WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group_t;
/* One pattern/mask/versions triple per group. / static const uint8_t WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_pattern[] = { / ... / }; static const uint8_t WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_mask[] = { / ... / }; static const WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_version_t WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_versions[] = { { 10240u, 16384u }, / 10240.16384 / / ... */ };
static const WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group_t WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_GROUPS[] = { { WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_pattern, WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_mask, (uint32_t)(sizeof(WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_pattern) / sizeof(WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_pattern[0])), (uint32_t)(sizeof(WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_versions) / sizeof(WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_versions[0])), WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group0_versions }, /* group 0 (RtlpInitializeThreadActivationContextStack) / / ... */ };
static const size_t WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_GROUP_COUNT = sizeof(WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_GROUPS) / sizeof(WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_GROUPS[0]);
#endif /* WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_H */
**Exemple d'intégration (corrigé pour correspondre aux types générés) :**```c
#include "rtlp_init_actx.h"
static inline int match_byte(uint8_t want, uint8_t got,
const uint8_t *mbits, uint32_t i) {
uint8_t bit = (mbits[i >> 3] >> (i & 7)) & 1u;
return bit ? (want == got) : 1;
}
static const uint8_t *
find_signature(const uint8_t *base, size_t size,
const uint8_t *pattern,
const uint8_t *mbits,
uint32_t sig_len) {
if (!base || !pattern || !mbits || sig_len == 0)
return NULL;
if (size < sig_len)
return NULL;
// Find first non-wildcard byte as anchor
uint32_t anchor = sig_len;
for (uint32_t i = 0; i < sig_len; ++i) {
if ((mbits[i >> 3] >> (i & 7)) & 1u) {
anchor = i;
break;
}
}
if (anchor == sig_len)
return base; // all wildcards
const uint8_t anchor_val = pattern[anchor];
const size_t last_pos = size - (size_t)sig_len;
for (size_t pos = 0; pos <= last_pos; ++pos) {
if (base[pos + anchor] != anchor_val)
continue;
uint32_t i = 0;
for (; i < sig_len; ++i) {
if (!match_byte(pattern[i], base[pos + i], mbits, i))
break;
}
if (i == sig_len)
return base + pos;
}
return NULL;
}
static void
fetch_signature(uint32_t major, uint32_t minor,
const WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group_t *groups,
size_t group_len,
const uint8_t **signature_dest,
const uint8_t **mask_dest,
uint32_t *signature_len_dest) {
*signature_dest = NULL;
*mask_dest = NULL;
*signature_len_dest = 0;
const WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group_t *closest = NULL;
uint32_t best_distance = 0xFFFFFFFFu;
for (size_t gi = 0; gi < group_len; ++gi) {
const WSIG_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_group_t *g = &groups[gi];
for (uint32_t vi = 0; vi < g->build_count; ++vi) {
uint32_t m = g->versions[vi].major;
uint32_t n = g->versions[vi].minor;
uint32_t distance = (m > major ? m - major : major - m) * 10000u +
(n > minor ? n - minor : minor - n);
if (distance < best_distance) {
best_distance = distance;
closest = g;
}
if (m == major && n == minor) {
*signature_dest = g->pattern;
*mask_dest = g->mask;
*signature_len_dest = g->length;
return;
}
}
}
if (closest) {
*signature_dest = closest->pattern;
*mask_dest = closest->mask;
*signature_len_dest = closest->length;
}
}
Vous pouvez ensuite brancher cela dans votre propre code spécifique au chargeur (par exemple en utilisant GetModuleHandleA, en parcourant les sections PE, etc.). L'en-tête ne fournit intentionnellement que des données ; les fonctions utilitaires restent à la charge du consommateur.
Pour les cas d'utilisation reposant uniquement sur des offsets, write_woff_header génère un petit en-tête décrivant une table triée d'entrées (major, minor, rva, file_offset).
Disposition (correspond à write_woff_header):```c
/* Auto-generated WOFF header for ntdll ! RtlpInitializeThreadActivationContextStack ! x64. */
#ifndef WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_H
#define WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_H
#include <stdint.h> #include <stddef.h>
#define WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_DLL_NAME "ntdll" #define WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_FUNCTION_NAME "RtlpInitializeThreadActivationContextStack" #define WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_ARCH "x64"
/* Per-build offset entry. */ typedef struct { uint32_t major; uint32_t minor; uint64_t rva; uint64_t file_offset; } WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_entry_t;
static const WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_entry_t WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_ENTRIES[] = { { 10240u, 16384u, 0x5B195ULL, 0x5A595ULL }, /* 10240.16384 (RtlpInitializeThreadActivationContextStack) / / ... (sorted by major, then minor) ... */ };
static const size_t WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_ENTRY_COUNT = sizeof(WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_ENTRIES) / sizeof(WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_ENTRIES[0]);
#endif /* WOFF_NTDLL_RTLPINITIALIZETHREADACTIVATIONCONTEXTSTACK_X64_H */
Cela est utile lorsque vous faites confiance aux offsets eux-mêmes et n'avez pas besoin de correspondance de motifs.
---
## Installation
### Prérequis
* Python 3.8+
* Git
* Connexion Internet (première exécution)
* ~10 Go d'espace disque pour le cache complet
### Installer via Pip```bash
pip install pe-signgen
git clone https://github.com/forentfraps/pe-signgen.git cd pe-signgen pip install -r requirements.txt pip install -e .
---
## Démarrage rapide
### Générer une signature```bash
pe-signgen --signature ntdll!LdrLoadDll
pe-signgen --signature kernel32!CreateFileW --offsets
### Enregistrer en JSON```bash
pe-signgen --signature ntdll!NtCreateFile -o out.json --output-format json
pe-signgen --signature DLL!FUNCTION [OPTIONS]
### Architecture```bash
--arch x64 # default
--arch arm64
--arch wow64
--os-version win10 # Only Windows 10 --os-version win11 # Only Windows 11 --min-version 10.0 # Minimum version --max-version 11.0 # Maximum version
### Contrôle de la longueur de signature```bash
--min-length 32 # Minimum signature length
--max-length 64 # Maximum signature length
-o, --output PATH # Output file path --output-format FORMAT # json | binary | cheader --offsets # Generate offsets instead of signatures
### Performances```bash
--workers 16 # Parallel workers (default: CPU count)
--no-cache # Disable caching
--no-git-update # Skip Winbindex updates
--verbose # Detailed output --quiet # Minimal output --no-progress # Disable progress bars
---
## Mise en cache
### Disposition du cache```text
~/.cache/pe-signgen/
│
├── dlls/ # Downloaded DLLs
├── pdbs/ # Downloaded PDBs
├── signatures/ # Generated signatures
└── winbindex_data/ # Winbindex metadata
pe-signgen --signature ntdll!NtCreateFile --no-cache
rm -rf ~/.cache/pe-signgen
export PE_SIGNGEN_CACHE=/custom/path pe-signgen --signature ntdll!NtCreateFile
---
## Performances
**Exemple de performances (CPU 12 cœurs, 100 Mbps) :**
| Opération | Temps |
| --------------------------------- | ----------- |
| Première exécution (sans cache) | 5–10 min |
| Exécution en cache | < 1 sec |
| Analyse par build | 0,1–0,5 sec |
| Exécution complète (1000 builds, 8 workers) | 2–4 min |
**Ressources requises :**
* **Disque :** ~10 Go pour le cache complet DLL/PDB
* **Mémoire :** ~500 Mo d'utilisation maximale
* **Réseau :** plusieurs Go lors de la première exécution
---
## Limitations connues
* **Couverture des versions Windows :** uniquement Windows **10 et 11** (limitation de Winbindex)
* **Disponibilité des builds :** toutes les builds Win10/11 ne sont pas présentes dans Winbindex
---
## Développement```bash
git clone https://github.com/forentfraps/pe-signgen.git
cd pe-signgen
pip install -e ".[dev]"
# Code formatting
black pe_signgen/
# Type checking
mypy pe_signgen/
Licence MIT – voir LICENSE.
Inspiré par le besoin de génération de signatures robuste et automatisée pour les API Windows internes.
Les contributions sont les bienvenues ! Merci de :