Статья ASM. РЕ файл – ломаем стереотипы. Часть-2 Секции.

Marylin

Mod.Assembler
Red Team
05.06.2019
326
1 451
BIT
696
Logo.png

Первая часть данного цикла о РЕ-файлах была посвящена различным заголовкам, в которых описывается общий скелет исполняемых файлов Win-NT. Теперь заглянем внутрь бинарника и рассмотрим отдельные составляющие, а это секции и оверлей. В силу того, что РЕ-файлы могут содержать в себе аж 17 типов (в корень отличающихся) объектов, охватить весь материал в рамках одной статьи никак не получится. Поэтому здесь мы остановимся лишь на секциях кода, данных и импорта, которые встречаются буквально во-всех файлах EXE/DLL. И даже в этом случае придётся максимально ограничить речитатив, ведь тут есть о чём поговорить.


1. Таблица секций «Image Section Table»

Чтобы далее не было лишний вопросов, определимся сразу с трактовкой. Так, «секцией» будем называть физическую единицу файла на диске, а «страницей» – образ этой секции в виртуальной памяти ОС. В дефолте, все компиляторы определяют размер секции равным 512-байт, а размер страницы для проецирования её в память 4 КБ. Такая двойственность одной сущности создаёт проблемы системному загрузчику образов, которому приходится применять поправки. В идеале он мечтает о клиенте, чей размер секции совпадал-бы с размером страницы (указываются в РЕ-заголовке). В этом случае лоадер просто взял-бы файл с диска, и без лишних телодвижений в таком-же виде тупо положил его в память. Но компиляторы нашего времени не практикуют подобный формат, хотя и имеют в запазухе опциональный ключ /ALIGN: для этих нужд.

Секции, страницы и их выравнивание это хорошо. Но имеется ли ограничение на общее кол-во этих секций в файле? В спецификации PECOFF об этом нет ни слова, хотя один из ведущих продуманов Microsoft «Мэтт Питрек» утверждает, что на NT4 лимит всё-же был, с потолком в 0x60=96 секций. Однако с тех пор утекло много воды, и начиная с Висты мы имеем уже два поколения плюс NT6.

Чем гадать на кофейной гуще, лучше просто проверим этот фактоид на практике, создав в подопытном приложении столько секций, сколько сможет унести РЕ-файл. Все примеры буду приводить на ассемблере FASM, макросы которого позволяют творить чудеса. Кстати загрузчик полностью игнорит имя секции (принимая во-внимание лишь её атрибуты), а потому секцию можно называть хоть именем своего любимого кота, ..или вообще оставить безымянной.

Fasm имеет директиву препроцессора rept N{..}, которая вставит в исходник N помещённых в фигурные скобки блоков. Это как-раз то, что нам доктор прописал. Возьмём исходник из предыдущей части статьи, в самом его хвосте создадим ещё одну секцию, и просто завернём её в макрос, указав вместо N число повторов. Вот конструкция для создания 1000 ксерокопий одной секции с именем «.hack». Достаточно определить внутри секции хотя-бы одну переменную, чтобы компилятор выделит под неё минимальных 512-байт:

C-подобный:
rept 1000 {
section '.hack' data writeable
          dd  0x12345678  }

Как результат имеем следущее..
Слева дефолтный файл где видно, что образ имеет всего 4 секции и занимает в памяти 20 КБ. Но добавление в хвост ещё 1000 секций тут-же отразилось на размерах в памяти как образа, так и NT-хидера, который разбух аж в 40 раз. Поскольку мод влияет только на расположенную после заголовка «таблицу-секций», то размер заголовка на диске остался прежним. Более того, добавление 28h-байтных дескрипторов на каждую секцию привело к тому, что первая «секция-кода» съехала ниже и соответственно изменился адрес точки-входа ОЕР. А в остальном файл нормально воспринимается загрузчиком, и никаких предупреждений о лимитах нет.

pe1000.png

Однако поле «NumberOfSection» в РЕ-заголовке размером в слово, а значит способно вместить макс 65536 секций. При попытке создать такой оверхед мой fasm уже загнулся с мессагой «Out-Of-Memory», но 55000 всё-же скрипя зубами из автомата настрелял. Правда печатающую дамп функцию printf() пришлось закомментировать, иначе зарываясь в подвал всё ниже и ниже консоль не могла уже вернуться к сведениям о заголовке. Из выхлопной трубы компилятора вышел экзешник размером в 30 МБ (хотя оригинал был 5К), и вот-что получилось в итоге:

pe55000.png

А как на счёт отладчика с дизассемблером? ..смогут-ли они разобрать такой исполняемый файл?
К удивлению х64dbg даже не поперхнулся и мгновенно остановился на ОЕР, хотя на вкладке «Карта памяти» оказалось зеро, без единого намёка на прилинкованные DLL. Зато IDA минуты три делала приседания перед заходом, но так и не осилив более 63 секций решила не мучить себя, и покорно сдалась.

IdaDbg.png


2. Проецирование одной секций на две страницы

Отсутствие физических связей между секцией на диске и выделенной для неё страницей в памяти приводит к тому, что мы можем отобразить одну секцию сразу на несколько вирт.страниц. Раньше этот довольно хитрый приём сажал на пятую точку почти все дизассемблеры, но со временем трюк раскусили, и сейчас практическое его применение под вопросом. Главное возможность такая есть, и думаю не будет лишним её рассмотреть.

Фактически, так мы создаём нуль-пространственный туннель между двумя пэйджами, когда изменения в странице(А) будут мгновенно отображаться на странице(В). Красок добавляет то, что данное утверждение является истиной только для образа секции в памяти, в то время как файла на диске это вообще не касается. Общую картину подобного жульничества можно представить так:

Layering.png

Ключевую роль играет здесь секция «.free» файла (можете обозвать её как угодно).
Перед созданием образа программы в памяти, загрузчик в цикле читает все дескрипторы из таблицы-секций, и обнаружив описатель тушки «.free», выделяет для неё страницу (см.первую схему). Далее в таблице следует дескриптор/описатель секции-кода, и соответственно её пэйдж загрузчик положит следующим в памяти. Чтобы достичь своей цели, мы должны заранее расположить секции в файле именно в таком порядке, иначе финт не сработает.

Важно заметить, что отображать на своего соседа можно как всю оригин.секцию, так и произвольную её часть. Если в планах захватить всё содержимое «.code», то размер секции «.free» на диске обязательно должен совпадать с размером «.code», в противном случае мы тупо оттяпаем хвост, и он не попадёт в зону проекции. Чтобы не привлекать к себе внимания, обычно малвать отображала только критически-важные блоки, в которых лежит что-то существенное (например процедуры проверки контрольных сумм или паролей), а остальной шлак в оригин.секции не трогала.

Средний уровень IQ у загрузчика низкий – не вникая в подробности он тупо делает то, что прописано в дескрипторах таблицы-секций, лишь изредка чекая в них лимиты. Именно это и даёт нам возможность издеваться над секциями в меру своей испорченности. Манипулируя всего 4-мя полями в дескрипторах, можно не только отображать одну секцию на пару вирт.страниц, но и помещать одну страницу в другую по типу матрёшки. Главное требование в таких играх – это строго придерживаться размеров в полях заголовка «File/SectionAlignment».

Для тех, кто читает эти строки через триплексы танка напомним, что RVA является вирт.адресом страницы относительно базы образа в памяти, а RAW представляет собой физ.адрес в скомпилированном бинарнике относительно его начала, т.е. нуля.

C-подобный:
struct SECTION_DESCRIPTOR    ;//<---- Размер 0x28 (40 байт)
  ObjectName        rb  8    ;// 8-байт под строку с именем секции
  VirtualSize       dd  0    ;// размер страницы в памяти (произвольный до 4ГБ)
  VirtualOffset     dd  0    ;//   ... rva адрес в памяти (всегда кратен 4096-байт)
  RawSize           dd  0    ;// размер секции   на диске (всегда кратен  512-байт)
  RawOffset         dd  0    ;//   ... raw адрес на диске (всегда кратен  512-байт)
  Reserved          rb  12   ;// (используется только в *.coff файлах)
  Characteristics   dd  0    ;// флаги секции -RWE-
ends
struct SECTION_TABLE
  Object1           SECTION_DESCRIPTOR
  Object2           SECTION_DESCRIPTOR
  ObjectN           SECTION_DESCRIPTOR  ;// N лежит в поле РЕ.NumberOfSection
ends

SectDesc.png

Как видно из рис.выше, первую секцию с адресом Raw=0x400, загрузчик отображает в вирт.страницу по адресу RVA=0x1000, а вторую кладёт рядом 0x2000. Теперь, чтобы отобразить один код на эти две самостоятельные страницы, нам нужно всего-то спроецировать первую секцию «.free» поверх второй «.code», для чего достаточно изменить выделенный синим офсет 0x400 на 0x600. Это заставит системный лоадер загружать содержимое секции кода сразу в две страницы 0x1000 и 0x2000, прибавив к этим значениям базу 0x00400000. В качестве демо напишем ламерский код проверки пароля и посмотрим, что из этого получится:

C-подобный:
format   pe64 console
entry    start
include 'win64ax.inc'
;//----------
;// Создаём фиктивную секцию, куда загрузчик скопирует код.
;// Обратите внимание, что изначально секция пуста и в ней нет ничего,
;// а только резерв, чтобы компилятор не вырезал её посчитав не нужной.
;// Если открыть EXE в hex-редакторе, увидим только 16 символов «А».

section '.free' code readable executable
        db      16 dup('A')

;//----------
;// Следом сразу идёт вполне легальная секция-кода

section '.code' code readable executable
start: sub     rsp,28h
frame
       cinvoke  printf,szPass
       cinvoke  gets,buff,12       ;// принимаем у юзера пасс

       cinvoke  strcmp,origin,buff ;// сравнить пароли!
        or      rax,rax
        jnz     @err
       cinvoke  printf,passOk

@exit: cinvoke  _getch
       cinvoke  exit,0
endf
@err:  cinvoke  printf, wrong
        jmp     @exit

;//----------
.data
szPass  db  10,' Type pass: ',0
wrong   db  10,' Wrong pass! :((( ',0
passOk  db  10,' Pass Ok!',0
origin  db  'codeby_net',0
buff    db  0
;//----------
section '.idata' import data readable
library  msvcrt, 'msvcrt.dll'
include 'api\msvcrt.inc'

Скомпилировав этот исходник в fasm’e клавишей F9, сразу откроем его в редакторе РЕ-файлов «CFF Explorer», и запросив таблицу, изменим Raw-адрес секции «.free» с 0х400 на 0х600. Если открыть теперь пропатченный бинарь в hex-редакторе «HxD» то увидим, что по Raw 0x400 и вправду ничего нет, а прописан лишь один параграф с указанными нами символами «А», а весь код лежит где и положено ему быть по Raw-адресу 0x600.

hackDesc.png

Убедиться в работоспособности трюка можно только загрузив программу в отладчик, т.к. в игру должен вступить системный загрузчик образов. Как видим, x64dbg ожидаемо встал на ОЕР и далее можно в штатном режиме производить отладку. Однако если по Ctrl+G перейти сейчас на страницу 0x1000, то увидим там как под копирку этот-же код. То-есть загрузчик и вправду спроецировал секцию-кода на 2 вирт.страницы. С этого момента, любые изменения на странице(А) будут тут-же отображаться на странице(В), что подтверждает теорию.

LayerResult.png

Обратите внимание на выделенную зелёным инструкцию JNE. Поскольку компилятор вычислил для перехода относительный адрес, то операнд на страницах разный, т.е. был 0x2066, а стал 0x1066. Это хорошо. В то-же время остальные адреса не изменились, например передаваемых в printf() тектовых строк в следующей секции-данных 0x3000. Если мы приложим небольшие усилия и запрограммируем для переходов не относительные, а абсолютные адреса (например через push/ret), можно было-бы в РЕ-заголовке изменить реальный адрес точки-входа с 0x2000 на 0x1000. Тогда инструменты статического анализа файла на диске недоумевали-бы, как может ОЕР указывать на пустую секцию «.free»?

Всю малину портит здесь то, что относительные смещения «RelativeOffset» указываются и для получения адресов функций Win32API, которые лежат в таблице IAT (Import Address Table). Секция импорта у нас расположена в самом конце программы, и соответственно под неё загрузчик выделил пэйдж 0х404000 – это видно и на рисунке с парсером «PEAnatomist» выше. Значит операндами у всех инструкций call должен быть VA=0x4040xx, а на нижнем скрине видим 0x4030xx (поэтому не отображаются и имена апи). Виной тому съехавший на 0x1000 вниз код.

Таким образом, проецировать исполняемый код на несколько вирт.страниц можно, только затраченное на правки время никак не оправдывает себя. По сути профита от этого как от козла молока – ну сделаем мы это, а дальше что? Гораздо лучше иметь дело со всевозможного рода данными, например структурами, переменными, или NT-заголовком. Полная свобода от привязки к адресам наоборот развязывает нам руки, и остаётся только задействовать мозг.


Заключение

Ну вот опять не уложился в лимит символов на статью, а ведь планировал охватить ещё и глобальную тему импорта. Придётся оставить её для следующей части, тем-более что материала там на увесистый томник. Зато теперь есть база, и я хоть сбросил с себя груз фундаментальных основ. Организация импорта сильно запутана в РЕ – это целых 4 независимых механизмах, для которых выделяется аж 3 поля в каталоге секций «DATA_DIRECTORY». Ни одна другая секция не удостоилась такой чести. Как результат, ошибок реализации хоть отбавляй, что играет на руку малвари. Для тех, кому лень собирать исходники самим, в скрепке найдёте два расписанных выше файла для тестов. Пока!
 

Вложения

  • PE-Section.zip
    8 КБ · Просмотры: 88
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!