Разные варианты вызова импортируемых функций

Тема в разделе "WASM.WIN32", создана пользователем Felther, 19 май 2022.

  1. Felther

    Felther New Member

    Публикаций:
    0
    Регистрация:
    16 май 2022
    Сообщения:
    10
    Всем привет, лазя по асм листингу, в crt коде я нашел разные вызовы импортируемых функций. Один из вызовов call dword ptr ds:[тут адрес в секция idata, в котором загрузчик записал адрес нужной функции], а другой call 0x004xxxx, по адресу 0x004xxxx стоит jmp, который просто как в первом случае переходит по адресу в idata, которые записал загрузчик. Так в чем замысел 2 случая? 1 случай работает быстрей чем 1.
     
  2. f13nd

    f13nd Well-Known Member

    Публикаций:
    0
    Регистрация:
    22 июн 2009
    Сообщения:
    1.786
    В первом случае например абсолютный адрес внутри инструкции. Если образ релоцируемый, винде придется обработать каждый такой релок при загрузке.
     
    Felther нравится это.
  3. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.360
  4. Felther

    Felther New Member

    Публикаций:
    0
    Регистрация:
    16 май 2022
    Сообщения:
    10
    Mikl___, не, я читал уже вашу статью) но у вас про это не было сказано
     
  5. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.360
    Felther,
    Если я вызываю допустим MessageBox через MessageBox, тогда для вызова функции требуется код E8.3D.00.00.00 (call 00.00.00.56 =5 байт) + FF.25.20.20.40.00 (jmp dword ptr [402020] = 6 байт) итого 5+6 = 11 байт. Если я вызываю MessageBox через _imp__MessageBoxA@16, тогда для вызова функции требуется код FF.15.70.00.20.40.00 (call [402020])= 7 байт. Что лучше (меньше)? 11 или 7? Наверное, 7. Но при многократном вызове MessageBox, каждому последующему вызову будет соответствовать 5 байт, а не 11. Тогда, когда вы будете использовать _imp__MessageBoxA@16, на каждый последующий call будет отводится по 7 байт. Результат в таблице
    способ1 call2 calls3 calls4 calls5 calls6 calls
    call MessageBox +
    jmp [_imp__MessageBoxA]
    1×5+6=112×5+6=163×5+6=214×5+6=265×5+6=316×5+6=36
    call [_imp__MessageBoxA]
    1×7=7​
    2×7=14​
    3×7=21​
    4×7=28​
    5×7=35​
    6×7=42​
    +4​
    +2​
    0​
    -2​
    -4​
    -6​
     
    Felther и youneuoy нравится это.
  6. Felther

    Felther New Member

    Публикаций:
    0
    Регистрация:
    16 май 2022
    Сообщения:
    10
    Mikl___, значит компилятор ставит способ вызова, в зависимости от количества вызовов функции(по крайней мере с включенной оптимизацией)? Например, если в программе один вызов функции MessageBox, в этом случае нету же смысла создавать переход к MessageBox через jmp, т.к при прямом вызове меньше байт + во время выполнения меньше действий процессору.
    А если этих вызовов MessageBox 100? Тогда проще, через jmp, т.к как сказали выше при прямом вызове загрузчику надо исправить 100 адресов, а через jmp только один адреса, ну и плюс меньше кода. Наверное так это работает)
     
  7. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.360
    Felther,
    я не знаю, как это работает на самом деле, но думаю, что вы правы... :yes3:
     
  8. f13nd

    f13nd Well-Known Member

    Публикаций:
    0
    Регистрация:
    22 июн 2009
    Сообщения:
    1.786
    Скорей всего все это работает от балды, то есть от дефолтных значений флагов компилера (соответствующих разным уровням аптимизаций, обозначенных цифрами), кастомизировать которые цэ-программисты не лезут, ибо не шарят что они значат и зачем их крутить. Ничем другим объяснить наличие например таблицы релоков в ехе'шниках я не могу.
     
    Mikl___ нравится это.
  9. Felther

    Felther New Member

    Публикаций:
    0
    Регистрация:
    16 май 2022
    Сообщения:
    10
    Кстати я не знаю почему, но у меня регион 0x00400000 занят(хз в нем хевентс гейт, но это совпадение наверное, в большинстве случаев он просто зарезервирован). Если, что в PE заголовке указан imagebase 0x00400000
     

    Вложения:

  10. f13nd

    f13nd Well-Known Member

    Публикаций:
    0
    Регистрация:
    22 июн 2009
    Сообщения:
    1.786
    Вероятно он занят потому, что не пригодился:pardon:Образ ехешника зачем-то релоцируемый и винда определила ему рандомное место, а 0x400000 использовала под любое-что-угодно.
     
  11. Felther

    Felther New Member

    Публикаций:
    0
    Регистрация:
    16 май 2022
    Сообщения:
    10
    Я не понял, что вы имеете ввиду. То, что если бы загрузчик расположил образ в регионе 0x00400000, то все равно адреса пришлось бы исправлять?
     
  12. f13nd

    f13nd Well-Known Member

    Публикаций:
    0
    Регистрация:
    22 июн 2009
    Сообщения:
    1.786
    "Образ ехешника зачем-то релоцируемый" - значит, что нету причин ему таковым быть, но это зачем-то сделано. Если бы случайно была выбрана та же база, то скорей всего все равно загрузчик бы отработал как обычно.
    Выключением ASLR по идее можно на это повлиять:
    Длл начнут грузиться по одним и тем же базам, а ехешник вроде по ImageBase. И мотивирована эта петрушка судя по всему борьбой с мифическими злохакерами, для которых предсказуемость адресов это жуткая уязвимость, без которой они как без рук (нет).
     
    Felther нравится это.
  13. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    266
    Есть еще вот какой фактор - если при компиляции программы ЯВУ компилятору заранее сообщить (директива а-ля dll_import), что функция находится в DLL, то он может выбрать оптимальный план - выделять под CALL 6 (если не ошибаюсь, для 32 бит) байт для косвенного прыжка через таблицу в idata. Но вот если компилятор заранее не знает, что функция находится в DLL, то он будет выбирать оптимальный для локального связывания 5-байтный CALL.
    И вот вдруг если на этапе линковки выясняется, что символ то (имя функции) не локальный для EXE, а находится во внешней DLL, то надо выкручиваться из ситуации. Можно, конечно, каждый такой вызов засунуть в таблицу динамической линковки, но это может вызывать туеву хучу действий при загрузке, а главное - разрушить одно из преимуществ неизменяемых сегментов кода - их можно зеркалить механизмом виртуальных страниц в разных процессах на одну и ту же физическую память причиняя экономию последней в рамках всей системы. Именно поэтому в x86-64 позаботились об относительных режимах адресации чтобы код был максимально эффективен и мог подвергаться релокации без правок максимально. Так или иначе линкер не может переиначивать и переделывать заложенные компилятором инструкции и поэтому можно выкрутиться создав блок с JMP-ами и перенаправить CALL-ы туда. Поправить при загрузке можно уже только единичные джампы.
    Поэтому документация по dll_import от MS говорит https://docs.microsoft.com/en-us/cp...-calls-using-declspec-dllimport?view=msvc-170 , что
    Причём это не единственные стратегии как связываться, у Borland если мне память не изменяет была своя схема.
     
  14. aa_dav

    aa_dav Active Member

    Публикаций:
    0
    Регистрация:
    24 дек 2008
    Сообщения:
    266
    P.S.
    Интересно еще сравнить со стратегией связывания на Linux: https://habr.com/ru/company/badoo/blog/323904/
    А там крайне забавная ситуация судя по статье - во первых никаких dllimport нет и с точки зрения кода нет никакой разницы будет ли символ слинкован статически (из .o) или подцеплен динамически (к .so). Последнее решается чисто на этапе линковки и компилятор про эти тонкости не знает.
    Поэтому компилятор на 32 битах x86 всегда вставляет 5-байтные относительные CALL-ы.
    И они на этапе линковки либо прямо перенаправятся на код в текущем образе, либо, если окажется, что символ находится в .so перенаправятся на таблицу распрыжек состоящую из JMP в секции .data. Опять таки сегмент кода будет read-only и сможет разделятся механизмом виртуальных страниц между разными процессами.
    Но таблица распрыжек тут не простая, а откладывающая фактическое связывание в таблице импорта GOT на время выполнения!
    [​IMG]
    GOT - это как раз таблица с фактическими адресами символов, но при загрузке они заполняются (просто прибавляется одинаковое смещение на разность базы в образе с базой по факту) так чтобы указывать на инструкции prepare resolver - при первом вызове процедура resolver вычислит и поместит в GOT[n] настоящий адрес внешнего символа и при последующих срабатываниях всё будет работать уже просто с одной распрыжкой.
    Так типа откладывается необходимость вычислять весь GOT в момент загрузки образа. Забавно, забавно.

    Поэтому возникает версия, что топиккастер как раз видит разницу между компиляторами MSVC и MinGW.
     
    youneuoy нравится это.
  15. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.360

    Вызов импортируемой функции, наивный способ

    Это перевод статьи "Calling an imported function, the naive way" Реймонда Ченя

    Библиотека импорта (import library) разрешает (resolve) символы импортируемых функций, но к ней не обращаются до этапа компоновки. Давайте посмотрим на наивную реализацию, когда компилятор слепо не осведомлён о существовании импортируемых функций.
    В статье "Как импортировались DLL функции в 16-битных Windows?" (How were DLL functions imported in 16-bit Windows?) показано, что Windows собирает вместе все места ее вызова в цепочку и создает запись импорта функции в таблице импорта модуля. В run-time эти записи исправляются загрузчиком ОС и все счастливы.
    Давайте посмотрим, как справится с этой же ситуацией наивный 32-битный компилятор. Компилятор сгенерирует обычную инструкцию вызова функции, перекладывая разрешение адреса на компоновщик. Линкер увидит, что этот внешний символ, на самом деле, является импортируемой функций, и, ой, прямой вызов нужно переделать в косвенный. Но компоновщик не может переписать код, сгенерированный компилятором. Что же делать компоновщику?
    Решение заключается во вставке ещё одно уровня косвенности (предупреждение: информация ниже не верна буквально, но она "достаточно правдоподобна". Мы копнём детали в следующих постах).
    Для каждой экспортируемой функции создаётся два символа. Первый из них - запись в таблице импортируемых функций. Назовём его __imp__FunctionName. Конечно же, наивный компилятор не знает ни про какой префикс __imp__. Он просто генерирует код для инструкции call FunctionName, ожидая что компоновщик подставит нужный адрес.
    Тут на сцену выходит второй символ. Он имеет имя FunctionName и является однострочной функцией, состоящей из одной-единственной инструкции: jmp_[__imp__FunctionName] (генерируется компоновщиком). Эта крохотная заглушка предназначена для удовлетворения внешних ссылок на функцию FunctionName, и в свою очередь она генерирует ссылку на __imp__FunctionName, которая разрешается записью в таблице импортируемых функций.
    Когда модуль загружается ― его зависимости импорта будут разрешены, и реальный адрес функции будет записан в __imp__FunctionName. Затем, когда код вызовет импортируемую функцию, то сгенерированный компилятором код вызовет функцию FunctionName, которая является заглушкой, которая и вызовет целевую функцию через косвенный вызов.
    Заметьте, что с наивным компилятором, если ваш код попытается взять адрес импортируемой функции, то он получит адрес заглушки, поскольку наивный компилятор оперирует с адресом функции FunctionName, не зная о том, что она импортируется из другого модуля и для неё, на самом деле, создаётся заглушка.
    Далее мы увидим, что с этой ситуацией сможет сделать менее наивный компилятор.

    Как менее наивный компилятор вызывает импортируемую функцию

    Это перевод статьи "How a less naive compiler calls an imported function" Реймонда Ченя

    Если функция объявлена со спецификатором dllimport, то это указывает компилятору Visual Studio C/C++, что эта функция импортируется из другого (исполняемого) модуля, а не является обычной функцией в этом же исполняемом модуле. Имея на руках эту информацию, компилятор генерирует немного другой код, поскольку теперь он осведомлён об особенностях импортируемых функций.
    Во-первых, теперь больше нет необходимости в функции-заглушке, потому что компилятор может сгенерировать инструкцию call_[__imp__FunctionName]. Кроме того, компилятор знает, что этот адрес (адрес импортируемой функции) никогда не меняется, и, соответственно, он может оптимизировать многократное использование этого адреса, например:
    Код (ASM):
    1.       mov   ebx, [__imp__FunctionName]
    2.       push  1
    3.       call  ebx ; FunctionName(1)
    4.       push  2
    5.       call  ebx ; FunctionName(2)
    (Примечание к сумасшедшим людям: подобная оптимизация означает, что у вас могут возникнуть проблемы, если вы исправляете таблицу импорта модуля после того, как код в модуле начал работу ― потому что указатель на функцию может быть сохранён в регистр до того, как вы начали править импорт. Рассмотрите случай с примером выше, когда вы изменили запись в таблице __imp__FunctionName после выполнения инструкции mov_ebx,[__imp__FunctionName]: ваша функция-перехватчик не будет вызвана, потому что старый указатель на функцию сохранён в регистре ebx).
    Аналогично, если ваша программа попытается взять адрес импортируемой функции, которая была объявлена со спецификатором dllimport, то компилятор распознает эту операцию и преобразует её в загрузку адреса из таблицы адресов импортируемых функций.
    В результате этого дополнительного знания, сообщаемого компилятору, функции-заглушки больше не нужны; компилятор знает, что ему надо идти прямо к таблице адресов импортированных функций.
     
    M0rg0t нравится это.
  16. TrashGen

    TrashGen ТрещГен

    Публикаций:
    0
    Регистрация:
    15 мар 2011
    Сообщения:
    1.008
    Адрес:
    подполье
    бро, ставь хардвеерные брейкпойнты на импорт и трассируй исключения! лучше всего перехвати kiuserexceptiondispatcher тупо jmp в начло ему или типа того
    --- Сообщение объединено, 15 июл 2022 ---
    или эти как их наномиты из int 3 состоящие! http://ru.und3rgr0und.org/wiki/Armadillo
    --- Сообщение объединено, 15 июл 2022 ---
    к слову, на reng.ru когда то неплохо попячили эм си рэма с его крякмеком с дройвером внутре. тупо скозале, наскоко помню, шо взрослые дядьки не запускают школопорожняк криводрайверовый, бгг
    --- Сообщение объединено, 15 июл 2022 ---
    тем более что всё это уже, вроде, было:
     

    Вложения:

  17. M0rg0t

    M0rg0t Well-Known Member

    Публикаций:
    0
    Регистрация:
    18 окт 2010
    Сообщения:
    1.550
    f13nd, ну на самом деле от ASLR есть толк; локально оно конечно до 1 места, а вот RCE под винду закодить крайне тяжело.
     
  18. ormoulu

    ormoulu Well-Known Member

    Публикаций:
    0
    Регистрация:
    24 янв 2011
    Сообщения:
    1.028
    Да мусье Финд так прикалывается скорее всего. Локально несёт смысл в случае ядра.