Здравствуйте, Хабралюди.
Представляю вам третью часть из моего цикла статей о исследовании крякмисов. В этом топике мы поговорим с вами о ручной распаковке некоторых пакеров и о преодолении не сложных антиотладочных методов.
Из инструментов мне понадобились:
Запакованная программа работает следующим образом:
Сначала запускается код распаковщика, который начинает расшифровывать запакованый програмный код. После окончания расшифровки делается прыжок на OEP программы и далее начинает выполняться уже распакованный програмный код.
Алгоритм распаковки будет таков:
1. Находим RVA OEP.
2. Дампим программу.
3. Восстанавливаем таблицу импорта.
4. Меняем точку входа на оригинальную.
Итак, нужный нам адрес OEP высчитывается по формуле:
Image Base — это адрес в памяти, начиная с которого программа загружена в память
OEP (Original Entry Point) — это адрес, с которого бы начала выполняться программа если бы не была упакованна.
Virtual Address (VA) — виртуальный адрес элемента в памяти
Relative Virtual Adress (RVA) — относительный виртуальный адрес. Адрес относительно ImageBase.
Ну вот к примеру, мы нашли OEP равный 00301000, а ImageBase равно 00300000, тогда RVA OEP будет равно 1000. Значение ImageBase можно узать посмотрев в любом редакторе PE заголовков.
После того, как мы найдём RVA OEP нам необходимо снять дамп программы. Дамп означает — область (часть) памяти или же файл, сохраненный на диск из памяти. Снять дамп — значит сохранить нужную область памяти (обычно занимаемую программой) на жесткий диск. В итоге получаем распакованную программу.
Далее нам необходимо восстановить таблицу импорта. Таблица импорта хранит информацию о функциях, используемых программой при ее работе. Исходно таблица импорта хранит в себе адреса, по которым в файле находятся имена импортируемых функций, т.е. функций используемых при работе программы. При запуске программы эти адреса (это все в памяти происходит) перезаписываются прямыми адресами имортируемых функций. Восстанавливать её необходимо потому, что те ячейки, в которых изначально лежат адреса имен функций, используемых для получения уже прямых адресов функций в любой версии операционной системы, уже заполнены адресами этих функций в той системе, в которой дампили программу. В этом случае информация об адресах имен функций уже не может быть восстановлена и при запуске такой программы будут использоваться уже записанные прямые адреса функций. А это приводит к неработоспособности программы на других версиях ОС.
И наконец восстанавливаем OEP. Это можно сделать с помощью любого редактора PE заголовков.
Вот и вся теория.
В этой статье мы рассмотрим два пакера. Это UPX и ASPack. Распаковка других пакеров не сильно будет отличаться от распаковки этих двух.
Скачиваем самую свежую версию. Пакуем что — нибудь. Запускаем это под отладчиком.
Во время расшифровки запакованного кода пакер во всю использует стек. Естественно, чтобы запакованная программа работала правильно, пакеру необходимо сохранить начальное значение стека и потом, после завершения распаковки, восстановить его. Практически во всех упаковщиках, когда они восстанавливают стек перед переходом на OEP, считывается значение в стеке по адресу esp-4.
Таким образом, в Olly ставим бряк командой «hr esp-4». Затем запускаем программу и видим, что бряк сработал тут:
Далее трассируем программу до OEP(грубо говоря становимся на OEP). С помошью плагина Olly Dump дампим программу.
Теперь осталось только восстановить импорт.Запускаем нашу запакованную программу и ImpREC. В списке процессов ImpREC'a находим нашу программу. В поле RVA вводим RVA OEP( выше описано как его найти). Нажимаем AutoSearch. После появления сообщения о том, что скорее всего что — то найдено нажимаем Get Imports и, если в списке появились функции, то нажимаем Fix Dump и выбираем наш дамп. Вот и всё программа распакована.
Тут всё аналогично, за исключением некоторого момента. После установки бряка мы попадаем сюда:
В остальном процедура одинакова.
Вот этот крякмис.
Распаковываем так как описано выше. Всё отлично распаковывается.(Для справки OEP = 00401000). После этого савим бряки на вызов функции GetDlgItemTextA, запускаем, вводим фейковый пасс, нажимаем на кнопку и попадаем сюда:
В EAX у нас находится длина введённого пароля. Командой SHL al,3 мы выполняем логический сдвиг значения AL влево на 3 и в идеале мы должны получить 78.Проведём процедуру обратную shl. Это shr 78,3 = 0F = 15 длина валидного пароля. Дальше я очень долго трассировал до некоторого момента и на пути мне встретилось несколько антиотладочных трюков:
Инструкция RDTSC возвращает в регистр EAX количество тактов с момента последнего сброса процессора. В коде выше мы видим два вызова этой инструкции а потом сравнение разницу их выводов с неким эталонным значением. Дело в том, что, когда программа выполняется без отладчика, разность тактов будет мала, а когда она под отладчиком то разность будет большая. Подобных моментов вам встретится много, просто пачте их или меняйте флаги. Когда вы до трассируете до следующего момента:
Обратите внимание на 0040127C. Тут совершается прыжок на несуществующий адрес, поэтому смело патчим переход на 00401281. Таких моментов будет несколько. Трассируете до такого кода:
Над этим моментом я очень долго думал. Оказалось, что это процедура генерации пароля. То есть пароль хранится в программе не в открытом виде. AL на протяжении всей генерации принимает значения 1 или 0. Так вот, протрассировав всю процедуру генерации пароля и выписав все значения, я получил огромную строку из двоичных значений(для удобства перевёл в десятичную систему):
учитывая то, что каждая восьмёрка символов(в двойчной строке, а я перевёл в десятичную, поэтому тут это каждый символ обрамлённый пробелом), генерировалась после того, как мы положили в EDX определённый символ нашего введённого пароля, то можно утверждать что строка выше и есть валидный пароль. Перекодировав её получилось «welldoneUfindme».
Антиотладочных методов очень много, начиная с этой статьи будем разбирать их по очереди, от простых к сложным.
Итак, на crackmes.de как раз есть специальный крякмис, который называется AntiOlly. Качаем его, запускаем и видим следующее окошко:
Тут нам говорят, что ничего не обнаружено и мы справились. Теперь мы имеем представление о том, как выглядит «хорошее» сообщение. Загружаем крякмис в Olly и видим следующее:
Ошибка вызвана тем, что Olly, проанализировав заголовок нашего крякмиса, нашла в нём(заголовке) ошибки. Но это поправимо. Загружаем наш крякмис в PE edior и переходим во вкладку Optional header. Моё внимание привлекло «слишком большое» значение параметров NumberOfRVAandSize, Base of Code и Base of Data. Обычно NumberOfRVAandSize = 0x00000010, Base of Code =00001000, Base of Data = 00002000. Меняем эти параметры на «обычные» и запускаем крякмис под отладчиком. Теперь ругательного сообщения не видно(т.е. оно осталось, но никак не повлияет на анализ) и мы можем спокойно анализировать крякмис. Итак, это была первая антиотладочная уловка.
Запустив крякмис под отладкой мы видим «плохое» сообщение:
Проанализировав программу мы находим участок антиотладочного кода:
Итак, первая вызывается функция GetTickCount. Эта функция возвращает время, которые прошло с момента старта системы в милисекундах. Дело в том что есть ещё один вызов этой функции. Далее, в последующем коде, замеряется разница между значениями полученными в результате выполнения этих функций. Это была вторая антиотладочная хитрость.
Далее следует вызов FindWindowA, которая ищет окно с заголовком OllyDbg. Ну и наконец вызов IsDebuggerPresent, которая просто проверяет отлаживается программа или нет. Если да то в Eax 1 если нет то 0.
вот проверки, которые проводит крякмис:
Думаю теперь понятно где патчить или редактировать регистры.
Большое спасибо вам за внимание.
Представляю вам третью часть из моего цикла статей о исследовании крякмисов. В этом топике мы поговорим с вами о ручной распаковке некоторых пакеров и о преодолении не сложных антиотладочных методов.
1. Ручная распаковка
Из инструментов мне понадобились:
- 1. OllyDbg
- 2. Плагин Olly Dump
- 3. ImpREC
- 4. PE tools
- 5. PEid
Теория
Запакованная программа работает следующим образом:
Сначала запускается код распаковщика, который начинает расшифровывать запакованый програмный код. После окончания расшифровки делается прыжок на OEP программы и далее начинает выполняться уже распакованный програмный код.
Алгоритм распаковки будет таков:
1. Находим RVA OEP.
2. Дампим программу.
3. Восстанавливаем таблицу импорта.
4. Меняем точку входа на оригинальную.
Итак, нужный нам адрес OEP высчитывается по формуле:
RVA OEP = VA OEP - ImageBase
, где:Image Base — это адрес в памяти, начиная с которого программа загружена в память
OEP (Original Entry Point) — это адрес, с которого бы начала выполняться программа если бы не была упакованна.
Virtual Address (VA) — виртуальный адрес элемента в памяти
Relative Virtual Adress (RVA) — относительный виртуальный адрес. Адрес относительно ImageBase.
Ну вот к примеру, мы нашли OEP равный 00301000, а ImageBase равно 00300000, тогда RVA OEP будет равно 1000. Значение ImageBase можно узать посмотрев в любом редакторе PE заголовков.
После того, как мы найдём RVA OEP нам необходимо снять дамп программы. Дамп означает — область (часть) памяти или же файл, сохраненный на диск из памяти. Снять дамп — значит сохранить нужную область памяти (обычно занимаемую программой) на жесткий диск. В итоге получаем распакованную программу.
Далее нам необходимо восстановить таблицу импорта. Таблица импорта хранит информацию о функциях, используемых программой при ее работе. Исходно таблица импорта хранит в себе адреса, по которым в файле находятся имена импортируемых функций, т.е. функций используемых при работе программы. При запуске программы эти адреса (это все в памяти происходит) перезаписываются прямыми адресами имортируемых функций. Восстанавливать её необходимо потому, что те ячейки, в которых изначально лежат адреса имен функций, используемых для получения уже прямых адресов функций в любой версии операционной системы, уже заполнены адресами этих функций в той системе, в которой дампили программу. В этом случае информация об адресах имен функций уже не может быть восстановлена и при запуске такой программы будут использоваться уже записанные прямые адреса функций. А это приводит к неработоспособности программы на других версиях ОС.
И наконец восстанавливаем OEP. Это можно сделать с помощью любого редактора PE заголовков.
Вот и вся теория.
Практика
В этой статье мы рассмотрим два пакера. Это UPX и ASPack. Распаковка других пакеров не сильно будет отличаться от распаковки этих двух.
UPX
Скачиваем самую свежую версию. Пакуем что — нибудь. Запускаем это под отладчиком.
Во время расшифровки запакованного кода пакер во всю использует стек. Естественно, чтобы запакованная программа работала правильно, пакеру необходимо сохранить начальное значение стека и потом, после завершения распаковки, восстановить его. Практически во всех упаковщиках, когда они восстанавливают стек перед переходом на OEP, считывается значение в стеке по адресу esp-4.
Таким образом, в Olly ставим бряк командой «hr esp-4». Затем запускаем программу и видим, что бряк сработал тут:
00472176 . 8D4424 80 LEA EAX,DWORD PTR SS:[ESP-80] //Процедура
0047217A > 6A 00 PUSH 0 //зачистки
0047217C . 39C4 CMP ESP,EAX //стека
0047217E .^75 FA JNZ SHORT 111.0047217A //нулями.
00472180 . 83EC 80 SUB ESP,-80
00472183 .^E9 386EFEFF JMP 111.00458FC0 //Прыжок на OEP
Далее трассируем программу до OEP(грубо говоря становимся на OEP). С помошью плагина Olly Dump дампим программу.
Теперь осталось только восстановить импорт.Запускаем нашу запакованную программу и ImpREC. В списке процессов ImpREC'a находим нашу программу. В поле RVA вводим RVA OEP( выше описано как его найти). Нажимаем AutoSearch. После появления сообщения о том, что скорее всего что — то найдено нажимаем Get Imports и, если в списке появились функции, то нажимаем Fix Dump и выбираем наш дамп. Вот и всё программа распакована.
ASPack
Тут всё аналогично, за исключением некоторого момента. После установки бряка мы попадаем сюда:
0046F416 75 08 JNZ SHORT Test_Com.0046F420
0046F418 B8 01000000 MOV EAX,1
0046F41D C2 0C00 RETN 0C
0046F420 68 C08F4500 PUSH Test_Com.00458FC0 //кладём в стек OEP
0046F425 C3 RETN // Косвенно переходим на OEP
В остальном процедура одинакова.
Крякмис на тему распаковки
Вот этот крякмис.
Распаковываем так как описано выше. Всё отлично распаковывается.(Для справки OEP = 00401000). После этого савим бряки на вызов функции GetDlgItemTextA, запускаем, вводим фейковый пасс, нажимаем на кнопку и попадаем сюда:
00401206 . E8 1B060000 CALL <JMP.&user32.GetDlgItemTextA> // Мы здесь
0040120B . 8B35 00604000 MOV ESI,DWORD PTR DS:[406000]
00401211 . 81C6 7F010300 ADD ESI,3017F
00401217 . 81EE 66060000 SUB ESI,666
0040121D . 81F6 ADDE0000 XOR ESI,0DEAD
00401223 . BB 33604000 MOV EBX,dddddddd.00406033
00401228 . C0E0 03 SHL AL,3 // Внимание!
0040122B . 83F8 78 CMP EAX,78 // Внимание!
0040122E . 0F85 9A050000 JNZ dddddddd.004017CE // Прыжок на "плохую" ветку
В EAX у нас находится длина введённого пароля. Командой SHL al,3 мы выполняем логический сдвиг значения AL влево на 3 и в идеале мы должны получить 78.Проведём процедуру обратную shl. Это shr 78,3 = 0F = 15 длина валидного пароля. Дальше я очень долго трассировал до некоторого момента и на пути мне встретилось несколько антиотладочных трюков:
004012B0 0F31 RDTSC //Вот
004012B2 8BC8 MOV ECX,EAX
004012B4 0F31 RDTSC //Вот
004012B6 2BC8 SUB ECX,EAX
004012B8 F7D1 NOT ECX
004012BA 81F9 00500000 CMP ECX,5000
004012C0 -7F FE JG SHORT crackme2.004012C0 // И Вот
Инструкция RDTSC возвращает в регистр EAX количество тактов с момента последнего сброса процессора. В коде выше мы видим два вызова этой инструкции а потом сравнение разницу их выводов с неким эталонным значением. Дело в том, что, когда программа выполняется без отладчика, разность тактов будет мала, а когда она под отладчиком то разность будет большая. Подобных моментов вам встретится много, просто пачте их или меняйте флаги. Когда вы до трассируете до следующего момента:
0040126A 0F31 RDTSC
0040126C 8BC8 MOV ECX,EAX
0040126E 0F31 RDTSC
00401270 2BC8 SUB ECX,EAX
00401272 F7D1 NOT ECX
00401274 81F9 00500000 CMP ECX,5000
0040127A 7C 05 JL SHORT crackme2.00401281
0040127C -E9 139C04EC JMP EC44AE94
00401281 EB 0D JMP SHORT crackme2.00401290
Обратите внимание на 0040127C. Тут совершается прыжок на несуществующий адрес, поэтому смело патчим переход на 00401281. Таких моментов будет несколько. Трассируете до такого кода:
004014F1 0FB613 MOVZX EDX,BYTE PTR DS:[EBX] ; наш пароль сейчас находится по адрессу расположенному в EBX, и сейчас мы заносим первый символ нашего пароля в EDX
004014F4 B9 08000000 MOV ECX,8
004014F9 AC LODS BYTE PTR DS:[ESI] ; подгружаем в EAX какой-то символ
004014FA 24 01 AND AL,1 ; and 1 с этим символом
004014FC 74 04 JE SHORT crackme2.00401502
004014FE D0E2 SHL DL,1
00401500 72 08 JB SHORT crackme2.0040150A
00401502 D0E2 SHL DL,1
00401504 0F82 BF020000 JB crackme2.004017C9 ; прыжок на плохую ветку программы
0040150A ^E2 ED LOOPD SHORT crackme2.004014F9
0040150C 43 INC EBX
0040150D 58 POP EAX
0040150E 48 DEC EAX
0040150F 0F84 9A020000 JE crackme2.004017AF
00401515 50 PUSH EAX
00401516 ^EB D9 JMP SHORT crackme2.004014F1
Над этим моментом я очень долго думал. Оказалось, что это процедура генерации пароля. То есть пароль хранится в программе не в открытом виде. AL на протяжении всей генерации принимает значения 1 или 0. Так вот, протрассировав всю процедуру генерации пароля и выписав все значения, я получил огромную строку из двоичных значений(для удобства перевёл в десятичную систему):
119 101 108 108 100 111 110 101 85 102 105 110 100 109 101
учитывая то, что каждая восьмёрка символов(в двойчной строке, а я перевёл в десятичную, поэтому тут это каждый символ обрамлённый пробелом), генерировалась после того, как мы положили в EDX определённый символ нашего введённого пароля, то можно утверждать что строка выше и есть валидный пароль. Перекодировав её получилось «welldoneUfindme».
2. Некоторые антиотладочные методы
Антиотладочных методов очень много, начиная с этой статьи будем разбирать их по очереди, от простых к сложным.
Итак, на crackmes.de как раз есть специальный крякмис, который называется AntiOlly. Качаем его, запускаем и видим следующее окошко:
Тут нам говорят, что ничего не обнаружено и мы справились. Теперь мы имеем представление о том, как выглядит «хорошее» сообщение. Загружаем крякмис в Olly и видим следующее:
Ошибка вызвана тем, что Olly, проанализировав заголовок нашего крякмиса, нашла в нём(заголовке) ошибки. Но это поправимо. Загружаем наш крякмис в PE edior и переходим во вкладку Optional header. Моё внимание привлекло «слишком большое» значение параметров NumberOfRVAandSize, Base of Code и Base of Data. Обычно NumberOfRVAandSize = 0x00000010, Base of Code =00001000, Base of Data = 00002000. Меняем эти параметры на «обычные» и запускаем крякмис под отладчиком. Теперь ругательного сообщения не видно(т.е. оно осталось, но никак не повлияет на анализ) и мы можем спокойно анализировать крякмис. Итак, это была первая антиотладочная уловка.
Запустив крякмис под отладкой мы видим «плохое» сообщение:
Проанализировав программу мы находим участок антиотладочного кода:
00401010 |. FFD7 CALL EDI // Вызываем функцию GetTickCount
00401012 |. 6A 00 PUSH 0
00401014 |. 68 34214000 PUSH AntiOlly.00402134
00401019 |. 8BF0 MOV ESI,EAX
0040101B |. FF15 DC204000 CALL DWORD PTR DS:[4020DC] //FindWindowA
00401021 |. 85C0 TEST EAX,EAX
00401023 |. 75 04 JNZ SHORT AntiOlly.00401029
00401025 |. 884424 0F MOV BYTE PTR SS:[ESP+F],AL
00401029 |> FF15 04204000 CALL DWORD PTR DS:[402004] //IsDebuggerPresent
Итак, первая вызывается функция GetTickCount. Эта функция возвращает время, которые прошло с момента старта системы в милисекундах. Дело в том что есть ещё один вызов этой функции. Далее, в последующем коде, замеряется разница между значениями полученными в результате выполнения этих функций. Это была вторая антиотладочная хитрость.
Далее следует вызов FindWindowA, которая ищет окно с заголовком OllyDbg. Ну и наконец вызов IsDebuggerPresent, которая просто проверяет отлаживается программа или нет. Если да то в Eax 1 если нет то 0.
вот проверки, которые проводит крякмис:
0040102F |. 85C0 TEST EAX,EAX // эта проверка после функции IsDebuggerPresent
00401031 |. 75 02 JNZ SHORT AntiOlly.00401035 // если дебаггер есть то прыгаем на 00401035
00401033 |. 32DB XOR BL,BL //если нет то обнуляем BL
00401035 |> FFD7 CALL EDI //Второй раз вызываем функцию GetTickCount
00401037 |. 2BF0 SUB ESI,EAX
00401039 |. 83FE 64 CMP ESI,64 // Сравниваем значения
0040103C |. 76 0D JBE SHORT AntiOlly.0040104B // Если ok то прыгаем на 0040104B
0040103E |. A1 44204000 MOV EAX,DWORD PTR DS:[402044]
00401043 |. 50 PUSH EAX
00401044 |. 68 3C214000 PUSH AntiOlly.0040213C
00401049 |. EB 3F JMP SHORT AntiOlly.0040108A // если не ok то идём по плохой ветке
0040104B |> 84DB TEST BL,BL
0040104D |. 74 14 JE SHORT AntiOlly.00401063 // Проверка результата функции IsDebuggerPresent
0040104F |. 8B15 44204000 MOV EDX,DWORD PTR DS:[402044]
00401055 |. A1 60204000 MOV EAX,DWORD PTR DS:[402060]
0040105A |. 52 PUSH EDX
0040105B |. 68 3C214000 PUSH AntiOlly.0040213C
00401060 |. 50 PUSH EAX
00401061 |. EB 2E JMP SHORT AntiOlly.00401091
00401063 |> 807C24 0F 00 CMP BYTE PTR SS:[ESP+F],0 //Проверка на то нашлось ли окно функцией FindWindow
00401068 |. 74 15 JE SHORT AntiOlly.0040107F
Думаю теперь понятно где патчить или редактировать регистры.
Большое спасибо вам за внимание.