Хакер № 11 (283) (ноябрь 2022) -20-21
Хакер № 11 (283) (ноябрь 2022) -20-21
Хакер № 11 (283) (ноябрь 2022) -20-21
АНАЛИЗИРУЕМ ДВОИЧНЫЕ
ФАЙЛЫ В 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);
int main() {
int *i = new int();
char *c = (char*)malloc(sizeof(char));
}
Запустим линтер:
cppcheck second.cpp
cppcheck first.cpp
Рекомендуется прогонять код под несколькими линтерами, так как все они
работают по‑разному, следовательно, каждый из них может обнаружить собс‐
твенный набор ошибок.
Пошаговое выполнение программы и контрольные точки останова в Linux
используются лишь в клинических случаях (типа трепанации черепа), когда все
остальные средства оказываются бессильными. Поклонникам Windows такой
подход кажется несовременным, ущербным и жутко неудобным, но это все
потому, что Windows-отладчики эффективно решают проблемы, которые
в Linux просто не возникают. Разница культур программирования между
Windows и Linux в действительности очень и очень значительна, поэтому преж‐
де, чем кидать камни в чужой огород, наведи порядок у себя. Непривычное
еще не означает неправильное. Точно такой же дискомфорт ощущает матерый
линуксоид, очутившийся в Windows.
#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; // Счетчик трассируемых инструкций
return 0;
Но зато эта функция делает все! При желании ты можешь за пару часов
написать собственный мини‑отладчик, специально заточенный под конкретную
проблему.
Аргумент _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,
который делает ему харакири.
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 filename
Для отладки активного процесса укажи в командной строке его ID, а для под‐
ключения коры (core dump) воспользуйся ключом:
--core==corename
target exec
На приаттаченный процесс:
target child
target core
b main
Эта команда устанавливает точку останова на функцию main языка C, а вот эта:
b _start
b *0x8048424
или
b *$eax
gdbgui filename
__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
#include <iostream>
int main()
{
std::cout << "Hello, world!" << std::endl;
return 0;
}
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!» на консоль…
Типы дизассемблеров
Что представляет собой 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».
ВЫВОДЫ