Ка­кие инс­тру­мен­ты исполь­зовать в Linux для ревер­са бинар­ных фай­лов? В этой статье мы рас­ска­жем, как для этих целей при­менять PTrace и GDB, и покажем, как выг­лядит работа с ними.

Отладка программ без исходников

Ре­дак­ция жур­нала «Хакер» сов­мес­тно с из­датель­ством БХВ решило адап­тировать под сов­ремен­ные реалии еще одну кни­гу Кри­са Кас­пер­ски — «Тех­ника отладки прог­рамм без исходных тек­стов». Вре­мя идет, и зна­ния уста­рева­ют, но опи­сан­ные в кни­ге тех­нологии вос­тре­бован­ны до сих пор. Мы акту­али­зиру­ем све­дения обо всех упо­мина­емых Кри­сом прог­рам­мных про­дук­тах: об опе­раци­онных сис­темах, ком­пилято­рах, средс­твах кодоко­пания.

А самое глав­ное, будет обновле­на аппа­рат­ная плат­форма с IA-32 на AMD64: имен­но этот переход в боль­шей сте­пени пов­лиял на тран­сфор­мацию прог­рам­мно­го обес­печения. Что­бы опти­мизи­ровать при­ложе­ние для новой архи­тек­туры, нуж­но исполь­зовать новые воз­можнос­ти язы­ка ассем­бле­ра и сов­ремен­ные коман­ды под­систе­мы работы с памятью. Все эти нюан­сы будут учте­ны в обновлен­ной вер­сии изда­ния.

 

Особенности отладки в Linux

Пер­вое зна­комс­тво с GDB (что‑то вро­де debug.com для MS-DOS, толь­ко мощ­нее) вызыва­ет у пок­лонни­ков Windows смесь разоча­рова­ния с отвра­щени­ем, а уве­сис­тая докумен­тация вго­няет в глу­бокое уны­ние, гра­нича­щее с суици­дом. Отов­сюду тор­чат рычаги управле­ния, но нету газа и руля. Не хва­тает толь­ко камен­ных топоров и зве­риных шкур. Как линук­соиды ухит­ряют­ся выжить в агрессив­ной сре­де это­го пер­вобыт­ного мира — загад­ка.

Нес­коль­ко стро­чек исходно­го кода UNIX еще пом­нят те древ­ние вре­мена, ког­да ничего похоже­го на инте­рак­тивную отладку не сущес­тво­вало и единс­твен­ным средс­твом борь­бы с ошиб­ками был ава­рий­ный дамп памяти. Прог­раммис­там при­ходи­лось месяца­ми (!) пол­зать по вороху рас­печаток, собирая рас­сыпав­ший­ся код в строй­ную кар­тину. Чуть поз­же появи­лась отла­доч­ная печать — опе­рато­ры вывода, понаты­кан­ные в клю­чевых мес­тах и рас­печаты­вающие содер­жимое важ­ней­ших перемен­ных. Если про­исхо­дит сбой, прос­тыня рас­печаток (в прос­торечии — «пор­тянка») поз­воля­ет уста­новить, чем занима­лась прог­рамма до это­го и кто имен­но ее так покоре­жил.

От­ладоч­ная печать сох­ранила свою акту­аль­ность и по сей день. В мире Windows она в основном исполь­зует­ся лишь в отла­доч­ных вер­сиях прог­раммы и уби­рает­ся из финаль­ной, что не очень хорошо: ког­да у конеч­ных поль­зовате­лей про­исхо­дит сбой, в руках оста­ется лишь ава­рий­ный дамп, на котором далеко не уедешь. Сог­ласен, отла­доч­ная печать куша­ет ресур­сы и отни­мает вре­мя. Вот почему в UNIX так мно­го сис­тем управле­ния про­токо­лиро­вани­ем — от стан­дар­тно­го syslog до прод­винуто­го Enterprise Event Logging. Они сок­раща­ют нак­ладные рас­ходы на вывод и жур­налиро­вание, зна­читель­но уве­личи­вая ско­рость выпол­нения прог­раммы.

Вот неп­равиль­ный при­мер исполь­зования отла­доч­ной печати:

#ifdef __DEBUG__
fprintf(logfile, "a = %x, b = %x, c = %x\n", a, b, c);
#endif

А вот — пра­виль­ный при­мер исполь­зования отла­доч­ной печати:

if (__DEBUG__)
fprintf(logfile, "a = %x, b = %x, c = %x\n", a, b, c);

От­ладоч­ная печать на 80% устра­няет пот­ребнос­ти в отладке, ведь отладчик исполь­зует­ся в основном для того, что­бы опре­делить, как ведет себя прог­рамма в кон­крет­ном мес­те: выпол­няет­ся условный переход или нет, что воз­вра­щает фун­кция, какие зна­чения содер­жатся в перемен­ных и т. д. Прос­то вле­пи сюда fprintf/syslog и пос­мотри на резуль­тат!

Че­ловек — не слу­га компь­юте­ра! Это компь­ютер при­думан для авто­мати­зации челове­чес­кой деятель­нос­ти (в мире Windows — наобо­рот), поэто­му Linux «механи­зиру­ет» поиск оши­бок нас­толь­ко, нас­коль­ко это толь­ко воз­можно. Вклю­чи мак­сималь­ный режим пре­дуп­режде­ний ком­пилято­ра или возь­ми авто­ном­ные верифи­като­ры кода (так­же извес­тные как ста­тичес­кие ана­лиза­торы), и баги побегут из прог­раммы, как мыщъ­хи с тонуще­го кораб­ля. Исто­ричес­ки самый пер­вый ста­тичес­кий ана­лиза­тор кода — LINT — дал имя всем его пос­ледова­телям — лин­теры. Windows-ком­пилято­ры тоже могут генери­ровать сооб­щения об ошиб­ках, по стро­гос­ти не усту­пающие GCC, но боль­шинс­тво прог­раммис­тов про­пус­кает их. Куль­тура прог­рамми­рова­ния, блин!

Су­щес­тву­ет мно­жес­тво лин­теров, как ком­мерчес­ких, так и сво­бод­ных, проп­риетар­ных и с откры­тым исходным кодом. Нап­ример, популяр­ный ста­тичес­кий ана­лиза­тор кода CppCheck слу­жит, как сле­дует из наз­вания, для ана­лиза C/C++-кода. Рас­простра­няет­ся в двух вари­антах: с откры­тыми исходни­ками и как плат­ный про­дукт. Во вто­ром слу­чае он име­ет пла­гины для всех мало‑маль­ски популяр­ных сред прог­рамми­рова­ния в Linux и Windows. CppCheck отли­чает­ся уни­каль­ным спо­собом ана­лиза, что сво­дит к миниму­му лож­ные сра­баты­вания.

Что­бы уста­новить CppCheck в Ubuntu, дос­таточ­но ввес­ти в кон­соль коман­ду

sudo apt-get install cppcheck

Те­перь мож­но про­верять фай­лы с кодом на наличие потен­циаль­ных оши­бок. Не мудрствуя лукаво, напишем код с глу­пой ошиб­кой:

int main() {
int *i = new int();
char *c = (char*)malloc(sizeof(char));
}

За­пус­тим лин­тер:

cppcheck second.cpp
CppCheck обнаружил две утечки памяти
CppCheck обна­ружил две утеч­ки памяти

Рас­смот­рим дру­гой при­мер:

cppcheck first.cpp
CppCheck обнаружил обращение за пределы массива
CppCheck обна­ружил обра­щение за пре­делы мас­сива

Ре­комен­дует­ся про­гонять код под нес­коль­кими лин­терами, так как все они работа­ют по‑раз­ному, сле­дова­тель­но, каж­дый из них может обна­ружить собс­твен­ный набор оши­бок.

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

 

PTrace — фундамент для GDB

GDB — это сис­темно незави­симый кросс‑плат­формен­ный отладчик. Как и боль­шинс­тво Linux-отладчи­ков, он осно­ван на биб­лиоте­ке PTrace, реали­зующей низ­коуров­невые отла­доч­ные при­мити­вы. Для отладки мно­гопо­точ­ных про­цес­сов и парал­лель­ных при­ложе­ний рекомен­дует­ся исполь­зовать допол­нитель­ные биб­лиоте­ки, пос­коль­ку GDB с мно­гопо­точ­ностью справ­ляет­ся не луч­шим обра­зом. Сре­ди соф­та для отладки мно­гопо­точ­ных при­ложе­ний осо­бую популяр­ность заво­евал TotalView. Этот прог­рам­мный пакет исполь­зует­ся для отладки прог­рамм на супер­компь­юте­рах, посему он не по кар­ману прос­тым смер­тным.

Внешний вид отладчика TotalView, специализирующегося на параллельных приложениях
Внеш­ний вид отладчи­ка TotalView, спе­циали­зиру­юще­гося на парал­лель­ных при­ложе­ниях

PTrace может перево­дить про­цесс в сос­тояние оста­нова и возоб­новлять его выпол­нение, читать и записы­вать дан­ные в адресном прос­транс­тве отла­жива­емо­го про­цес­са, читать и записы­вать регис­тры цен­траль­ного про­цес­сора.

На архи­тек­туре x86-64 это регис­тры обще­го наз­начения, сег­мен­тные регис­тры (дос­тавши­еся ей по нас­ледс­тву), регис­тры SSE и отла­доч­ные регис­тры семей­ства DRx (они нуж­ны для орга­низа­ции аппа­рат­ных точек оста­нова). В Linux еще мож­но манипу­лиро­вать слу­жеб­ными струк­турами отла­жива­емо­го про­цес­са и отсле­живать вызов сис­темных фун­кций. В «ори­гиналь­ном» UNIX это­го нет, и недос­тающую фун­кци­ональ­ность при­ходит­ся реали­зовы­вать уже в отладчи­ке.

Вот при­мер исполь­зования PTrace в Linux:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int pid; // PID отлаживаемого процесса
int wait_val; // Сюда wait записывает
// возвращаемое значение
long long counter = 1; // Счетчик трассируемых инструкций
// Расщепляем процесс на два
// Родитель будет отлаживать потомка
// (обработка ошибок для наглядности опущена)
switch (pid = fork())
{
case 0: // Дочерний процесс (его отлаживают)
// Папаша, ну-ка, потрассируй меня!
ptrace(PTRACE_TRACEME, 0, 0, 0);
// Вызываем программу, которую надо отрассировать
// (для программ, упакованных шифрой, это не сработает)
execl("/bin/ls", "ls", 0);
break;
default: // Родительский процесс (он отлаживает)
// Ждем, пока отлаживаемый процесс
// не перейдет в состояние останова
wait(&wait_val);
// Трассируем дочерний процесс, пока он не завершится
while (WIFSTOPPED(wait_val) /* 1407 */)
{
// Выполнить следующую машинную инструкцию
// и перейти в состояние останова
if (ptrace(PTRACE_SINGLESTEP,
pid, (caddr_t) 1, 0)) break;
// Ждем, пока отлаживаемый процесс
// не перейдет в состояние останова
wait(&wait_val);
// Увеличиваем счетчик выполненных
// машинных инструкций на единицу
counter++;
}
}
// Вывод количества выполненных машинных инструкций на экран
printf("== %lld\n", counter);
return 0;
}

В резуль­тате выпол­нения это­го при­ложе­ния на моей машине в кон­соль переда­ется сле­дующий вывод.

Вывод приложения ptrace_test
Вы­вод при­ложе­ния ptrace_test
 

PTrace и его команды

В user-mode дос­тупна все­го лишь одна фун­кция:

ptrace((int _request, pid_t _pid, caddr_t _addr, int _data))

Но зато эта фун­кция дела­ет все! При желании ты можешь за пару часов написать собс­твен­ный мини‑отладчик, спе­циаль­но заточен­ный под кон­крет­ную проб­лему.

Ар­гумент _request фун­кции ptrace важ­ней­ший из всех — он опре­деля­ет, что мы будем делать. Заголо­воч­ные фай­лы в BSD и Linux исполь­зуют раз­личные опре­деле­ния, зат­рудняя перенос при­ложе­ний PTrace с одной плат­формы на дру­гую. По умол­чанию мы будем исполь­зовать опре­деле­ния из заголо­воч­ных фай­лов Linux.

  • PTRACE_TRACEME — перево­дит текущий про­цесс в сос­тояние оста­нова. Обыч­но исполь­зует­ся сов­мес­тно с fork, хотя встре­чают­ся так­же и самот­расси­рующиеся при­ложе­ния. Для каж­дого из про­цес­сов вызов PTRACE_TRACEME может быть сде­лан лишь однажды. Трас­сировать уже трас­сиру­емый про­цесс не получит­ся (менее зна­чимое следс­твие — про­цесс не может трас­сировать сам себя, сна­чала он дол­жен рас­щепить­ся). На этом осно­вано боль­шое количес­тво анти­отла­доч­ных при­емов, для пре­одо­ления которых при­ходит­ся исполь­зовать отладчи­ки, работа­ющие в обход PTrace. Отла­жива­емо­му про­цес­су посыла­ется сиг­нал, перево­дящий его в сос­тояние оста­нова, из которо­го он может быть выведен коман­дой PTRACE_CONT или PTRACE_SINGLESTEP, выз­ванной из кон­тек­ста родитель­ско­го про­цес­са. Фун­кция wait задер­жива­ет управле­ние материн­ско­го про­цес­са до тех пор, пока отла­жива­емый про­цесс не перей­дет в сос­тояние оста­нова или не завер­шится (тог­да она воз­вра­щает зна­чение 1407). Осталь­ные аргу­мен­ты игно­риру­ются.
  • PTRACE_ATTACH — перево­дит в сос­тояние оста­нова уже запущен­ный про­цесс с задан­ным PID, при этом про­цесс‑отладчик ста­новит­ся его пред­ком. Осталь­ные аргу­мен­ты игно­риру­ются. Про­цесс дол­жен иметь тот же самый UID, что и отла­жива­ющий про­цесс, и не быть про­цес­сом setuid/setduid (или отла­живать­ся катало­гом root).
  • PTRACE_DETACH — прек­раща­ет отладку про­цес­са с задан­ным PID (как по PTRACE_ATTACH, так и по PTRACE_TRACEME) и возоб­новля­ет его нор­маль­ное выпол­нение. Все осталь­ные аргу­мен­ты игно­риру­ются.
  • PTRACE_CONT — возоб­новля­ет выпол­нение отла­жива­емо­го про­цес­са с задан­ным PID без раз­рыва свя­зи с про­цес­сом‑отладчи­ком. Если addr == 0, выпол­нение про­дол­жает­ся с мес­та пос­ледне­го оста­нова, в про­тив­ном слу­чае — с ука­зан­ного адре­са. Аргу­мент _data зада­ет номер сиг­нала, посыла­емо­го отла­жива­емо­му про­цес­су (ноль — нет сиг­налов).
  • PTRACE_SINGLESTEP — пошаго­вое выпол­нение про­цес­са с задан­ным PID: выпол­нить сле­дующую машин­ную инс­трук­цию и перей­ти в сос­тояние оста­нова (под x86-64 это дос­тига­ется взво­дом фла­га трас­сиров­ки, хотя некото­рые хакер­ские биб­лиоте­ки исполь­зуют аппа­рат­ные точ­ки оста­нова). BSD тре­бует, что­бы аргу­мент addr был равен 1, Linux хочет видеть здесь 0. Осталь­ные аргу­мен­ты игно­риру­ются.
  • PTRACE_PEEKTEXT/PTRACE_PEEKDATA — чте­ние машин­ного сло­ва из кодовой области и области дан­ных адресно­го прос­транс­тва отла­жива­емо­го про­цес­са соот­ветс­твен­но. На боль­шинс­тве сов­ремен­ных плат­форм обе коман­ды пол­ностью экви­вален­тны. Фун­кция ptrace при­нима­ет целевой addr и воз­вра­щает счи­тан­ный резуль­тат.
  • PTRACE_POKETEXT, PTRACE_POKEDATA) — запись машин­ного сло­ва, передан­ного в _data, по адре­су addr.
  • PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_GETFPXREGS) — чте­ние регис­тров обще­го наз­начения, сег­мен­тных и отла­доч­ных регис­тров в область памяти про­цес­са‑отладчи­ка, задан­ную ука­зате­лем _addr. Это сис­темно‑зависи­мые коман­ды, при­емле­мые толь­ко для x86/x86-64 плат­формы. Опи­сание регис­тро­вой струк­туры содер­жится в фай­ле <machine/reg.h>.
  • PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_SETFPXREGS — уста­нов­ка зна­чения регис­тров отла­жива­емо­го про­цес­са путем копиро­вания содер­жимого реги­она памяти по ука­зате­лю _addr.
  • PTRACE_KILL — посыла­ет отла­жива­емо­му про­цес­су сиг­нал sigkill, который дела­ет ему хараки­ри.
 

Поддержка многопоточности в GDB

Оп­ределить, под­держи­вает ли твоя вер­сия GDB мно­гопо­точ­ность или нет, мож­но при помощи коман­ды

info thread

Она выводит све­дения о потоках, а для перек­лючений меж­ду потока­ми исполь­зуй сле­дующую коман­ду:

thread N

Под­держи­вает­ся отладка мно­гопо­точ­ных при­ложе­ний:

info threads
4 Thread 2051 (LWP 29448) RunEuler (lpvParam=0x80a67ac) at eu_kern.cpp:633
3 Thread 1026 (LWP 29443) 0x4020ef14 in __libc_read () from /lib/libc.so.6
* 2 Thread 2049 (LWP 29442) 0x40214260 in __poll (fds=0x80e0380, nfds=1, timeout=2000)
1 Thread 1024 (LWP 29441) 0x4017caea in __sigsuspend (set=0xbffff11c)
(gdb) thread 4
 

Краткое руководство по GDB

GDB — это кон­соль­ное при­ложе­ние, выпол­ненное в клас­сичес­ком духе коман­дной стро­ки.

Внешний вид отладчика GDB
Внеш­ний вид отладчи­ка GDB

И хотя за вре­мя сво­его сущес­тво­вания GDB успел обрасти ворохом кра­сивых гра­фичес­ких морд (сре­ди них DDD, Data Display Debugger, — ста­рей­ший и самый популяр­ный интерфейс), инте­рак­тивная отладка в сти­ле WinDbg в мире Linux край­не непопу­ляр­на.

Отладчик DDD — графический интерфейс к GDB
От­ладчик DDD — гра­фичес­кий интерфейс к GDB

Как пра­вило, это удел эмиг­рантов с Windows-плат­формы, соз­нание которых необ­ратимо иска­лече­но иде­оло­гией «око­шек». Гру­бо говоря, если WinDbg — сле­сар­ный инс­тру­мент, то GDB — токар­ный ста­нок с прог­рам­мным управле­нием. Ког­да‑нибудь ты полюбишь его.

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

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии