История использования USB
Ни для кого уже не секрет, что информация о разного рода активности многочисленных компонентов операционной системы попадает в реестр и файлы в виде записей заданного формата. При этом, информация эта нередко содержит чувствительные пользовательские данные: историю посещенных браузером страниц, кеш данных программ, информацию о подключаемых устройствах и многое многое другое. Во основном журналирование обеспечивается функциональными особенностями пользовательских программ, которые имеют встроенные алгоритмы сохранения истории операций, отчасти это возможно благодаря архитектурным особенностям ядра/HAL операционной системы, которые, производя конфигурирационные действия с устройствами, сохраняют информацию о последних в виде записей в системном реестре. Из всего многообразия подобной информации, в рамках данной статьи нас будет интересовать исключительно история использования USB устройств.
Система создает артефакты в момент обнаружения (инициализации) устройства (сменных накопителей, модемов, гаджетов, камер, медиаплееров и прч.) на шине компьютера. Дополнительным плюсом данного материала будет возможность сбора доказательной базы по факту неправомерного использования рабочей станции пользователя в корпоративной среде при помощи незадекларированных USB-устройств.
Не так давно в нашу жизнь вошел интерфейс USB, привнеся в неё довольно существенные изменения. Неожиданно многие вещи стали проще, отпала необходимость инсталляции устройства во внутренний интерфейсный разъем (шина), или внешний интерфейс, требующий перезагрузки станции для корректной инициализации устройства, да и сам процесс конфигурирования устройств стал, во множестве случаев, тривиальнее. На интерфейсе USB появились тысячи разнообразных по назначению устройств, которые достаточно было подключить к разъему на панели корпуса, после чего от момента подключения до состояния «готов к работе», порой проходили считанные минуты. Наряду с очевидными достоинствами: легкостью конфигурирования/использования, компактностью, функциональностью, подобные устройства сразу стали источником проблем как для безопасности персональных данных самого пользователя, так и безопасности корпоративных сред. Компактный, легко маскируемый «брелок» с интерфейсом USB может запросто явиться той ахиллесовой пятой, которая станет причиной «падения» гиганта корпоративной безопасности. Если порты USB в корпоративных рабочих станциях находятся без надлежащего контроля со стороны политик безопасности, то любое приспособление может запросто выступить в качестве средства для обхода периметра безопасности компании. И тут уж насколько хватит фантазии «взломщика», например, достаточно пронести на флешке свежий, не определяемый антивирусами вредоносный код и выполнить его (умышленно или нет), и вот вам уже прецедент, поскольку даже без локальных административных привилегий учетной записи пользователя сохраняется пространство для маневра. Не меньшую опасность представляют и USB-модемы, которые, в случае установки (а при использовании локальных/доменных политик по умолчанию это достигается достаточно просто), могут выступить в роли неконтролируемого канала передачи данных, по которому может осуществляться передача чувствительной корпоративной информации за пределы защищенного внутреннего периметра. При этом, даже декларируемые (заявленные/согласованные) пользовательские устройства (например, телефоны) могут содержать в своих микропрограммах или операционных системах уязвимости, которые, в случае эксплуатации, наносят вред не только владельцу, но и могут выступать в роли средства несанкционированного доступа к служебным данным. Поэтому, в случае возникновения инцидента информационной безопасности, связанного с эксплуатацией USB-устройств,
В связи со всем перечисленным, достаточно важно не только уметь ограничивать использование устройств, но и иметь доступ к истории USB подключений в системе. Этому вопросу и будет посвящена данная статья. Сразу оговорюсь, что весь список точек создания информации о подключении тех или иных устройство чрезвычайно обширен, поэтому по теме данной статьи мы будем рассматривать лишь историю использования USB устройств.
Перечисление (энумерация) USB устройств
Поскольку я сам в данном вопросе «плаваю», перед тем как перейти к практике, предлагаю немного усилить нашу теоретическую базу и описать терминологию, которая будет использоваться на протяжении всего материала. Сразу оговорюсь, что мы не будем освещать все виды взаимодействия, выполняющиеся на шине USB на аппаратном уровне, а сосредоточимся исключительно на основных понятиях, относящихся к USB-устройствам и требующихся нам для понимания практической стороны вопроса.
Подключение любого нового оборудования сопряжено с выполнением модулями ядра системы Windows предопределенных фаз опроса и инициализации. Начинается всё с того, что при подключении устройства к разъему USB, контроллером USB (встроенным в чипсет на материнской плате) генерируется аппаратное прерывание. Драйвер USB, ответственный за обработку данного прерывания, запрашивает статус порта, и если статус указывает на подключенное устройство, то ответственными подсистемами ядра производится последовательность действий, которую условно можно разделить на две стадии:
- Нумерование устройства;
- Установка драйвера устройства;
Ядро (?) инициирует к вновь подключенному устройству серию запросов GET_DESCRIPTOR с различными типами запрашиваемых дескрипторов ( Device , Configuration , LangID , iProduct ). Запросы опрашивают устройство на предмет наличия серии дескрипторов, представляющих собой структуры данных, описывающие возможности USB устройства.
Отсюда следует вывод, что любое USB-устройство должно уметь реагировать на запросы от хоста и иные события на шине. В ответ на подобного рода запросы, микрокод устройства возвращает из ПЗУ требуемую информацию. Данные, возвращаемые устройством в ответ на запросы разнообразных дескрипторов, являются важными для операционной системы, поскольку именно часть этих данных представляет собой различного рода идентификаторы, используемые системой в дальнейшем в процессе нумерования устройства. Давайте приведем наиболее значимую информацию:
Название поля | Терминология Windows | Размер (байт) | Комментарий |
---|---|---|---|
idVendor | VID | 2 | Идентификатор производителя устройства. При присвоении идентификатора производителя, соответствующее числовое значение вносится в реестр производителей. |
idProduct | PID | 2 | Идентификатор продукта. Назначается производителем устройства. Product ID используется для дифференциации продуктов в рамках одного производителя. |
bcdDevice | REV | 2 | Идентификатор ревизии. Используется для дифференциации разных аппаратных модификаций в рамках одной модели устройства. Может использоваться при выпуске новой версии платы/контроллера/логики. |
bDeviceClass, bFunctionClass, bInterfaceClass | Class | 1 | Класс устройства. Используется для задания класса схожих устройств с общим набором идентичных свойств. |
bDeviceSubclass, bFunctionSubClass, bInterfaceSubclass | SubClass (SUB) | 1 | Подкласс устройства. Используется для задания подкласса схожих устройств в рамках класса. |
bDeviceProtocol, bFunctionProtocol, bInterfaceProtocol | Protocol (Prot, PROTO) | 1 | Протокол устройства. Используется для задания протокола для устройств в рамках класса и подкласса. |
iProduct | Product | ? | Текстовая строка-описатель продукта. Когда устройство подключено к компьютеру, данная информация отображается в Диспетчере устройств. |
iSerialNumber | Serial | ? | Серийный номер. Используется для уникализации абсолютно одинаковых устройств, например две одинаковых флешки. Назначается и поддерживается производителем устройства. Связанный механизм носит имя Сериализация. Сериализация так же участвует в уникальной идентификации устройства, поскольку добавляет еще один уровень уникальности. |
Наверняка многие из перечисленных в таблице полей Вам уже встречались в составе значений различных параметров в том же Диспетчере устройств либо в разнообразных системных лог-файлах.
Помимо стандартных дескрипторов, существуют еще так называемые Дескрипторы Microsoft (Microsoft OS Descriptors, MOD), которые содержат специфичную для ОС Windows информацию. Для поддержки производителей, чьи устройства из-за функциональных особенностей не подходят под стандартный набор классов, Майкрософт разработала набор специальных классов и собственных дескрипторов. Пользовательское и системное ПО может идентифицировать устройства, принадлежащие к разработанным Майкрософт классам устройств путем опроса устройства на предмет наличия дескрипторов Microsoft. Помимо поддержки описанных классов устройств, дескрипторы Microsoft имеют и иное применение, например при помощи них можно использовать память устройства для хранения файлов помощи, иконок, списков адресов URL, настроек системного реестра и других данных, используемых для обеспечения прозрачности установки и достижения смежных целей. Устройства, поддерживающие дескрипторы Microsoft, должны хранить специальный строковый дескриптор в прошивке с фиксированным индексом 0xEE . Операционные системы Windows XP SP1 и более поздние запрашивают этот строковый дескриптор у устройства при первом его подключении.
Более того, использование пары VID / PID в дескрипторе любого USB-устройства предписывается спецификацией, согласно которой данные параметры должны быть уникальны для каждой модели устройства. Ну это, опять же, все в теории, а на практике пару раз встречались экземпляры устройств, при работе с которыми становилось очевидно, что значения VID/PID взяты произвольно, либо взяты свободные значения (!) из реестра производителей. Понятно кому выгодно подобным заниматься 🙂 Ну это скорее редко встречающаяся ситуация, когда производителю хочется сэкономить на внесении в реестр производителей.
Затем, после того, как у устройства запрошены ключевые параметры, для USB устройства создан уникальный идентификатор HardwareID ( CompatibleID ), однозначно идентифицирующий устройство/класс устройства. Драйвер USB-концентратора уведомляет специализированный модуль ядра под названием Диспетчер Plug-n-Play (PnP Manager) о новом устройстве. Диспетчер PnP получает идентификаторы HardwareID и CompatibleID устройства и пытается обнаружить устройства с аналогичными идентификаторами HardwareID/CompatibleID. В этот момент в системе создается узел устройства (devnode), что является, по сути, первым отпечатком USB устройства в системе. Если похожее устройство найдено, то производится установка соответствующих драйверов в автоматическом режиме, если же не найдено, то Диспетчер PnP получается уведомление о новом устройстве и далее действует по определенным правилам, описание которых выходит за рамки данного материала.
Эксперимент
В Сети много противоречивой информации относительно истории подключения USB, поэтому давайте проведем собственный эксперимент по выявлению всех возможных системных местоположений, формирующих историю USB подключений. С целью выявления следов подключения USB стоит отследить абсолютно все изменения, происходящие в системе в момент подключения USB устройства. С этой целью на просторах Сети была найдена замечательная утилита под названием SysTracer, которая обладает всем необходимым функционалом. Утилита настолько проста и функциональна, что во многих случаях она окажется незаменимым средством в руках специалиста, поскольку предоставляет возможность сделать КРАЙНЕ полезное действие: создать мгновенный снимок (snapshot) состояния ключевых компонентов системы, таких как реестр и файлы. В качестве системы я использовал чистую инсталляцию Windows 7 Professional, при этом главным требованием было отсутствие каких-либо подключений внешних носителей. Итак, делаем снимок чистой системы, затем вставляем тестовую USB-флешку SanDisk Cruzer mini 1.0Gb и через некоторое время делаем второй снимок. Встроенными средствами утилиты SysTracer сравниваем полученные образы с выводом отчета. В итоге у нас получился некий набор системных изменений, среди которых мы попытаемся выбрать именно те, которые могут относиться к следам подключения USB устройств. Выбранный мною метод имеет и свои недостатки, поскольку наблюдения за активностью системы применительно к USB устройствам не проводилось «в динамике», то есть мы не работали с открытыми с подключаемого носителя файлами (.docx/.xlsx) в различных пользовательских приложениях, а это могло привести к тому, что мы можем упустить факты попадания частей информации с USB-носителя в файлы подкачки, различные временные файлы кеша и файлы иного назначения. Поэтому материал, скорее всего, потребует последующей доработки.
История в файлах
После изучения изменений файловой части системных изменений, подтвердился факт того, что в операционной системе Windows 7 все действия над устройствами отражаются в следующих журнальных файлах:
Статья SetupAPI – информация об устройствах
Перед любым системным программистом, рано или поздно встаёт задача определения текущего оборудования системы. Причин для этого может быть много, например: установить драйвер на девайс, собрать лог всех устройств для последующей работы с ними и т.п. Если мы не будем готовы к таким поворотам судьбы, то придётся переобуваться в воздухе (на скорую руку штудируя доки), в результате чего на выходе получим как-минимум кривой софт.
Да. в штатной поставке Windows имеется оснастка WMI Control – Windows Management Instrumentation (инструментарий управления Win), которая прекрасно решает проблемы данного характера. Однако вызывать сторонние сервисы и приложения из своих программ слишком накладно по времени, и наше приложение будет жутко тормозить. Личные опыты показали, что одну и ту-же задачу WMI решает аж в 60 раз медленнее, чем если этот-же функционал реализовать прямым вызовом системных функций WinAPI – аргумент явно не в пользу WMI, хотя данная оснастка и требует от нас меньших телодвижений.
Так-что оставим этот инструментарий для сис-админов (хотя не факт, что большая часть из них в полной мере знакомы с ним), а мы попробуем собрать инфу об устройствах с подручными средствами, для чего воспользуемся услугами специально предназначенным для этого набором функций под общим названием setupAPI . Этот набор живёт в одноимённой системной библиотеке setupapi.dll, которая выдаёт на экспорт без малого 600-функций. Жалко, что fasm о них ничего не знает и не имеет служебных структур для работы с ней, но это дело поправимое (см.скрепку в конце статьи). Ознакомиться с кол-вом функций и их именами можно в дизассемблере\отладчике W32Dasm, что демонстрирует скрин ниже:
Основное назначение библиотеки setupapi.dll – это установка драйверов устройств и законченная их регистрация в системном реестре. Данная библиотека состоит из двух самостоятельных модулей:
Не зная тонкостей функционирования драйверов (а их в системе вагон и маленькая тележка, и каждый со-своим нравом), конфигуратор трогать рискованно. Поэтому, чтобы-бы после наших экспериментов система не превратилась в труп, в своих программах мы будем использовать исключительно функции SetupDi_XXX. Эта группа функций как-нельзя лучше подходит для тестов и ознакомления, при этом имея довольно мощный функционал.
———————————————
Знакомство с классами устройств, и их идентификаторами GUID
Современная архитектура компьютерных систем имеет огромное количество устройств, и только часть из них внешние. К внешним относятся жёсткий диск, клавиатура, мышь, модем, монитор и прочая ботва, с которой нас знакомили на уроках информатики. Однако внутри чипсета материнской платы зарыты внутренние устройства – это различного рода шины, таймеры, контролёры, мосты и много другое. Более того, в системе могут присутствуют одинаковые по типу, но разные по назначению устройства, а значит их нужно как-то фильтровать.
Таким образом, все устройства в системе (будь-то внешние или внутренние) разделяются на классы, например: класс шины PCI, класс USB, видео-класс, класс принтеров, модемов, клавиатуры и т.д. В системе, буквально любое устройство обязательно принадлежит к какому-нибудь классу. Каждый класс устройств идентифицируется своим GUID«ом (глобальный уникальный идентификатор), который представляет из-себя 16-байтную (128 битную) запись вида: <50127dc3-0f36-415e-a6cc-4cb3be910b65>. Схематически это можно представить так:
Информационной базой всех устройств является системный реестр, где для каждого класса-устройств выделена своя ветвь. Все классы собраны в разделе реестра HKLM\SYSTEM\CurrentControlSet\Control\Class. В этой папке мы найдём зарегистрированные когда-либо в системе GUID’ы и даже тех классов, привязанных устройств к которым в текущий момент может и не быть. Например подключили мы к компьютеру месяц назад камеру, а потом убрали её за ненадобность. Это нужно учитывать при сканировании устройств, выставляя флаг DIGCF_PRESENT=2 (только активные девайсы).
При такой\древовидной организации базы-данных, чтобы получить информацию о конкретном устройстве мы должны указать его класс в виде идентификатора GUID. В ответ на этот запрос, система возвратит нам дескриптор данного класса (см.предыдущий рис.), который послужит указателем на соответствующую инфо-базу. Теперь просто сканируем эту базу от подвала до чердака, и получаем из реестра голограмму всех устройств указанного класса.
Забегая вперёд скажу, что для каждого из найденных устройств, функция SetupDiGetDeviceRegistryProperty() как пылесос может вытянуть до 37-ми его характеристик – именно такое количество SPDRP-флагов мы можем передавать ей в аргументе. Как-говорится – хоть лопатой греби..
В талмуде мелкомягких, глобальный идентификатор GUID описывается вполне легальной структурой. Например на рисунке выше, GUID характеризует класс USB-устройств и имеет значение: <36fc9e60-c465-11cf-8056444553540000>. Если копнуть доки, то можно найти значения GUID всех (само)настраивающихся устройств PnP – в представлении ассемблера каждая запись выглядит так, и я собрав их в инклуде setupapi.inc, прикрепил его в скрепке (в штатной поставке fasm’a их нет):
То-есть в значении GUID, первым идёт двойное слово, дальше слово, ещё слово и в конце — массив байт. Поскольку GUID представляет из-себя определяющее класс-устройств уникальное число, то это число эстафетой передаётся от версии к версии Windows. Не знаю как на остальных системах (особенно х64), но на моих эксперементальных XP и Win-7 идентификаторы GUID совпадают, что позволяет писать кросплатформенные приложения на одном инклуде. Кстати Microsoft тоже утверждает, что GUID’ы классов будут соблюдаться ими в последующих версиях операционных систем, иначе зачем нужно было прописывать их в константах сишных хидеров?
Основные API-функции для сбора информации
Будем считать, что с теорией разобрались – перейдём к практической части..
Из указанных 600-функций библиотеки setupapi.dll мы будем использовать всего 6-7. Эти функции позволят нам выжать достаточно информации от первого свидания с этой либой. В качестве демонстрации напишем приложение, которое перечислит все идентификаторы GUID системы и отобразит в читабельном виде, какому именно классу принадлежит тот-или-иной GUID. Здесь в окопах нас поджидают некоторые нюансы – разберём их в кратце..
1. В цикле обходит все классы и возвращает нам их GUID’ы функция CM_Enumerate_Classes() с таким прототипом:
Если коротко, то нам нужно организовать цикл и на каждой его итерации, начиная с нуля увеличивать индекс класса. Эта функция BOOL, так-что если она вернёт EAX=1, значит мы обошли всю базу и пора из цикла выходить. Второй аргумент – это указатель на 16 байтный буфер, в который функция будет сбрасывать GUID текущего индекса. Третий аргумент не используется и должен быть установлен в нуль.
2. Чтобы привести полученный GUID в читабельную строку (его мы получим в виде hex-значения), задействуем специально предназначенную для этого функцию из библиотеки ole32.dll StringFromGUID2() . Всё-что ей нужно, это указатель на 16-тиричный GUID для преобразования, и указатель на приёмный буфер, куда она сбросит результирующую строку. В случае успеха, fn. возвращает длину записанной в буфер строки. Если получим нуль, значит приёмный буф слишком мал и в аргумент cchMax вернётся трубуемая длинна. Вот её прототип:
Эта функция сбрасывает в буфер GUID в виде Unicode-строки, значит для вывода на консоль её нужно будет преобразовать в Ascii. Для этого будем просто читать по 2-байта, и перезаписывать в тот-же буфер по одному байту (т.е. отсекать парные нули). Вот как выглядит эта информация в секции-данных программы, после того-как функция StringFromGUID2() отработает:
3. На заключительном этапе, чтобы наша GUID-строка несла в себе хоть какую-то информацию, нужно будет по GUID получить строку с именем класса-устройства (см.зелёный блок на рисунке выше). В этом нам поможет функция из setupapi.dll с говорящим за себя названием SetupDiGetClassDescription() . Как и предыдущая, эта функция требует на входе указатель на 16-тиричный GUID, а из своей выхлопной трубы со-свистом выдаёт строковое представление данного класса-устройств (обе функции сами вставляют терминальный нуль в конце):
Из рисунка выше видно, что эта функция возвращает строку в кодировке cp-1251 (зелёный блок), т.е. кириллицей. Если мы планируем выводить информацию в виндовую консоль, то в своём пространстве консоль воспринимает только дос-кодировку OEM-866 . Соответственно если не перекодировать этот выхлоп, то вместо текста, на экране получим инопланетные крякозябры. Для этого воспользуемся функцией из user32.dll под названием CharToOem() , которая изменит кодировку прямо не отходя от кассы, в том-же буфере.
4. Можно прикрутить в исходник код профилировщика, чтобы лицезреть время, потраченное программой на реализацию задуманного. Мы просто на входе в основную процедуру запомним текущие тики системного таймера функцией GetTickCount() , а на выходе из процедуры запросим их опять, и вычислим разницу. Так мы получим хоть какой-то метроном, который отстучит нам потраченное на исполнение кода время. Теме профайлеров мы посвятим отдельную статью, а пока будем довольствоваться этим алго времён динозавров.
Теперь, законченная реализация кода для вывода имени-класса по его GUID может выглядеть примерно так:
Что мы тут имеем? Значит всего классов-устройств в системе равно 60, и соответственно столько-же идентификаторов GUID. Пусть вас не смущает время выполнение данного кода 560 миллисекунд – это тестировалось на виртуальной машине VirtualBox, скорость которой можно сравнить с активностью уставшей улитки. К примеру на скрине ниже я запустил этот-же кодес на реальном процессоре под хр, так хрюша пробежала эту дистанцию всего за 32 мс – как говорится, почувствуйте разницу: 560/32=17.5 раз быстрее:
Перечисляем все, или принадлежащие только к одному классу устройства
Получив GUID’ы классов всех устройств проследуем дальше, и попробуем отфильтровать лог по конкретному классу-устройств, например собрать информацию только об устройствах USB или тех, что повесились на шине PCI и т.п. Такую задачу решает аккорд всего из трёх функций, и все они прописаны в библиотеке setupapi.dll. Обычно эти функции имеют приоритет друг перед другом, т.е. их нужно вызывать в определённой последовательности – рассмотрим её..
1. SetupDiGetClassDevs() – возвращает дескриптор инфо-базы по GUID-класса (см.рис.2), или нуль в случае ошибки. На входе принимает 4 аргумента, первые-три из которых опциональны и могут быть равны нулю (в этом случае фильтр отключается и сканируются все устройства). В природе, на все случаи жизни имеются три распространённых шаблона аргументов этой функции, которые закоментированы в исходнике ниже – это шаблон для всех устройств, фильтр по текстовой маске (возможные варианты: USB/PCI/PCMCIA/SCSI), и фильтрация устройств по классу GUID.
2. SetupDiEnumDeviceInfo() – заполняет структуру SP_DEVINFO_DATA, которая описывает конкретный элемент в классе-устройств. Больше эта функция ничего не делает. Позже, мы должны будем передать указатель на эту структуру третьей функции в этой тусовке, чтобы она черпала с неё информацию. Для перечисления всего списка-устройств указанного класса, нужно поместить данную функцию в цикл, каждый раз увеличивая значение индекса на 1. Эта функция BOOL и возвращает EAX=0 при ошибке (последний элемент в списке):
3. SetupDiGetDeviceRegistryProperty() – последняя, довольно творческая единица и делает всю черновую работу. Именно эта функция лезет в системный реестр, заполняя наш приёмный буфер нарытими данными. Для начала посмотрим на её аргументы, а потом разберёмся с деталями:
Из все этой братии аргументов, нам интересен лишь третий, под кличкой ‘Property’. Он спрашивает у нас, информацию какого характера мы хотим получить? Вот где можно разгуляться с баяном в руках, подставляя в него одну из 37-ми констант (см.инклуд setupapi.inc в скрепке). Эта константа известна как SPDRP – Setup Device Registry Property , или выбор свойства из куста реестра. К сожалению аргумент не позволяет инструкцией OR задавать сразу несколько констант, поэтому если мы хотим за один подход вытянуть несколько строк различной инфы, нужно вызывать эту функцию N-ное количество раз, подставляя в этот аргумент соответствующие значения.
Здесь нужно учитывать, что если нам нужно имя устройства, то оно может хранится в одном из двух полей информационной базы – это Description и Name (зависит от типа устройства). Поэтому чтобы не попасть в просак, для надёжности нужно запрашивать имя сразу два раза – первый раз с аргументом SPDRP_DEVICEDESC , и если функция SetupDiGetDeviceRegistryProperty() вернёт ошибку (или пустую строку в буфере), то второй раз подставить SPDRP_FRIENDLYNAME . Не сбрасывайте это со-счетов..
Без практики, понять эту теоритическую муть довольно сложно, так-что соберём всё сказанное под один колпак и напишем небольшое приложение. Здесь я запрашиваю у системы информацию по GUID’у класса «Контролёры жёстких дисков». В инклуде эта переменная значится как GUID_DEVCLASS_HDC . Дальше, подставив в SPDRP-константу соответствующие значения, получаю: имя, адрес на шине PCI, и вендора обнаруженных устройств. Поиграйтесь с этой константой и получите информацию различного рода:
Здесь мы рассмотрели всего 1% из имеющихся 600 функций в библиотеки setupapi.dll. За бортом осталась довольно могучая SetupDiGetDeviceInterfaceDetail() и многие другие. Однако когда-нибудь нужно сделать первый шаг к покорению этой вершины, что открывает богатые возможности для программирования железа из пользовательского режима. Кстати MSDN хорошо раскрывает эту тему и содержит много полезных материалов – учите и вам обязательно зачтётся.
Под занавес статьи, хочу привести пример bat-файла, который поможет вам искать различные константы в огромном море сишных (и не только) инклуд. Он универсальный и ищет текст по указанной маске, рекурсивно обходя все папки и файлы на жёстком диске. Просто кидаете его в корневую папку и подставляете текст для поиска в аргумент команды FINDSTR между двумя прямыми слэшами. Пошурша некоторое время блинами диска, батник вернём вам директории и имена файлов, где имеется указанный текст – очень удобно (для отображения кириллицы, сохраните его в кодировке OEM-866):