Сообщения об ошибках и отладочные сообщения

Очень сильно помогают отладочные сообщения, если они имеются. В некотором смысле, отладочные сообщения, это отчет о том, что сейчас происходит в программе. Зачастую, это printf() -подобные функции, которые пишут куда-нибудь в лог, а бывает так что и не пишут ничего, но вызовы остались, так как эта сборка — не отладочная, а release.

Если в отладочных сообщениях дампятся значения некоторых локальных или глобальных переменных, это тоже может помочь, как минимум, узнать их имена. Например, в Oracle RDBMS одна из таких функций: ksdwrt(). Осмысленные текстовые строки вообще очень сильно могут помочь. Дизассемблер IDA может сразу указать, из какой функции и из какого её места используется эта строка. Встречаются и смешные случаи 6.

Сообщения об ошибках также могут помочь найти то что нужно. В Oracle RDBMS сигнализация об ошибках проходит при помощи вызова некоторой группы функций.

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

копирования, просто по сообщению об ошибке.

Константы

Люди, включая программистов, часто используют круглые числа вроде 10, 100, 1000, в т.ч. и в коде. практикующие реверсеры, обычно, хорошо знают их в шестнадцатеричном представлении: 10=0xA, 100=0x64, 1000=0x3E8,

10000=0x2710.

Иногда попадаются константы 0xAAAAAAAA (10101010101010101010101010101010) и

0x55555555 (01010101010101010101010101010101) — это чередующиеся биты. Это помогает отличить некоторый сигнал от сигнала где все биты включены (1111 …) или выключены (0000 …).

Например, константа 0x55AA используется как минимум в boot-секторе, MBR1, и в ПЗУ2 плат-расширений IBM-компьютеров. Некоторые алгоритмы, особенно криптографические, используют хорошо различимые константы, которые при помощи IDA легко находить в коде.

Например, алгоритм MD53 инициализирует свои внутренние переменные так:

var int h0 := 0x67452301

var int h1 := 0xEFCDAB89

var int h2 := 0x98BADCFE

var int h3 := 0x10325476

Если в коде найти использование этих четырех констант подряд — очень высокая вероятность что эта функция имеет отношение к MD5.

Еще такой пример это алгоритмы CRC16/CRC32, часто, алгоритмы вычисления контрольной суммы по CRC используют заранее заполненные таблицы, вроде:

/** CRC table for the CRC−16. The poly is 0x8005 (x^16 + x^15 + x^2 + 1) */

u16 const crc16_table[256] = {

0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,

0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,

0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,

Системные вызовы (syscall-ы)

(«kernel space») и не имеющие («user space»).

В первой категории ядро ОС и, обычно, драйвера. Во второй категории всё прикладное ПО. Например, ядро Linux в kernel space, но Glibc в user space. Это разделение очень важно для безопасности ОС: очень важно чтобы никакой процесс не мог испортить что-то в других процессах или даже в самом ядре ОС. С другой стороны, падающий драйвер или ошибка внутри ядра ОС обычно приводит к kernel panic или BSOD.

Защита x86-процессора устроена так что возможно разделить всё на 4 слоя защиты (rings), но и в Linux, и в Windows, используются только 2: ring0 («kernel space») и ring3 («user space»).

Системные вызовы (syscall-ы) это точка где соединяются вместе оба эти пространства. Это, можно сказать, самое главное API предоставляемое прикладному ПО.

В Windows NT таблица сисколлов находится в SSDT. Работа через syscall-ы популярна у авторов шеллкодов и вирусов, потому что там обычно бывает трудно определить адреса нужных функций в системных библиотеках, а syscall-ами проще пользоваться, хотя и придется писать больше кода из-за более низкого уровня абстракции этого API. Также нельзя еще забывать, что номера syscall-ов могут отличаться от версии к версии OS.

Пример: игра Windows XP «Сапёр».

Для тех, кто не очень хорошо играет в Сапёра (Minesweeper), можно попробовать найти все скрытые мины в отладчике. Как мы знаем, Сапёр располагает мины случайным образом, так что там должен быть генератор случайных чисел или вызов стандартной функции Си rand() . Вот что хорошо в реверсинге продуктов от Microsoft, так это то что часто есть PDB-файл со всеми символами (имена функций, и т.д.). Когда мы загружаем winmine.exe в IDA, она скачивает PDB файл именно для этого исполняемого файла и добавляет все имена. И вот оно, только один вызов rand() в этой функции:

.text:01003940 ; __stdcall Rnd(x)

.text:01003940 _Rnd@4 proc near ; CODE XREF: StartGame()+53

.text:01003940 ; StartGame()+61

.text:01003940

.text:01003940 arg_0 = dword ptr 4

.text:01003940

.text:01003940 call ds:__imp__rand

.text:01003946 cdq

.text:01003947 idiv [esp+arg_0]

.text:0100394B mov eax, edx

.text:0100394D retn 4

.text:0100394D _Rnd@4 endp

Так её назвала IDA и это было имя данное ей разработчиками Сапёра.

Функция очень простая:

int Rnd(int limit)

{

return rand() % limit;

};

(В PDB-файле не было имени «limit»; это мы назвали этот аргумент так, вручную.)

Так что она возвращает случайное число в пределах от нуля до заданного предела.

Rnd() вызывается только из одного места, это функция с названием StartGame() , и как видно, это именно тот код, что расставляет мины:

.text:010036C7 push _xBoxMac

.text:010036CD call _Rnd@4 ; Rnd(x)

.text:010036D2 push _yBoxMac

.text:010036D8 mov esi, eax

.text:010036DA inc esi

.text:010036DB call _Rnd@4 ; Rnd(x)

.text:010036E0 inc eax

.text:010036E1 mov ecx, eax

.text:010036E3 shl ecx, 5 ; ECX=ECX*32

.text:010036E6 test _rgBlk[ecx+esi], 80h

.text:010036EE jnz short loc_10036C7

.text:010036F0 shl eax, 5 ; EAX=EAX*32

.text:010036F3 lea eax, _rgBlk[eax+esi]

.text:010036FA or byte ptr [eax], 80h

.text:010036FD dec _cBombStart

.text:01003703 jnz short loc_10036C7

Сапёр позволяет задать размеры доски, так что X (xBoxMac) и Y (yBoxMac) это глобальные переменные. Они передаются в Rnd() и генерируются случайные координаты. Мина устанавливается инструкцией OR на 0x010036FA . И если она уже была установлена до этого (это возможно, если пара функций Rnd() сгенерирует пару, которая уже была сгенерирована), тогда TEST и JNZ на 0x010036E6 перейдет на повторную генерацию пары. cBombStart это глобальная переменная, содержащая количество мин. Так что это цикл. Ширина двухмерного массива это 32 (мы можем это вывести, глядя на инструкцию SHL , которая умножает одну из координат на 32).

Размер глобального массива rgBlk можно легко узнать по разнице между меткой rgBlk в сегменте данных и следующей известной меткой. Это 0x360 (864):

.data:01005340 _rgBlk db 360h dup(?) ; DATA XREF: MainWndProc(x,x,x,x)+574

.data:01005340 ; DisplayBlk(x,x)+23

.data:010056A0 _Preferences dd ? ; DATA XREF: FixMenus()+2

...

864/32 = 27.

Так что размер массива 27.32? Это близко к тому что мы знаем: если попытаемся установить размер доски в установках Сапёра на 100 . 100, то он установит размер 24 . 30. Так что это максимальный размер доски здесь. И размер массива фиксирован для доски любого размера. Посмотрим на всё это в OllyDbg. Запустим Сапёр, присоединим (attach) OllyDbg к нему и увидим содержимое памяти по адресу где массив rgBlk

( 0x01005340 ) 1. Так что у нас выходит такой дамп памяти массива:

Address Hex dump

01005340 10 10 10 10|10 10 10 10|10 10 10 0F|0F 0F 0F 0F|

01005350 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

01005360 10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|

01005370 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

01005380 10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|

01005390 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

010053A0 10 0F 0F 0F|0F 0F 0F 0F|8F 0F 10 0F|0F 0F 0F 0F|

010053B0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

010053C0 10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|

010053D0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

010053E0 10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|

010053F0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

01005400 10 0F 0F 8F|0F 0F 8F 0F|0F 0F 10 0F|0F 0F 0F 0F|

01005410 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

01005420 10 8F 0F 0F|8F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|

01005430 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

01005440 10 8F 0F 0F|0F 0F 8F 0F|0F 8F 10 0F|0F 0F 0F 0F|

01005450 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

01005460 10 0F 0F 0F|0F 8F 0F 0F|0F 8F 10 0F|0F 0F 0F 0F|

01005470 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

01005480 10 10 10 10|10 10 10 10|10 10 10 0F|0F 0F 0F 0F|

01005490 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

010054A0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

010054B0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

010054C0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

OllyDbg, как и любой другой шестнадцатеричный редактор, показывает 16 байт на строку. Так что каждая 32-байтная строка массива занимает ровно 2 строки. Это уровень для начинающих (доска 9*9). Тут еще какая-то квадратная структура, заметная визуально (байты 0x10). Нажмем «Run» в OllyDbg чтобы разморозить процесс Сапёра, потом нажмем в случайное место окна Сапёра, попадаемся на мине, но теперь видны все мины:

Рисунок 5. Мины.

Сравнивая места с минами и дамп, мы можем обнаружить что 0x10 это граница, 0x0F — пустой блок, 0x8F — мина. Теперь добавим комментариев и также заключим все байты 0x8F в квадратные скобки:

border:

01005340 10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F

01005350 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #1:

01005360 10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F

01005370 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #2:

01005380 10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F

01005390 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #3:

010053A0 10 0F 0F 0F 0F 0F 0F 0F[8F]0F 10 0F 0F 0F 0F 0F

010053B0 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #4:

010053C0 10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F

010053D0 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #5:

010053E0 10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F

010053F0 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #6:

01005400 10 0F 0F[8F]0F 0F[8F]0F 0F 0F 10 0F 0F 0F 0F 0F

01005410 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #7:

01005420 10[8F]0F 0F[8F]0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F

01005430 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #8:

01005440 10[8F]0F 0F 0F 0F[8F]0F 0F[8F]10 0F 0F 0F 0F 0F

01005450 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

line #9:

01005460 10 0F 0F 0F 0F[8F]0F 0F 0F[8F]10 0F 0F 0F 0F 0F

01005470 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

border:

01005480 10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F

01005490 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

Теперь уберем все байты связанные с границами (0x10) и всё что за ними:

0F 0F 0F 0F 0F 0F 0F 0F 0F

0F 0F 0F 0F 0F 0F 0F 0F 0F

0F 0F 0F 0F 0F 0F 0F[8F]0F

0F 0F 0F 0F 0F 0F 0F 0F 0F

0F 0F 0F 0F 0F 0F 0F 0F 0F

0F 0F[8F]0F 0F[8F]0F 0F 0F

[8F]0F 0F[8F]0F 0F 0F 0F 0F

[8F]0F 0F 0F 0F[8F]0F 0F[8F]

0F 0F 0F 0F[8F]0F 0F 0F[8F]

Да, это всё мины, теперь это очень хорошо видно, в сравнении с рисунком 4.

Вот что интересно, это то что мы можем модифицировать массив прямо в OllyDbg. Уберем все мины заменив все байты 0x8F на 0x0F, и вот что получится в Сапёре:

Рисунок. 6. Все мины убраны в отладчике.

Также уберем их все и добавим их в первом ряду:

Рисунок. 7. Мины, установленные в отладчике.

Отладчик не очень удобен для подсматривания (а это была наша изначальная цель), так что напишем маленькую утилиту для показа содержимого доски:

// Windows XP MineSweeper cheater

// written by dennis(a)yurichev.com for http://beginners.re/ book

#include <windows.h>

#include <assert.h>

#include <stdio.h>

int main (int argc, char * argv[])

{

int i, j;

HANDLE h;

DWORD PID, address, rd;

BYTE board[27][32];

if (argc!=3)

{

printf ("Usage: %s <PID> <address>\n", argv[0]);

return 0;

};

assert (argv[1]!=NULL);

assert (argv[2]!=NULL);

assert (sscanf (argv[1], "%d", &PID)==1);

assert (sscanf (argv[2], "%x", &address)==1);

h=OpenProcess (PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, PID);

if (h==NULL)

{

DWORD e=GetLastError();

printf ("OpenProcess error: %08X\n", e);

return 0;

};

if (ReadProcessMemory (h, (LPVOID)address, board, sizeof(board), &rd)!=TRUE)

{

printf ("ReadProcessMemory() failed\n");

return 0;

};

for (i=1; i<26; i++)

{

if (board[i][0]==0x10 && board[i][1]==0x10)

break; // end of board

for (j=1; j<31; j++)

{

if (board[i][j]==0x10)

break; // board border

if (board[i][j]==0x8F)

printf ("*");

else

printf (" ");

};

printf ("\n");

};

CloseHandle (h);

};

Просто установите PID и адрес массива ( 0x01005340 для Windows XP SP3 English) и она покажет его. Она подключается к win32-процессу по PID-у и просто читает из памяти процесса по этому адресу.

3.2 Взлом программ

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

Допустим, представим себе программу, которая что-то делает. Например, отказывается запускаться после 30 дней использования, выводя стандартное окошко с сообщением (из курса программирования знаем,что это окно создаётся стандартная функция MessageBox). Итак, у нас есть первое наблюдение: программа создаёт окно. Если перевод системного времени на компьютере не помогает обмануть программу и заставить её работать дольше, чем положено,- это второе наблюдение. Из него следует, что программа проверяет текущую дату не на основе показаний внутренних часов Windows. Предполагаем, что скорее всего программа либо уже сделала пометку "больше не запускаться" где-нибудь в реестре или на диске, либо всё-таки определяет текущее время, но каким-либо хитрым способом. Например, читая дату последнего доступа или дату модификации какого-либо файла. Если программа не просто "задумывается" при запуске, но ещё и шуршит винчестером, вероятность второго варианта сильно повышается. Теперь начинаем проверять эти варианты. В первом случае нам однозначно проще докопаться до истины, установив точки прерывания на все вызовы MessageBox в программе, и выяснять, какой из условных переходов позволяет избежать появления этого сообщения. Во втором случае в качестве отправной точки можно использовать всевозможное функции GetFileTime, CompareFileTime, FindFirstFile, FindNextFile и т. д.

У большинства защит, как бы аккуратно они ни были реализованы, всё-таки есть слабое место. Главная задача его найти. Одно из слабых мест это глобальные переменные. В такой переменной может храниться состояние программы (зарегистрирована/не зарегистрирована), число запусков (или количество дней) до истечения демонстрационного периода или результат проверки серийного номера на правильность (правильный/не правильный) и т. д.

Другой встречающийся приём, эффективный в основном, при ограничении максимального или минимального значения какого-либо числового параметра (это может быть , например, количество создаваемых документов, число запусков и т. п.)- поиск константы, с которой производится сравнение, и модификация либо самой константы, либо условия проверки. Таким образом, заменив значение константы, мы можем пользоваться программой, несмотря на то, что она не зарегистрирована.

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

Существуют и другие тонкости и способы обнаружений уязвимостей, но их невозможно найти без ряда необходимых инструментов:

-Дизассемблеры и отладчики.
Традиционно оба эти инструмента используются в паре, поскольку дизассемблер выдаёт лишь "чистый код". Отладчик же позволяет анализировать код в процессе его работы, отслеживать и изменять состояния регистров.

-Декомпиляторы и специализированные отладчики.

Они превращают код, понятный лишь компилятору, в форму , более удобную для понимания человеком.

-Распаковки и утилиты для снятия дампа процессов.

Дизассемблировать запакованную или зашифрованную программу невозможно, но очень хочется получить хоть какой-то листинг, можно попытаться извлечь из памяти компьютера дамп в момент её работы. Этот дамп уже можно более или менее успешно дизассемблировать. Более того, на основе дампа можно воссоздать исполняемый файл программы, причём этот файл будет успешно загружаться, запускаться и работать.

-Утилиты анализа файлов.

Они нужны, когда требуется быстро узнать, каким упаковщиком или защитным ПО обработана та или иная программа, найти все текстовые строки в дампе памяти, посмотреть содержимое файла в виде текстовых строк, вывести в файл список импортируемых и экспортируемых программой функций и многое другое.

-Шестнадцатеричные редакторы и редакторы ресурсов.

-Утилиты мониторинга.

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

2. Утилиты, которые отслеживают сам факт возникновения каких-либо событий.

Наши рекомендации