Погружение в технику и философию gdb

         

Изменение потока выполнения


Программа, загруженная командой "file" (или указанная в командной строке), находится в бесформенном состоянии, представляющим собой всего лишь совокупность байт, записанных в выделенном регионе адресного пространства. Новый процесс для нее еще не создан и трассировать ее невозможно. Во всяком случае до тех пор, пока мы не дадим команду "run" или "r" (которой обычно предшествует установка точки останова на функцию main или start). Будучи запущенной, программа будет работать до тех пор, пока не встретит точку останова или не получит no-stop

сигнал (см. "обработка сигналов"). Применение команды "run" к уже запущенной программе, приведет к ее перезапуску (в конфигурации по умолчанию отладчик запрашивает подтверждение).

Продолжить работу программы, остановленной по точке останова или сигналу, можно командой "continue" ("c"), действующей так же, как и "run" (т.е. работающей до точки останова/сигнала).

Чтобы передать управление по произвольному адресу, необходимо сделать "jump" ("j") за которым следует адрес, имя функции или регистр. В частности, "*$pc" по своему действию аналогична команде "continue", "j foo" передает управление на метку/функцию foo (если только она присутствует в таблице символов), а "*0x80484AA" – прыгает на адрес 80484AAh. Если одни и те же адреса используются многократно, их можно загнать в пользовательскую переменную командой "set $my_foo=0x80484AA", а затем использовать ее в качестве параметра команды jump – "*$my_foo". Отладчик soft-ice, кстати, ничего подобного делать не умеет!

Команда "until" ("u") продолжает выполнение программы вплоть до указанного адреса (например, "*0x080484ED"), при достижении которого останавливается и передает управление отладчику.
Как и "
jump", команда "until" поддерживает работу не только с метками и адресами, но и переменными, значительно упрощая взлом. Без аргументов "until" аналогична команде "nexti" (см. "трассировка") — она переходит на следующую машинную команду, пропуская функции и циклы.

Рассмотрим следующий код:

 

.text:080484EB             jb     short loc_80484EF    ; à на выход из цикла

.text:080484ED             jmp    short loc_8048532    ; à к началу тела цикла

.text:080484EF             lea    eax, [ebp + var_28]  ; первая команда за концом цикла


фрагмент цикла, демонстрирующего сущность команды until


Команда "until", отданная на строке 80484EBh, равносильна "u *0x80484EF" — она выполняет цикл и передает управление отладчику только по выходу из него. Очень удобно!

Если нам необходимо дождаться выхода из функции, автоматически остановившись при встрече c RET — на этот случай предусмотрена команда "finish", аналогичная команде "P RET" отладчика soft-ice. Вместо пошаговой трассировки программы, gdb просматривает фрейм предыдущей функции (что можно сделать командой "backtrace" или "bt") и устанавливает точку останова на адрес возврата, что обеспечивает максимальную эффективность выполнения, но… если раскрутить стек и восстановить цепочку фреймов отладчику не удается, команда "finish" отказывает в работе.

Команда "return", в отличии от "finish", приводит к немедленному возвращению в материнскую функцию без выполнения оставшегося "хвоста". soft-ice

этого делать не умеет, а зря! "return" очень полезная и довольно часто используемая команда.

Еще soft-ice не умеет вызывать функции, а gdb это делает без всякого напряжения командой "call" за которой следует имя функции/переменная/регистр или адрес. Аргументы (если они есть) передаются в круглых скобах по Си-соглашению, то есть заносятся в стек справа налево, которые выталкиваются из стека все той же командой "call". Например, "call foo(1,2,3)" или "call 0x8048384(1,2,3)".

При желании можно даже выполнить команду оболочки, не выходя из отладчика. Это делается так: "shell ls" или "shell man open". Просто фанатически удобно! Впрочем, аналогичного результата можно добиться, открыв дополнительную консоль. Поскольку, gdb – это отладчик прикладного уровня, он, в отличии от gdb, не замораживает систему, позволяя использовать естественную многозадачность без извращения с "shell".





чтение и модификация памяти при помощи команды p (print)


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

 

(gdb) x/16x $pc      # просмотр 16 двойных слов в hex-виде, начиная от $pc

0x80483ef <main+9>
:     0xb8f0e483      0x00000000      0x04c7c429      0x00000624

0x80483ff <main+25>
:    0xffbfe800      0x458dffff      0x24048988      0xfffeb4e8

0x804840f <main+41>
:    0x90c3c9ff      0x90909090      0x90909090      0x90909090

0x804841f <main+57>
:    0xe5895590      0xf6315657      0x0cec8353      0x0000a0e8

 

(gdb) x/16xb $pc     # просмотр 16 байт слов в hex-виде, начиная от $pc

0x80483ef <main+9>
:     0x83    0xe4    0xf0    0xb8    0x00    0x00    0x00    0x00

0x80483f7 <main+17>
:    0x29    0xc4    0xc7    0x04    0x24    0x06    0x00    0x00



отображение машинной команды по заданному адресу


Вместо адреса можно использовать любое другое выражение, переменную или регистр (если регистры доступны), однако, отлаживать программу в таком режиме _крайне_ неудобно и лучше задействовать режим автоматического отображения, задаваемый командой "display".

Режим автоматического отображения позволяет выводить значение любого выражения, регистра, ячейки памяти, машинной инструкции при каждой остановке gdb (например, при пошаговом выполнении). Команда "display/i $pc" (которую достаточно дать один раз за весь сеанс), будет отображать одну машинную инструкцию под $pc за раз, однако, это не очень удобно и на практике постоянно возникает необходимость узнать — какая же будет следующая инструкция за выполняемой. Мыщъх обычно выводит по три инструкции за раз: "display/3i $pc" и довольствуется жизнью в полный рост.

(gdb) display/3i $pc

1: x/3i $pc

0x200008:     mov    $0x2a,%bl

0x20000a:     jmp    0x200040

0x20000f:     add    %al,(%edx)

(gdb) ni

0x0020000a in ?? ()

1: x/3i $pc

0x20000a:     jmp    0x200040

0x20000f:     add    %al,(%edx)

0x200011:     add    %al,(%ebx)

(gdb) ni

0x00200040 in ?? ()

1: x/3i $pc

0x200040:     jmp    0x200046

0x200045:     mov    $0xe8,%al

0x200047:     movsl  %ds:(%esi),%es:(%edi)



при установке точки наблюдения/останова


Команда "ignore n x" устанавливает счетчик игнорирования точки наблюдения/останова n, пропуская первые x срабатываний, что особенно полезно в циклах.

Если при срабатывании точки останова/наблюдения необходимо выполнить некоторую последовательность операций, можно воспользоваться командой "commands n" (где – номер точки наблюдения/останова), пример использования которой показан ниже:

 

commands 6

printf "hello, world\n"

ends



загрузка файла gdb-demo в отладчик из командной строки с аргументами


Отладчик печатает приглашение "(gdb)", ожидая ввода команд. При желании, отлаживаемый файл можно загрузить непосредственно из отладчика командой "file":

#gdb -q

(gdb)fileload gdb-demo

Reading symbols from gdb-demo...done



автоматическое исполнение


Получить информацию по точкам останова поможет команда "info break" ("i b" или "info watchpoints"). При запуске без аргументов она выдаст данные о состоянии всех точек наблюдения/останова. Если же необходимо "проэкзаменовать" какую-то конкретную точку наблюдения/останова, достаточно указать ее номер, например:

 

(gdb) i b 13

Num Type           Disp Enb Address    What

13  read watchpoint keep y              *134513676

        stop only if $eax == 4

(gdb)



загрузка файла gdb-demo из отладчика


Внимание!

В отличие от soft-ice/turbo-debugger/ollydbg и прочих windows-отладчиков, в gdb программа после загрузки еще _не_ готова к работе! Она не имеет регистрового контекста и потому команды трассировки нам недоступны, однако, мы можем устанавливать точки останова внутри программы (не на библиотечные функции!), просматривать/модифицировать память, дизассемблировать код и т. д.

Обычно, первым (разумным) действием после загрузки становится установка точки останова на функцию main

(главную функцию языка Си) или _start – точку входа в программу, что осуществляется командой "tb адрес/имя", устанавливающей "одноразовую" точку останова, после чего можно смело пускать программу командой "run" (или "r"), зная, что отладчик "всплывет" в точке останова.

#gdb -q gdb-demo

(gdb) tb main

Breakpoint 1 at 0x8048473

(gdb) r

Starting program: /home/kpnc/gdb/gdb-demo

0x08048473 in main ()



При запуске без аргументов она


Команда "clear" (она же "delete") используется для удаления точек наблюдения/останова. При запуске без аргументов она удаляет _все_ точки (при этом отладчик запрашивает подтверждение), если же необходимо удалить какую-то одну конкретную точку — достаточно задать ее номер, например: "delete 13", так же можно удалить целый диапазон точек останова/наблюдения: "delete 1-6" удаляет точки наблюдения с первой по шестую включительно и никакого подтверждения при этом не запрашивается, так что будьте на чеку!

Команды "enable" и "disable" используются для временного включения/выключения точек наблюдения/останова и имеют тот же самый синтаксис, что и "delete".


установка точки останова на main


Загрузка исполняемых файлов без символьной информации. Если символьная информация отсутствует (например, была отрезана утилитой strip, как _очень_ часто и бывает), то установка точек останова на _start/main

становится невозможной и мы должны указать отладчику "физический" адрес точки входа, который можно получить, например, при помощи утилиты objdump, запущенной с ключом -f:

#strip gdb-demo

#objdump -f gdb-demo

gdb-demo:     O : i386, EXEC_P, HAS_SYMS, D_PAGED

архитектура:  i386, флаги 0x00000112:

EXEC_P, HAS_SYMS, D_PAGED

начальный адрес 0x08048300

# gdb -q gdb-demo

(no debugging symbols found)...

(gdb) b main

Function "main" not defined.

Make breakpoint pending on future shared library load? (y or [n]) n

# ^ установка точки останова на main провалилась, (т.к. символьной информации нет)

#   отладчик предложил установить ее позднее, когда такой символ станет доступен,

#   но мы от этого отказались, поскольку такой символ не станет доступен никогда

(gdb) tb *0x8048300

Breakpoint 1 at 0x8048300

# ^ установка точки останова по непосредственному адресу прошла успешно

(gdb) r

Starting program: /home/kpnc/gdb/gdb-demo

(no debugging symbols found)...

0x08048300 in ?? ()



загрузка программы со стрипнутой символьной инфой


Подключение к уже запущенному процессу. Если процесс, который необходимо отлаживать, _уже_ запущен, к нему можно подключиться либо указав его идентификатор вместе с ключом "-pid" в командной строке, либо воспользовавшись командой "attach идентификатор", непосредственно из самого отладчика. Отсоединиться от процесса можно либо командой "detach" (запущенной без аргументов) или же выходом из отладчика по команде "quit" (или "q"). После отсоединения процесс продолжает свою работу в нормальном режиме, а если его необходимо завершить, на помощь приходит команда "kill", убивающая текущий отлаживаемый процесс.

#ps -a

PID    TTY    TIME          CMD

8189   pts/7  00:00:00      gdb_demo

8200   pts/5  00:00:00      ps

# gdb -q -pid 8189

Attaching to process 8189

Reading symbols from /home/kpnc/gdb/gdb_demo...done.

Reading symbols from /lib/libc.so.6...done.

Reading symbols from /lib/ld-linux.so.2...done.

0x400f2ab8 in read () from /lib/libc.so.6

(gdb)



отображение дампа программы с помощью команды "x"


Хакеров, начинающих свой жизненный путь с gdb такая форма подачи информации, быть может и устроит, но пользователям soft-ice она покажется слишком расточительной. Хорошо бы получить классический hex-дамп… И это действительно можно сделать! Достаточно создать реализацию своей собственной команды (назовем ее "ddd"), отображающий дамп с помощью функции "printf". В отличии от soft-ice, отладчик gdb бесконечно расширяем и если нас что-то не устаивает, практически всегда возможно переделать это под свой вкус.

Исходный код команды "ddd" выглядит так:

 

define ddd

set $ddd_p = 0

printf "%08Xh:",$arg0

while $ddd_p++ < 16

printf " %02X",*(unsigned char*)$arg0++

end

printf "\n"

end



подключение к уже запущенному процессу через командную строку


#gdb -q

(gdb) attach 8189

Attaching to process 8189

Reading symbols from /home/kpnc/gdb/gdb_demo...done.

Reading symbols from /lib/libc.so.6...done.

Reading symbols from /lib/ld-linux.so.2...done.

0x400f2ab8 in read () from /lib/libc.so.6

(gdb)



исходный код пользовательской команды ddd, выводящий дамп в стиле soft-ice и turbo-debugger


А вот пример ее использования:

 

(gdb) set $t = $esp

(gdb) ddd $t

BFFFFA50h: 8E FF 77 01 E0 FA FF BF 38 6E

BFFFFA5Ah: 01 40 34 FB FF BF 94 FA FF BF

BFFFFA64h: D0 42 03 40 88 77 01 40 2F 00



подключение к уже запущенному процессу командной attach


Загрузка программ с потрепанными заголовками. Если заголовок elf-файла умышленно искажен (как, например, в случае, описанным в статье "особенности дизассемблирования под LINUX на примере tiny-crackme", опубликованной в "хакере"), то gdb наотрез откажется загружать его. Пример такого файла можно найти на: www.crackmes.de/users/yanisto/tiny_crackme/.

Выход — циклим elf в точке входа, запускаем и подключаемся к процессу командой "attach" (или "gdb -pid идентификатор"), а после попадания в отладчик восстанавливаем оригинальные байты и приступаем к трассировке в обычном режиме. Покажем, как это осуществить на практике.

Загружаем tiny-crackme в любой hex-редактор (например, в hte или hiew), переходим в точку входа (в hiew'е это осуществляется нажатием <ENTER>
(для перехода в hex-режим), <F8>
[header], <F5>
[entry]). Запоминаем (записываем на бумажку) содержимое двух байт под курсором (в нашем случае они равны B3h 2Ah) и заменяем их на EBh FEh, что соответствует инструкции jumps $.



дамп памяти, выведенный пользовательской командой "ddd"


Необходимость введения дополнительной переменной (в данном случае это переменная "$t") объясняется тем, что команда "ddd" спроектирована так, чтобы отображать по 10h байт за раз, начиная с указанного адреса, а по нажатию на <ENTER>
gdb – повтор последней команды) — следующие 10h байт и т. д., при этом переданный команде аргумент используется для хранения последнего отображенного адреса. Вызов "ddd $esp" приведет к тому, что значение регистра $esp окажется увеличенным на 10h, что развалит программу.

Естественно, при желании можно переписать команду "ddd" так, чтобы она не имела никаких побочных эффектов и отображала не 10h

байт памяти, а ровно столько, сколько ей скажут.



загрузка зацикленного файла с потрепанным заголовком и восстановление оригинальных байт


Здесь "$pc" (с учетом регистра!) – условное обозначение регистра-счетчика команд (program count), а "*(unsigned char*)" – явное преобразование типа, без которого gdb ни за что не сможет определить размер записываемой ячейки. Довольно длинная конструкция и у нас возникает естественное желание ее сократить.

Отладчик помнит историю команд и чтобы не вводить уже введенную команду, достаточно нажать "стрелку вверх" и отредактировать строку. В нашем случае — заменить "$pc = 0xB3" на "($pc+1) = 0x2A". Уже короче! Но… все равно длинно. (Примечание: по умолчанию gdb не сохраняет историю команд и она действительна только в пределах одного сеанса, чтобы задействовать автоматическое сохранение, необходимо набрать "set history save on", чтобы не делать это при каждом запуске gdb, можно занести это последовательность в .gdbinit файл расположенный в HOME или текущей директории).

И вот тут мы подходим к одному из главных преимуществ gdb над soft-ice. Отладчик gdb неограниченно расширяем и поддерживает продвинутый интерпретатор, позволяющий среди прочего объявлять свои переменные, начинающиеся со знака "$", с которыми можно делать что угодно.

Улучшенный вариант выглядит так:

(gdb)set $i = $pc

(gdb)set *(unsigned char*)$i++ = 0xB3

(gdb)set *(unsigned char*)$i++ = 0x2A



дизассемблерный фрагмент файла, защищенного протектором burneye


Попытка отладки программы в нормальном режиме приводит к ее краху (см. рис. 4), если, конечно, в момент возбуждения сигнала не увеличить содержимое ячейки 5375748h на единицу.

 



восстановление ячеек памяти с использованием переменной $i


Здесь, после ввода "set *(unsigned char*)$i++ = 0xB3" мы нажимаем "стрелку вверх" и всего лишь меняем 0xB3 на 0x2A (переменная $i увеличивается сама), что намного короче, но... все равно длинно и нудно.

А давайте объявим свою собственную пользовательскую команду! Это делается с помощью "define" и в нашем случае выглядит так:

(gdb)define dd

type command for definition of "dd".

end with a line saying just "end".

>
set *(unsigned char*) $arg0 = $arg1

>
end



объявление пользовательской переменной dd, записывающей байт по указанному адресу


Обратите внимание, как gdb изменил тип приглашения (">
"), когда началось определение команды! Закончив писать, мы говорим "end" и новая команда добавляется в память gdb наряду со всеми остальными. Она принимает два аргумента $arg0 – адрес по которому писать и $arg1 – записываемый байт.

Теперь для восстановления байт в точке входа, достаточно дать следующую последовательность команд (внимание! если написать "dd $pc++ 0xB3", то после выполнения команды регистр $pc увеличится на единицу, что никак не входит в наши планы!):

(gdb)set $i = $pc

(gdb)dd $i++ 0xB3

(gdb)dd $i++ 0x2A



восстановление ячеек памяти через пользовательскую команду


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

define dd

set *(unsigned char*)$arg0 = $arg1

end



файл n2k_cmd, содержащий определение команды dd


Загрузка командного файла в память осуществляется командой "source имя_файла" (в нашем случае: "source n2k_cmd"), причем, поскольку gdb поддерживает автозавершение ввода (свойственное практически всем UNIX-программам) совершенно необязательно выписывать "source" целиком. Достаточно набрать "so" и нажать <TAB>
. Отладчик самостоятельно допишет остальное. Если существует несколько команд, начинающихся с "so", вместо автозавершения раздастся мерзкий писк, сигнализирующей о неоднозначности. Повторное нажатие TAB'а приводит к выводу всех возможных вариантов.

Создавая свои собственные команды (загружаемые из .gdbinit файла или вручную), мы не только обеспечиваем комфортную работу, но и увеличиваем производительность труда! Те, кто обвиняют gdb в неудобстве, просто не умеют затачивать его под себя, но мы, мыщъх'и, — умеем! Кстати, сейчас как раз время чтобы что-нибудь заточить.



автоматическое отображение 3х инструкций при трассировке в формате AT&T


Для автоматического отображения значения регистров достаточно дать команду "display регистр", где регистр — $eax, $ebx, $ecx и т. д. Для регистра-указателя текущего положения стека существует специальное имя — $sp, которое можно использовать наравне с $esp (точно так же, как $pc <-->
$eip). Автоматических отображений может быть создано сколько угодно и любое из них всегда может быть уделено командой "undisplay n1 n2 .. nn", где nx – номер отображения, которой можно узнать по команде "info display". Временно выключить отображение помогает команда "disable display n1 n2 ... nn", а enable display – включает обратно.

Переключение режима дизассемблирования ATT&T|Intel. По умолчанию, gdb использует синтаксис AT&T, но может выводить инструкции и в формате Intel, для чего достаточно дать команду "set disassembly-flavor intel", а чтобы вернуться назад: "set disassembly-flavor att". Вот, сравните это с листингом 14.

(gdb) set disassembly-flavor intel

(gdb) display/3i $pc

1: x/3i $pc

0x200008:     mov    bl,0x2a

0x20000a:     jmp    0x200040

0x20000f:     add    BYTE PTR [edx],al

(gdb) ni

0x0020000a in ?? ()

1: x/3i $pc

0x20000a:     jmp    0x200040

0x20000f:     add    BYTE PTR [edx],al

0x200011:     add    BYTE PTR [ebx],al

(gdb)

0x00200040 in ?? ()

1: x/3i $pc

0x200040:     jmp    0x200046

0x200045:     mov    al,0xe8

0x200047:     movs   es:[edi],ds:[esi]



автоматическое отображение 3х инструкций при трассировке в формате Intel


Перенаправление ввода/вывода. По умолчанию, gdb связывает со стандартным вводом/выводом отлаживаемой программы текущую консоль, в результате чего сообщения программы перемешиваются с сообщениями отладчика. Чтобы навести порядок, необходимо перенаправить в/в программы в отдельную консоль, что осуществляется командой "tty консоль". Открываем новую консоль, даем UNIX-команду "tty" для определения ее имени (например, "/dev/ps/6"), возвращаемся к консоли отладчика и говорим: "tty /dev/ps/6".

Вывод выражение на экран. Для вывода выражений используется команда "print" или ее более короткий псевдоним "p" за которым следует выражение.

Например:

(gdb) p 2*2

$1 = 4

(gdb) p $1 + 3

$2 = 7

(gdb) p $sp

$3 = (void *) 0xbffffb40

# вывод значение $sp

(gdb) p/x *(unsigned int*) $sp

$4 = 0x1

# вывод ячейки, на которую указывает $sp в hex-формате

(gdb) p/u *(unsigned int*) $sp

$5 = 1

# вывод ячейки, на которую указывает $sp в unsigned dec-формате

(gdb) p *0xbffffB3F

$6 = 256

# вывод содержимого ячейки в dec-формате (по умолчанию)

(gdb) p/x *0xbffffB3F

$7 = 0x100

# вывод содержимого ячейки в hex-формате



демонстрация возможностей print


Как видно, при каждом выводе значения, "print" создает переменную, которую можно использовать в последующих выражениях, что _очень_ удобно. Так же доступа функция "printf" со стандартным набором спецификаторов, которая особенно удобна в командных файлах. Например: 'printf "%x %x %x\n",$eax,$ebx,$ebx', выводит значение сразу трех регистров. Обратите внимание на отсутствие круглых скобок вокруг нее!



Обработка сигналов


Сигналом— называется асинхронное событие, происходящее в программе и чем-то напоминающее структурные исключения в Windows. Сигналы делятся на фатальные и не фатальные. Пример не фатального сигнала является SIGALRM, возбуждаемый при срабатывании интервального таймера. А вот при нарушения доступа к памяти генерируется сигнал SIGSEGV, завершающий программу в аварийной режиме (если только программист не предусмотрел специальный обработчик).

Отладчик gdb

перехватывает все сигналы и в зависимости от своей конфигурации либо передает сигнал программе, либо "поглощает" его, делая вид, что ничего интересного не происходит.

Посмотреть текущую конфигурацию gdb можно с помощью команды "info signals" (она же "info handle"), а для изменения реакции gdb необходимо воспользоваться "handle сигнал поведение", где сигнал — название сигнала (например, "SIGSEGV"), а поведение — реакция отладчика на возникновения сигнала, описываемая следующими ключевыми словами:

 

nostop    при получении этого сигнала GDB не останавливает программу;

stop        при получении этого сигнала GDB останавливает программу;

print        при получении данного сигнала GDB выводит сообщение о нем на экран;

noprint   GDB не замечает этот сигнал;

pass        GDB позволяет программе увидеть этот сигнал;

nopass   GDB массирует этот сигнал, не позволяя программе увидеть его.

 



Подготовка к отладке


Загрузка исполняемых файлов в отладчик обычно осуществляется заданием их имени (при необходимости– с путем) в командной строке. При этом полезно указывать ключ "?quiet" (или, сокращенно, "-q") для подавления надоедливого копирайта.

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

#gdb -q gdb-demo



Погружение в технику и философию gdb или отладка двоичных файлов под gdb


крис касперски ака мыщъх, no-email

GDB – один из самых мощных отладчиков из всех когда-либо созданных, однако, переход с soft-ice на gdb обычно протекает бывает очень болезненнымо. запустив gdb, мы попадаем в совершенно иной мир, похожий на дремучий лес, в котором очень легко заблудиться, но мыщъх покажет как обустроить gdb для хакерский целей, вырыть уютную нору и сделать свои первые шаги на пути к истинному Дао gdb.



Погружение в технику и философию gdb (окончание)


крис касперски ака мыщъх, no-email

 

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



Прежде, чем начинать трассировку


В отличии от soft-ice (и даже debug.com!), gdb не показывает мнемоники машинных инструкций при трассировке, если его об этом не просят, что сильно смущает новичков, но идеологически так намного правильнее. Отобразить машинную команду по произвольному адресу можно с помощью команды "x/iадрес", например:

(gdb)x/i 0x200008

0x200008:     jmp 0x200008



Работа с памятью и регистрами


Дамп памяти для хакеров— это святое. Без него не обходится ни один взлом. Просматривать/модифицировать память можно разными способами. Например, с помощью команды "print"/"printf":

 

(gdb) p $esp         # вывод содержимого регистра esp

$1 = (void *) 0xbffffa50

 

(gdb) p (char) $esp  # вывод младшего байта регистра esp

$2 = 80 'P'

 

(gdb) p *0x80483ef   # вывод содержимого двойного слова в десятичном знаковом виде

$3 = -1192172413

 

(gdb) p/x *0x80483ef # вывод содержимого двойного слова в hex-виде

$4 = 0xb8f0e483

 

(gdb) p/u *0x80483ef # вывод содержимого двойного слова в десятичном беззнаковом виде

$5 = 3102794883

 

(gdb) p $esp = 0     # присвоение регистру esp значения 0

$6 = (void *) 0x0

 

(gdb) p/x *((char*)0x8048413)=0x90

$7 = 0x90            # присвоение значение 90h байту по адресу 8048413h



реальная установка точек останова происходит только при начале выполнения программы


Аппаратные точки наблюдения на запись ячейки задаются командой "watch" ("wa"), команды "rwatch" ("rw") и "awatch" ("aw") устанавливают точки наблюдения на чтение и чтение/запись соответственно (например, "rw *0xBFFFFA50", а вот "rw *$esp" уже не срабатывает и отладчик сообщает "Attempt to dereference a generic pointer") . Как и в случае с точками останова по выполнению, сообщение "Hardware read watchpoint N: адрес" не означает ровным счетом ничего и при попытке запуска/продолжения выполнения программы отладчик может сказать: "Could not insert hardware watchpoint N", обламывая нас по полной программе.

Все точки наблюдения/останова могут быть условными, то есть срабатывать только в том случае, если значение выражения, стоящего после "if", истинно. Например, "b if $eax==0", или "rw *0xBFFFFA60 if (*((unsigned int*)$esp)!=0x669)".

При установке всякой точки наблюдения/останова отладчик присваивает ей номер, который, во-первых, высвечивается при ее срабатывании, а, во-вторых, может быть использован в командах управления точками наблюдения/останова. Номер последней сработавшей точки останова автоматически заносится в переменную $bpnum.

 

(gdb) hb main

Hardware assisted breakpoint 1 at 0x80483ef

(gdb) r

Starting program: /home/kpnc/gdb/gdb_demo

 

Breakpoint 1, 0x080483ef in main ()



просмотр реакции отладчика gdb на различные сигналы


Некоторые сигналы (например, сигнал SIGTRAP, возникающий при достижении программной точки останова) отладчик резервирует для своих собственных нужд. Этим обстоятельством пользуются многие защищенные программы, определяющие находятся ли они под отладкой или нет. Они устанавливают свой собственный обработчик SIGTRAP и затем выполняют инструкцию INT 03h. При выполнении без gdb управление получает обработчик, в противном случае сигнал поглощается отладчиком и обработчик уже не получает управление.

В частности, файлы, упакованные протектором burneye содержат следующий код:

 

0x053714a7:   mov    $0x5,%ebx

0x053714ac:   mov    $0x5371a0c,%ecx

0x053714b1:   mov    $0x30,%edx

0x053714b6:   mov    %edx,%eax

0x053714b8:   int    $0x80

0x053714ba:   add    $0x5375a00,%esi

0x053714c0:   mov    %esi,0xfffffd24(%ebp)

0x053714c6:   int3

0x053714c7:   cmpl   $0x0,0x5375748

0x053714ce:   jne    0x53714e2

0x05371a0c:   push   %ebp

0x05371a0d:   mov    %esp,%ebp

0x05371a0f:   incl   0x5375748

0x05371a15:   leave

0x05371a16:   ret



www.gnu.org/software/gdb


Это — не пересказ документации по gdb и не руководство по командам. Это — несистематическое, неструктурированное описание основных возможностей gdb, адаптированное под хакерские цели. Мыщъх будет забегать вперед, пускаться в длинные отступления, ходить кругами и возвращаться обратно, но… по другому готовить о gdb просто не получается.



собственная интерактивная "морда" отладчика gdb


Все графические морды (типа DDD) идут в топку, поскольку дискредитируют философию интерактивной отладки и превращают gdb в некоторое подобие морской свинки (и не свинки, и не морской), к тому же gdb имеет свою собственную встроенную "морду", вызываемую ключом "-tui" командой строки и ориентированную преимущественно на отладку приложений с исходными текстами. Для хакеров же она практически бесполезна.



попытка отладки программы


Но существует гораздо более простой и элегантный путь. Дождавшись "ругательства" отладчика по поводу поимки сигнала SIGTRAP мы даем команду "handle SIGTRAP pass" и передаем программе управление командой "continue". И все будет работать как часы!

 



зацикливание программы в hiew'e


Сохраняем изменения, запускам файл, определяет его pid, подключаем к процессу отладчик. На этот раз gdb хоть и ругается на неверный формат, но все-таки подключается к процессу, предоставляя нам полную свободу действий. Но прежде, чем начать трассировку, необходимо расциклить файл, вернув пару байт из точки входа на место.

Модификация памяти (регистров и переменных) осуществляется командой "set", в нашем случае вызываемой следующим образом:

./tiny-crackme

# запускаем зацикленный tiny-crackme и переходим на соседней консоли

# ps -a

PID    TTY    TIME          CMD

13414  pts/7  00:00:03      tiny-crackme

13419  pts/5  00:00:00      ps

# gdb -q

(gdb) attach 13414

Attaching to process 13414

"/home/kpnc/gdb/tiny-crackme": not in executable format:

File format not recognized

# ^ gdb ругается на неверный формат файла,

#   но все-таки аттачится к процессу

(gdb) set *(unsigned char*)$pc = 0xB3

(gdb) set *(unsigned char*)($pc+1) = 0x2A

# ^ восстановление оригинальных байт командой set



Точки останова


Отладчик gdb

поддерживает два типа точек останова: останов по выполнению кодаbreakpoint, и останов по обращению к данным (так же называемый точками наблюдения) — watch-point. (Еще gdb

поддерживает точки перехвата, но для отладки программ без исходных текстов они практически бесполезны).

Точки останова могут быть как программными, так и аппаратными. Программная точка останова по выполнению на x86-платформе представляет собой однобайтовую инструкцию CCh (INT 03h), а программный watch-point реализуется путем пошаговой трассировки программы с отслеживанием обращений к подопытной ячейке, что, во-первых, крайне непроизводительно, а, во-вторых, некоторые программы просто не позволяют себя трассировать. Аппаратных точек останова на x86 всего четыре, программных же можно устанавливать сколько угодно.

Программная точка останова по исполнению задается командой "break" ("b") за которой следует имя функции/адрес/регистр или переменная. Например, "b main", "*0x80484BC". Команда "tbreak" ("tb") устанавливает одноразовую точку останова, которая автоматически удаляется при срабатывании. Аппаратная точка останова задается командой "hbreak" ("hb"), а временная аппаратная точка — "thbreak" ("thb"). После установки аппаратной точки останова отладит сообщает "Hardware assisted breakpoint N at адрес", однако, это еще не значит, что операция завершилась успешно. Для проверки можно установить хоть тысячу аппаратных точек останова и все будет ОК, но вот только при запуске программы командами "run" или "continue" отладчик может сообщить: "Warning: Cannot insert hardware breakpoint N".

 



Трассировка


С трассировкой связны всего две команды: "stepin" ("si n") выполняет n следующих инструкций с заходом в циклы и функции, а "nexti n" ("ni n") – без захода. При запуске без аргументов выполняется только одна инструкция. Нажатие на <ENTER> автоматически повторяет последнюю команду ("stepi" или "nexti"), что значительно ускоряет трассировку (кстати, лупить по <ENTER'у> намного удобнее, чем давить на любую из функциональных клавиш, используемых для трассировки Windows-отладчиками).

Команды "step n"/"next n" (упоминание о которых можно найти в документации на gdb) ориентированы на работу с исходными текстами и выполняют n строк, а в отсутствии символьной информации трассируют программу вплоть до ее завершения, что не есть хорошо.

 



BSD существует множество отладчиков, но


Под LINUX/ BSD существует множество отладчиков, но общепризнанный лидер это, бесспорно, GDB, входящий в состав практически любого дистрибутива. Внешне (только внешне!) схожий с debug.com, он так и дышит мощью, поражающей воображение и потрясающей создание по мере его освоения.
Да-да, именно освоения! В отличии от soft-ice, gdb основан на невизуальных концепциях и ориентирован на удобство работы, а совсем не на легкость освоения. В нем заложено _столько_ возможностей, что их совершенно невозможно загнать в прокрустово ложе визуального интерфейса. По количеству органов управления gdb сравним разве что с истребителем и прежде, чем эта махина стронется с места, приходится перетрахать сотни страниц документации, отказаться от всех прежних привычек и понятий, вывернуть сознание наизнанку и поехать крышей. Зато потом soft-ice покажется жалкой поделкой, на которую невозможно смотреть без содрогания.


Мы успешно загрузили исполняемый файл


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



Вот мы и познакомились с основными возможностями могучего отладчика gdb. Однако, это лишь малая часть того, на что он способен. Все остальное— содержится в штатной документации и… исходных текстах (документация, как водится, содержит множество упущений). Тем не менее, для большинства хакерских задач рассмотренных нами команд будет вполне достаточно.