Статья API шпионаж - инструменты и трюки

Во всех современных системах имеются воины теневого фронта. Эти гонцы шастают туда-сюда, всё что-то вынюхивают, и записывают логи в свои блокноты для последующего анализа. Они прячутся под маской легальных мониторов безопасности Win, хотя представляют собой типичных шпионов Spy. В ответ свободные кодеры ответили ОС целым классом аналогичного по смыслу софта на любой вкус: FileMon (в народе филемон), DiskMon, RegMon, ProcMon, и прочие производные от них. Но в контексте данной статьи мы остановимся только на API-шпионах - что это за фрукты и с чем их едят.

Оглавление:
1. Детали механизма​
2. Софт для API-шпионажа​
3. Плагин "Logexts" в отладчике WinDBG​
- своя реализация в скриптах​
4. Выводы​



1. Детали механизма

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

Примитивные мониторы API хукают импорт прямо из пользовательского режима, более продвинутые инжектят свои DLL в память клиентов, однако серьёзный шпион всегда таскает с собой собственный драйвер, что позволяет ему залечь на самое дно системы. При этом у любого шпиона имеется своя база-данных DB, где хранится полный список функций из системных DLL, с кол-вом параметров для каждой из них. Благодаря именно базе становится возможным вести правильные логи, в которых будут лежать валидные аргументы функций. Вот фрагмент из такой базы для либы Kernel32.dll:

Код:
[kernel32.dll]
    AddAtomA ,1
    AddAtomW ,1
    AllocConsole ,0
    AllocateUserPhysicalPages ,3
;.......
    Beep ,2
    BeginUpdateResourceA ,2
    BeginUpdateResourceW ,2
    BindIoCompletionCallback ,3
;.......
    CopyFileA ,3
    CopyFileExA ,6
    CreateConsoleScreenBuffer ,5
    CreateDirectoryA ,2
    CreateFileA ,7
;.......
    DeleteAtom ,1
    DeleteFileA ,1
    DeviceIoControl ,8
    DuplicateHandle ,7
;.......


2. Софт для API-шпионажа

Довольно долго достойным шпионом был Kerberos от Рустема Фасихова. Он отлично справлялся со своими обязанностями на 32-битных WinXP, пока не ввалилась в нашу жизнь 64-битная эра, отправив на свалку сотни тонн уникального кода. Но автор Kerberos'a не забросил свой продукт, и немного причесав ему фейс выпустил в свет уже под другим именем API Logger, хотя реальную поддержку х64 почему-то так и не реализовал. Это простой, компактный (размер всего 16к), а потому наверное самый быстрый инструмент в своём классе для х32, который несомненно заслуживает внимания. Кстати на "Kerberos" есть сурки, т.ч. при желании можно будет самим портировать его и на х64.

API-хукинг и перехват вызовов WinAPI через WinDBG Logexts

Не последнюю роль в API-шпионах играют фильтры, благодаря которым можно отсеивать в логах вызовы неинформативных функций. Например, если не взвести галку "Only EXE calls" в опциях выше (хук api только из этого экзешника), то получим массу ненужного/промежуточного хлама, перебирать который придётся потом в ручную. Как видим в свежей версии есть и встроенный профайлер - помимо самих аргументов функций, он замерят и потраченное на них время как в милли/сек (указывается в первых квадратных скобках в конце), так и в тиках счётчика производительности QPC (Query Performance Counter, последний в строке). Вот пример лога:

Код:
API Logger v1.9
(C)2004-2012 black_ninja

QPC Frequency = 14318180 tics/sec

Functions ready to hook: 1282
Total function in DB   : 1282
Include modules        : KERNEL32.DLL

LOG START

StackTr~.EXE 00403006  GetCurrentProcessId() ret: 00000E38  [6] [86]
StackTr~.EXE 00403014  OpenProcess(001F0FFF, 00000000, 00000E38) ret: 000000D8 [59] [845]
StackTr~.EXE 004031A2  GetTickCount() ret: 01FD85BC         [4] [66]
StackTr~.EXE 004031A2  GetTickCount() ret: 01FD85BC         [3] [43]
StackTr~.EXE 0040309A  GetModuleHandleA(00403088: "user32.dll")  ret: 752D0000 [9]  [141]

LOG END

А вот ещё один шпион наших дней API Monitor - он раздаётся бесплатно сразу в двух ипостасях х32/64, и благодаря наличию своего драйвера, представляет собой тяжёлую артиллерию. Это уже серьёзный инструмент для профессионалов с массой тумблеров, выводит просто огромное кол-во информации, включая подробное описание параметров. Мощный фильтр позволяет выбирать не только библиотеки DLL, но и функциональные их составляющие (например только работа с файлами, памятью или устройствами внутри одной kernel32.dll). В целом монитор производит хорошее впечатление, что подтверждается на практике.

ApiMon.webp


3. Плагин "Logexts" в отладчике WinDBG

Сторонний софт от энтузиастов это хорошо, но согласитесь намного комфортнее держать весь арсенал в одном месте, и такой вариант предлагает нам WinDBG. Прямо не отходя от кассы, мы можем загрузить его расширение "Logexts", которое выступает в роли пусть и примитивного, но шпиона. Его логи - это точная ксерокопия Kerberos'a, хотя фильтр намного мощнее.

Команда !load подгружает плагин, и далее help выводит ознакомительную справку. При этом во всех командах е = включить опцию, а d выключить (enable/disable):

Код:
0:000> kd> !load logexts
0:000> kd> !logexts.help

Windows API Logging Extensions  v3.01

Main control:
  !loge [dir]                 Enable logging. Output directory optional.
  !logi [dir]                 Initialize but don't enable logging.
  !logd                       Disable logging.

Output:
  !logo                       List output settings.
  !logo [e|d] [d|t|v]         Enable/disable output:
                                d - Debugger
                                t - Text file
                                v - Verbose log
Categories:
  !logc                       List all categories.
  !logc p #                   List APIs in category #.
  !logc [e|d] *               Enable/disable all categories.
  !logc [e|d] # [#] [#] ...   Enable/disable category #.

Buffer access:
  !logb p                     Print buffer contents to debugger.
  !logb f                     Flush buffer to log files.

Debugging Logexts:
  !logs                       Print statistics.
  !logh [i|c]                 Print hook info (import | com).
  !logspew [e|d]              Enable/disable logexts info messages.

Module inclusion/exclusion:
  !logm                       Display module inclusion/exclusion list.
  !logm [i|x] [DLL] [DLL] ... Specify module inclusion/exclusion list.

0: kd>

Значит !loge активизирует шпиона. Команду желательно вводить последней уже после того, как настроим все остальные опции. В дефолте каталог с логами создаётся на раб.столе, хотя при желании можно задать и свой путь в аргументе !loge. Вот типичная последовательность:

1. Командой !logo проверить, включена-ли опция ведения логов в файл *.txt и если нет, активировать её !logo e t.​
2. Фильтром на уровне самих DLL управляет !logm, но есть нюанс. Проблема в том, что работа самого плагина основана на api из Kernel32.dll, а потому ручное подключение (inclusion) этой DLL в круг подозреваемых может привести к мёртвому циклу (плаг будет бесконечно ловить сам-себя). Именно по этой причине он ругается на команду !logm i kernel32.dll, хотя в дефолте выборочные api из этой либы уже подключены. Но хуже всего то, что подключение одной автоматически отключает все остальные DLL, в результате чего можно получить пустой лог. Если подключать - только с явным перечислением всех необходимых, кроме Kernel32.dll:​

Код:
0:000> !logm i user32.dll gdi32.dll advapi32.dll ntdll.dll
Included modules:
  user32.dll    
  gdi32.dll      
  advapi32.dll  
  ntdll.dll

3. Тонкие настройки фильтра скрыты в !logc, а текущие можно просмотреть командой без параметров. Это предпочтительный вариант, чем оперировать на более низком уровне самих DLL. Здесь можно выбрать список перехвата api по назначению, а не по их именам, например только работа с памятью(16), файловый ввод-вывод(15), девайсы(7), и т.д. Просмотреть содержимое каждой категории можно аргументом(р). Обратите внимание, что в дефолте включены абсолютно все, в результате чего в логе получим много мусора. Поэтому лучше фильтровать вывод аргументом(е) выборочно - сначала все отключаем(d*), а затем включаем(e) только интересные.​

Код:
0:000> !logc
Categories:

  1 AdvApi32                        Enabled
  2 AtomFunctions                   Enabled
  3 AVIFileExports                  Enabled
  4 Clipboard                       Enabled
  5 ComponentObjectModel            Enabled
  6 DebuggingAndErrorHandling       Enabled
  7 DeviceFunctions                 Enabled
  8 Direct3D                        Enabled
  9 DirectDraw                      Enabled
 10 DirectPlay                      Enabled
 11 DirectSound                     Enabled
 12 GDI                             Enabled
 13 HandleAndObjectFunctions        Enabled
 14 HookingFunctions                Enabled
 15 IOFunctions                     Enabled
 16 MemoryManagementFunctions       Enabled
 17 Multimedia                      Enabled
 18 Printing                        Enabled
 19 ProcessesAndThreads             Enabled
 20 RegistryFunctions               Enabled
 21 Shell                           Enabled
 22 StringManipulation              Enabled
 23 ThreadLocalStorage              Enabled
 24 User32                          Enabled
 25 User32StringExports             Enabled
 26 Version                         Enabled
 27 WinSock2                        Enabled

0:000> !logc p 15
IOFunctions:
  _hread                          KERNEL32.DLL
  _hwrite                         KERNEL32.DLL
  _lclose                         KERNEL32.DLL
  _lcreat                         KERNEL32.DLL
  _llseek                         KERNEL32.DLL
  _lopen                          KERNEL32.DLL
  _lread                          KERNEL32.DLL
  _lwrite                         KERNEL32.DLL
  AreFileApisANSI                 KERNEL32.DLL
  CancelIo                        KERNEL32.DLL
  CopyFileA                       KERNEL32.DLL
  CopyFileExA                     KERNEL32.DLL
  CreateDirectoryA                KERNEL32.DLL
  CreateDirectoryExA              KERNEL32.DLL
  CreateFileA                     KERNEL32.DLL
  CreateIoCompletionPort          KERNEL32.DLL
  DefineDosDeviceA                KERNEL32.DLL
  DeleteFileA                     KERNEL32.DLL
  FindClose                       KERNEL32.DLL
........

0:000> !logc d *
All categories disabled.

0:000> !logc e 15 16
 15 IOFunctions                     Enabled
 16 MemoryManagementFunctions       Enabled

4. Как только покончим с настройками, можно включить теперь и логирование командой !loge. А вообще это не принципиально и активировать логгер можно в любой момент хоть в самом начале. Это даёт возможность налету менять настройки фильтра уже в процессе отладки. Теперь запускаем на исполнение клиента командой g, и получаем подробный лог вызовов api с их аргументами на текущий момент. Отчёты будут оседать как в файл, так и в окно отладчика:​

Код:
0:000> !loge
Logexts injected. Output: "C:\Users\Marylin\Desktop\LogExts\"
Logging enabled.

0:000> g
Parsing the manifest files...
Location: C:\Program Files\Debugging Tools for Windows (x64)\winext\manifest\main.h
   Parsing file "main.h" ...
   Parsing file "winerror.h" ...
   Parsing file "kernel32.h" ...
   Parsing file "debugging.h" ...
   Parsing file "processes.h" ...
   Parsing file "memory.h" ...
   Parsing file "registry.h" ...
   Parsing file "fileio.h" ...
   Parsing file "strings.h" ...
   Parsing file "user32.h" ...
   Parsing file "clipboard.h" ...
   Parsing file "hook.h" ...
   Parsing file "gdi32.h" ...
   Parsing file "winspool.h" ...
   Parsing file "version.h" ...
   Parsing file "winsock2.h" ...
   Parsing file "advapi32.h" ...
   Parsing file "uuids.h" ...
   Parsing file "com.h" ...
   Parsing file "shell.h" ...
   Parsing file "ole32.h" ...
   Parsing file "ddraw.h" ...
   Parsing file "winmm.h" ...
   Parsing file "avifile.h" ...
   Parsing file "dplay.h" ...
   Parsing file "d3d.h" ...
   Parsing file "d3dtypes.h" ...
   Parsing file "d3dcaps.h" ...
   Parsing file "d3d8.h" ...
   Parsing file "d3d8types.h" ...
   Parsing file "d3d8caps.h" ...
   Parsing file "dsound.h" ...
Parsing completed.

Thrd e58 000007FEFE45EA8F  LocalAlloc( LMEM_ZEROINIT 0x00000030) -> 0x0000000002607E30
Thrd e58 000007FEFD7C628E  LocalAlloc( LMEM_FIXED    0x000004D0) -> 0x00000000002A4020
Thrd e58 000000007748F9C0  HeapAlloc( 0x001C0000 0x00000000 0x00000200) -> 0x00000000002A1840

ModLoad: 000007fe`fb6a0000 000007fe`fb6f6000   C:\Windows\system32\uxtheme.dll
ModLoad: 000007fe`f16d0000 000007fe`f16dc000   C:\Program Files\Punto Switcher\PSHook64.dll
ModLoad: 000007fe`fd6f0000 000007fe`fd709000   C:\Windows\system32\imagehlp.dll

Thrd e58 000000007748461F  GetSystemDirectoryW( 261) -> 0x00000013 ( "C:\Windows\system32")
Thrd e58 000000007748EDB0  GlobalLock( 0x03650008)   -> 0x0265E0B0
Thrd e58 00000000774B215A  GlobalSize( 0x03650008)   -> 0x00042036
..........


3.1. Своя реализация в скриптах

Плагин выше шпионит? Да. Хорошо? Не очень.. т.к. даже при тщательной фильтрации в логе много мусора, но это уже генетическая особенность самого WinDBG, а не плагина. Он логирует не только вызовы api из конкретно нашего приложения, но и уходит в глубину во-вложенные с заходом в call. Это полезно, но не всегда, а потому можно пойти другим путём и собрать API-шпион вручную, отправив всю последовательность команд во внешний скрипт.

В арсенале WinDBG есть т.н. "точки-останова по соответствию" bm (break match). В своём аргументе команда позволяет использовать подстановочные знаки, что чрезвычайно полезно, когда необходимо отлаживать все экспортируемые функции DLL, или только группу, имена которых соответствуют заданному шаблону.

Код:
0:000> bm /a kernel32!ReadFile
  1: 00000000`773705b8 @!"kernel32!ReadFile"

0:000> bm /a ntdll!NtMap*
  2: 00000000`775ea630 @!"ntdll!NtMapCMFModule"
  3: 00000000`775e9a20 @!"ntdll!NtMapViewOfSection"
  4: 00000000`775e97d0 @!"ntdll!NtMapUserPhysicalPagesScatter"
  5: 00000000`775ea640 @!"ntdll!NtMapUserPhysicalPages"

0:000>

Как видим, отладчик создал 5 брейков - первый указан явно, а остальные с подстановочным знаком(), т.е. все функции из Ntdll.dll, имена которых начинаются с префикса NtMap_xx(). Поскольку в режиме User число точек-останова не ограничего (хотя в режиме kernel их может быть макс.32), можно поставить брейки хоть на все функции(), но это перегрузит отладчик, поэтому лучше использовать фильтр.

Более того, в команде bm мы можем определить, что делать отладчику по достижении точки-останова - это условие необходимо заключить в кавычки. Например такая конструкция покажет содержимое регистров RCX/RDX, где будут лежать первые два аргумента:

Код:
0:000> bm /a kernel32!ReadFile ".printf \"RCX = %p. RDX = %p\", @rcx,@rdx"

Но аргументов у функций может быть много, и перечислять их все в одной ком.строке отладчика довольно муторно. Поэтому имеет смысл вынести последовательность команд во внешний файл-скрипта, и вызывать его с параметром командой $$>a< scriptName.txt kernel32!Create*. Вот пример:

Код:
$$ ====================================================
$$ Останов на всех API или по маске в указанном модуле
$$ Использование: $$>a< "script.txt" module_name
$$ Пример: $$>a< "breakapi.txt" kernel32!Create*
$$ ====================================================

$$ Установить брейк на точку-входа в программу
bp @$exentry; g

$$ Удалить все точки-останова (clear)
bc *

$$ Переменная для параметра скрипта
r $t0 = 0

$$ Установить брейк с именем функции из параметра
bm /a ${$arg1} "

$$ Распечатать всё, что нужно...
    .printf \"\\n>>> ${$arg1} called\\n\";
    .printf \"  RCX    (arg1): %p \\n\", @rcx;
    .printf \"  RDX    (arg2): %p \\n\", @edx;
    .printf \"  R8     (arg3): %p \\n\", @r8;
    .printf \"  R9     (arg4): %p \\n\", @r9;
    .printf \"  RSP+20 (arg5): %p \\n\", poi(@rsp+0x20);
    .printf \"  RSP+28 (arg6): %p \\n\", poi(@rsp+0x28);
    .printf \"  RSP+30 (arg7): %p \\n\", poi(@rsp+0x30);
    .printf \"  Retn  address: %p \\n\\n\", poi(@rsp);

$$ Отобразить содержимое стека
    k n

    .echo
 "

Обратите внимание на экранирующий символ(\) - он нужен, чтобы отделить параметры команды .printf" ", от блока условий команды bm" ", т.к. они используют одинаковые символы("). Результатом работы данного скрипта будет такой лог, в который можно добавить ещё произвольное кол-во информации на своё усмотрение:

Код:
0:000> $$>a< "d:\wdbScript.txt" kernel32!GetCurrent*

Breakpoint 0 hit
  1: 00000000`7736dca8 @!"kernel32!GetCurrentDirectoryA"
  2: 00000000`77374d38 @!"kernel32!GetCurrentProcess"
  3: 00000000`773ca210 @!"kernel32!GetCurrentConsoleFont"
  4: 00000000`77372f20 @!"kernel32!GetCurrentThreadIdStub"
  5: 00000000`773ca150 @!"kernel32!GetCurrentConsoleFontEx"
  6: 00000000`77372f60 @!"kernel32!GetCurrentThreadStub"
  7: 00000000`77374a90 @!"kernel32!GetCurrentProcessIdStub"
  8: 00000000`7737b478 @!"kernel32!GetCurrentDirectoryW"
  9: 00000000`773a9100 @!"kernel32!GetCurrentUmsThread"
 10: 00000000`77374a98 @!"kernel32!GetCurrentProcessId"
 11: 00000000`77372f68 @!"kernel32!GetCurrentThread"
 12: 00000000`773a8840 @!"kernel32!GetCurrentActCtx"
 13: 00000000`7737b470 @!"kernel32!GetCurrentDirectoryWStub"
 14: 00000000`77374d30 @!"kernel32!GetCurrentProcessStub"
 15: 00000000`7736dca0 @!"kernel32!GetCurrentDirectoryAStub"
 16: 00000000`773920cc @!"kernel32!GetCurrentJaEraIndex"
 17: 00000000`77372f30 @!"kernel32!GetCurrentThreadId"

0:000> g

>>> kernel32!GetCurrent* called
  RCX    (arg1): 000007fffffda000
  RDX    (arg2): 0000000000402000
  R8     (arg3): 000007fffffda000
  R9     (arg4): 0000000000402000
  RSP+20 (arg5): 0000000000000000
  RSP+28 (arg6): 0000000000000000
  RSP+30 (arg7): 000000007737556d
  Retn  address: 000000000040200e

 #  Child-SP           RetAddr            Call Site
00  00000000`0006ff28  00000000`0040200e  kernel32!GetCurrentProcessStub
01  00000000`0006ff30  00000000`00000000  image00000000_00400000+0x200e

kernel32!GetCurrentProcessStub:
00000000`77374d30 eb06       jmp     kernel32!GetCurrentProcess (00000000`77374d38)

Здесь видно, что скрипт не распознаёт число аргументов у функции, поэтому выводит все возможные. Решает эту проблему база-данных, как в примере выше использует шпион Kerberos.

А что с отладчиком х64Dbg? Для него тоже можно написать аналогичный скрипт, только в этом нет необходимости, т.к. существует плагин "xAnalyzer", который декодирует аргументы всех системных DLL, причём делает это весьма качественно.

x64dbg.webp


4. Выводы

В природе есть много API-шпионов, и на описанных здесь свет клином не сошёлся. Например в качестве универсального решения можно рассматривать утилиты "WinAPIOverride" и "SpyStudio", причём некоторые распространяются с исходниками, а потому изучить их гены будет проще. Но под каким именем не скрывался-бы шпион, это незаменимое оружие для отладки упакованных программ. Так, одним выстрелом мы сможем получить подробную карту вызовов api с сжатого кода, пропустив нудный процесс пошаговой распаковки. Это касается и динамической загрузки библиотек и функций через LoadLibrary() + GetProcAddress(). В общем от шпионов одни плюсы, и ни одного минуса, а использовать их или нет - решать вам. Всем удачи, пока!
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab