Эта запись является продолжением которая расширяет понятия и смысл
внутренних структур NE программы. Эта часть содержит много информации о
таблице сегментов и релокациях для каждого сегмента.

Перед тем, как начать

Sunflower использует немного другие аббревиатуры для типов данных
Заголовки столбцов в таблицах имеют формат Название:тип

Тип

Расшифровка

s

строка

1

BYTE

2

WORD

4

DWORD

8

QWORD

h

дескриптор

f

bool флаг

В названиях столбцов есть сокращения.
- # - номер;
- * - указатель.
Например, поле #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

FIARQQ_FJARQQ

Итого

В этой части я разобрал и попытался описать таблицу сегментов
и по-сегментных релокаций в NE-исполняемых файлов.
К сожалению эта часть очень важна, потому что таблица сегментов и релокации
очень сильно влияет на обработку образа программы, заодно активно используется в
реверс-инженеринге или просто при дизассемблировании.

В следующих частях это будет очень нужно.

Комментарии (0)