
Универсальная генерация сигнатур для любых системных функций из всех сборок Windows с использованием Winbindex
Кросс-версионные бинарные сигнатуры и RVA-смещения для функций Windows PE
pe-signgen — это инструмент для специалистов по реверс-инжинирингу и исследователей безопасности, который автоматически генерирует:
Основная идея — предоставить систематический и надёжный способ доступа к неэкспортируемым функциям в сборках Windows 10/11. Инструмент использует:
⚠️ Поддержка версий Windows
pe-signgenподдерживает только Windows 10 и Windows 11. Это осознанное проектное решение: Winbindex не предоставляет полных данных для более старых версий.
Неэкспортируемые внутренности Windows
Генерация сигнатур для таких функций, как LdrpInitializeTls, RtlpInsertInvertedFunctionTableEntry и т. д.
Исследование игрового хакинга / анти-чита Генерация стабильных сигнатур, переживающих обновления игр
Исследования в области безопасности Поиск критически важных для безопасности процедур во всех сборках Windows
Автоматизация Скриптуемая генерация сигнатур и смещений для целых наборов внутренних API
pe-signgen предоставляет три различных формата вывода для разных сценариев использования:
Структурированные данные для автоматизации, написания скриптов и интеграции с другими инструментами.```bash pe-signgen --signature ntdll!NtCreateFile -o ntcreatefile.json --output-format json
**Структура вывода:**```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 и minor выводятся из строки сборки разделением по первой точке ..
Пример: "10240.16384" → major = 10240, minor = 16384.build — это исходная строка ключа сборки, используемая внутри.Компактные, готовые к исполнению двоичные форматы, оптимизированные для встраиваемых систем и сканирования с низкими накладными расходами.
Они соответствуют раскладке на диске, реализованной в write_wsig() и write_woff().
Магическое число: WSO\0 (0x57 0x53 0x4F 0x00)
Текущая версия: 1
Назначение: Хранение двоичных сигнатур с масками подстановки и связанными версиями сборок Windows
┌─────────────────────────────────────┐ │ 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) │ └─────────────────────────────────────┘
**Важные замечания по компоновке (соответствует `write_wsig`)**
* После заголовка имена DLL и функций записываются как байты UTF‑8.
* Для каждой группы сигнатур записываются байты паттерна и байты маски, после чего следует массив сборки для этой группы.
* Эти области для каждой группы **не** сгруппированы глобально по типу: паттерны, маски и массивы сборки могут чередоваться.
* Построитель выравнивается по **4 байтам** перед каждым массивом сборки и перед таблицей групп. Это может приводить к появлению отступов (padding).
* Потребители должны **всегда** следовать смещениям в заголовке и записях групп; **не** полагайтесь на концептуальную диаграмму для физической непрерывности.
##### Компоновка заголовка (36 байт)```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
Каждая группа сигнатур представляет собой уникальный шаблон, который применяется к одной или нескольким сборкам 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
##### Запись версии сборки (8 байт)
Каждая запись сборки идентифицирует конкретную версию Windows, которая использует эту подпись.```c
typedef struct {
uint32_t major; // e.g. 19041
uint32_t minor; // e.g. 1234
} wsig_build_t; // 8 bytes
major и minor получаются при разбиении строки сборки ("A.B" → A, B). Исходная строка сборки не хранится в бинарном формате; если она нужна, храните её отдельно (она присутствует в JSON-выводе).
Маска — это битовая маска, где каждый бит соответствует одному байту в сигнатурном шаблоне:
Пример:``` 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)
Маскирующие байты хранятся и интерпретируются в **порядке бит от младшего к старшему (little-endian)** внутри каждого байта (точно так же, как в C-хелперах и `parse_signature`):```c
uint8_t bit = (mask_bytes[byte_index >> 3] >> (byte_index & 7)) & 1u;
*_len для определения длины; не читайте дальше этого.Магическое число: WOF\0 (0x57 0x4F 0x46 0x00)
Текущая версия: 1
Назначение: хранение прямых RVA и файловых смещений для функций в сборках 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) │ └─────────────────────────────────────┘
Layout details (matches `write_woff`):
* After the header placeholder, the DLL and function names are written as UTF‑8 bytes.
* For each build, the matched symbol name is written as a UTF‑8 string (no terminator). These form a simple string pool.
* The writer then aligns to 4 bytes and writes the fixed-size entries table.
* Each entry contains offsets (`matched_off`, `matched_len`) pointing into this string pool.
##### Расположение заголовка (36 байт)```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
Каждая запись сопоставляет сборку Windows с расположением функции в этой сборке.```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
##### Примечания по использованию
* **RVA** — это смещение в памяти, когда DLL загружается по своему предпочтительному базовому адресу.
* **File offset** — это исходная позиция в PE-файле на диске.
* **Совпавший символ** может отличаться от запрошенной функции (например, перенаправленные экспорты).
Строка сохраняется один раз в пуле строк; `matched_off`/`matched_len` ссылаются на неё.
* Записи **отсортированы по версии сборки** (сначала `major`, затем `minor`) для эффективного поиска.
---
#### Коды архитектур
Оба двоичных формата используют одну и ту же кодировку архитектуры (через `ARCH_CODE_MAP`):
| Code | Architecture | Description |
| ---- | ------------ | ---------------------------- |
| 1 | x64 | 64-разрядная AMD64/Intel64 |
| 2 | ARM64 | 64-разрядная ARM (AArch64) |
| 3 | WoW64 | 32-разрядная x86 в 64-разрядной Windows |
Неизвестные строки архитектуры по умолчанию считаются `1` (x64) внутренне; CLI ограничивает значения поддерживаемым набором.
---
### 3. **Формат C-заголовков**
Готовые к компиляции C-заголовки с типобезопасными структурами и массивами данных.
`pe-signgen` может создавать два *типа* C-заголовков:
* **WSIG-заголовки** — для данных сигнатур и масок (из `write_wsig_header`).
* **WOFF-заголовки** — для таблиц прямых смещений RVA/файловых смещений (из `write_woff_header`).
Опция `--output-format cheader` выбирает C-заголовки; её сочетание с `--offsets` переключает между вариантами WSIG и WOFF.
#### WSIG C-заголовок```bash
pe-signgen --signature ntdll!RtlpInitializeThreadActivationContextStack \
-o rtlp_init_actx.h --output-format cheader
Сгенерированная структура заголовка (упрощённая, соответствует 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 */
**Пример интеграции (исправлено в соответствии со сгенерированными типами):**```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;
}
}
Затем вы можете интегрировать это в свой код, специфичный для загрузчика (например, используя GetModuleHandleA, обходя PE-секции и т.д.). Заголовок намеренно предоставляет только данные; вспомогательные функции остаются на усмотрение потребителя.
Для случаев использования, где требуются только смещения, write_woff_header создаёт небольшой заголовок, описывающий отсортированную таблицу записей (major, minor, rva, file_offset).
Структура (соответствует 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 */
Это полезно, когда вы доверяете самим смещениям и не нуждаетесь в сопоставлении с шаблоном.
---
## Установка
### Предварительные требования
* Python 3.8+
* Git
* Подключение к интернету (при первом запуске)
* ~10 ГБ дискового пространства для полного кэша
### Установка из 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 .
---
## Быстрый старт
### Создание сигнатуры```bash
pe-signgen --signature ntdll!LdrLoadDll
pe-signgen --signature kernel32!CreateFileW --offsets
### Сохранить как JSON```bash
pe-signgen --signature ntdll!NtCreateFile -o out.json --output-format json
pe-signgen --signature DLL!FUNCTION [OPTIONS]
### Архитектура```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
### Контроль длины подписи```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
### Производительность```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
---
## Кэширование
### Структура кэша```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
---
## Производительность
**Пример производительности (12-ядерный CPU, 100 Мбит/с):**
| Операция | Время |
| --------------------------------- | ----------- |
| Первый запуск (без кэша) | 5–10 мин |
| Запуск с кэшем | < 1 сек |
| Анализ каждой сборки | 0.1–0.5 сек |
| Полный прогон (1000 сборок, 8 воркеров) | 2–4 мин |
**Требования к ресурсам:**
* **Диск:** ~10 ГБ для полного кэша DLL/PDB
* **Память:** ~500 МБ пиковое использование
* **Сеть:** Несколько ГБ при первом запуске
---
## Известные ограничения
* **Охват версий Windows:** Только Windows **10 и 11** (ограничение Winbindex)
* **Доступность сборок:** Не каждая сборка Win10/11 существует в Winbindex
---
## Разработка```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/
Лицензия MIT – см. LICENSE.
Вдохновлено потребностью в надёжной автоматизированной генерации сигнатур для внутренних Windows API.
Вклад приветствуется! Пожалуйста: