Иногда реверс-инжиниринг бывает элегантен и целенаправлен, когда, например, вы решаете сложную задачу и пытаетесь выяснить предназначение непонятной незадокументированной функции, и как можно использовать эту функцию наилучшим образом. Однако эта статья про другое. В этой заметке мы рассмотрим поиск скрытых возможностей посредством перехода между случайными функциями в памяти! Подобным образом обычно легко вызвать крах приложения, но может произойти и так, что вы найдете нечто полезное. Данная техника полезна для поиска скрытой функциональности, но есть некоторые ограничения. Метод работает только для приложений, которые вы можете отлаживать. За некоторыми исключениями (например, я использовал подобную технику для выхода из криво реализованной песочницы), приведенный способ в основном пригоден для анализа, а не для эксплуатации уязвимостей или расширения привилегий. Создание тестового бинарного файла Начнем с создания простейшей подопытной программы. Вы можете воспользоваться как 32 / 64-битным бинарником для Linux, так и исходным кодом и Make-файлом. Полный исходник #include <stdio.h> void random_function() { printf("You called me!\n"); } int main(int argc, char *argv[]) { printf("Can you call random_function()?\n"); printf("Press <enter> to continue\n"); getchar(); printf("Good bye!\n"); } Code #include <stdio.h> void random_function() { printf("You called me!\n"); } int main(int argc, char *argv[]) { printf("Can you call random_function()?\n"); printf("Press <enter> to continue\n"); getchar(); printf("Good bye!\n"); } Сохраните этот код в файле jumpdemo.c и скомпилируйте при помощи следующей команды: gcc -g -O0 -o jumpdemo jumpdemo.c Code gcc -g -O0 -o jumpdemo jumpdemo.c Опция -O0 добавляется для того, чтобы компилятор не выполнял оптимизации. Например, не удалял неиспользованные функции под видом «помощи». Если вы скачали вышеуказанный файл, то после распаковки архива можете просто запустить командуmake. В учебном контексте предположим, что все бинарные файлы компилируются с символами. То есть вы можете видеть имена функций! IDA– мое любимое приложение для анализа бинарных файлов, но для нашей цели утилиты nm более, чем достаточно: $ nm ./jumpdemo 0000000000601040 B __bss_start 0000000000601030 D __data_start 0000000000601030 W data_start 0000000000601038 D __dso_handle 0000000000601040 D _edata 0000000000601048 B _end 0000000000400624 T _fini U getchar@@GLIBC_2.2.5 w __gmon_start__ 0000000000400400 T _init 0000000000400630 R _IO_stdin_used w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable w _Jv_RegisterClasses 0000000000400620 T __libc_csu_fini 00000000004005b0 T __libc_csu_init U __libc_start_main@@GLIBC_2.2.5 0000000000400577 T main U puts@@GLIBC_2.2.5 0000000000400566 T random_function 0000000000400470 T _start 0000000000601040 D __TMC_END__ Code $ nm ./jumpdemo 0000000000601040 B __bss_start 0000000000601030 D __data_start 0000000000601030 W data_start 0000000000601038 D __dso_handle 0000000000601040 D _edata 0000000000601048 B _end 0000000000400624 T _fini U getchar@@GLIBC_2.2.5 w __gmon_start__ 0000000000400400 T _init 0000000000400630 R _IO_stdin_used w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable w _Jv_RegisterClasses 0000000000400620 T __libc_csu_fini 00000000004005b0 T __libc_csu_init U __libc_start_main@@GLIBC_2.2.5 0000000000400577 T main U puts@@GLIBC_2.2.5 0000000000400566 T random_function 0000000000400470 T _start 0000000000601040 D __TMC_END__ Все, что вы видите выше, является символами. Функции с префиксом T можно вызывать. Однако функции, имена которых начинаются с символа нижнего подчеркивания, являются встроенными, и мы можем проигнорировать эти методы (но в реальной жизни, конечно, не стоит игнорировать все то, что начинается с нижнего подчеркивания). Для нас представляют интерес две функции «main» и «random_function». Загрузка файла в gdb Перед вызовом одной из вышеупомянутых функций, нужно загрузить бинарный файл в отладчик gdb. В директории, где находится бинарник, введите следующую команду: $ gdb -q ./jumpdemo Reading symbols from ./jumpdemo...(no debugging symbols found)...done. (gdb) Code $ gdb -q ./jumpdemo Reading symbols from ./jumpdemo...(no debugging symbols found)...done. (gdb) Флаг –q отключает вывод необязательных результатов. После того как вы окажетесь в командной строке (gdb),бинарный файл будет загружен и готов к запуску, но еще не запущен. Можно проверить, если, например, ввести команду continue: (gdb) continue The program is not being run. Code (gdb) continue The program is not being run. Отладчик gdb – очень мощная утилита со множеством различных команд. Общую справочную информацию можно получить, если ввести команду help.Также можно использовать команду help<command>для получения более детальной справки по конкретной команде (например, helpbreak). Попробуйте! Простой вызов Теперь, когда файл загружен в gdb, можно выполнить запуск при помощи команды run (перед этим не лишним будет ознакомиться со справкой help run!). Вы получите те же самые результаты, как если бы выполнение происходило обычным образом. В конце происходит возврат в начало. При желании можете продолжать запуск снова и снова, однако никаких полезных результатов вы не получите. Чтобы изменить логику работы во время выполнения приложения, нужно запустить программу и остановиться, пока не произошло завершение. Наиболее распространенный способ решить эту задачу – поставить точку останова (help break) на функции main: $ gdb -q ./jumpdemo Reading symbols from ./jumpdemo...(no debugging symbols found)...done. (gdb) break main Breakpoint 1 at 0x40057b (gdb) Code $ gdb -q ./jumpdemo Reading symbols from ./jumpdemo...(no debugging symbols found)...done. (gdb) break main Breakpoint 1 at 0x40057b (gdb) Затем запускаем программу и смотрим, что происходит: (gdb) run Starting program: /home/ron/blogs/jumpdemo Breakpoint 1, 0x000000000040057b in main () (gdb) Code (gdb) run Starting program: /home/ron/blogs/jumpdemo Breakpoint 1, 0x000000000040057b in main () (gdb) Теперь приложение оказалось приостановлено во время выполнения, и мы можем управлять дальнейшим процессом. Далее можно просматривать/редактировать участки памяти, изменять регистры, возобновлять выполнение, переходить к другим участкам кода и много всего другого! В нашем случае, как вы уже могли догадаться, мы будем перемещается выполнение в другую часть программы. Если говорить конкретно, то при помощи команды jump (helpjump!) будем возобновлять выполнение, начиная с функции random_function(): $ gdb -q ./jumpdemo Reading symbols from ./jumpdemo...(no debugging symbols found)...done. (gdb) break main Breakpoint 1 at 0x40057b (gdb) run Starting program: /home/ron/blogs/jumpdemo Breakpoint 1, 0x000000000040057b in main () (gdb) help jump Continue program being debugged at specified line or address. Usage: jump <location> Give as argument either LINENUM or *ADDR, where ADDR is an expression for an address to start at. (gdb) jump random_function Continuing at 0x40056a. You called me! [Inferior 1 (process 11391) exited with code 017] Code $ gdb -q ./jumpdemo Reading symbols from ./jumpdemo...(no debugging symbols found)...done. (gdb) break main Breakpoint 1 at 0x40057b (gdb) run Starting program: /home/ron/blogs/jumpdemo Breakpoint 1, 0x000000000040057b in main () (gdb) help jump Continue program being debugged at specified line or address. Usage: jump <location> Give as argument either LINENUM or *ADDR, where ADDR is an expression for an address to start at. (gdb) jump random_function Continuing at 0x40056a. You called me! [Inferior 1 (process 11391) exited with code 017] Задуманное реализовано! Программа вывела фразу «You called me!», а, значит, функция random_function() отработала успешно. Код 017 означает, что выход произошел не совсем корректно, однако мы сознательно пошли на этот шаг, когда запустили случайную функцию вне всякого контекста. Если по каким-то причинам вы не можете поставить точку останова (возможно, программа была скомпилирована без символов, или вы не знаете, где находится функция main), то можно проделать тот же самый трюк без точек останова, если нажать ctrl-c во время выполнения бинарного файла: (gdb) run Starting program: /home/ron/blogs/jumpdemo Can you call random_function()? Press <enter> to continue ^C Program received signal SIGINT, Interrupt. 0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84 84 ../sysdeps/unix/syscall-template.S: No such file or directory. (gdb) jump random_function Continuing at 0x40056a. You called me! Program received signal SIGSEGV, Segmentation fault. 0x0000000000000001 in ?? () Code (gdb) run Starting program: /home/ron/blogs/jumpdemo Can you call random_function()? Press <enter> to continue ^C Program received signal SIGINT, Interrupt. 0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84 84 ../sysdeps/unix/syscall-template.S: No such file or directory. (gdb) jump random_function Continuing at 0x40056a. You called me! Program received signal SIGSEGV, Segmentation fault. 0x0000000000000001 in ?? () Насчет segmentation fault не переживайте. Как и в случае с кодом 017, подобное сообщение выводится, если программа не понимает, что делать после выполнения функции random_function. Еще один трюк, который вы можете проделать, перейти обратно к main(), чтобы программа «думала», что начинается выполнение заново: (gdb) run Starting program: /home/ron/blogs/jumpdemo Can you call random_function()? Press <enter> to continue ^C Program received signal SIGINT, Interrupt. 0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84 84 ../sysdeps/unix/syscall-template.S: No such file or directory. (gdb) jump main Continuing at 0x40057b. Can you call random_function()? Press <enter> to continue Code (gdb) run Starting program: /home/ron/blogs/jumpdemo Can you call random_function()? Press <enter> to continue ^C Program received signal SIGINT, Interrupt. 0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84 84 ../sysdeps/unix/syscall-template.S: No such file or directory. (gdb) jump main Continuing at 0x40057b. Can you call random_function()? Press <enter> to continue Я часто использую переход к main во время разработки эксплоита, чтобы проверить, получилось ли выполнить код без создания рабочего шелл-кода. Если программа начинает выполняться с начала, значит, у нас получилось поменять текущую инструкцию!