Примеры реальных взломов
Вид материала | Документы |
- Учебник: / Страницы: , 162.62kb.
- В. А. Кулаков московский инженерно-физический институт (государственный университет), 28.6kb.
- И. Закарян И. Филатов, 3215.74kb.
- Задачи : Формирование выборки стран, сбор реальных данных ввп на душу населения 1990-2007, 59.29kb.
- Программа по курсу «Функциональный анализ», 36.73kb.
- 11 готовых сочинений на лингвистическую тему, 59.56kb.
- Экзаменационная программа по стилистике русского языка понятие стиль. Характеристика, 20.43kb.
- Визуализатор алгоритма программа, наглядно демонстрирующая работу алгоритма в пошаговом, 57.48kb.
- Методика по расчету (оценке) общего объема денежных доходов и реальных располагаемых, 252.43kb.
- Планирование аудита и основные его этапы. Тесты средств контроля и примеры их применения, 10.84kb.
таинства stealth импорта API-функций (часть II)
или как устроена HaronGetProcAddress
Выполнение функции HaronGetProcAddress начинается с загрузки смещения PE заголовка в регистр EAX (MOV EAX, [EBP +3Ch]) с использованием регистра EBP в качестве базового указателя на адрес загрузки динамической библиотеки в памяти. Затем полученное смещение используется для вычисления указателя на IMAGE_DIRECTORY (MOV EDI, [EAX + EBP +78h]), содержащую среди прочего и смещение таблицы экспорта, которая хранится в пером же ее элементе. Следовательно, машинная команда LEA EAX, [EAX +EBP +78h] и загружает указатель на EXPORT_TABLE в регистр EDI. Размер таблицы экспорта загружается в регистр EAX машинной командной MOV EAX, [EAX + EBP 7C], а затем посредством алгебраического сложения размера таблицы экспорта с указателем на ее начало, мы получает указатель на ее конец, который и засылаем в стек. В листинге $ в квадратных скобках отмечено его смещение относительного плавающего фрейма функции.
Далее тем же самым Макаром Харон получает и засылает в стек: адрес ASCIIZ-строки, содержащей имя DLL-файла, количество входов в export address table и name pointer table. Видите, как все просто!
001B:0044658D PUSH EBP [-38h]
001B:0044658E MOV EAX,[EBP+3C] ; смещение PE-заголовка
001B:00446591 MOV EDI,[EAX+EBP+78] ; указатель на export directory
001B:00446595 PUSH EDI [-34h] ;
001B:00446596 OR EAX,EAX ;
001B:00446598 MOV EAX,[EAX+EBP+7C] ; размер export directory
001B:0044659C LEA EAX,[EDI+EAX-01] ; на конец export directory
001B:004465A0 PUSH EAX [-30h] ; на конец export directory
001B:004465A1 JNZ 004465A5 ; есть PE-заголовок?
001B:004465A3 XOR EBP,EBP ; EBP := 0
001B:004465A5 MOV EAX,[EDI+EBP+1C] ; Export Address Table RVA
001B:004465A9 ADD EAX,EBP ; EAX := EAX + 0 == EAX
001B:004465AB PUSH EAX [-2Сh] ; Export Address Table RVA
001B:004465AC MOV EAX,[EDI+EBP+20] ; Name Pointer RVA
001B:004465B0 ADD EAX,EBP ; EAX := EAX + 0 == EAX
001B:004465B2 PUSH EAX [-28h] ; Name Pointer RVA
001B:004465B3 MOV EAX,[EDI+EBP+24] ; Ordinal Table RVA
001B:004465B7 ADD EAX,EBP
001B:004465B9 PUSH EAX [-24h] ; Ordinal Table RVA
001B:004465BA PUSH DWORD PTR [EDI+EBP+18] [-20h] ; number of name pointers
001B:004465BE XOR EDI,EDI ; EDI := 0
001B:004465C0 SUB ESP,20 ; резервируем память для loc_var
001B:004465C3 JMP 00446631
Листинг 80 "ручной" разбор IMAGE_DIRECTORY (различные смысловые группы команд залиты своим цветом)
После того, как структура EXPORT_TABLE разобрана до последнего винтика, Харон, предварительно обнулив регистр EDI и зарезервировав 20h байт стековой памяти под локальные переменные, совершает прыжок по адресу 446631h.
Тут он читает байт (не двойное слово как обычно!) на который указывает регистр ESI (а указывает он, как мы помним по коду проанализированному ранее, на какую-то подозрительную таблицу, содержащую сплошную тарабарщину). Вот он, – момент истины! Сейчас мы разберемся что это за мешанина такая и как с ней работать!
Прочитанный байт суммируется с константной 37h и засылается в регистр ECX, расширясь до двойного слова. Хм, похоже здесь спрятана зашифрованная длина некоторой структуры. Быть может, строки зашифрованного имени API – функции?! Очень похоже на то, но не будет спешить, предоставив событиям развиваться своим чередом. Как бы там ни было, проверив на неравенство нулю, Харон прыгает блохой на адрес 4465С5h (ну что это за прыжки по всему коду, а?!)
001B:00446631 LODSB ; читаем байт
001B:00446632 ADD AL,37 ; расшифровываем его
001B:00446634 MOVZX ECX,AL ; засылаем в ECX
001B:00446637 JNZ 004465C5 ; если счетчик не ноль, то прыгаем
Листинг 81 расшифровка длины строки, содержащей имя API- функции
Приземлившись в местечке 4465С5h мы натыкаемся на тривиальный расшифровщик, циклически сдвигающий каждый байт загружаемой строки на три бита влево и записывающий его в стек, – в заранее зарезервированную для этой цели область памяти. Обратите внимание, как элегантно вставляется завершающий строку нуль – MOV [EDI], CL и никаких лаптей! Поскольку, после завершения последней машинной команды STOSB регистр EDI указывает на следующий за концом строки байт, а регистр CL, использующийся в качестве счетчика цикла, по его завершению равен естественно нулю, то Харон выгодно использует преимущества ассемблера как языка с неограниченной свободой для изощренного программирования. Попробуйте написать такое на ЯВУ!
Впрочем, это уже второстепенные детали, а нас сейчас больше всего интересует вопрос, так что же такое здесь расшифровывалось. Как? Разве вы не наблюдали за расшифровкой в процессе ее выполнения?! Ну конечно же, никто из нас не смог удержаться от соблазна, чтобы не подсмотреть, что же такое записывается по адресу содержащемся в регистре EDX, а находится там… (см. листинг $+1). Вот это да! Там находится вполне читабельная строка "LoadLibraryA".
001B:004465C5 MOV EDX,EDI ; сохраняем EDI в регистре EDX
001B:004465C7 MOV EDI,ESP ; EDI на вершину стека
001B:004465C9 PUSH ECX ; заносим в стек длину строки
001B:004465CA LODSB ; читаем очередной байт
001B:004465CB ROL AL,03 ; расшифровываем его
001B:004465CE STOSB ; кидаем расшифровку в стек
001B:004465CF LOOP 004465CA ; мотаем цикл
001B:004465D1 MOV [EDI],CL ; ставим завершающий нуль
Листинг 82 расшифровщик зашифрованных строк (ключевая команда расшифровки выделена жирным цветом и взята в рамку)
0023:0012FF78 4C 6F 61 64 4C 69 62 72-61 72 79 41 AA F5 12 00 LoadLibraryA....
0023:0012FF88 00 00 00 00 00 00 00 00-0A 00 00 00 00 00 00 00 ................
0023:0012FF98 37 03 00 00 44 71 ED 77-B2 77 ED 77 68 64 ED 77 7...Dq.w.w.whd.w
0023:0012FFA8 93 BF 05 00 40 64 05 00-00 00 E8 77 9E 67 44 00 ....@d.....w.gD.
Листинг 83 расшифрованное имя функции (выделено жирным цветом и взято в рамку)
Так значит, по адресу 4460С8h находится массив зашифрованных строк с именами используемых Хароном API-функций! Теперь мы уже в состоянии написать скрипт для IDA, который бы расшифровал таблицу имен API – функций, упрощая тем самым дизассемблирование файла (наша конечная цель – восстановить в дизассемблере таблицу stealth-импорта, поскольку без этого дизассемблирование линкера просто нереально).
Напомним вкратце алгоритм расшифровки. Берем первый байт таблицы имен, добавляем к нему "магическое" число 37h и используем полученное значение как длину расшифровываемой строки, над каждым байтом которой проводим операцию циклического сдвига на три позиции влево. Стоп! Язык IDA-Си не поддерживает циклических сдвигов! Ну и какая в этом беда? Реализуем эту операцию "вручную" на базе логических сдвигов и операторов AND и OR!
// расшифровщик таблицы имен
#define X_ADD 0x37
#define P (a + p + 1)
static main()
{
auto _beg, _end, a, count, p, x, x1, x2, s0;
_beg = SelStart(); _end = SelEnd(); p = _beg;
if (_beg == -1)
{
Warning("не выделена область для расшифровки!");
return 0;
}
Message("начинаем расшифровку с %x по %x путем ROL 3\n", _beg, _end);
while(p < _end)
{
s0 = "";
count = (Byte(p) + X_ADD) & 0xFF; PatchByte(p, count);
for (a = 0; a < count; a++)
{
x1 = (Byte(P) >> 5); x2 = (Byte(P) << 3); x = x1 | x2;
s0 = s0 + form("%c", x); PatchByte(P, x);
}
Message("%s\n",s0 ); MakeComm(p, s0);
p = P;
}
}
Листинг 84 скрипт для IDA, расшифровывающий зашифрованные имена API-функций
В конечном итоге (если все сделано правильно) расшифрованная таблица имен будет выглядеть так (ниже для экономии места приведен всего лишь ее фрагмент):
.text:004460C8 aLoadlibrarya db 12,'LoadLibraryA',0
.text:004460D6 aGetprocaddress db 14,'GetProcAddress'
.text:004460E5 aIsdebuggerprese db 17,'IsDebuggerPresent',0
.text:004460F8 aClosehandle db 11,'CloseHandle' ;
.text:00446104 aCreatedirectory db 16,'CreateDirectoryA'
.text:00446115 aCreateeventa db 12,'CreateEventA' ;
.text:00446122 aCreatefilea db 11,'CreateFileA' ;
.text:0044612E aCreatefilemappi db 18,'CreateFileMappingA'
.text:00446141 aDeletefilea db 11,'DeleteFileA' ;
.text:0044614D aFiletimetolocal db 23,'FileTimeToLocalFileTime' ;
.text:00446165 aFillconsoleoutp db 27,'FillConsoleOutputCharacterA';
.text:00446181 aFindclose db 09,'FindClose' ;
.text:0044618B aFindfirstfilea db 14,'FindFirstFileA' ;
.text:0044619A aFindnextfilea db 13,'FindNextFileA' ;
.text:004461A8 aFindresourceexw db 15,'FindResourceExW' ;
.text:004461B8 aFlushconsoleinp db 23,'FlushConsoleInputBuffer' ;
Листинг 85 таблица имен после расшифровки
Тем временем, жизнь продолжается и трассировка программы приводит нас к тому самому коду, который и осуществляет "ручное" импортирование функций. Первым делом в регистр EDI загружается… черт возьми, что в него загружается? Во всяком случае IDA не может внятно сказать нам что. Давайте вернемся в начало функции. Вспомним, что Харон предварительно заносил в стек декодированные элементы EXPORT_TABLE, а затем передвинул указатель вершины стека на 20h байт вверх. Таким образом, машинная команда "MOV EDI, [ESP + 24h]" загружает содержимое затолкнутое в стек первым, предшествующим ей PUSH'ем. А это есть количество экспортируемых динамической библиотекой имен!
А что делает машинная команда XCHG ESI, [ESP]? То, что она обменивает местами значение регистра ESI и двойного слова, лежащего на вершине стека, это извините за грубость, и дураку понятно. А вот что лежит на вершине стека? Двойное слово, содержащее длину строки с именем функции (помните последнюю инструкцию PUSH ECX?).
Затем в EDX загружается количество экспортируемых имен, временно сохраненных до этого в регистре EDI, а сам EDI отныне будет использоваться как счетчик импортов (хитрая функция Харона за один раз может импортировать и более одной функции, что значительно увеличивает ее производительность в сравнении с кучей вызовов GetProcAddress).
В счетчик ECX загружается длина импортируемого имени, увеличенного на единицу (LEA ECX, [ESI + 01]) и затем мы входим в "голову" очень интересного цикла, который вместо тупого перебора всех экспортов один за другим, осуществляет поиск требуемого импорта продвинутым алгоритмов "вилки". Используя тот факт, что имена API-функций, экспортируемые системными библиотеками, отсортированы по алфавиту, Харон анализирует флаг переноса, установленный машинной командой CPMSB и, в зависимости от результатов сравнения, прыгает либо "назад", либо "вперед". Пара регистров EDI/ESI задает диапазон поиска (индекс первого и последнего экспортируемого имени соответственно), а конструкция LEA EDX, [EDI + ESI]/SHR EDX, 1 вычисляет середину этого диапазона. Собственно, это и есть ключевой момент в подпрограмме поиска имени, а все остальное – традиционно и неинтересно.
Единственное, о чем имеет смысл упомянуть: вычисление адресов локальных переменных в плавающем кадре стека. Как определить к каким именно ячейкам памяти обращаются инструкции MOV EDI, [ESP + 24], XCHG ESI, [ESP], LEA ECX, [ESP + 10] и LEA EAX, [ESP + 38]? Начнем с первой из них. Используя квадратные скобки, расставленные в листинге $-6, мы можем заключить, что в ячейке, отстоящей от вершины стека на 24h байт, храниться переменная, содержащая в себе address table entries, однако это не так и прогон под отладчиком позволяет установить, что в данной ячейке находится абсолютно другое значение, – number of name pointers, соответствующее относительному смещению в 20h. Откуда же взялась разница в четыре байта? Ее "съела" команда 4465C9:PUSH ECX, сместившая указатель стека на одно двойное слово вверх. Эта маленькая невнимательность чуть не стоила нам нескольких часов, ушедших на выяснение на кой такой хрен программе потребовалось использовать address table entries в качестве счетчика. Поэтому, функции с плавающим фреймом лучше всего исследовать в IDA PRO, которая автоматически отслеживает значение регистра указателя стека в каждой точке программы. К сожалению, IDA PRO не панацея и даже она не избавляет нас от необходимости думать головой, а не руками. Харон очень изящно обул механизм идентификации локальных переменных, – IDA PRO "видит" засылку в стек 4465B2:PUSH EAX, но не считает эту ячейку локальной переменной, а потому и не отслеживает к ней обращения. Говоря другими словами, дизассемблер не рискует утверждать, что инструкции 4465B2:PUSH EAX и 4465D3:MOV EDI, [ESP + 24] на самом деле адресуют одну и туже ячейку памяти! (Собственно, навряд ли это делалось с целью защиты, сегодня так поступают и многие оптимизирующие компиляторы).
Следующая по списку команда XCHG ESI, [ESP] сдергивает с верхушки стека двойное слово, только что засунутое туда инструкций 4465C9:PUSH ECX (длина строки импортируемого имени) и помещает его в регистр ESI, возвращая в стек его прежнее значение.
Соответственно, машинная команда 4465EE:LEA ESI, [ES + 10] загружает в регистр ESI указатель на… на второе слово, считая от вершины стека (первый байт имени импортируемой функции)! Спрашиваете, как мы получили такой результат? Во-первых, мы посчитали размер трех двойных слов, засылаемых в стек командами PUSH ECX, PUSH ESI и PUSH EDI, во-вторых, учили предшествующее им двойное слово (длину строки), закинутое в стек командой 4465C9:PUSH ECX. В итоге у нас получилось четыре двойных слова, а 4 х 4 = 16 или 10h в шестнадцатеричной системе исчисления. Но что находится в данной позиции стека? Вернувшись в окрестности инструкции 4465C9:PUSH ECX, мы видим последовательность следующих машинных команд: MOV EDI, ESP/PUSH ECX/…/STOSB. Ага! Вот оно! Вся территория от текущей стека и на 20h байт вниз занята расшифрованным именем импортируемой функции!
После этого будет уже не трудно рассчитать содержимое LEA EAX, [ESP + 38] (address table entries), тогда смысл команды MOV EDI, [EDX*4 + EAX] сводится к следующему: регистр EDX – это индекс текущей позиции в address table, "4" – это размер одного элемента таблицы, тогда EDX*4 + EAX есть указатель на соответствующее ему экспортируемое имя.
001B:004465D3 MOV EDI,[ESP+24] ; кол-во экспортируемых имен
001B:004465D7 XCHG ESI,[ESP] ; длина строки импорт. имени
001B:004465DA XCHG EDX,EDI ; вершина диапазона
001B:004465DC LEA ECX,[ESI+01] ; длина имени + завершающий ноль
001B:004465DF MOV ESI,EDX ; на последний экспорт
001B:004465E1 DEC ESI ; поджимаем "дно" диапазона
001B:004465E2 CMP EDI,ESI ; вершина еще не упала на дно?
001B:004465E4 JG 00446641 ; –> искать больше нечего (ошибка)
001B:004465E6 LEA EDX,[EDI+ESI] ; сумма конца и начала
001B:004465E9 SHR EDX,1 ; середина между дном и вершиной
001B:004465EB PUSH ECX ; \
001B:004465EC PUSH ESI ; + сохраняем регистры
001B:004465ED PUSH EDI ; /
001B:004465EE LEA ESI,[ESP+10] ; расшифрованное имя импорта
001B:004465F2 MOV EAX,[ESP+38] ; на address table
001B:004465F6 MOV EDI,[EDX*4+EAX] ; извлекаем очередной экспорт
001B:004465F9 ADD EDI,EBP ; получаем указатель на имя
001B:004465FB REPZ CMPSB ; это то имя, что нам надо?
001B:004465FD POP EDI ; \
001B:004465FE POP ESI ; + восстанавливаем регистры
001B:004465FF POP ECX ; /
001B:00446600 JZ 00446609 ; нужное имя найдено
001B:00446602 JB 004465DF ; мы взяли слишком низко
001B:00446604 MOV EDI,EDX ; мы взяли слишком высоко…
001B:00446606 INC EDI ; …опускаемся поближе ко дну
001B:00446607 JMP 004465E2 ; мотаем цикл
Листинг 86 "ручное" импортирование API – функций прогрессивным методом вилки (заливкой выделена логическая структура кода)
Отыскав необходимую ему функцию в таблице экспортируемых имен, Харон использует ее индекс для определения ее ординала, который в свою очередь используется для вычисления конечного RVA-адреса.
001B:00446609 MOV EAX,[ESP+28] ; на ordinal table
001B:0044660D MOV EDI,EDX ; текущий индекс
001B:0044660F MOVZX ESI,WORD PTR [EDI*2+EAX] ; извлекаем "наш" ординал
001B:00446613 MOV EAX,[ESP+30] ; на export address table
001B:00446617 MOV ECX,[ESI*4+EAX] ; читаем элемент таблицы
001B:0044661A INC EDI ; следующий индекс
001B:0044661B LEA EAX,[ECX+EBP+00] ; получаем адрес "нашей" функции
001B:0044661F CMP ECX,[ESP+38] ; на export directory
001B:00446623 POP ESI ; на след. зашифрованное имя
001B:00446624 JB 0044662C ; --> мы в пределах export table
001B:00446626 CMP [ESP+30],ECX ; мы в пределах address table?
001B:0044662A JAE 00446665 ; ошибка
001B:0044662C MOV [EBX],EAX ; заносим полученный адрес в DYN
001B:0044662E ADD EBX,04 ; на следующим элемент DYN
001B:00446631 LODSB ; следующий шифрованный байт
001B:00446632 ADD AL,37 ; расшифровываем
001B:00446634 MOVZX ECX,AL ; перепихиваем в ECX
001B:00446637 JNZ 004465C5 ; если не ноль, то продолжаем
Листинг 87 определение адреса экспортируемой функции
Полученный адрес записывается в ячейку на которую указывает регистр EBX и… постой, а на что у нас вообще указывает EBX? Пролистывая экран дизассемблера вверх мы нигде не находим и следов его инициализации. Только по возвращению в материнскую функцию нам удается определить, что в EBX явным образом загружается значение 44CC0Ch. Смотрим дизассемблером: что это такое? Ага, это неинициализированная область памяти с кучей перекрестных ссылок, ведущих к командам CALL. Похоже это и есть та самая изощренная таблица импорта, которую мы так долго искали! Давайте условимся называть ее таблицей динамического импорта или DYN_TABLE.
Очевидно, нашей первоочередной задачей будет ее восстановление. Ничего не говорящие адреса в стиле CALL [44CC0Ch] мы заменим символьными именами соответствующих им функций. Как это сделать? Давайте исходить из того, что функция HaronGetProcAddress загружает все импорты один за другим в порядке согласным с очередностью их перечисления в таблице зашифрованных имен (вообще-то, это не совсем так, но в качестве рабочей гипотезы сойдет). Поскольку все импортируемые имена нами уже расшифрованы остается лишь дать каждому элементу массива DYN_TABLE соответствующее ему имя. Чтобы не тратить попусту время возней вручную, мы автоматизируем этот процесс, наскоро набив на консоли следующий скрипт:
// восстанавливает динамическую таблицу импорта
static main()
{
auto a, b, c, p_src, p_dst, s;
p_src = 0x4460C8; // начало расшифрованных имен
p_dst = 0x44CC08;
while ( p_src < 0x446584)
{
Message("%s",Name(p_src));
MakeName(p_dst, "_"+Name(p_src));
p_src = NextHead(p_src, -1);
p_dst = NextHead(p_dst, -1);
}
}
Листинг 88 скрипт для восстановления DYN_TABLE
Все! Теперь все динамические адреса восстановлены и мы можем приступать к анализу программного кода прямо в дизассемблере (до восстановления динамической таблицы импорта эту задачу приходилось решать лишь в отладчике). Однако, даже беглая проверка показывает, что DYN_TABLE восстановлена не совсем правильно. Как утверждает наш скрипт, в третьем по счету ее элементе содержится функция IsDebuggerPresent, в то время как просмотр дампа в отладчике показывает несколько иную картину – CloseHandle и вообще, имена всех последующих функций сдвинуты на единицу. Что еще за чудеса?! Ну ладно, разберемся! Пока же в качестве временного решения проблемы просто уменьшим адрес первого элемента DYN_TABLE на размер двойного слова, тем самым компенсировав этот непонятный сдвиг.