Вызов случайных функций в gdb

Вызов случайных функций в gdb

В этой заметке мы рассмотрим поиск скрытых возможностей посредством перехода между случайными функциями в памяти

image

Автор: Ron Bowes

Иногда реверс-инжиниринг бывает элегантен и целенаправлен, когда, например, вы решаете сложную задачу и пытаетесь выяснить предназначение непонятной незадокументированной функции, и как можно использовать эту функцию наилучшим образом. Однако эта статья про другое.

В этой заметке мы рассмотрим поиск скрытых возможностей посредством перехода между случайными функциями в памяти! Подобным образом обычно легко вызвать крах приложения, но может произойти и так, что вы найдете нечто полезное.

Данная техника полезна для поиска скрытой функциональности, но есть некоторые ограничения. Метод работает только для приложений, которые вы можете отлаживать. За некоторыми исключениями (например, я использовал подобную технику для выхода из криво реализованной песочницы), приведенный способ в основном пригоден для анализа, а не для эксплуатации уязвимостей или расширения привилегий.

Создание тестового бинарного файла

Начнем с создания простейшей подопытной программы. Вы можете воспользоваться как 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");
}

Сохраните этот код в файле jumpdemo.c и скомпилируйте при помощи следующей команды:

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__

Все, что вы видите выше, является символами. Функции с префиксом T можно вызывать. Однако функции, имена которых начинаются с символа нижнего подчеркивания, являются встроенными, и мы можем проигнорировать эти методы (но в реальной жизни, конечно, не стоит игнорировать все то, что начинается с нижнего подчеркивания).

Для нас представляют интерес две функции «main» и «random_function».

Загрузка файла в gdb

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

$ gdb -q ./jumpdemo
Reading symbols from ./jumpdemo...(no debugging symbols found)...done.
(gdb)

Флаг –q отключает вывод необязательных результатов. После того как вы окажетесь в командной строке (gdb), бинарный файл будет загружен и готов к запуску, но еще не запущен. Можно проверить, если, например, ввести команду continue:

(gdb) continue
The program is not being run.

Отладчик gdb – очень мощная утилита со множеством различных команд. Общую справочную информацию можно получить, если ввести команду help. Также можно использовать команду help <command> для получения более детальной справки по конкретной команде (например, help break). Попробуйте!

Простой вызов

Теперь, когда файл загружен в 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)

Затем запускаем программу и смотрим, что происходит:

(gdb) run
Starting program: /home/ron/blogs/jumpdemo

Breakpoint 1, 0x000000000040057b in main ()
(gdb)

Теперь приложение оказалось приостановлено во время выполнения, и мы можем управлять дальнейшим процессом. Далее можно просматривать/редактировать участки памяти, изменять регистры, возобновлять выполнение, переходить к другим участкам кода и много всего другого!

В нашем случае, как вы уже могли догадаться, мы будем перемещается выполнение в другую часть программы. Если говорить конкретно, то при помощи команды jump (help jump!) будем возобновлять выполнение, начиная с функции 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]

Задуманное реализовано! Программа вывела фразу «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 ?? ()

Насчет 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

Я часто использую переход к main во время разработки эксплоита, чтобы проверить, получилось ли выполнить код без создания рабочего шелл-кода. Если программа начинает выполняться с начала, значит, у нас получилось поменять текущую инструкцию!

Реальный пример

С целью демонстрации этой техники на реальном примере я скомпилировал утилиту THC-Hydra стандартным образом (./configure && make) и запустил следующую команду:

$ nm ./hydra
0000000000657abc b A
0000000000657cb4 B accntFlag
U alarm@@GLIBC_2.2.5
000000000043baf0 T alarming
0000000000657ae4 B alarm_went_off
00000000004266f0 T analyze_server_response
0000000000654200 B apop_challenge
U ASN1_OBJECT_free@@OPENSSL_1.0.0
U __assert_fail@@GLIBC_2.2.5
0000000000655c00 B auth_flag
0000000000657ab8 b B
0000000000408b60 T bail
000000000044b760 r base64digits
000000000044b6e0 r base64val
0000000000433450 T bf_get_pcount
?

Выясняется, что в приложении более 700 экспортированных символов. Этот список можно сократить, если поставить фильтр по префиксу T, который соответствует символам из кода, но все равно перечень будет большим. Я вручную посмотрел список и нашел символы, которые могут оказаться рабочими (т.е. функции, которые могут выводить информацию без необходимости ввода параметров, отражающих текущий контекст).

Вначале я запустил приложение со «стандартным» набором аргументов, чтобы понять, что выводится обычно:

$ gdb -q --args ./hydra -l john -p doe localhost http-head
Reading symbols from ./hydra...(no debugging symbols found)...done.
(gdb) run
Starting program: /home/ron/tools/hydra-7.1-src/hydra -l john -p doe localhost http-head
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Hydra v7.1 (c)2011 by van Hauser/THC & David Maciejak - for legal purposes only

Hydra (http://www.thc.org/thc-hydra) starting at 2018-10-15 14:47:12
Error: You must supply the web page as an additional option or via -m
[Inferior 1 (process 3619) exited with code 0377]
(gdb)

В этой команде добавился ключ --args, который сигнализирует gdb о том, что бинарный файл будет запускаться с дополнительными аргументами.

После того как я зафиксировал, что выводится в обычном случае, затем поставил точку останова на функции main (как и в тестовом примере) и после срабатывания перешел к функции help():

$ gdb -q --args ./hydra -l john -p doe localhost http-head 
Reading symbols from ./hydra...(no debugging symbols found)...done.
(gdb) break main
Breakpoint 1 at 0x403bf0
(gdb) run
Starting program: /home/ron/tools/hydra-7.1-src/hydra -l john -p doe localhost http-head [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000000000403bf0 in main ()
(gdb) jump help
Continuing at 0x408420. Syntax: (null) [[[-l LOGIN|-L FILE] [-p PASS|-P FILE]] | [-C FILE]] [-e nsr] [-o FILE] [-t TASKS] [-M FILE [-T TASKS]] [-w TIME] [-W TIME] [-f] [-s PORT] [-x MIN:MAX:CHARSET] [-SuvV46] [server service [OPT]]|[service://server[:PORT][/OPT]]

Options: -R restore a previous aborted/crashed session -S perform an SSL connect ..

Также я попробовал запустить функцию help_bfg:

(gdb) jump help_bfg
Continuing at 0x408580.
Hydra bruteforce password generation option usage:

-x MIN:MAX:CHARSET

MIN is the minimum number of characters in the password
MAX is the maximum number of characters in the password
CHARSET is a specification of the characters to use in the generation
valid CHARSET values are: 'a' for lowercase letters,
'A' for uppercase letters, '1' for numbers, and for all others,
just add their real representation.

Examples:
-x 3:5:a generate passwords from length 3 to 5 with all lowercase letters
-x 5:8:A1 generate passwords from length 5 to 8 with uppercase and numbers
-x 1:3:/ generate passwords from length 1 to 3 containing only slashes
-x 5:5:/%,.- generate passwords with length 5 which consists only of /%,.-

The bruteforce mode was made by Jan Dlabal, http://houbysoft.com/bfg/
[Inferior 1 (process 9980) exited with code 0377]

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

Затем я попробовал перейти к функции hydra_debug(), которая выдала чуть более интересные результаты:

(gdb) jump hydra_debug
Continuing at 0x408b40.
[DEBUG] Code: ???? Time: 1539640228
[DEBUG] Options: mode 0 ssl 0 restore 0 showAttempt 0 tasks 0 max_use 0 tnp 0 tpsal 0 tprl 0 exit_found 0 miscptr (null) service (null)
[DEBUG] Brains: active 0 targets 0 finished 0 todo_all 0 todo 0 sent 0 found 0 countlogin 0 sizelogin 0 countpass 0 sizepass 0
[Inferior 1 (process 7761) exited normally]

Функция предназначена для вывода статистики, но поскольку вызов происходит вне всякого контекста, появилась всякая белиберда.

Все последующий функции, которые я пробовал запускать, ничего не выводили или «роняли» приложение. Собственно, сей факт не является неожиданным. Если вы вызываете функцию без параметров, но которая ожидает определенные параметры, функция пытается получить доступ некорректному участку памяти. Удивительно то, что этот трюк вообще работает. Мы оказались очень удачливы в том, что произошел вывод строки, а не некорректной памяти (что привело бы к аварийному завершению приложения).

Заключение

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

Подписывайтесь на каналы "SecurityLab" в TelegramTelegram и Яндекс.ДзенЯндекс.Дзен, чтобы первыми узнавать о новостях и эксклюзивных материалах по информационной безопасности.