gdb-bin

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

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

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

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

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

Рисунок 1 www.gnu.org/software/gdb — официальный сайт отладчика gdb (русская документация находится на mitya.pp.ru/gdb)

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

Рисунок 2 собственная интерактивная «морда» отладчика gdb

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

Рисунок 3 родная стихия профессионалов

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

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

#gdb -q gdb-demo

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

#gdb -q –args gdb-demo arg1 arg2…argN

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

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

#gdb -q

(gdb)file gdb-demo

Reading symbols from gdb-demo…done

Листинг 3 загрузка файла 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 ()

Листинг 4 установка точки останова на 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 ?? ()

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

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

#ps -a

PIDTTYTIMECMD

8189pts/700:00:00gdb_demo

8200pts/500:00:00ps

# 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)

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

#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)

Листинг 7 подключение к уже запущенному процессу командной 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 $.

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

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

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

./tiny-crackme

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

# ps -a

PIDTTYTIMECMD

13414pts/700:00:03tiny-crackme

13419pts/500:00:00ps

# 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

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

Здесь «$pc» (с учетом регистра!) – условное обозначение регистра-счетчика команд (programcount), а «*(unsignedchar*)« – явное преобразование типа, без которого 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

Листинг 9 восстановление ячеек памяти с использованием переменной $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

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

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

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

(gdb)set $i = $pc

(gdb)dd $i++ 0xB3

(gdb)dd $i++ 0x2A

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

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

define dd

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

end

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

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

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

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

(gdb)x/i 0x200008

0x200008:jmp 0x200008

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

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

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

(gdb) display/3i $pc

1: x/3i $pc

0x200008:mov$0x2a,%bl

0x20000a:jmp0x200040

0x20000f:add%al,(%edx)

(gdb) ni

0x0020000a in ?? ()

1: x/3i $pc

0x20000a:jmp0x200040

0x20000f:add%al,(%edx)

0x200011:add%al,(%ebx)

(gdb) ni

0x00200040 in ?? ()

1: x/3i $pc

0x200040:jmp0x200046

0x200045:mov$0xe8,%al

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

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

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

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

(gdb) set disassembly-flavor intel

(gdb) display/3i $pc

1: x/3i $pc

0x200008:movbl,0x2a

0x20000a:jmp0x200040

0x20000f:addBYTE PTR [edx],al

(gdb) ni

0x0020000a in ?? ()

1: x/3i $pc

0x20000a:jmp0x200040

0x20000f:addBYTE PTR [edx],al

0x200011:addBYTE PTR [ebx],al

(gdb)

0x00200040 in ?? ()

1: x/3i $pc

0x200040:jmp0x200046

0x200045:moval,0xe8

0x200047:movses:[edi],ds:[esi]

Листинг 15 автоматическое отображение 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 в unsigneddec-формате

(gdb) p *0xbffffB3F

$6 = 256

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

(gdb) p/x *0xbffffB3F

$7 = 0x100

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

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

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

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