Примеры реальных взломов: UniLink v1.03 от Юрия Харона — Архив WASM.RU

Все статьи

Примеры реальных взломов: UniLink v1.03 от Юрия Харона — Архив WASM.RU

Баста! Надоело! Все эти уродские защиты… (см. описания четырех предыдущих взломов) только портят настроение и еще, - чего доброго - вызывают у читателей смутное сомнение: а не специально ли автор подобрал такие простые программы? Может быть, он - автор - вообще не умеет ничего серьезного ломать?! Уметь-то он (вы уж поверьте) умеет, но публично описывать взлом "серьезных" программ - боязно, а в "несерьезных" хороших защит мне как-то и не попадалось. Хотя, стоп! Ведь есть же такой программный продукт как UniLink, созданный опытнейшим системщиком Юрием Хароном (хорошо известным всем членам туссовски FIDO7.SU.C-CPP; если же вы никогда не заглядывали туда ранее, не поленитесь, сходите на Google, поднимите архив конференции и почитайте, - уверяю вас, вы не пожалеете). Достаточно сказать, что один лишь bag-list на UniLink - настоящая кладезь информации, перечисляющая больше количество ошибок операционной системы и ее окружения.

Наша цель - отучить UniLink ругаться на trial expired при запуске (из уважения к Харону необходимо отметить, что взлом проводится исключительно из спортивного интереса и природного любопытства, какие либо корыстные цели тут не причем - линкер абсолютно бесплатен и может быть свободно скачен по следующему адресу: ftp://ftp.styx.cabel.net/pub/UniLink/ulnbXXXX.zip, где XXXX - номер версии). Цитирую со слов Харона "Любая бета через полтора месяца начнёт "ругаться", что мол она expired :). Сделано это, просто как напоминание. в силу заинтересованности в том, что бы тестировались последнии билды". Так что, ломая линкер помните, что взлом еще не освобождает от beta-тестирования ;-).

Несмотря на бесплатность линкера, Харон очень неплохо его защитил. Во всяком случае у меня на полный анализ защиты (включая развернутое описания взлома и отвлечения на повседневную текучку) ушла добрая неделя! Сейчас, когда пишутся эти строки, даже жалко, что защита так быстро сломалась и то интересное, во что еще можно вонзить свои зубы, закончилось. Впрочем, лучше отложим всю эту ностальгию до лучших времен в сторону, и вспомним, как эта неделя "эротических развлечений с защитой" собственно и начиналась…

…привычным движением руки загружаем исполняемый файл линкера в свою любимую IDA и… IDA грязно ругается по поводу того, что… "can't find translation for virtual address 00000000, continue?". Хм, ну что нам еще остается делать, - покорно жмем "Yes", чтобы сделать "continue". Увы! Наш фокус не увенчался успехом - на экране возникает еще одно ругательство "File read error at 0004C7AC (may be bad PE structure), continue?". Обречено жмем "Yes" и… …IDA просто исчезает. Да-да! Именно исчезает, даже не успев перед смертью выдать сообщение о критической ошибке!!!

Интересный формат файла, однако! Пытаясь выяснить что же в нем содержится такое нехорошее, что так не понравилось IDA, мы решаем натравить на него утилиту dumpbin. Щас! Разбежались - при попытке вывести таблицу импорта, dumpbin выдает сообщение о внутренней ошибке "DUMPBIN: error: Internal error during DumpImports", и только что успев скинуть контекст аварийно прекращает свою работу. Вот, значит, как?! Ну, защита, держись! Сейчас мы заглянем внутрь тебя "вручную", - каким ни будь низкоуровневым инструментом, ну, например, HIEW'ом…

Облом-с! При попытке сделать "prepare import data" HIEW скручивает дулю и, выдав нам на прощание трогательно красное окошко с надписью "Import name No free memory" банально виснет. Конкурирующий с ним QVIEW умирает и вовсе без каких либо пояснений. Утилита "PEDUMP" от Мэта Питтрека (известнейшего исследователя недр Windows) хоть и не виснет, но выдает сообщение о критической ошибке приложения и автоматически прибивается операционной системой. Так, чем еще можно исследовать внутренний формат PE-файла? На ум приходит efd (Executable File Dumper) от Ильфака, но даже эта утилита не справляется - выдав сообщение "Can't find translation for 000002F6 (758.)" она просто прекращает свою работу. Dump PE от Clive Turvey поступает аналогично. Дизассемблер De Win от Милюкова - виснет. Win DASM не виснет, но и не дизассемблирует. Даже знаменитый PROCDUMP распаковывать этот файл отказывается, правда позволяет сделать rebuild PE-заголовка, однако, после такой операции полученный файл становится неработоспособным. В, общем, этот список можно продолжать бесконечно…

Кошмар! Защиты, срывающие крышу отладчику, - это я еще понимаю, но вот чтобы так агрессивно сопротивляется дизассемблеру! Причем, не какой-то одному, конкретно взятому дизассемблеру, а всем дизассемблерам сразу. И в это же самое время защита ухитряется работать в любой Windows-совестимой операционной системе, включая NT и w2k, а, значит, никаких грязных хаков не использует. Харон по определению гений!

Вот мы и столкнулись с тем самым случаем, когда приходится дизассемблировать не готовым дизассемблером, а своими собственными руками и головой! Тяпнув для храбрости пивка, запускаем Иду и загружаем нашего подопытного в бинарном режиме, то есть без анализа заголовков файла. Файл, естественно, успешно загружается. Теперь, открываем свой MSDN на странице "Microsoft Portable Executable and Common Object File Format Specification" и вдумчиво читаем все, что в там написано. Без четкого представления о структуре и порядке загрузке PE-файлов Харонову защиту нам ни за что не сломать. Если чтение фирменных спецификаций вызывает проблемы, попробуйте обратится к сторонним источникам. В том же MSDN содержится масса статей, посвященных исследованию PE?формата, в частности: "The Portable Executable File Format from Top to Bottom" by Randy Kath, русский перевод которой ("Исследование переносимого формата исполнимых файлов сверху вниз") легко найти в Сети. На худой конец можно обойтись и одним лишь заголовочным файлом WINNT.H, входящим в штатный комплект поставки любого windows-компилятора (но разобраться с "голым" WINNT.H сумеет лишь гений!)

Наша задача состоит в том, чтобы вручную проанализировать все заголовки, все секции и все поля исследуемого файла, пытаясь определить: что же такого необычного есть в каждом из них. Спрашиваете: "необычное" - это вообще как? Навскидку можно предположить по крайней мере три варианта: а) защита использует документированные, но малоизвестные возможности PE?файлов, не поддерживаемые распространенными дизассемблерами; б) защита использует недокументированные особенности (и/или поля) PE?файлов, не поддерживаемые дизассемблерами, но корректно обрабатываемые операционной системой; в) разночтения спецификаций PE?формата привели к тому, что разработчики ОС трактовали отдельные поля заголовков по-своему, а разработчики дизассемблеров - по-своему, в результате чего появилась возможность создать такой извращенный файл, корректно загрузить который сумеет одна лишь система, а все остальные исследовательские программы конкретно обломаются на его анализе.

Из пункта "а" со всей очевидностью следует, что для анализа защищенного файла одной лишь документации явно недостаточно, ведь нам требуется не только убедиться в соответствии всех полей исследуемого файла фирменной спецификации, но и выяснить: насколько эти поля вообще типичны. Другими словами нам необходим практический опыт работы с PE-файлами, а если его нет, - что ж, возьмите несколько заведомо неизвращенных PE-файлов и основательно проштудируйте их от пола до потолка.

С пунктом "б" справится сложнее. Допустим, в фирменной спецификации такое-то поле помечено как неиспользуемое, а в защищенном файле здесь прописано некоторое значение. Как быть? (Дизассемблировать загрузчик операционной системы не предлагать). Да очень просто! Берем hiew старой версии - той, которая ничего не знает о PE и никак его не анализирует - и перебиваем "неиспользуемое" поле нулями или любым другим значением, пришедшимся нам по вкусу. Если это не нарушит работоспособности защищенного файла, - по всей видимости это поле действительно не используется и, соответственно, наоборот.

Пункт "в" еще более сложен. Никакие прямолинейные решения тут не действуют и все, что нам остается - вдумчиво читать каждую букву исходной спецификации и… нет! не стремиться "понять" ее, а пытаться представить себе: как она вообще должна быть понята, чтобы загрузчик операционной системы работал, а дизассемблер - нет. Дайте волю своему воображению, напрягите интуицию - весь многих тонкостей PE-форматов составители документации просто не описали. С другой стороны, сами разработчики ОС данный формат не с потолка брали и по тем же самым спецификациям его и реализовывали. Задумайтесь: а как бы вы реализовали загрузку PE-файла в память? Какие бы комбинации свойств PE-файла вы могли бы использовать для его защиты?

Первое, что нам приходит в голову: инициализация некоторых критических ячеек памяти посредством добавления их адреса в таблицу перемещаемых элементов. А что, это мысль! Особенно привлекательной в этом плане выглядит таблица перемещаемых элементов из old exe - заглушки, расположенной перед PE-файлов и большинством дизассемблеров просто игнорируемой. Но обращает ли системный загрузчик внимание на эти элементы или нет, - вот ведь в чем вопрос! Хорошо, давайте посмотрим на восстановленный old exe заголовок, извлеченный нами из защищенного файла.

seg000:00000000 ; OLD EXE HEADER
seg000:00000000	cc			  db 'MZ'
seg000:00000002	e_cblp			  dw 405
seg000:00000004	e_cp			  dw 1
seg000:00000006	e_crlc			  dw 0
seg000:00000008	e_cparhdr		  dw 4
seg000:0000000A	e_minalloc		  dw 33
seg000:0000000C	e_maxalloc		  dw 33
seg000:0000000E	e_ss			  dw 16h
seg000:00000010	ccaaa			  dw 512
seg000:00000012	e_csum			  dw 0
seg000:00000014	e_ip			  dw 106
seg000:00000016	e_cs			  dw 0
seg000:00000018	e_lfarlc		  dw offset RelocationTable
seg000:0000001A	e_ovno			  dw 0
seg000:0000001C	ae_res			  db 'UniLink!'
seg000:00000024	e_OEMid			  dw 0
seg000:00000026	e_OEMinfo		  dw 1
seg000:00000028	e_res2			  db 14h dup(0)
seg000:0000003C	e_lfanew		  dd offset IMAGE_NT_SIGNATURE_PE ; "PE"

Баста карапузики! Нас обломали! Никаких перемещаемых элементов в DOS-заглушке нет, о чем поле e_ovno красноречиво и свидетельствует (в дизассемблерном листинге оно выделено жирным шрифтом). Да и во всех остальных отношениях, old exe заголовок выглядит вполне корректным и приличным. Ладно, лиха беда начало! Отталкиваясь от значения поля e_lfanew, переходим по содержащемуся в нем смещению на заголовок PE-файла.

seg000:00000198 ; NEW EXE HEADER
seg000:00000198 IMAGE_NT_SIGNATURE_PE db 'PE',0,0	; DATA XREF: seg000:0000003C
seg000:0000019C Machine               dw 14Ch	; IMAGE_FILE_MACHINE_I386
seg000:0000019E NumberOfSection       dw 3		; три секции
seg000:000001A0 TimeDateStamp         dd 3D4EE158h	; временная метка
seg000:000001A4 PointerToSymbolTable  dd 0		; указатель на таблицу символов
seg000:000001A8 NumberOfSymbols       dd 0		; кол-во символов ноль, т.е. нет
seg000:000001AC SizeOfOptionalHeader  dw 0C0h	; размер опционального заголовка
seg000:000001AC ;           а вот это   ^^^^ уже интересно: зная, за концом
seg000:000001AC ; опционального заголовка сразу же следуют заголовки сегментов,
seg000:000001AC ; пытаемся проверить корректность этого поля "на глаз":
seg000:000001AC ; складываем 0x1B0 (начало опционального заголовка) c 0xC0
seg000:000001AC ; (указанный размер заголовка) и получаем 0x270.
seg000:000001AC ; смотрим - по этому смещению в файле расположено слово ".text",
seg000:000001AC ; значит, размер заголовка указан правильно.
seg000:000001AC ; Но… в то же самое время 0xC0 - это крайне нетипичный размер для
seg000:000001AC ; опционального заголовка и все, исследуемые мной файлы, содержали
seg000:000001AC ; совсем другое значение, - а именно 0xE0.
seg000:000001AC ; за счет чего же "наш" заголовок оказался меньше? очевидно,
seg000:000001AC ; защищенный файл содержит урезанный массив data directory, что
seg000:000001AC ; теоретически должно восприниматься всеми дизассемблерами нормально,
seg000:000001AC ; но вот полной увечности у нас в этом нет. Как быть? Представляется
seg000:000001AC ; логичным найти (или создать) PE-файл с урезанной data directory
seg000:000001AC ; и натравить на него дизассемблер (ту же IDA) - интересно зависнет
seg000:000001AC ; он или нет? А вот как создать такой файл, не имея под руками
seg000:000001AC ; соответствующего линкера? Просто пропадчить заголовок в готовом
seg000:000001AC ; PE-файле нельзя, т. к. за концом data directory загрузчик ожидает
seg000:000001AC ; увидеть каталог сегментов, а при "искусственном" уменьшении размера
seg000:000001AC ; заголовка там окажется "хвост" от data directory, что приведет
seg000:000001AC ; дизассемблер в сильное замешательство. "вырезать" кусочек
seg000:000001AC ; data directory из файла так же невозможно, ведь при этом посыплются
seg000:000001AC ; все смещения, что так же приведет к непредсказуемой реакции
seg000:000001AC ; дизассемблера при попытке анализа такого файла. А если… Постойте-ка!
seg000:000001AC ; ведь можно просто сдвинуть каталог сегментов на место
seg000:000001AC ; "освободившихся" после усечения заголовка элементов data directory?!
seg000:000001AC ; а знаете, это должно сработать! ОК, вооружившись hiew'ом усекаем
seg000:000001AC ; размер заголовка любого заведомо нормального файла до 0xC0 и
seg000:000001AC ; перемещаем каталог сегментов на 0x20 байт "вверх". Запускаем сам
seg000:000001AC ; фал. Работает? Работает! Загружаем файл в дизассемблер… Работает!!!
seg000:000001AC ; ОК, значит, размер заголовка в 0xC0 действительно допустим
seg000:000001AC ; продолжаем анализ….
seg000:000001AC ;
seg000:000001AE Characteristics        dw 30Fh	; IMAGE_FILE_RELOCS_STRIPPED|
seg000:000001AE					; IMAGE_FILE_EXECUTABLE_IMAGE|
seg000:000001AE 					; IMAGE_FILE_LINE_NUMS_STRIPPED|
seg000:000001AE 					; IMAGE_FILE_32BIT_MACHINE |
seg000:000001AE					; IMAGE_FILE_DEBUG_STRIPPED
seg000:000001AE ; атрибуты файла несколько нетипичны. обычно встречается 0x10F,
seg000:000001AE ; а не 0x30F (т.е. в нормальных файлах отсутствует флаг 
seg000:000001AE ; IMAGE_FILE_DEBUG_STRIPPED даже когда они не содержат никакой
seg000:000001AE ; отладочной инфы), но с другой стороны, так даже и правильнее.
seg000:000001AE ; Эксперименты показывают, что исправление 0x10F на 0x30F в
seg000:000001AE ; остальных файлах (ес-но без дебужной инфы) проходит безболезненно,
seg000:000001AE ; значит, собака зарыта не здесь

Вот мы и выяснили, что PE-заголовок защищенного файла не содержит абсолютно ничего интересно, и если кто и завешивает HIEW и срывает IDA крышу, то уж точно не он. Что ж, сделав короткий перерыв (для "пивка"), продолжим наше утомительное исследование формата PE-файла, на сей раз взявшись за так называемый опциональный заголовок (optional header), следующий за концом PE-заголовка.

seg000:000001B0 ; ОПЦИОНАЛ ХИДЕР
seg000:000001B0 ; ==============
seg000:000001B0 Magic                  dw 10Bh	; NORMAL EXE (все ОК)
seg000:000001B2 MajorLinkerVersion     db 1		; версия линкера
seg000:000001B3 MinorLinkerVersion     db 3		; версия линкера
seg000:000001B4 SizeOfCode             dd 49817h	; размер кода 
seg000:000001B4					; выглядит вполне нормально.
seg000:000001B4					; т. е. при длине exe-файла в
seg000:000001B4					; 0x4C7AA байт, потребности в
seg000:000001B4					; 0x49817 байт вполне
seg000:000001B4					; удовлетворяются
seg000:000001B4
seg000:000001B8 SizeOfInitializedData   dd 3008h	; размер секции
seg000:000001B8					; инициализированных данных
seg000:000001B8					; выглядит вполне нормально
seg000:000001B8
seg000:000001BC SizeOfUninitializedData dd 0		; нет секции
seg000:000001BC					; неинициализированных данных
seg000:000001C0 AddressOfEntryPoint    dd 46673h	; адрес	точки входа
seg000:000001C4 BaseOfCode		  dd 1000h	; базовый адрес сегмента кода,
seg000:000001C4 					; забегая вперед, отметим,
seg000:000001C4					; что этот адрес в точности равен
seg000:000001C4					; адресу сегмента .text, так что
seg000:000001C4					; тут все законно
seg000:000001C4
seg000:000001C8 BaseOfData              dd 4B000h	; базовый адрес сегмента данных,
seg000:000001C8 					; проверка подтверждает его
seg000:000001C8 					; корректность
seg000:000001C8
seg000:000001CC ImageBase               dd 400000h	; image base абсолютно нормальный
seg000:000001D0 SectionAlignment        dd 1000h	; выравнивание секций по границе
seg000:000001D0					; в 4Кб, что ОК
seg000:000001D0
seg000:000001D4 FileAlignment           dd 200h	; выравнивание файла по границе 
seg000:000001D4 					; в 512 байт, что ОК
seg000:000001D8 MajorSysVersion         dw 4		; версия требуемой системы, ОК
seg000:000001DA MinorSysVersion         dw 0		; ОК
seg000:000001DC MajorImageVersion       dw 1		; версия приложения, ОК
seg000:000001DE MinorImageVersion       dw 0		; OK
seg000:000001E0 MajorSubsystemVersion   dw 4		; версия подсистемы, ОК
seg000:000001E2 MinorSubsystemVersion   dw 0		; OK
seg000:000001E4 Win32VersionValue       dd 0		; OK
seg000:000001E8 SizeOfImage             dd 52000h	; размер образа файла в памяти
seg000:000001E8 					; выглядит вполне достоверно
seg000:000001E8
seg000:000001EC SizeOfHeaders           dd 400h	; размер всех заголовков, ОК
seg000:000001F0 CheckSum                dd 0		; нет контрольной суммы, ОК
seg000:000001F4 Subsystem               dd 3		; кол-во секций, ОК
seg000:000001F4 					; (дальше мы их все найдем)
seg000:000001F4
seg000:000001F8 SizeOfStackReserve      dd 100000h	; кол-во резервируемой памяти
seg000:000001F8 					: под стек, ОК
seg000:000001F8
seg000:000001FC SizeOfStackCommit       dd 2000h	; кол-во выделенной под стек
seg000:000001FC 					; памяти, ОК
seg000:000001FC
seg000:00000200 SizeOfHeapReserve       dd 100000h	; кол-во резервируемой под кучу
seg000:00000200 					; памяти, ОК
seg000:00000200
seg000:00000204 SizeOfHeapCommit        dd 1000h	; кол-во выделенной под кучу 
seg000:00000204					; памяти, ОК
seg000:00000204
seg000:00000208 LoaderFlags             dd 0		; не используется, ОК
seg000:0000020C NumberOfRvaAndSizes     dd 0Ch	; кол-во элементов в 
seg000:0000020C					; IMAGE_DATA_DIRECTORY

…и опциональный заголовок не содержит ничего интересного, но вот IMAGE DATA DIRECTORY, расположенная за ним следом, - дело другое и буквально с третий по счету строки мы выходим на след защиты:

seg000:00000210 IMAGE_DATA_DIRECTORY   dd 0		; EXPORT dir
seg000:00000214                        dd 0
seg000:00000218
seg000:00000218 Import Table
seg000:00000218                        dd offset IMPORT_TABLE ;

Вот она - ссылка на таблицу импорта, - ту самую таблицу, которая приводит к буйному замешательству огромное количество дизассемблеров и срывает крышу всем PE-утилитам вместе взятым. Посмотрим на нее?

seg000:0004B000 IMPORT_TABLE              dd 94010F0Eh  ; DATA XREF: seg000:00000218^o
seg000:0004B000                                         ; flags
seg000:0004B004                           dd 4000696h   ; date start
seg000:0004B008                           dd 54414C46h  ; foward index
seg000:0004B00C                           dd offset unk_39A39
seg000:0004B010                           dd 8965410h   ; import addres
seg000:0004B014

Пошла вода в хату! Оказывается, в таблице импорта вместо нормальных полей содержится какой-то голимый "мусор", который кое-что проясняет. С такой таблицей импорта дизассемблеры работать просто не могут и… если проверка корректности содержимого таблицы импорта отсутствует, они - виснут, в противном же случае, - аварийно прерывают свою работу с сообщением об ошибке.

Но это совершенно не объясняет как с такой защитой ухитряется работать загрузчик операционной системы? Уж не имеем ли мы дело с некоторыми недокументированными особенностями? Или, быть может, по этим "мусорным" адресам в оперативной памяти расположено что-то особенное? Последнее навряд ли! Поскольку защита успешно функционирует во всех windows-подобных системах, представляется сомнительным, что содержимое данных адресов всегда и везде одно и то же (кстати, беглая проверка отладчиком, это допущение с треском опровергает). Недокументированные возможности? Хм, непохоже… да если так - где прикажите искать реально импортируемые адреса?! Ладно, двигаемся дальше, может быть нам и повезет…

seg000:00000268 ; Bound Import
seg000:00000268                         dd offset bound_import_table
seg000:0000026C                         dd 1Ch

Ага! Держи Тигру за хвост! Защита использует документированное, но малоизвестное поле bound import, - представляющее собой альтернативный механизм импорта функций из DLL. Смотрим, что у нас там…

seg000:000002E8 ; bound import table
seg000:000002E8 TimeDateStamp          dd 0FFFFFFFFh	; DATA XREF: seg000:0000268
seg000:000002EC OffsetModuleName       dw 0Eh		; относительное смещение
seg000:000002EC 						; строки, содержащей имя
seg000:000002EC 						; импортируемой DLL
seg000:000002EC 						; 0x2E8 + 0xE == 0x2F6
seg000:000002EC 						; где мы обнаруживаем
seg000:000002EC 						; "kernel32.dll", что
seg000:000002EC 						; очевидно, уже не мусор!
seg000:000002EC 
seg000:000002EE NumberOfModuleForward   dw 0			; ничего не импортируем?!
seg000:000002F0 Reserverd               dw 0
seg000:000002F2                         dd 0
seg000:000002F6 aKernel32_dll           db 'kernel32.dll',0 ; DATA XREF: seg000:049E0C

Вот это уже явно не мусор, а вполне удобоваримая таблица импорта, загружающая динамическую библиотеку kernel32.dll, и импортирующая…. Как это так - никаких функций?! Странно… Но ведь защита все-таки работает (пусть час от часу становится все менее и менее понятно как). Хорошо, давайте рассуждать логически. Программ, не импортирующих никаких функций, под Windows NT существовать в принципе не может. Даже если защита использует native API (т. е. обращается к системным функциям напрямую через прерывание 2Eh), операционный загрузчик окажется не в состоянии загрузить такое приложение, поскольку ему необходимо, чтобы на адресное пространство загружаемого процесса была спроецирована библиотека kernel32.dll. Это в Windows 9x, где системные библиотеки автоматически отображаются на адресные пространства процессов, "голые" файлы работают безо всяких проблем, а в NT, отображающий только явно загруженные библиотеки, такой фокус уже не проходит. А, знаете, это многое объясняет! Теперь становится понятно в частности почему таблица импорта не содержит в себе ни одной функции, - они просто не нужны! Ссылка на kernel32.dll присутствует лишь затем, чтобы спроецировать эту библиотеку на адресное пространство процесса, как этого требует системный загрузчик. Хорошо, но как быть с "мусором" в стандартной таблице импорта? Как ни крути, а такие извращения системный загрузчик скорее удавится, чем обработает… Увы, нам нечего ответить на этот вопрос и, скрепя сердце, его вновь приходится откладывать, надеясь, что последующий анализ отделит свет от тьмы и все расставит по своим местам…

seg000:00000270 ; НАЧАЛО СЕГМЕНТОВ
seg000:00000270 a_text                db '.text',0,0,0
seg000:00000278 vir_size_text         dd 49817h	; размер секции text в памяти
seg000:0000027C virt_addr_text        dd 1000h	; адрес проекции на память
seg000:00000280 szRawData_text        dd 49810h	; размер в файле
seg000:00000284 pRawData_text         dd 400h	; смещение начала секции в файле
seg000:00000288 pReloc_text           dd 0
seg000:0000028C pLineNum_text         dd 0
seg000:00000290 nReloc_text           dw 0
seg000:00000292 nLineNum_text         dw 0
seg000:00000294 FLAG_TEXT             dd 60000020h	; code | executable | readable

Вот мы и добрались до каталога сегментов! IMAGE HEADER секции ".text" выглядит вполне типично и никаких подозрений у нас не вызывает, но вот следующая за ним секция ".data" очень многое прояснеет…

seg000:00000298 a_data               db '.data',0,0,0
seg000:000002A0 vir_size_data        dd 3008h	; размер секции .data в памяти
seg000:000002A4 vir_addr_data        dd 4B000h	; адрес	проекции на память
seg000:000002A8 szRawData_data       dd 14h		; размер в файле
seg000:000002AC pRawData_data        dd 49E00h	; смещение в файле
seg000:000002B0 pReloc_data          dd 0
seg000:000002B4 pLineNum_data        dd 0
seg000:000002B8 nReloc_data          dw 0
seg000:000002BA nLineNum_data        dw 0
seg000:000002BC FLAG_DATA            dd 0C0000040h	; readable | writeable

Ну и что здесь интересного? - спросит иной читатель. А вот что - присмотритесь повнимательнее куда именно грузится содержимое данной секции. Если верить выделенной жирным шрифтом строке, - то по адресу IMAGE_BASE + 0x4B000. Ничего не напоминает? Во-первых, адрес 0x4B000 "волшебным" образом совпадает с адресом "мусорной" таблицы импорта (те, кто поимел сект с защитой этот адрес надолго запомнят, кстати Харону не мешало бы его немножко замаскировать, чтобы он не так бросался в глаза). Во-вторых, изобразив процесс проецирования секций графически (см. рис. 0x05) мы с удивлением обнаружим, что секция .data расположена не следом за секцией .text (как это обычно и бывает), а находится внутри нее. Действительно, давайте подсчитаем: виртуальный адрес секции .text равен 0x1000, а ее размер - 0x49817, и последний байт секции приходится на адрес 0x59817, что превышает виртуальный адрес секции .data, равный 0x4B000.

Так вот оно что! Поскольку, секции отображаются на память в порядке их перечисления в каталоге (недокументированно, но факт!), то содержимое секции .data затирает область адресов 0x4B000 - 0x4E008! А что там у нас расположено?! ТАБЛИЦА ИМПОРТА!!! В дисковом файле по смещению 0x4B000 действительно расположен чистейшей воды мусор (и это косвенно подтверждается тем, что изменения первых 0x14 байт работу программы не нарушают), а истинная таблица импорта расположена непосредственно в секции .data, которой соответствует смещение 0x49E00 дискового файла. Заглянем: что у нас там?!

seg000:00049E00 RealImportTable           dd offset IAT ; OriginalFirstThunk
seg000:00049E04 TimeDateStamp1            dd 1
seg000:00049E08 ForwarderChain            dd 0FFFFFFFFh ; no forward
seg000:00049E0C Name                      dd offset aKernel32_dll ; "kernel32.dll"
seg000:00049E10 FirstThunk                dd offset IAT

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

seg000:0004B014 IAT                       dd 47440600h  ; DATA XREF: seg000:00049E00^o
seg000:0004B014                                         ; seg000:00049E10^o
seg000:0004B018                           dd 50554F52h
seg000:0004B01C                           dd 69A8Bh
seg000:0004B020                           dd 0FF03FF11h
seg000:0004B024                           db    2 ;
seg000:0004B025                           db  4Ch ; L

Мать родная! Ну почему ты не родишь меня обратно?! Опять вместо символических имен или на худой конец - ординалов, нам попадается этот проклятый мусор! Хотя, - подождите минуточку - давайте попробуем определить что будет расположено по данному адресу после загрузки программы. Возвращаясь к описанию секции .data, мы обнаруживаем, что упустили один очень важный момент. Виртуальный размер секции .data (0x3008 байт) намного больше ее физического размера (0x14 байт) и потому, регион 0x4B014 - 49E008 будет заполнен нулями, а ведь "мусорная" IAT как раз и расположена по адресу 0x4B014! Следовательно, после загрузки ее содержимое окажется заполнено одними нулями, что соответствует пустой таблице импорта функций. Фу-х! Невероятно, но мы действительно в этом разобрались!!! Кстати, подобный прием и широко используется авторами упаковщиков исполняемых файлов.

seg000:000002C0 seg000:000002C0 b_rsrc			  db '.rsrc',0,0,0
seg000:000002C8 vir_size_rsc    dd 27ACh	; размер секции	rsrc в памяти
seg000:000002CC vir_addr_rsc    dd 4F000h	; адрес	проекции на память
seg000:000002D0 szRawData_rsc   dd 27ACh	; размер в файле
seg000:000002D4 pRawData_rsc    dd 4A000h	; смещение секции в файле
seg000:000002D8 pReloc_rsc      dd 0
seg000:000002DC pLineMun_rsc    dd 0
seg000:000002E0 nReloc_rsc      dw 0
seg000:000002E2 nLineNum_rsc    dw 0
seg000:000002E4 FLAG_RSC        dd 50000040h	; initalized data |
seg000:000002E4 				; shareable |	readable

Аналогичным образом поступает и секция .rsrc, внедрясь в середину секции .text (но секцию .data она не перекрывает), причем, для ослепления некоторых дизассемблеров тут используется еще один хитрый примем: указанный "физический" размер секции .rsrc "вылетает" за пределы дискового файла. Системному загрузчику - хоть бы что, а вот некоторые исследовательские утилиты от этого и крышей поехать могут.


Динамическое замещение таблицы импорта в процессе загрузки PE-файла

Настало время проверить наши предположения на практике. Давайте загрузим эту извращенную программу отладчиком и посмотрим что содержится в памяти по адресу IMAGE_BASE + 0x4B000 = 0x44B000: мусор или нормальная таблица импорта? Отладчик soft-ice (как это и следовало ожидать) обламывается с отладкой этого извращенного файла, просто проскакивая точку входа, а вот WDB сполна оправдывая репутацию фирмы Microsoft (это не ирония!), пусть и не без ругательств, но все-таки загружает наш подопытный файл и послушно останавливается в точке входа.

Module Load: F:\IDAP\HARON\ulink.exe  (symbol loading deferred)
Thread Create:  Process=0, Thread=0
Module Load: C:\WINNT\SYSTEM32\ntdll.dll  (symbol loading deferred)
Module Load: C:\WINNT\SYSTEM32\kernel32.dll  (symbol loading deferred)
Module Load: C:\WINNT\SYSTEM32\ntdll.dll  (could not open symbol file)
Module Load: F:\IDAP\HARON\ulink.exe  (could not open symbol file)
Module Load: C:\WINNT\SYSTEM32\kernel32.dll  (could not open symbol file)
Stopped at program entry point

Обратите внимание на выделенную жирным шрифтом строку: отладчику показалось, что отлаживаемая программа импортирует некоторые функции… из самой себя! Но мы-то, излазившие защищенный файл вдоль и поперек, хорошо знаем, что за исключением kernel32.dll, никаких других экспортируемых и/или импортируемых библиотек здесь нет и такое поведение отладчика, судя по всему, объясняется все тем же самым "мусором". ОК, переключаем свое внимание на окно с дампом памяти, заставляя ее отобразить содержимое таблицы импорта:

0x0023:0x0044B000 14 b0 04 00 01 00 00 00 ff ff ff ff f6 02 00 00 ................
0x0023:0x0044B010 14 b0 04 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0023:0x0044B020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Ура! Открываем на радостях пиво! Содержимое памяти доказательно подтверждает, что загрузка файла действительно происходит именно так, как мы и предполагали! Хорошо, но что же нам теперь делать? То бишь, найти-то причину помешательства дизассемблеров мы нашли, но вот как ее нейтрализовать? Ну, это не вопрос! Достаточно лишь скопировать 0x14 байт памяти с адреса 0x49E00 по адресу 0x4B000 и скорректировать ссылку на IAT, направив ее на любое, заполненное нулями, место.

…HIEW теперь заглатывает защищенную программу и даже не пикает! А IDA… а IDA по прежнему отказываться обрабатывать этот файл и с завидным упорством слетает. В чем же причина? Вы, конечно, будете смеяться, но истинный виновник есть ни кто иной как Microsoft! Если бы не ее жутко прогрессивная платформа NET… А, впрочем, чего это я разворчался? Сами смотрите:

 (o) Microsoft.Net assembly [pe.ldw]
 ( ) Portable executable for IBM PC (PE) [pe.ldw]
 ( ) MS-DOS executable (EXE) [dos.ldw]
 ( ) Binary file

Вот это да! С роду такого не было! Чтобы IDA да не правильно опознала формат файла!!! Перемещаем радио-кнопку на одну позицию вниз (ведь мы имеем дело отнюдь не с Microsoft Net assembly, а с PE!) и… IDA успешно открывает файл. Причем, с восстановлением таблицы импорта можно было и не возиться, - IDA просто ругнулась на мусор и все! Но кто ж знал?! Задним умом все мы крепки…

Короче, возвращаясь к нашим баранам (в данном случае - к терпеливо ожидающему нас отладчику) в точке входа дизассемблерный текст выглядит так:

00446673 55               push        ebp
00446674 68AECF4200       push        42CFAEh
00446679 8BDC             mov         ebx,esp
0044667B 2403             and         al,3
0044667D 7203             jb          00446682
0044667F FE4302           inc         byte ptr [ebx+2]
00446682 D7               xlat        byte ptr [ebx]
00446683 27               daa
00446684 81042453970000   add         dword ptr [esp],9753h
0044668B 1AC9             sbb         cl,cl
0044668D 9F               lahf
0044668E FF33             push        dword ptr [ebx]
00446690 FC               cld
00446691 C3               ret

Не очень-то это похоже на осмысленный код программы! Может быть, это снова мусор? Маловероятно, - ведь отладчик использует штатный системный загрузчик PE-файлов и потому показывает образ файла таким, какой он в действительности есть, ну… если, конечно, защита тем или иным образом не противостоит отладке. Ладно, отставив разговорчики в строю, начинам трассировать код и… с первых же строк впадаем в некоторое замешательство. Защита опрашивает начальное значение регистра EAX, которое (если верить отладчику!) как будто бы равно нулю, но полной уверенности в этом у нас нет, - еще со времен старушки MS-DOS многие отладчики славились тем, что самостоятельно инициализировали регистры после загрузки, чем и выдавали себя (в частности, при нормальной загрузке файла регистр SI содержал в себе адрес первой исполняемой команды, а при загрузке под отладчиком Turbo Debugger и иже с ним, был равен нулю). Вообще-то, закладываться на "предопределенные" значения регистров - дурной тон. Никто не гарантирует, что в следующих версиях Windows что ни будь не изменится, и если такое вдруг произойдет, то защита откажет в работе, обломав не только хакеров, но и легальных пользователей. Впрочем, начальное значение регистра EAX (AX) по жизни равно нулю, и с некоторой натяжной за это можно зацепиться.

Далее защита непонятно зачем увеличивает старшее слово, только что закинутое в стек, на единицу и вызывает абсолютно бесполезные команды XLAT, DAA, ADD, SBB и… загружает регистр флагов в EAX. Уж не пытает ли она этим самым обнаружить флаг трассировки? Затем делает RETN для передачи управления по адресу: (0x42CFAE + 0x10000) + 0x9753 == 0x446701

.text:00446701		       mov     edi, esi
.text:00446703		       mov     esi, ebx
.text:00446705		       sub     dword ptr [esi],	1006Fh
.text:0044670B		       lodsw
.text:0044670D		       bswap   eax
.text:0044670F		       inc     byte ptr	[esi]
.text:00446711		       lodsb
.text:00446712		       mov     ah, al
.text:00446714		       lodsb
.text:00446715		       bswap   eax
.text:00446717		       mov     ebp, eax
.text:00446719		       movzx   ecx, cl
.text:0044671C		       push    dword ptr [ebp+6Bh]
.text:0044671F		       lea     eax, [esi-8]
.text:00446722		       xchg    eax, fs:[ecx]
.text:00446725		       mov     edx, eax
.text:00446727		       inc     edx
.text:00446728		       jz      short loc_44672D
.text:0044672A		       mov     edx, [eax+4]
.text:0044672D 
.text:0044672D loc_44672D:			       ; CODE XREF: .text:00446728 j
.text:0044672D		       xchg    eax, [esp]
.text:00446730		       pushf
.text:00446731		       lea     ebx, [eax+21ADFh]
.text:00446737		       jnz     short loc_446745
.text:00446739		       lea     edi, [edi+0ACh]
.text:0044673F		       mov     dword_44CAF8, edi
.text:00446745 
.text:00446745 loc_446745:			       ; CODE XREF: .text:00446737 j
.text:00446745		       bts     dword ptr [esi-0Ch], 8
.text:0044674A		       jb      short loc_446753
.text:0044674C		       popf
.text:0044674D		       call    $+5
.text:00446752		       retf

…отладчик доходит лишь до RETF и после этого сразу же "дохнет". К тому же, остается совершенно непонятным, что же собственно делает этот запутанный и витиеватый код? При желании, конечно, с ним можно разобраться, но… нужно ли? Ведь отладить нашу подопытную мы все равного не сможем, во всяком случае в WDB.

Хорошо, зайдем с другого конца. Предположим, что программа работает с операционной системой не напрямую (через native API), а через подсистему win32 (win32 API). Тогда, установив точку останова на любую API-функцию, вызываемому программой, мы автоматически попадем в гущу "нормального" программного кода, уже распакованного (расшифрованного?) защитой. Весь вопрос в том: какие именно API-функции вызывает программа. Ну, пусть это будет GetVersion, - с вызова которой начинается стартовый код практически любой программы. Запускаем soft-ice, нажимаем , даем команду "bpx GetVersion", выходим из отладчика, вызываем unlink.exe и… ничего не происходит! - Отладчик не всплывает! Выходит, исследуемая нами программа не использует GetVersion! Что ж, удаляем предыдущую точку останова и пытаемся "забрейкать" CreateFileA (ну должен ли линкер как-то открывать файлы!!!). Так, , bpx CreateFileA, x… Ура! Это срабатывает! Отладчик перехватывает вызов защищенной программы и, после выхода из тела CreateFileA по команде P RET (в CreateFileA для нас действительно нет ничего интересного), мы оказывается в следующем коде:

001B:00416DEB  CALL    [USER32!CharToOemBuffA]
001B:00416DF1  PUSH    00000104
001B:00416DF6  LEA     EAX,[ESP+08]
001B:00416DFA  PUSH    EAX
001B:00416DFB  LEA     EDX,[ESP+0C]
001B:00416DFF  PUSH    EDX
001B:00416E00  CALL    [KERNEL32!GetShortPathNameA]
001B:00416E06  TEST    EAX,EAX
001B:00416E08  JZ      00416E2B
001B:00416E0A  LEA     EDX,[ESP+04]
001B:00416E0E  PUSH    00
001B:00416E10  PUSH    27
001B:00416E12  PUSH    03
001B:00416E14  PUSH    00
001B:00416E16  PUSH    01
001B:00416E18  PUSH    80000000
001B:00416E1D  PUSH    EDX
001B:00416E1E  CALL    [KERNEL32!CreateFileA]
001B:00416E24  MOV     EBX,EAX
001B:00416E26  CMP     EBX,-01
001B:00416E29  JNZ     00416E35
001B:00416E2B  CALL    [KERNEL32!GetLastError]
001B:00416E31  MOV     ESI,EAX
001B:00416E33  JMP     00416E5B

Обратите внимание: несмотря на отсутствие таблицы импорта, программа каким-то загадочным образом все-таки импортирует из kernell32.dll все, необходимые ей API-функции. Очень хорошо! Секс с native API и прочими извратами программистской хитрости отменяется! И мы остаемся в среде привычной нам подсистемы win32 API. Как именно осуществляется импорт - вот это уже другой вопрос! Кстати, давайте заглянем в одну такую функцию дизассемблером:

.text:00416E18           push 80000000h
.text:00416E1D           push edx
.text:00416E1E           call dword_44CC20 ; в отладчике это было KERNEL32!CreateFileA
.text:00416E24           mov  ebx, eax
.text:00416E26           cmp  ebx, 0FFFFFFFFh
.text:00416E29           jnz  short loc_416E35
…
.data:0044CC14 dword_44CC14   dd ?                    ; DATA XREF: sub_416DA0+AD^r
.data:0044CC14                                        ; sub_416DA0+F9^r ...
.data:0044CC18 dword_44CC18   dd ?                    ; DATA XREF: .text:0041A10E^r
.data:0044CC1C dword_44CC1C   dd ?                    ; DATA XREF: .text:0041A1AA^r
.data:0044CC20 dword_44CC20   dd ?                    ; DATA XREF: sub_416DA0+7E^r
.data:0044CC20                                        ; sub_416F3C+AB^r
.data:0044CC24 dword_44CC24   dd ?                    ; DATA XREF: sub_416DA0+DF^r
.data:0044CC24                                        ; sub_416F3C+128^r
.data:0044CC28 dword_44CC28   dd ?                    ; DATA XREF: sub_416F3C+1AE^r
.data:0044CC28                                        ; sub_417158+F1^r ...
.data:0044CC2C dword_44CC2C   dd ?                    ; DATA XREF: sub_419DD8+3C^r
.data:0044CC2C                                        ; sub_41AD20+12E^r ...
.data:0044CC30 dword_44CC30   dd ?                    ; DATA XREF: .text:004014C4^r
.data:0044CC34 dword_44CC34   dd ?                    ; DATA XREF: sub_419DD8+31^r
.data:0044CC34                                        ; .text:0041A3E5^r ...
.data:0044CC38 dword_44CC38   dd ?                    ; DATA XREF: sub_419DD8+1E^r
.data:0044CC38                                        ; .text:0041A3A4^r ...

Смотрите! В дисковом файле адресов импортируемых функций просто нет и таблица импорта судя по всему заполняется защитой динамически. А это значит, что в дизассемблере мы просто не сможем разобраться: какая именно функция в какой точке программы вызывается. Или… все-таки сможем?! Достаточно просто скинуть импорт работающей программы в дамп, а затем просто загрузить его в IDA! Затем, отталкиваясь от адресов экспорта, выданных "dumpbin /EXPORTS kernel32.dll", мы без труда приведем таблицу импорта в нормальный вид. Итак, прокручивая экран дизассемблера вверх, находим где у этой таблицы расположено ее начало или нечто на него похожее (если мы ошибемся - ничего странного не произойдет, просто часть функций останется нераспознанными и когда мы с ними столкнемся лицом к лицу, эту операцию придется повторять вновь). Вот, кажется, мы нашли, что искали, смотрите:

.data:0044CC09                                         ; sub_43E6D4+22A^r ...
.data:0044CC0A                 db ? ; unexplored
.data:0044CC0B                 db ? ; unexplored
.data:0044CC0C                 db ? ; unexplored
.data:0044CC0D                 db ? ; unexplored
.data:0044CC0E                 db ? ; unexplored
.data:0044CC0F                 db ? ; unexplored
.data:0044CC10                 db ? ; unexplored
.data:0044CC11                 db ? ; unexplored
.data:0044CC12                 db ? ; unexplored
.data:0044CC13                 db ? ; unexplored
.data:0044CC14 dword_44CC14    dd ?                    ; DATA XREF: sub_416DA0+AD^r
.data:0044CC14                                         ; sub_416DA0+F9^r ...
.data:0044CC18 dword_44CC18    dd ?                    ; DATA XREF: .text:0041A10E^r
.data:0044CC1C dword_44CC1C    dd ?                    ; DATA XREF: .text:0041A1AA^r
.data:0044CC20 dword_44CC20    dd ?                    ; DATA XREF: sub_416DA0+7E^r
.data:0044CC20                                         ; sub_416F3C+AB^r
.data:0044CC24 dword_44CC24    dd ?                    ; DATA XREF: sub_416DA0+DF^r
.data:0044CC24                                         ; sub_416F3C+128^r
.data:0044CC28 dword_44CC28    dd ?                    ; DATA XREF: sub_416F3C+1AE^r
.data:0044CC28                                         ; sub_417158+F1^r ...
.data:0044CC2C dword_44CC2C    dd ?                    ; DATA XREF: sub_419DD8+3C^r
.data:0044CC2C                                         ; sub_41AD20+12E^r ...

Условимся считать адрес 0044CC14h началом. Используя точку останова на CreateFileA вновь вламываемся в программу и, отключив окно "data" командой wd, скидываем таблицу импорта в хистори: "d 44CC14". Выходим из Айса, запускаем NuMega Symbol Loader и записываем историю команд в файл winice.log (или любой другой по вашему вкусу). И как со всем этим нам теперь работать? Рассмотрим это на примере функции "call dword_44CC78". Прежде всего мы должны выяснить, какое значение находится в загруженной программе по адресу: 0x44CC87. Открываем winice.log по и смотрим:

0010:0044CC78 77E8668C  77E8F51E  77E93992  77E8DBF8      .f.w...w.9.w...w
0010:0044CC88 77E93F05  77E85493  77E87BE4  77E87D16      .?.w.T.w.{.w.}.w
0010:0044CC98 77E8C0A6  77E8AF8E  77E8878A  77E8BDE8      ...w...w...w...w
0010:0044CCA8 77E94911  77E9499C  77E9138C  77E8D019      .I.w.I.w...w...w

Теперь, обратившись к таблице экспорта kernel32.dll, определяем: а) базовый адрес ее загрузки (в данном случае: 0x77E80000); б) имя функции, сумма RVA и IMAGE BASE которой совпадает со значением 0x77E8668C. Вычитаем из 0x77E8668C базовый адрес загрузки - 0x77E80000 и получаем: 0x668C. Ищем строку 0x668C простым контекстным поиском и…

       302  12D 0000668C GetLastError

…это оказывается ни кто иной, как GetLastError, что и требовалось доказать. Конечно, восстанавливать весь импорт вручную - крайне скучно и утомительно. Но кто нам сказал, что мы должны это делать именно вручную?! Ведь дизассемблер IDA поддерживает скрипты, что позволяет автоматизировать всю рутинную работу (подробнее о языке скрпитов можно прочитать в книге "Образ мышления - дизассемблер IDA" от Криса Касперски, то есть, собственно, меня).

ОК, еще один барьер успешно взять. Воодушевленные успехом и доверху наполненные выпитым во время хака пивом, мы продолжаем! В плане возвращения к нашим баранам, сосредоточим свои усилия на загрузчике таблице импорта, расположенном по всей видимости где-то недалеко от точки входа. Несмотря на то, что soft-ice по-прежнему упорно проскакивает Entry Point, обламываясь с загрузкой защищенного файла (впрочем, другие версии soft-ice с этим справляются на ура), мы можем легко обхитрить защиту просто воткнув в точку входа бряк поинт. Поскольку, бряк поиск должен устанавливаться во вполне определенном контексте, используем уже известную нам нычку с CreateFileA. Итак, "bpx CreateFileA", , запускаем unlink и, когда soft-ice "всплывает" даем: "bpx 0x446673" (адрес точки входа), выходим из soft-ice и… запускаем ulink вновь. Отладчик тут же всплывает:

001B:00446673  55                  PUSH    EBP
001B:00446674  68AECF4200          PUSH    0042CFAE
001B:00446679  8BDC                MOV     EBX,ESP
001B:0044667B  2403                AND     AL,03
001B:0044667D  7203                JB      00446682
001B:0044667F  FE4302              INC     BYTE PTR [EBX+02]
001B:00446682  D7                  XLAT
001B:00446683  27                  DAA

Знакомые места! Трассируем код до тех пор пока на не встретится подозрительный RETF (от RET FAR - далекий возврат), передающий управление по следующему адресу:

001B:77F9FB90  8B1C24              MOV     EBX,[ESP]
001B:77F9FB93  51                  PUSH    ECX
001B:77F9FB94  53                  PUSH    EBX
001B:77F9FB95  E886B3FEFF          CALL    77F8AF20
001B:77F9FB9A  0AC0                OR      AL,AL
001B:77F9FB9C  740C                JZ      77F9FBAA
001B:77F9FB9E  5B                  POP     EBX
001B:77F9FB9F  59                  POP     ECX

Судя по адресу, этот код принадлежит непосредственно самой операционной системе (а точнее - NTDLL.DLL) и представляет собой функцию KiUserExceptionDispatcher. Но что это за функция? Ее описание отсутствует в SDK, но поиск по MSDN обнаруживает пару статей Мета Питтрека, посвященных механизмам функционирования SEH и функции KiUserExceptionDispatcher в частности.

Структурные исключения! Ну конечно же! Какая защита обходится без них! Ладно, разберемся, ворчим мы себе под нос, продолжая трассировку защиты дальше. Увы! В той же точке, где WDB терял над программой контроль, soft-ice просто слетает. Ах, вот значит как!!! Ну, защита, держись!!!

(продолжение следует)

2002-2013 (c) wasm.ru