Хакер № 11 (283) (ноябрь 2022) -20-21

Скачать как pdf или txt
Скачать как pdf или txt
Вы находитесь на странице: 1из 2

ВЗЛОМ

АНАЛИЗИРУЕМ ДВОИЧНЫЕ
ФАЙЛЫ В LINUX ШТАТНЫМИ
СРЕДСТВАМИ

Крис Касперски Юрий Язев


Известный российский Широко известен под
хакер. Легенда ][, ex- псевдонимом yurembo.
редактор ВЗЛОМа. Т акже Программист, разработчик
известен под видеоигр, независимый
псевдонимами мыщъх, исследователь. Старый
nezumi (яп. 鼠, мышь), n2k, автор журнала «Хакер».
elraton, souriz, tikus, muss, [email protected]
farah, jardon, KPNC.

Ка­кие инс­т ру­м ен­т ы исполь­з овать в 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 first.cpp

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

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

PTRACE — ФУНДАМЕНТ ДЛЯ GDB

GDB — это сис­темно незави­симый кросс‑плат­формен­ный отладчик. Как и


боль­шинс­тво Linux-отладчи­ков, он осно­ван на биб­лиоте­ке PTrace, реали­‐
зующей низ­коуров­невые отла­доч­ные при­мити­вы. Для отладки мно­гопо­точ­ных
про­цес­сов и парал­лель­ных при­ложе­ний рекомен­дует­ся исполь­зовать допол­‐
нитель­ные биб­лиоте­ки, пос­коль­ку GDB с мно­гопо­точ­ностью справ­ляет­ся
не луч­шим обра­зом. Сре­ди соф­та для отладки мно­гопо­точ­ных при­ложе­ний
осо­бую популяр­ность заво­евал 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 И ЕГО КОМАНДЫ

В 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

Продолжение статьи →
ВЗЛОМ ← НАЧАЛО СТАТЬИ

ОТЛАДКА ПРОГРАММ
БЕЗ ИСХОДНИКОВ
АНАЛИЗИРУЕМ ДВОИЧНЫЕ ФАЙЛЫ В LINUX
ШТАТНЫМИ СРЕДСТВАМИ

КРАТКОЕ РУКОВОДСТВО ПО GDB

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


дной стро­ки.

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

И хотя за вре­мя сво­его сущес­тво­вания GDB успел обрасти ворохом кра­сивых


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

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

Как пра­вило, это удел эмиг­рантов с Windows-плат­формы, соз­нание которых


необ­ратимо иска­лече­но иде­оло­гией «око­шек». Гру­бо говоря, если WinDbg —
сле­сар­ный инс­тру­мент, то GDB — токар­ный ста­нок с прог­рам­мным управле­‐
нием. Ког­да‑нибудь ты полюбишь его.
Для отладки на уров­не исходных тек­стов прог­рамма дол­жна быть откомпи­‐
лиро­вана с отла­доч­ной информа­цией. В GCC для это­го нуж­но добавить ключ -
g. Если отла­доч­ная информа­ция недос­тупна, GDB будет отла­живать прог­рамму
на уров­не дизас­сем­блер­ных команд.
Обыч­но имя отла­жива­емо­го фай­ла переда­ется в коман­дной стро­ке:

gdb filename

Для отладки активно­го про­цес­са ука­жи в коман­дной стро­ке его ID, а для под­‐
клю­чения коры (core dump) вос­поль­зуйся клю­чом:

--core==corename

Все три парамет­ра мож­но заг­ружать одновре­мен­но, попере­мен­но перек­люча­‐


ясь меж­ду ними коман­дой target.
Пе­рек­люча­емся на отла­жива­емый файл:

target exec

На при­атта­чен­ный про­цесс:

target child

Или на дамп коры:

target core

Не­обя­затель­ный ключ -q подав­ляет вывод копирай­та. Заг­рузив прог­рамму


в отладчик, мы дол­жны уста­новить точ­ку оста­нова. Для это­го слу­жит коман­да
break (она же b).

b main

Эта коман­да уста­нав­лива­ет точ­ку оста­нова на фун­кцию main язы­ка C, а вот эта:

b _start

на точ­ку вхо­да в ELF-файл (впро­чем, в некото­рых фай­лах она называ­ется


по‑дру­гому). Мож­но уста­новить точ­ку оста­нова и на про­изволь­ный адрес:

b *0x8048424

или

b *$eax

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


понима­ет два «обще­сис­темных» регис­тра: $pc — ука­затель команд и $sp —
сте­ковый ука­затель. Толь­ко пом­ни, что непос­редс­твен­но пос­ле заг­рузки прог­‐
раммы в отладчик никаких регис­тров у нее еще нет, они появ­ляют­ся толь­ко
пос­ле запус­ка отла­жива­емо­го про­цес­са на выпол­нение (коман­да run, она же
r).
От­ладчик самос­тоятель­но реша­ет, какую точ­ку оста­нова уста­новить, прог­‐
рам­мную или аппа­рат­ную, и луч­ше ему не пре­пятс­тво­вать (коман­да при­нуди­‐
тель­ной уста­нов­ки аппа­рат­ной точ­ки оста­нова hbreak работа­ет не на всех вер­‐
сиях отладчи­ка). Точ­ки оста­нова на дан­ные в GDB называ­ются точ­ками наб­‐
людения — watch point. Перечис­лю основные при­емы работы с отладчи­ком.
1. Ко­ман­да watch addr вызыва­ет отладчик вся­кий раз, ког­да содер­жимое
addr изме­няет­ся, а awatch addr — при чте­нии или записи в addr.
2. Ко­ман­да rwatch addr реаги­рует толь­ко на чте­ние, но работа­ет не во всех
вер­сиях отладчи­ка.
3. Прос­мотреть спи­сок уста­нов­ленных точек оста­нова и наб­людения мож­но
коман­дой info break.
4. Ко­ман­да clear уда­ляет все точ­ки оста­нова.
5. Ко­ман­да clear addr уда­ляет все точ­ки оста­нова, уста­нов­ленные на дан­‐
ную фун­кцию, адрес или номер стро­ки.
6. Ко­ман­ды enable и disable поз­воля­ют вре­мен­но вклю­чать и отклю­чать
точ­ки оста­нова. Точ­ки оста­нова под­держи­вают раз­витый син­таксис
условных команд, опи­сание которо­го мож­но най­ти в докумен­тации.
7. Ко­ман­да continue (c) возоб­новля­ет выпол­нение прог­раммы, прер­‐
ванное точ­кой оста­нова.
8. Ко­ман­да next N (n N) выпол­няет N сле­дующих строк кода без вхо­да,
а step N (s N) сo вхо­дом во вло­жен­ные фун­кции. Если чис­ло N
не задано, по умол­чанию выпол­няет­ся одна стро­ка.
9. Ко­ман­ды nexti и stepi дела­ют то же самое, но работа­ют не со стро­ками
исходно­го тек­ста, а с машин­ными коман­дами. Обыч­но они исполь­зуют­ся
сов­мес­тно с коман­дой display/i $pc (x/i $pc), пред­писыва­ющей
отладчи­ку отоб­ражать текущую машин­ную коман­ду. Ее дос­таточ­но
вызывать один раз за сеанс.
10. Ко­ман­да jump addr переда­ет управле­ние в про­изволь­ную точ­ку прог­‐
раммы, а call addr/fname вызыва­ет фун­кцию fname с аргу­мен­тами!
Это­го нет даже во мно­гих Windows-отладчи­ках. А как час­то оно тре­бует­ся!
11. Дру­гие полез­ные коман­ды:
• finish — про­дол­жать выпол­нение до выхода из текущей фун­кции;
• until addr (u addr) — про­дол­жать выпол­нение, пока ука­зан­ное
мес­то не будет дос­тигну­то, при запус­ке без аргу­мен­тов — оста­новить
выпол­нение при дос­тижении сле­дующей коман­ды (акту­аль­но для цик­‐
лов!);
• return — немед­ленно вер­нуть­ся в дочер­нюю фун­кцию.
12. Ко­ман­да print (p) выводит зна­чение:
• вы­раже­ния (нап­ример, p 1+2);
• со­дер­жимого перемен­ной (p my_var);
• со­дер­жимого регис­тра (p $eax);
• ячей­ки памяти (p *0x8048424, p *$eax).
13. Ес­ли необ­ходимо вывес­ти нес­коль­ко яче­ек, вос­поль­зуйся коман­дой x/Nh
addr, где N — количес­тво выводи­мых яче­ек. Ста­вить сим­вол звез­дочки
перед адре­сом в этом слу­чае не нуж­но.
14. Ко­ман­да info registers (i r) выводит зна­чение всех дос­тупных
регис­тров.
15. Мо­дифи­циру­ет содер­жимое яче­ек памяти/регис­тров коман­да set:
• set $eax = 0 записы­вает в регистр eax ноль;
• set var my_var = $ecx прис­ваивает перемен­ной my_var зна­чение
регис­тра ecx;
• set {unsigned char*}0x8048424=0xCC записы­вает по бай­товому
адре­су 0x8048424 чис­ло 0xCC.
16. Ко­ман­да disassemble _addr_from _addr_to выда­ет содер­жимое
памяти в виде дизас­сем­блер­ного лис­тинга, фор­мат пред­став­ления которо­‐
го опре­деля­ется коман­дой set disassembly-flavor.
17. Ко­ман­ды info frame, info args, info local отоб­ража­ют содер­‐
жимое текуще­го фрей­ма сте­ка, аргу­мен­ты фун­кции и локаль­ные перемен­‐
ные. Для перек­лючения на фрейм материн­ских фун­кций слу­жит коман­да
frame N. Коман­да backtrace (bt) дела­ет то же самое, что и call
stack в Windows-отладчи­ках. При иссле­дова­нии дам­пов коры она незаме­‐
нима.

Ко­роче говоря, приб­лизитель­ный сеанс работы с GDB выг­лядит так: гру­зим


прог­рамму в отладчик, отда­ем ему коман­ду b main, а если не сра­бота­ет, то b
_start, затем r, пос­ле чего отла­жива­ем прог­рамму по шагам (n/s),
при желании задав парамет­ры (x/i $pc), что­бы GDB показы­вал, что у нас
выпол­няет­ся в дан­ный момент. Выходим из отладчи­ка по коман­де quit (q).
Опи­сание осталь­ных команд ищи в докумен­тации. Теперь по край­ней мере ты
не заб­лудишь­ся в ней.
Еще есть гра­фичес­кий интерфейс gdbgui, который запус­кает­ся внут­ри бра­‐
узе­ра.

Еще один гра­фичес­кий интерфейс к GDB, выпол­няющий­ся в веб‑бра­узе­ре

Он пред­став­ляет собой сер­верное при­ложе­ние, написан­ное на Python, и уста­‐


нав­лива­ется через pip:

sudo pip install gdbgui --upgrade

На выпол­нение он запус­кает­ся подоб­но GDB:

gdbgui filename

Срав­нение Linux-отладчи­ков с Windows-отладчи­ками показы­вает зна­читель­ное


отста­вание пос­ледних и их неп­рофес­сиональ­ную нап­равлен­ность. Трех­мерные
кноп­ки, мас­шта­биру­емые икон­ки, всплы­вающие меню — все это, конеч­но,
очень кра­сиво, но в GDB про­ще написать мак­рос или исполь­зовать уже
готовый (бла­го все, что толь­ко было мож­но зап­рограм­мировать, здесь зап­‐
рограм­мирова­ли задол­го до нас, поль­зуйся — не хочу).
Меж­ду тем отла­доч­ные средс­тва в Linux не замыка­ются на одном толь­ко
GDB. Одна­ко GDB с течени­ем вре­мени доказы­вает свою исклю­читель­ность.
Как мы уви­дели, GDB пок­рыва­ет все задачи отладки, и дру­гого в Linux не надо.
Единс­твен­ное, чего ему недос­тает, — нор­маль­ный ядер­ный отладчик сис­‐
темно­го уров­ня, ори­енти­рован­ный на работу с дво­ичны­ми фай­лами без сим­‐
воль­ной информа­ции и исходных тек­стов. Тяжелое детс­тво и ски­тание по мно­‐
жес­тву плат­форм наложи­ло на UNIX мрач­ный отпе­чаток в виде стрем­ления
к перено­симос­ти и кросс‑плат­формен­ности. Какое там хакерс­тво в таких усло­‐
виях! Впро­чем, дос­тупность исходных тек­стов дела­ет эту проб­лему неак­туаль­‐
ной.

ТРАССИРОВКА СИСТЕМНЫХ ФУНКЦИЙ

Пе­рех­ват сис­темных фун­кций — это нас­тоящее окно во внут­ренний мир


подопыт­ной прог­раммы, показы­вающее име­на вызыва­емых фун­кций, их аргу­‐
мен­ты и коды воз­вра­та. Отсутс­твие «лиш­них» про­верок на ошиб­ки — болезнь
всех начина­ющих прог­раммис­тов, и отладчик — не луч­шее средс­тво для их
поис­ка. Вос­поль­зуем­ся штат­ной ути­литой strace.
Вот про­токол, получен­ный с помощью strace. Смот­ри, перед тем как уме­‐
реть, прог­рамма откры­вает файл my_good_file, не находит его и, как следс­‐
твие, сбра­сыва­ет кору, впа­дая в нир­вану. Разуме­ется, это прос­тей­ший слу­чай,
но «пра­вило десяти» гла­сит, что девянос­то про­цен­тов вре­мени отладки ухо­дит
на поиск оши­бок, которые вооб­ще недос­той­ны того, что­бы их искать!
По­иски бага с помощью strace выг­лядят при­мер­но так:

__sysctl(0xbfbffb28,0x2,0x2805bce8,0xbfbffb24,0x0,0x0) = 0 (0x0)
mmap(0x0,32768,0x3,0x1002,-1,0x0) = 671469568 (0x2805d000)
geteuid() = 0 (0x0)
getuid() = 0 (0x0)
getegid() = 0 (0x0)
getgid() = 0 (0x0)
open("/var/run/ld-elf.so.hints",0,00) = 3 (0x3)
read(0x3,0xbfbffb08,0x80) = 128 (0x80)
lseek(3,0x80,0) = 128 (0x80)
read(0x3,0x28061000,0x4b) = 75 (0x4b)
close(3) = 0 (0x0)
access("/usr/lib/libc.so.4",0) = 0 (0x0)
open("/usr/lib/libc.so.4",0,027757775600) = 3 (0x3)
fstat(3,0xbfbffb50) = 0 (0x0)
read(0x3,0xbfbfeb20,0x1000) = 4096 (0x1000)
mmap(0x0,626688,0x5,0x2,3,0x0) = 671502336 (0x28065000)
mmap(0x280e5000,20480,0x3,0x12,3,0x7f000) = 672026624
(0x280e5000)
mmap(0x280ea000,81920,0x3,0x1012,-1,0x0) = 672047104
(0x280ea000)
close(3) = 0 (0x0)
sigaction(SIGILL,0xbfbffba8,0xbfbffb90) = 0 (0x0)
sigprocmask(0x1,0x0,0x2805bc1c) = 0 (0x0)
sigaction(SIGILL,0xbfbffb90,0x0) = 0 (0x0)
sigprocmask(0x1,0x2805bbe0,0xbfbffbd0) = 0 (0x0)
sigprocmask(0x3,0x2805bbf0,0x0) = 0 (0x0)
open("my_good_file",0,0666) ERR#2 'No such file
or directory'
SIGNAL 11
SIGNAL 11
Process stopped because of: 16
process exit, rval = 139

ДИЗАССЕМБЛИРОВАНИЕ В LINUX

Штат­ным дизас­сем­бле­ром в Linux явля­ется ути­лита objdump. Ском­пилиру­ем


при­мер HelloWorld:

#include <iostream>
int main()
{
std::cout << "Hello, world!" << std::endl;
return 0;
}

Ис­поль­зуем для это­го коман­ду

g++ helloworld.cpp -o helloworld

И сра­зу дизас­сем­бли­руем исполня­емый файл сле­дующей коман­дой, перенап­‐


равив вывод в файл, потому что он получит­ся длин­ным:

objdump -M intel -d helloworld > code.txt

В парамет­ре -M ука­зыва­ется архи­тек­тура, для которой обра­баты­вает­ся файл.


Зна­чени­ями могут выс­тупать кон­крет­ные архи­тек­туры (x86-64, i386, i8086)
или, как в дан­ном слу­чае, син­таксис ассем­бле­ра — intel,att. Вто­рое зна­‐
чение опре­деля­ет син­таксис AT&T. Параметр -d ука­зыва­ет на то, что надо
дизас­сем­бли­ровать весь файл.
По­лучим такой дизас­сем­блер­ный лис­тинг (при­веде­но с сок­ращени­ями):

helloworld: file format elf64-x86-64



Disassembly of section .text:

00000000000010c0 <_start>:
10c0: f3 0f 1e fa endbr64
10c4: 31 ed xor ebp,ebp
10c6: 49 89 d1 mov r9,rdx
10c9: 5e pop rsi
10ca: 48 89 e2 mov rdx,rsp
10cd: 48 83 e4 f0 and rsp,0xfffffffffffffff0
10d1: 50 push rax
10d2: 54 push rsp
10d3: 45 31 c0 xor r8d,r8d
10d6: 31 c9 xor ecx,ecx
10d8: 48 8d 3d ca 00 00 00 lea rdi,[rip+0xca] # 11a9 <
main>
10df: ff 15 f3 2e 00 00 call QWORD PTR [rip+0x2ef3]
# 3fd8 <__libc_start_main@GLIBC_2.34> — вызов main

10e5: f4 hlt
10e6: 66 2e 0f 1f 84 00 00 cs nop WORD PTR [rax+rax*1+0x0]
10ed: 00 00 00


00000000000011a9 <main>:
11a9: f3 0f 1e fa endbr64
11ad: 55 push rbp
11ae: 48 89 e5 mov rbp,rsp
11b1: 48 8d 05 4c 0e 00 00 lea rax,[rip+0xe4c]
# 2004 <_IO_stdin_used+0x4>
11b8: 48 89 c6 mov rsi,rax
11bb: 48 8d 05 7e 2e 00 00 lea rax,[rip+0x2e7e]
# 4040 <_ZSt4cout@GLIBCXX_3.4>
11c2: 48 89 c7 mov rdi,rax
11c5: e8 c6 fe ff ff call 1090
<_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
# Вывод строки «Hello, world!» на консоль…

11ca: 48 8b 15 ff 2d 00 00 mov rdx,QWORD


PTR [rip+0x2dff]
#3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_
ES6_@GLIBCXX_3.4>
11d1: 48 89 d6 mov rsi,rdx
11d4: 48 89 c7 mov rdi,rax
11d7: e8 c4 fe ff ff call 10a0 <_ZNSolsEPFRSoS_E@plt>
# …вслед за ней вывод символа конца строки

11dc: b8 00 00 00 00 mov eax,0x0


11e1: 5d pop rbp
11e2: c3 ret

Ис­полня­емый файл для Linux — ELF-файл — содер­жит отличные от PE-фай­ла


сек­ции. Но сек­ция с име­нем .text игра­ет важ­ную роль — содер­жит исполня­‐
емый код. Обра­ти вни­мание: в выведен­ном objdump дизас­сем­блер­ном коде
роль сим­вола начала ком­мента­рия игра­ет решет­ка — #. Фун­кция _start под­‐
готав­лива­ет сре­ду выпол­нения перед вызовом main. А в пос­ледней про­исхо­‐
дит под­готов­ка и вывод стро­ки на экран. Меж­ду тем objdump смог опре­делить
имя единс­твен­ной фун­кции — main.

Типы дизассемблеров
Что пред­став­ляет собой objdump? Вро­де он неп­лохо спра­вил­ся со сво­ей
задачей. Но задача эта была самая эле­мен­тарная! Мы ее при­вели лишь
для того, что­бы оце­нить спо­соб­ность дизас­сем­бле­ра прев­ращать нолики
и еди­ницы в ассем­блер­ные инс­трук­ции. Тем не менее, если бы у нас была
прог­рамма с условны­ми перехо­дами, цик­лами и вызова­ми фун­кций, резуль­‐
тат бы не был нас­толь­ко иде­аль­ным!
А все потому, что objdump — линей­ный дизас­сем­блер. Он прос­то переби­‐
рает все сег­менты кода в дво­ичном фай­ле, декоди­руя и пре­обра­зуя их
в коман­ды. Подоб­ным обра­зом ведет себя боль­шинс­тво прос­тых дизас­сем­‐
бле­ров. Проб­лемы могут воз­никнуть в тот момент, ког­да вмес­то кода дизас­‐
сем­блер встре­тит дан­ные. И, находясь в пол­ном неведе­нии, пре­обра­зует их
в ассем­блер­ные мне­мони­ки. Хуже того, ког­да блок дан­ных закон­чится, дизас­‐
сем­блер оста­нет­ся в рас­син­хро­низо­ван­ном сос­тоянии отно­ситель­но текуще­го
кода. Хорошо хоть, что ско­ро он все рав­но вой­дет в колею бла­года­ря спе­цифи­‐
ке кода на плат­форме x86.
Ина­че ведут себя рекур­сивные дизас­сем­бле­ры. Они учи­тыва­ют поток
управле­ния, дру­гими сло­вами, во вре­мя ана­лиза бинар­ника они про­гоня­ют
прог­рамму на собс­твен­ном вир­туаль­ном про­цес­соре, дизас­сем­бли­руя код,
попада­ющий­ся на пути. Этот под­ход показы­вает в точ­ности такой код, который
выпол­няет­ся физичес­ким про­цес­сором. Безус­ловно, этот метод поз­воля­ет
избе­жать декоди­рова­ния дан­ных, потому что про­цес­сор в здра­вом уме их
не выпол­няет!
К рекур­сивным дизас­сем­бле­рам отно­сит­ся мно­го раз выручав­шая нас IDA
Pro. Ког­да она встре­чает дан­ные, она переда­ет управле­ние челове­ку, потому
что вос­ста­нов­ление пер­воначаль­ного вида дан­ных оста­ется нерешен­ной тех­‐
ничес­кой задачей. Речь идет о слож­ных типах дан­ных: о мас­сивах, струк­турах
и клас­сах. Оди­нокую перемен­ную (или нес­коль­ко перемен­ных) IDA рас­кусит
без тру­да и без помощи челове­ка.
Меж­ду тем рекур­сивные дизас­сем­бле­ры тоже могут стра­дать дет­ски­ми
болез­нями. Нап­ример, не каж­дый поток управле­ния лег­ко прос­ледить. В силу
их ста­тичес­кой при­роды дизас­сем­бле­рам быва­ет слож­но обна­ружить адре­са
кос­венных перехо­дов или вызовов под­прог­рамм. Тог­да в бой всту­пают раз­ные
эвристи­чес­кие механиз­мы под кон­крет­ные ком­пилято­ры. Но это тема отдель­‐
ного раз­говора.
В пос­ледние годы в Linux осо­бое мес­то занима­ют дизас­сем­бле­ры
Radare2 и Ghidra. Оба пред­став­ляют собой бес­плат­ные про­дук­ты с откры­тым
исходным кодом. Пер­вый появил­ся на свет в 2006 году, тог­да еще в качес­тве
дис­кового редак­тора. Сей­час это мно­гофун­кци­ональ­ный инс­тру­мент хакера.
Ghidra — ори­енти­рован­ный на спе­цов дизас­сем­блер, раз­работан­ный
Агентством наци­ональ­ной безопас­ности США и выпущен­ный на прос­торы
интерне­та в 2019 году как ответ несок­рушимой IDA Pro. Мы под­робнее погово­‐
рим об этих инс­тру­мен­тах в сле­дующий раз.

WWW
А если тебе не тер­пится поз­накомить­ся с эти­ми
инс­т ру­мен­т ами поб­лиже пря­мо сей­час, обя­‐
затель­но про­читай статьи «Бит­ва пот­рошите­лей.
Выбира­ем луч­ший редак­т ор для вскры­т ия
исполня­емых фай­лов Windows», «Ghidra vs IDA
Pro. На что спо­собен бес­плат­ный тул­кит
для ревер­са, соз­д анный в АНБ» и «Ghidra vs
crackme. Обка­т ыва­ем кон­курен­т а IDA Pro на при­‐
мере решения хит­рой крэк­ми с VM».

ВЫВОДЫ

В сегод­няшней статье мы рас­смот­рели доб­ротный набор кодоко­пате­ля в Linux.


Этот набор име­ется прак­тичес­ки в каж­дом дис­три­бути­ве, даже в таком user-
friendly, как Ubuntu. Кро­ме того, мы поуп­ражня­лись с каж­дым инс­тру­мен­том
на дос­таточ­но эле­мен­тарных при­мерах, что­бы пер­вые шаги кодоко­пания
в новой сре­де с неп­ривыч­ки не показа­лись тебе черес­чур слож­ными. Меж­ду
тем эти экспе­римен­ты поз­волили нам почувс­тво­вать вкус отладки и дизас­сем­‐
бли­рова­ния кода в Linux и оце­нить их воз­можнос­ти на прак­тике.