Эта запись является продолжением которая расширяет понятия и смысл
внутренних структур NE программы. Эта часть содержит много информации о
таблице сегментов и релокациях для каждого сегмента.
Перед тем, как начать
Sunflower использует немного другие аббревиатуры для типов данных
Заголовки столбцов в таблицах имеют формат Название:тип
Тип |
Расшифровка |
---|---|
|
строка |
|
|
|
|
|
|
|
|
|
дескриптор |
|
|
В названиях столбцов есть сокращения.
- #
- номер;
- *
- указатель.
Например, поле #Segment:4
это "Номер сегмента" размерностью в DWORD
, а
*Relocations:8
это "Указатель на релокации", размерностью в QWORD
.
Надеюсь, примеры приведенные далее не собьют с толку.
Обзор
Таблица сегментов - это таблица которая предоставляет информацию о следующих сегментах кода или данных.
Как и в PE
исполняемых файлах - это отдельная структура, лежащая перед всеми областями, для которых
она создана и она не крепится за каждой сущностью.
+ +
| e_segtab -----+
| | |
| ... | | e_lfanew + e_segtab
+-------------------+ |
| .CODE <---+
| .CODE WITHIN_RELOCS
| .DATA DISCARD |
| .DATA PRELOAD, HASMASK
+-------------------+
| ... |
| ... |
| ... |
+-------------------+
| .CODE |
+-------------------+
| .CODE |
+-------------------+
| .DATA |
+-------------------+
| .DATA |
+-------------------+
| ... |
| |
+-------------------+ EOF
NE-Заголовок держит специально два поля для этой структуры данных. Это e_segtab
,
или относительное смещение структуры относительно начала заголовка, и e_cseg
,
которое хранит количество записей в этой таблице.
let real_segtab: u16 = e_lfanew + e_segtab;
// first value holds in MZ header.
// second value holds in NE header.
Если вы ожидаете определение Microsoft для этой структуры,
я обязательно его выделю:
The segment table contains an entry for each segment in the executable file.
The number of segment table entries are defined in the segmented EXE header.
The first entry in the segment table is segment number 1.
Что в переводе означает следующее: "Таблица сегментов содержит сущности
для каждого сегмента в исполняемом файле. Количество сущностей в таблице сегментов
определено в сегментном заголовке исполняемого файла. Первая сущность в талблице это
сегмент №1".
Сами сегменты безымянны. Они лишь имеют специальный флаг, который интуитивно
определяет их права.
0x0000
-.CODE
;0x0001
-.DATA
.
Это всё. Никаких .BSS
или данных только для чтения, (например .rdata
), нет.
Формат записи информации о сегменте
Сначала, лучше представить то, что пишет Microsoft, затем
уже подробнее расписать эти понятия.
TYPE Microsoft DESCRIPTION
WORD Logical-sector offset (n byte) to the contents of the segment
data, relative to the beginning of the file. Zero means no
file data.
WORD Length of the segment in the file, in bytes. Zero means 64K.
WORD Flag word
0x0000 = .CODE-segment type.
0x0001 = .DATA-segment type.
0x0010 = MOVEABLE Segment is not fixed.
0x0040 = PRELOAD Segment will be preloaded; read-only if
this is a data segment.
0x0100 = RELOC_INFO Set if segment has relocation records.
0xF000 = DISCARD Discard priority.
WORD Minimum allocation size of the segment, in bytes. Total size
of the segment. Zero means 64K.
Первое слово в записи означает смещение сектора к содержанию сегмента
данных. Если это слово равно нулю - В этом сегменте не будет данных.
Второе слово говорит количество байт в сегменте.
Если это поле будет равно нулю - его при обработке надо заменять на
DWORD
значение - 0x10000
.
Флаги сегмента, кроме .CODE
и .DATA
представленные выше
очень нужны загрузчику для подготовки образа в памяти.
- MOVEABLE
- содержимое сегмента можно перемещать после загрузки;
- PRELOAD
- сегмент будет загружаться значительно раньше. Если это .DATA
- то это данные только для чтения;
- RELOC_INFO
- после записи сегмента следует Таблица релокаций для этого сегмента.
Самое важное здесь для анализа данных это наличие таблицы релокаций.
Формат записи информации о сегменте как небезопасная структура
Все поля записи о сегменте были описаны выше.
Таблица сегментов (в перемешку с по-сегментными релокациями) это
не выровненная, не заполняемая небезопасная структура данных.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
#[repr(C)]
struct Segment {
pub e_seg_offset: Lu16, // Logical Sector Offset
pub e_seg_length: Lu16, // Length
pub e_flags: Lu16,
pub e_min_alloc: Lu16,
}
Попытайтесь запомнить, что нули для e_seg_length
и e_min_alloc
нельзя воспринимать как нули. Это значения выше границы 16-разрядного слова.
То есть 0x10000
, (в то время как большея граница слова 0xFFFF
).
Чтобы продемонстрировать то, как интерпретируется эта таблица
сегментов, я вызову SunFlower для файла из пакета OS/2 1.1 - CMD.EXE
.
Type:s |
#Segment:4 |
Offset:2 |
Length:2 |
Flags:2 |
Minimum Allocation:2 |
Characteristics:s |
---|---|---|---|---|---|---|
.CODE |
0x1 |
0x1 |
0x5BCA |
0xD00 |
0x5BCA |
|
.CODE |
0x2 |
0x30 |
0x6388 |
0xD00 |
0x6388 |
|
.CODE |
0x3 |
0x63 |
0x41A4 |
0xD00 |
0x41A4 |
|
.CODE |
0x4 |
0x85 |
0x1FB9 |
0xD00 |
0x1FB9 |
|
.CODE |
0x5 |
0x96 |
0x1CBF |
0xD00 |
0x1CBF |
|
.DATA |
0x6 |
0xA5 |
0x1191 |
0xD41 |
0x3430 |
HAS_MASK PRELOAD |
Заметьте, что последний сегмент отмеченный как .DATA
имеет флаг PRELOAD
.
Это обозначает для загрузчика и для ОС, что этот сегмент данных только для чтения. Впринципе, так это и работает
По-сегментные релокации
"Per-segment relocations" или "Segment relocations" или "Fixup records" или "Per segment data" имеют один и тот же смысл для NE слинкованных программ.
А наличие по-сегментных релокаций критически важно для анализа данных.
Это скорее всего даже активно используется при реверс-инжинеринге. По крайней мере встроенная в Ghidra поддержка
для NE сегментных файлов очень часто пользуется таблицей релокаций.
The location and size of the per-segment data is defined in the segment table entry for the segment.
If the segment has relocation fixups, as defined in the segment table entry flags, they directly follow the segment data in the file.
Позиция и размер по-сегментных релокаций определяется в таблице сегментов, точнее в записи о сегменте.
Вот часть документа Microsoft по этому вопросу.
Единственное, что я тут переписал - это типы данных, потому что определенно
найдутся такие как я, кто подумает о dw
как о DWORD
а не define WORD
.
WORD Number of relocation records that follow.
A table of relocation records follows. The following is the format
of each relocation record.
BYTE Source type.
0Fh = SOURCE_MASK
00h = LOBYTE
02h = SEGMENT
03h = FAR_ADDR (32-bit pointer)
05h = oFFSET (16-bit offset)
BYTE Flags byte.
03h = TARGET_MASK
00h = INTERNALREF
01h = IMPORTORDINAL
02h = IMPORTNAME
03h = OSFIXUP
04h = _ADDITIVE_
WORD Offset within this segment of the source chain.
If the _ADDITIVE_ flag is set, then target value is added to
the source contents, instead of replacing the source and
following the chain.
The source chain is an 0xFFFF
terminated linked list within this segment of all
references to the target.
The target value has four types that are defined in the flag
byte field.
Первое 16-разрядное поле это количество релокаций, которые
раз за разом имеют один и тот же "заголовок".
Часть заголовка каждой релокации это следующие три поля.
Source Type
описывает тип источника, на что ссылается запись;Flags Byte
определяет интерпретацию байт после "заголовка";
По-Сегментные релокации | Internal Reference (внутренняя ссылка)
Тип внутренней ссылки в таблице релокаций обозначает,
что следующая информация говорит на что внутри загруженной программы
надо делать далёкий вызов (сокр. callf
).
INTERNALREF
BYTE Segment number for a fixed segment, or 0FFh for a
movable segment.
BYTE 0
WORD Offset into segment if fixed segment, or ordinal
number index into Entry Table if movable segment.
Держите в голове, что поле сегмента можно объединить с пустым (u8 + u8 = u16
) и объединить это со следущем полем смещения.
Вот как здесь фигурируют далёкие указатели.
// pub r_count: u16; // Количество релокаций читается отдельно
// Поэтому в заголовок релокаций это входит
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
#[repr(C)]
struct InternalReferenceReloc {
// Заголовок каждой записи
pub r_srcs: Lu8,
pub r_flag: Lu8,
pub r_offset: Lu16,
// internal reference
// сегмент (u8 + u8) и смещение (u16) можно объединить в FAR указатель.
pub r_segment: Lu8,
pub r_always_zero: Lu8,
pub r_offset_index: Lu16,
}
В итоге байты пренадлежащие только для внутренней ссылки
суммарно составляют 4 байта. (байты внутренней ссылки это FAR
указатель).
Предлагаю вызвать Sunflower для подопытного CMD.EXE
, чтобы найти запись о внутренней ссылке.
ATP:1 |
RTP:1 |
RTP:s |
IsAdditive:f |
OffsetInSeg:2 |
SegType:2 |
Target:2 |
TargetType:s |
Mod#:2 |
Name:2 |
Ordinal:2 |
Fixup:s |
---|---|---|---|---|---|---|---|---|---|---|---|
0x2 |
0x0 |
[Internal] |
[False] |
0xE2 |
2 |
0x0 |
[MOVABLE] |
0x0 |
@0 |
0x0 |
По-Сегментные релокации | Ordinal/Name Import (импорт)
Импорты по ординалу и импорты по имени отличаются только на одно поле.
Только для импортируемой по имени сущности интерпретация байт будет такова:
WORD Index into module reference table for the imported
module.
WORD Offset within Imported Names Table to procedure name
string.
А для импорта по ординалу интерпретация байт выглядит очень похоже,
просто последнее поле будет не смещением до имени процедуры, а уже установленным ординалом функции.
WORD Index into module reference table for the imported
module.
WORD Procedure ordinal.
И у меня отличные новости! Суммарно это тоже 4 байта. Или 32 бита.
Это значит, что не смотря на тип релокации, структура и выравнивание таблицы сохраняется.
То есть запись о релокации любого типа имеет постоянный один размер в 4 байта. Это серьезно облегчает работу.
Это поможет вам отнестись к этому проще и не прибегать к странным или запутанным решениям.
Теперь прочтем количество записей о импортах, и составим структуру этого типа релокации.
// пропускаю r_count
struct ImportingNameReloc {
pub r_srcs: Lu8,
pub r_flag: Lu8,
pub r_offset: Lu16,
// Импорт по имени. (Нужно знать смещение названия процедуры)
pub r_modtab_offset: Lu16, // <-- offset for DLL name
pub r_imptab_offset: Lu16, // <-- offset for procedure ASCII name
}
И так же соберем анонимный импорт (по ординалу).
struct ImportingOrdinalReloc {
pub r_srcs: Lu8,
pub r_flag: Lu8,
pub r_offset: Lu16,
// Импорт по ординалу
pub r_modtab_offset: Lu16, // <-- index in modtab of DLL name
pub r_procedure_ord: Lu16, // <-- value of ordinal
}
Я хочу продемонстрировать это на практике. Опять вызову отчет для CMD.EXE
из OS/2 1.1
ATP:1 |
RTP:1 |
RTP:s |
IsAdditive:f |
OffsetInSeg:2 |
SegType:2 |
Target:2 |
TargetType:s |
Mod#:2 |
Name:2 |
Ordinal:2 |
Fixup:s |
---|---|---|---|---|---|---|---|---|---|---|---|
0x3 |
0x1 |
[Import] |
[False] |
0x25 |
0 |
0x0 |
[] |
0x4 |
@2 |
0x0 |
Теперь внимательно посмотрите в таблицу и найдите импорт по ординалу.
Это импорт из библиотеки, название которой надо искать зная, что это четвертый
модуль в таблице ссылок, а процедура @2
. Больше здесь ничего для нас нет.
Немного больше о импортах | (можно пропустить)
Если вы знаете про анонимные импорты - можете спокойно пропускать
это.
Так, если вы читали что-то о PE
формате (полн. "Portable Executable"),
вы может быть помните, что внутри таких файлах
содержится секция импорта или импорты раскиданы совершенно по разным местам,
но информация о них записана в двух таблицах - это IAT
(полн. Import Addresses Table)
и ILT
(полн. "Import Lookup Table").
Записи в таблице ILT
разделяются применяя 0x80000000
-маску (для 32-разрядных программ).
Все, кто побитово возвращают ложь - имеют название функции, а вот те, кто не возвращают -
имеют это загадочное число.
На самом деле, все куда проще, и я полагаю пришло именно из NE-формата.
В понимании NE-сегментного файла ординал это порядковый номер процедуры/функции
в специальной таблице адресов. По этому номеру, зная модуль "откуда этот номер",
загрузчик во время выполнения может обработать адрес процедуры и её вызвать.
Разделим YOUR_MOD.DLL на две части
Дерево проекта Псевдо-DEF файл.
+------------------------------+ +---------------------------+
| YOUR_MOD.def |------>| module: EXE |
+------------------------------+ | mem_model: compact |
| header_1.h -> header_1.c | | your_func_1 @1 |
| header_2.h -> header_2.c | | your_func_100 @100 |
| header_3.h -> header_3.c | | your_secret_func @14 |
| ... | | ... |
+------------------------------+
Файлы-определители *.DEF
, (если их так можно называть),
ранее подсказывали компилятору и линкеру, как именно и во что именно
надо собрать проект в итоге.
По-Сегментные релокации | Operating System Fixup
И самый последний регион в этом документе это OSFixup
релокации.
OSFixup это a инструкция для чисел с плавающей запятой, которую Windows или OS/2 будет "чинить" когда эмулируется FPU со-процессор
WORD Operating System fixup type.
Floating-point fixups.
0x0001 = FIARQQ-FJARQQ
0x0002 = FISRQQ-FJSRQQ
0x0003 = FICRQQ-FJCRQQ
0x0004 = FIERQQ
0x0005 = FIDRQQ
0x0006 = FIWRQQ
WORD 0x0000
Тип, который содержит в себе J
предположительно будет вторым в последовательности команд.
(включая релокации и прерывания, поддерживаемые различными платформами)
И этот тип релокации тоже размером в 4 байта.
Я не смог найти файл, в котором был бы пример такого рода релокации. Поэтому я продемонстрирую
как предположительно это может выглядеть. (Я заполнил строку случайными значениями, на основе того, что знаю)
ATP:1 |
RTP:1 |
RTP:s |
IsAdditive:f |
OffsetInSeg:2 |
SegType:2 |
Target:2 |
TargetType:s |
Mod#:2 |
Name:2 |
Ordinal:2 |
Fixup:s |
---|---|---|---|---|---|---|---|---|---|---|---|
0x0 |
0x3 |
[OSFixup] |
[False] |
0xE2 |
2 |
0x0 |
[ ] |
0x0 |
@0 |
0x0 |
|
Итого
В этой части я разобрал и попытался описать таблицу сегментов
и по-сегментных релокаций в NE-исполняемых файлов.
К сожалению эта часть очень важна, потому что таблица сегментов и релокации
очень сильно влияет на обработку образа программы, заодно активно используется в
реверс-инженеринге или просто при дизассемблировании.
В следующих частях это будет очень нужно.