В своё время один из клиентов сообщил нам, что на Itanium его программа завершалась аварийно.

Постойте, не закрывайте статью!

На Itanium клиент выявил проблему, но она свойственна и всем остальным архитектурам, так что продолжайте чтение.

Код выглядел примерно так:

struct REMOTE_THREAD_INFO
{
    int data1;
    int data2;
    int data3;
};

static DWORD CALLBACK RemoteThreadProc(REMOTE_THREAD_INFO* info)
{
    try {
        ... use the info to do something ...
    } catch (...) {
        ... ignore all exceptions ...
    }
    return 0;
}
static void EndOfRemoteThreadProc()
{
}

// Error checking elided for expository purposes
void DoSomethingCrazy()
{
    // Calculate the number of code bytes.
    SIZE_T functionSize = (BYTE*)EndOfRemoteThreadProc - (BYTE*)RemoteThreadProc;

    // Allocate memory in the remote process
    SIZE_T allocSize = sizeof(REMOTE_THREAD_INFO) + functionSize;
    REMOTE_THREAD_INFO* buffer = (REMOTE_THREAD_INFO*)
      VirtualAllocEx(targetProcess, NULL, allocSize, MEM_COMMIT,
        PAGE_EXECUTE_READWRITE);

    // Write data to the remote process
    REMOTE_THREAD_INFO localInfo = { ... };
    WriteProcessMemory(targetProcess, buffer,
                       &localInfo, sizeof(localInfo));

    // Write code to the remote process
    WriteProcessMemory(targetProcess, buffer + 1,
                       (void*)RemoteThreadProc, functionSize);

    // Execute it!
    CreateRemoteThread(targetProcess, NULL, 0,
                       (LPTHREAD_START_ROUTINE)(buffer + 1),
                       buffer);
}

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

Смысл заключался в том, что клиент хотел внедрить некий код в целевой процесс, поэтому использовал Virtual­Alloc для выделения памяти под этот процесс. Первая часть блока данных содержала какие-то данные, которые нужно было передать. Вторая часть блока данных содержала байты кода, которые нужно было исполнить, и клиент запускал эти байты кода при помощи Create­Remote­Thread.

Скажу прямо: сама идея, на которой построен этот код, фундаментально неверна.

Клиент сообщил, что этот код «отлично работал на 32-битных x86 и 64-битных x86», но не работает на Itanium.

На самом деле, я удивлён, что он работал даже на x86!

Структура программы подразумевает, что весь код в RemoteThreadProc не зависит от позиции. Требование независимости сгенерированного кода от позиции отсутствует. Например, один из вариантов генерации кода для операторов switch заключается в использовании таблицы переходов, и эта таблица состоит из абсолютных адресов x86.

На самом деле, очевидно, что код не является независимым от позиции, потому что в нём используется обработка исключений C++, а в реализации обработки исключений компилятора Microsoft используется таблица, сопоставляющая точки исполнения с операторами catch, чтобы было понятно, какой оператор catch использовать. И если бы использовался catch с фильтрацией, то существовали бы дополнительные таблицы для определения того, применяется ли фильтр catch к выданному исключению.

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

Но мы знаем, что ссылки на содержимое за пределами тела функции будут присутствовать, потому что блок C++ try/catch вызывает функции в библиотеке C runtime support library.

И x86-64, и Itanium используют для обработки исключений коды раскрутки (unwind codes), а в целевом процессе отсутствуют попытки регистрации этих кодов.

Предполагаю, что клиенту повезло, и исключений не выдавалось, или, по крайней мере, они выдавались достаточно редко, чтобы это осталось незамеченным при тестировании.

Кроме того, нет гарантий того, что EndOfRemoteThreadProc будет размещена в памяти непосредственно после RemoteThreadProc. На самом деле, нет даже гарантий того, что EndOfRemoteThreadProc будет иметь отдельную сущность. Компоновщик может выполнить свёртывание COMDAT, при котором несколько идентичных функций соединяются в одну. Даже если отключить свёртывание COMDAT, то Profile-Guided Optimization переместит функции по отдельности и маловероятно, что они окажутся в одном месте.

На самом деле, не существует даже требования, чтобы байты кода функции RemoteThreadProc вообще были смежными! Profile-Guided Optimization изменяет порядок базовых блоков и код одной функции может оказаться разбросанным по разным частям программы (это зависит от паттернов использования).

И даже без Profile-Guided Optimization оптимизация этапа компиляции может встроить часть функции или функцию целиком, поэтому одна функция может иметь множество копий в памяти, каждая из которых была оптимизирована под свою конкретную точку вызова.

Также существуют особые правила для Itanium, гарантировано обеспечивающие аварийное завершение на Itanium.

У процессоров Itanium все команды должны быть выровнены по 16-байтным границам, но приведённый выше код не соответствует этому требованию. Кроме того, на Itanium указатели функций указывают не на первый байт кода, а на структуру дескриптора, содержащую пару указателей: один на gp функции, второй на первый байт кода. (Тот же паттерн используется в PowerPC.)

Я сообщил представителю клиента, что написанное им пытается проделать очень подозрительные действия и походит на вирус. Представитель клиента объяснил, что всё наоборот: клиент является поставщиком популярного антивирусного ПО! В продукте клиента есть важная функциональность, которая реализована на основе этой техники удалённого инъектирования кода, и на данном этапе они не могут от неё отказаться.

Теперь я уже был напуган.

Более безопасным1 способом инъектирования кода в процесс была бы загрузка кода в качестве библиотеки при помощи Load­Library. Она бы вызвала загрузчик, который бы проделал всю работу по реализации необходимых исправлений, правильно бы распределил память с корректным выравниванием, регистрацией защиты потока управления и таблиц раскрутки исключений, загрузил бы зависимые библиотеки и в целом правильно подготовил среду выполнения для запуска нужного кода.

С тех пор от этого клиента не поступало никаких известий.

1 Я не сказал, что это безопасный способ инъектирования кода. Он всего лишь более безопасный.

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


  1. v1000
    30.12.2021 13:09
    +8

    Я сообщил представителю клиента, что написанное им пытается проделать очень подозрительные действия и походит на вирус. Представитель клиента объяснил, что всё наоборот: клиент является поставщиком популярного антивирусного ПО!

    Как раньше шутили про антивирус Касперского, что, учитывая как сильно он замедляет систему, он не особо отличается от вирусов, от который должен защищать. Еще и денег за лицензию требует.


    1. mayorovp
      30.12.2021 14:13
      +26

      Про антивирус Касперского шутили по-другому: замедление системы — это и есть основной способ защиты от вирусов. Им просто не хватает для работы ресурсов!


  1. rstepanov
    30.12.2021 13:11
    +8

    клиент является поставщиком популярного антивирусного ПО! 

    Это несколько проясняет ситуацию почему при установке антивирусов часто появляются совершенно необъяснимые проблемы, которых не было раньше. Жаль, что не раскрыто больше подробностей о том, кто, как и для чего использовал такие интересные техники программирования.


  1. zvszvs
    30.12.2021 14:07
    +4

    1 Я не сказал, что это безопасный способ инъектирования кода. Он всего лишь более безопасный.

    Можно ли хоть какой-то способ инъектирования кода считать безопасным?
    Риторический вопрос...


    1. bogolt
      30.12.2021 14:13
      +7

      современное инъектирование кода:
      1. скачать гугл хром
      2. выполнить в нем свой жс


      1. pfffffffffffff
        30.12.2021 21:50

        Гугли  Spectre и Meltdown


    1. grishkaa
      31.12.2021 02:36

      Можно ли хоть какой-то способ инъектирования кода считать безопасным?
      В macOS можно подгрузить свою динамическую библиотеку в чужой процесс и подменить реализацию какого-нибудь метода в нём на свою через objc runtime. Выглядит достаточно безопасно, как по мне.


      1. zvszvs
        31.12.2021 13:44
        +2

        Тут скорее филосовский вопрос. "Инъекция кода" подразумевает добавление кода в процесс, который этого не ожидает. Можно ли считать безопасным процесс, который затрагивает двоих, но проинформирован о нем только один.
        Аналогия с людьми вообще прекрасная выходит. )


        1. m0tral
          31.12.2021 18:10
          +1

          Да вообще дичь конечно, особенно в виде копирования кода метода, не, ну в embedded такое ещё нормально, ну плюс все очень сильно зависит от настроек компилятора и линкера, но там ты контролируешь распределение памяти, а тут реально стрёмно.


          1. DCNick3
            31.12.2021 19:21

            Ну вообще так вполне можно написать. Просто собрать отдельный испольняемый файл, position-independent, в каком-нибудь flat binary формате, подключить его как ресурс и будет счастье. А так получается да, выстрел в ногу


  1. ifilonov
    30.12.2021 14:10

    Смахивает на код из detours, библиотеки и для перехвата функций внутри процесса и встраивания своего кода в чужие процессы. Кстати, если мне память не изменяет, библиотеки от microsoft.


    1. KanuTaH
      30.12.2021 15:08
      +2

      Скажем так - в Detours есть возможность копировать в целевой процесс некие данные, и при большом желании наверное можно написать что-нибудь вроде DetourAttach(..., DetourCopyPayloadToProcessEx(...)), но это вовсе не означает, что так и надо делать :) По-хорошему штатное использование как раз-таки предусматривает загрузку кода через dll injection (через DetourCreateProcessWithDlls() или вручную), а там уже DllMain через Detours API сделает все что надо (а именно, немного пропатчит первые несколько байт целевой функции, заменив их на вызов соответствующей функции из загруженной dll'ки).


  1. realimba
    30.12.2021 17:49
    +6

    Да ладно, вполне себе рабочий и распространенный в узких кругах прием (если делать правильно), например, похожим образом пропихивают код из юзермода в ядро чтобы не переподписывать драйвер. Другой вопрос, насколько конструкция стабильна и безопасна (кого это парит в нашей реальности)?

    PS. А в том что все "поставщики популярного антивирусного ПО" говноделы никто не сомневался..


    1. firehacker
      30.12.2021 21:19
      +2

      PS. А в том что все "поставщики популярного антивирусного ПО" говноделы никто не сомневался..

      Внедрять CPP-код — это нечто.


    1. ramzes2
      30.12.2021 23:03
      +4

      А не подскажите, какие драйвера так делают? Это прям супер дыра в безопасности!


  1. firehacker
    30.12.2021 20:57
    +1

    Уважаю Рэймонда Чена, но в данном случае он напускает жуть там, где не нужно.


    Кроме того, нет гарантий того, что EndOfRemoteThreadProc будет размещена в памяти непосредственно после RemoteThreadProc

    Есть такая гарантия: компилятор генерирует сущности машинного кода в строгом соответствии с тем, кем они фигурировали в исходном файле.


    На самом деле, нет даже гарантий того, что EndOfRemoteThreadProc будет иметь отдельную сущность. Компоновщик может выполнить свёртывание COMDAT, при котором несколько идентичных функций соединяются в одну.

    И действительно, но COMDAT folding выполняется линкером и только тогда, когда указана опция /OPT:ICF. Уже при /OPT:REF линкер может только выкидывать сущности, которые нигде не referenced, но не сливать (merge) их и не переставлять по своему усмотрению. Но у нас к RemoteThreadProc и EndOfRemoteThreadProc точно есть обращения, так что выкинуты они не будут. Казалось бы, я хочу посоветовать отключить COMDAT Folding, чтобы гарантировать работоспособность кода? Но тогда мы потеряем неплохую оптимизацию в пределах всего бинарника.


    На самом деле, всё гораздо проще и лучше. Не надо глобально отключать COMDAT folding в процессе линковки. Чтобы COMDAT folding мог выкидывать или медждить ненужные или дублирующиеся куски объектных файлов, сам объектный файл должен быть сгененирован компилятором с использованием так называемого function-level linking (ключ /Gy компилятора), при котором каждая функция, каждая vftable, каждая переменная помещаеются в OBJ-файл упакованными в отдельную COMDAT-секцию.


    Но если не указывать ключ /Gy и не использовать function-level linking, а мы можем сделать это для одного отдельно взятого исходного файла, то компилятор всю начинку, например, все процедуры положить в одну монолитную секцию .text. И линкер с такой монолитной секцией (где сразу куча процедур) ничего не сможет и не будет делать: они либо откинет всю секцию целиком, если ни на одну сущность из неё нигде больше нет ссылок, либо всю секцию целиком включит в состав выходного исполняемого файла.


    То есть внедряемую процедуру и процедуру конца маркера мы выносим в отдельный .c/.cpp файл и только этот отдельный файл компилируем с /Gy-, а все остальные можем по прежнему компилировать с /Gy и использовать /OPT:ICF при линковке.


    Profile-Guided Optimization изменяет порядок базовых блоков и код одной функции может оказаться разбросанным по разным частям программы (это зависит от паттернов использования).

    Может. А может и не может: нужно посмотреть, нет ли тонких настроек PGO, позволяющих не перекраивать отдельные фрагменты программы. Но есть кое что, что точно остановит PGO от «расколбашивания» нашей маленькой внедряемой процедуры по всему образу. PGO никогда не переставляет ничего между секциями. Вся хирургия и всё перекраивание происходит в пределах одной секции.


    Поэтому волшебная #pragma alloc_text() с указанием секции «.inject» вынесла бы внедряемую и маркерную процедуру в отдельную секцию, и они бы гарантированно остались в пределах секции. Собственно, тогда и маркерная процедура не нужна: внеднять в чужой процесс целесообразно секцию целиком.


    Тактика «внедряем секцию целиком» решила бы даже ранее упомянутые проблему перефрагментированности кода с помощью PGO, если бы мы её отдельно не решали.
    Более того, она бы решила и проблему, которую Чен описал позже: проблему выравниваний для отдельных архитектур. Секция и вся начинка гарантированно имела бы правильное выравнивание, а выделяя под неё память в АП чужого процесса, мы бы выделяли правильной границы выравнивания (по другому VirtualAlloc и не может работать в принципе).


    И даже без Profile-Guided Optimization оптимизация этапа компиляции может встроить часть функции или функцию целиком, поэтому одна функция может иметь множество копий в памяти, каждая из которых была оптимизирована под свою конкретную точку вызова.

    Может. Но инлайнинг можно отключить ключами при компилировании каждого отдельного исходного файла. И поместив внедряемый код в отдельный исходник, мы можем его скомпилировать так, чтобы инлайнинг был запрещён.


    Более безопасным способом инъектирования кода в процесс была бы загрузка кода в качестве библиотеки при помощи Load­Library

    Очевидно, что LoadLibrary слишком «громкий» способ внедрения в другой процесс для антивирусного ПО. Подозрительный процесс, вирусный или заражённый вирусом, может воспрепятствовать внедрению в себя со стороны подобным способом одним из множества способов.


    1. grechnik
      30.12.2021 21:29
      +6

      Есть такая гарантия: компилятор генерирует сущности машинного кода в строгом соответствии с тем, кем они фигурировали в исходном файле.

      Да ну?

      C:\temp>type main.c
      #include <stdio.h>
      
      extern void RemoteThreadProc(void);
      extern void EndOfRemoteThreadProc(void);
      
      int main()
      {
              printf("%p %p\n", &RemoteThreadProc, &EndOfRemoteThreadProc);
              return 0;
      }
      
      C:\temp>type inject.c
      #include <stdio.h>
      
      void RemoteThreadProc(void);
      void DoSomethingUseful(void);
      void EndOfRemoteThreadProc(void);
      
      void RemoteThreadProc(void) {
              printf("RemoteThreadProc\n");
              DoSomethingUseful();
      }
      void DoSomethingUseful(void) {
              printf("DoSomethingUseful\n");
      }
      void EndOfRemoteThreadProc(void) {
              printf("EndOfRemoteThreadProc\n");
      }
      C:\temp>cl /O2 main.c inject.c
      Оптимизирующий компилятор Microsoft (R) C/C++ версии 19.29.30138 для x86
      (C) Корпорация Майкрософт (Microsoft Corporation).  Все права защищены.
      
      main.c
      inject.c
      Создание кода...
      Microsoft (R) Incremental Linker Version 14.29.30138.0
      Copyright (C) Microsoft Corporation.  All rights reserved.
      
      /out:main.exe
      main.obj
      inject.obj
      
      C:\temp>main.exe
      00901070 00901060


      1. firehacker
        30.12.2021 22:11
        -1

        Вы obj-файл дампите (dumpbin-ом, например) и смотрите какой там порядок следования сущностей, а не конечный результат, который на свет производится линкером.


        1. grechnik
          30.12.2021 22:13
          +3

          Как скажете:

          C:\temp>dumpbin /disasm inject.obj
          Microsoft (R) COFF/PE Dumper Version 14.29.30138.0
          Copyright (C) Microsoft Corporation.  All rights reserved.
          
          
          Dump of file inject.obj
          
          File Type: COFF OBJECT
          
          _DoSomethingUseful:
            00000000: 68 00 00 00 00     push        offset ??_C@_0BD@GJCDPNPO@DoSomethingUseful?6@
            00000005: E8 00 00 00 00     call        _printf
            0000000A: 59                 pop         ecx
            0000000B: C3                 ret
          
          _EndOfRemoteThreadProc:
            00000000: 68 00 00 00 00     push        offset ??_C@_0BH@IKBAAHGF@EndOfRemoteThreadProc?6@
            00000005: E8 00 00 00 00     call        _printf
            0000000A: 59                 pop         ecx
            0000000B: C3                 ret
          
          _RemoteThreadProc:
            00000000: 68 00 00 00 00     push        offset ??_C@_0BC@EGMKFOBF@RemoteThreadProc?6@
            00000005: E8 00 00 00 00     call        _printf
            0000000A: 68 00 00 00 00     push        offset ??_C@_0BD@GJCDPNPO@DoSomethingUseful?6@
            0000000F: E8 00 00 00 00     call        _printf
            00000014: 83 C4 08           add         esp,8
            00000017: C3                 ret
          
          ___local_stdio_printf_options:
            00000000: B8 00 00 00 00     mov         eax,offset ?_OptionsStorage@?1??__local_stdio_printf_options@@9@9
            00000005: C3                 ret
          
          __vfprintf_l:
            00000000: FF 74 24 10        push        dword ptr [esp+10h]
            00000004: FF 74 24 10        push        dword ptr [esp+10h]
            00000008: FF 74 24 10        push        dword ptr [esp+10h]
            0000000C: FF 74 24 10        push        dword ptr [esp+10h]
            00000010: E8 00 00 00 00     call        ___local_stdio_printf_options
            00000015: FF 70 04           push        dword ptr [eax+4]
            00000018: FF 30              push        dword ptr [eax]
            0000001A: E8 00 00 00 00     call        ___stdio_common_vfprintf
            0000001F: 83 C4 18           add         esp,18h
            00000022: C3                 ret
          
          _printf:
            00000000: 56                 push        esi
            00000001: 8B 74 24 08        mov         esi,dword ptr [esp+8]
            00000005: 6A 01              push        1
            00000007: E8 00 00 00 00     call        ___acrt_iob_func
            0000000C: 83 C4 04           add         esp,4
            0000000F: 8D 4C 24 0C        lea         ecx,[esp+0Ch]
            00000013: 51                 push        ecx
            00000014: 6A 00              push        0
            00000016: 56                 push        esi
            00000017: 50                 push        eax
            00000018: E8 00 00 00 00     call        ___local_stdio_printf_options
            0000001D: FF 70 04           push        dword ptr [eax+4]
            00000020: FF 30              push        dword ptr [eax]
            00000022: E8 00 00 00 00     call        ___stdio_common_vfprintf
            00000027: 83 C4 18           add         esp,18h
            0000002A: 5E                 pop         esi
            0000002B: C3                 ret
          
            Summary
          
                    90 .chks64
                    60 .debug$F
                    64 .debug$S
                    2F .drectve
                    3C .rdata
                    85 .text$mn
          


          1. firehacker
            30.12.2021 22:25

            С ключом /Gy- компилируйте, пожалуйста, чтобы говорить о порядке следования функций в пределах секции, а не о порядке вывода секций dumpbin-ом.


            1. grechnik
              30.12.2021 23:55
              +1

              С ключом /Gy- это ещё и от версии компилятора зависит. Вот, например, компилятор из Visual Studio 2010:

              C:\temp\vs2010>cl /c /O2 /Gy- /I"E:\programs\compilers\Microsoft Visual Studio 10.0\VC\include" C:\temp\inject.c
              Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
              Copyright (C) Microsoft Corporation.  All rights reserved.
              
              inject.c
              
              C:\temp\vs2010>dumpbin /disasm inject.obj
              Microsoft (R) COFF/PE Dumper Version 10.00.30319.01
              Copyright (C) Microsoft Corporation.  All rights reserved.
              
              
              Dump of file inject.obj
              
              File Type: COFF OBJECT
              
              _DoSomethingUseful:
                00000000: 68 00 00 00 00     push        offset ??_C@_0BD@GJCDPNPO@DoSomethingUseful?6?$AA@
                00000005: E8 00 00 00 00     call        _printf
                0000000A: 59                 pop         ecx
                0000000B: C3                 ret
                0000000C: CC                 int         3
                0000000D: CC                 int         3
                0000000E: CC                 int         3
                0000000F: CC                 int         3
              _EndOfRemoteThreadProc:
                00000010: 68 00 00 00 00     push        offset ??_C@_0BH@IKBAAHGF@EndOfRemoteThreadProc?6?$AA@
                00000015: E8 00 00 00 00     call        _printf
                0000001A: 59                 pop         ecx
                0000001B: C3                 ret
                0000001C: CC                 int         3
                0000001D: CC                 int         3
                0000001E: CC                 int         3
                0000001F: CC                 int         3
              _RemoteThreadProc:
                00000020: 68 00 00 00 00     push        offset ??_C@_0BC@EGMKFOBF@RemoteThreadProc?6?$AA@
                00000025: E8 00 00 00 00     call        _printf
                0000002A: 68 00 00 00 00     push        offset ??_C@_0BD@GJCDPNPO@DoSomethingUseful?6?$AA@
                0000002F: E8 00 00 00 00     call        _printf
                00000034: 83 C4 08           add         esp,8
                00000037: C3                 ret
              
                Summary
              
                        30 .debug$F
                        6C .debug$S
                        2F .drectve
                        3C .rdata
                        38 .text
              

              (тут, кстати, видно, что printf ещё была обычной внешней функцией). В VS2019 для конкретно этого примера ключ /Gy- дополнительно переупорядочивает функции в более ожидаемом порядке. Для более сложных примеров уже детально ковыряться надо (и перспектива получить ответ "ну конечно же никто не будет в столь системном коде столь творчески инстанцировать плюсовые шаблоны" не особенно вдохновляет), мне лень.


              1. grechnik
                31.12.2021 02:03

                UPD: с VS2019 забавнее, для конкретно этого примера /Gy- переупорядочивает в порядке первого объявления, а не реализации. Соответственно, если мы, как добропорядочные сишники, вынесли внешние объявления в отдельный inject.h (чтобы не нарваться на ситуацию "реализацию поменяли, объявления в других файлах .c поменять забыли")

                void RemoteThreadProc(void);
                void EndOfRemoteThreadProc(void);

                в inject.c поместили реализации всех вспомогательных функций между этими двумя

                #include <stdio.h>
                #include "inject.h"
                
                void DoSomethingUsefulButNotInlinable(int n);
                
                void RemoteThreadProc(void) {
                	printf("RemoteThreadProc\n");
                	DoSomethingUsefulButNotInlinable(42);
                }
                void DoSomethingUsefulButNotInlinable(int n) {
                	if (n) DoSomethingUsefulButNotInlinable(n - 1);
                	printf("DoSomethingUsefulButNotInlinable %d\n", n);
                }
                void EndOfRemoteThreadProc(void) {
                	printf("EndOfRemoteThreadProc\n");
                }

                и рассчитываем, что в бинарнике они будут идти в таком же порядке и можно смело memcpy-ить всё это куда угодно... нас может спасти только инлайнинг всего подряд, потому что с /Gy- порядок этих функций будет RemoteThreadProc, EndOfRemoteThreadProc, DoSomethingUsefulButNotInlinable, а без него DoSomethingUsefulButNotInlinable, EndOfRemoteThreadProc, RemoteThreadProc (вряд ли стоит копипастить сюда третий ассемблерный листинг подряд только чтобы проиллюстрировать порядок; я бы дал ссылку на godbolt.org, но там управляющая система, судя по всему, не дизассемблирует бинарник, а просит листинг у компилятора через /Fa и не исключено, что ещё и переупорядочивает его потом).


    1. grechnik
      31.12.2021 02:42
      +2

      Очевидно, что LoadLibrary слишком «громкий» способ внедрения в другой процесс для антивирусного ПО. Подозрительный процесс, вирусный или заражённый вирусом, может воспрепятствовать внедрению в себя со стороны подобным способом одним из множества способов.

      Вирусный процесс может запросто и от CreateRemoteThread защищаться (каждый новый поток, в том числе созданный удалённо, вызывает TLS callbacks и DllMain всех dll-ек, пока адрес грядущей передачи управления лежит себе на стеке), и каждые пять секунд сканировать всю свою память на предмет executable-страниц вне dll-ек (палевно, системные библиотеки себе такого не позволяют), и каждую секунду перечислять список потоков в своём процессе... Единственный выигрышный ход — не играть в эту игру и вообще не запускать свой код в контексте чужого процесса, всё остальное — противостояние щита и меча.


    1. lorc
      31.12.2021 17:04
      +3

      Есть такая гарантия: компилятор генерирует сущности машинного кода в строгом соответствии с тем, кем они фигурировали в исходном файле.

      Неа. Единственная гарантия — это что видимое поведение кода будет соответствовать тому что написал программист. И то, если программист не допустил UB. Все остальные ваши размышления — они о поведении конкретной версии конкретного компилятора.


      Тот же gcc даже с -O2 такую кашу из кода делает, что часто точных границ функций тупо не найти, особенно если в пределах одного файла есть несколько функций с похожими кусками (включая прологи и эпилоги).


  1. jt3k
    01.01.2022 02:50

    Мысль о том, что крупный производитель антивирусов представил эту концепцию в производстве, ужасает.


    1. z0ic
      01.01.2022 21:18

      Скорее всего они хотели реализовать удалённую подгрузку кода к собственному процесcу. Хотя, кто его знает.