Примеры реальных взломов: 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 заголовок, извлеченный нами из защищенного файла.
Код (Text):
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-файла.
Код (Text):
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-заголовка.
Код (Text):
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, расположенная за ним следом, - дело другое и буквально с третий по счету строки мы выходим на след защиты:
Код (Text):
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-утилитам вместе взятым. Посмотрим на нее?
Код (Text):
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-подобных системах, представляется сомнительным, что содержимое данных адресов всегда и везде одно и то же (кстати, беглая проверка отладчиком, это допущение с треском опровергает). Недокументированные возможности? Хм, непохоже… да если так - где прикажите искать реально импортируемые адреса?! Ладно, двигаемся дальше, может быть нам и повезет…
Код (Text):
seg000:00000268 ; Bound Import seg000:00000268 dd offset bound_import_table seg000:0000026C dd 1ChАга! Держи Тигру за хвост! Защита использует документированное, но малоизвестное поле bound import, - представляющее собой альтернативный механизм импорта функций из DLL. Смотрим, что у нас там…
Код (Text):
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 присутствует лишь затем, чтобы спроецировать эту библиотеку на адресное пространство процесса, как этого требует системный загрузчик. Хорошо, но как быть с "мусором" в стандартной таблице импорта? Как ни крути, а такие извращения системный загрузчик скорее удавится, чем обработает… Увы, нам нечего ответить на этот вопрос и, скрепя сердце, его вновь приходится откладывать, надеясь, что последующий анализ отделит свет от тьмы и все расставит по своим местам…
Код (Text):
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" очень многое прояснеет…
Код (Text):
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 дискового файла. Заглянем: что у нас там?!
Код (Text):
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" и, нажав, на
смотрим:
Код (Text):
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! Следовательно, после загрузки ее содержимое окажется заполнено одними нулями, что соответствует пустой таблице импорта функций. Фу-х! Невероятно, но мы действительно в этом разобрались!!! Кстати, подобный прием и широко используется авторами упаковщиков исполняемых файлов.
Код (Text):
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 (это не ирония!), пусть и не без ругательств, но все-таки загружает наш подопытный файл и послушно останавливается в точке входа.
Код (Text):
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, никаких других экспортируемых и/или импортируемых библиотек здесь нет и такое поведение отладчика, судя по всему, объясняется все тем же самым "мусором". ОК, переключаем свое внимание на окно с дампом памяти, заставляя ее отобразить содержимое таблицы импорта:
Код (Text):
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… А, впрочем, чего это я разворчался? Сами смотрите:
Код (Text):
(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 просто ругнулась на мусор и все! Но кто ж знал?! Задним умом все мы крепки…
Короче, возвращаясь к нашим баранам (в данном случае - к терпеливо ожидающему нас отладчику) в точке входа дизассемблерный текст выглядит так:
Код (Text):
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):
.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 для нас действительно нет ничего интересного), мы оказывается в следующем коде:
Код (Text):
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):
.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", мы без труда приведем таблицу импорта в нормальный вид. Итак, прокручивая экран дизассемблера вверх, находим где у этой таблицы расположено ее начало или нечто на него похожее (если мы ошибемся - ничего странного не произойдет, просто часть функций останется нераспознанными и когда мы с ними столкнемся лицом к лицу, эту операцию придется повторять вновь). Вот, кажется, мы нашли, что искали, смотрите:
Код (Text):
.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 по
и смотрим:
Код (Text):
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 простым контекстным поиском и…
Код (Text):
302 12D 0000668C GetLastError…это оказывается ни кто иной, как GetLastError, что и требовалось доказать. Конечно, восстанавливать весь импорт вручную - крайне скучно и утомительно. Но кто нам сказал, что мы должны это делать именно вручную?! Ведь дизассемблер IDA поддерживает скрипты, что позволяет автоматизировать всю рутинную работу (подробнее о языке скрпитов можно прочитать в книге "Образ мышления - дизассемблер IDA" от Криса Касперски, то есть, собственно, меня).
ОК, еще один барьер успешно взять. Воодушевленные успехом и доверху наполненные выпитым во время хака пивом, мы продолжаем! В плане возвращения к нашим баранам, сосредоточим свои усилия на загрузчике таблице импорта, расположенном по всей видимости где-то недалеко от точки входа. Несмотря на то, что soft-ice по-прежнему упорно проскакивает Entry Point, обламываясь с загрузкой защищенного файла (впрочем, другие версии soft-ice с этим справляются на ура), мы можем легко обхитрить защиту просто воткнув в точку входа бряк поинт. Поскольку, бряк поиск должен устанавливаться во вполне определенном контексте, используем уже известную нам нычку с CreateFileA. Итак, "bpx CreateFileA",
, запускаем unlink и, когда soft-ice "всплывает" даем: "bpx 0x446673" (адрес точки входа), выходим из soft-ice и… запускаем ulink вновь. Отладчик тут же всплывает:
Код (Text):
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 - далекий возврат), передающий управление по следующему адресу:
Код (Text):
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 просто слетает. Ах, вот значит как!!! Ну, защита, держись!!!
(продолжение следует) © Крис Касперски
Примеры реальных взломов: UniLink v1.03 от Юрия Харона
Дата публикации 8 апр 2003