Как стать автором
Обновить

Комментарии 28

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

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

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

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

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

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

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

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

Гугли  Spectre и Meltdown

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

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

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

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

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

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

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

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

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


Кроме того, нет гарантий того, что 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 слишком «громкий» способ внедрения в другой процесс для антивирусного ПО. Подозрительный процесс, вирусный или заражённый вирусом, может воспрепятствовать внедрению в себя со стороны подобным способом одним из множества способов.

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

Да ну?

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

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

Как скажете:

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

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

С ключом /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- дополнительно переупорядочивает функции в более ожидаемом порядке. Для более сложных примеров уже детально ковыряться надо (и перспектива получить ответ "ну конечно же никто не будет в столь системном коде столь творчески инстанцировать плюсовые шаблоны" не особенно вдохновляет), мне лень.

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 и не исключено, что ещё и переупорядочивает его потом).

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

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

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

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


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

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

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

Пока что производители антивирусов в linux/unix не научились работать без административных полномочий если честно. Поэтому наивно рассчитывать, что антивирус не будет являться основной угрозой приложению. Об этом вам косвенно скажет все уровни поддержки - оборудования, ИС, ОС, СУБД. Никто за антивирус на ОС ответственности нести не захочет. Антивирусы же в свою очередь отказываются тестировать себя в контексте приложений и конкретного оборудования. Как минимум наш самый главный антивирус :) В документации по поддерживаемым ОС Касперский смело пишет, что поддерживаются ОС с версии такой-то и выше. Т.е. они не тестируют верхние версии. Они просто наивно надеются что проблем не будет. А проблемы есть. Антивирус очень требователен к CPU даже в режиме мониторинга. Нагрузка выростает на 30-60% на абсолютно пустых ВМ. Влияние на промышленных системах - задержки выполнения запросов до 40%. Ни один безопасник вам об этом не расскажет, потому что поставить антивирус на Linux это идея фикс в свете того что Касперский уверенно теряет рынок Windows систем.

НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории