• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Codeby Games Решение таска - цепи. Мои страдания, и что я делал. Так и не получилось решить.

xverizex

Green Team
29.12.2022
25
13
BIT
217
Так как у меня получилось решить "задачку - Канарейка", то я решил взять что-то более сложное и оценить свои силы, но в pwn категории сложных задач не было, только легкие и средние. Потыкав на несколько задач, я обнаружил, что в задачке "Цепи" решили всего 9 человек, когда как в других задач преобладает 10 решений, а то и больше. Меня заинтересовала эта задачка именно этим, и я хотел посмотреть насколько она сложная. Как таковых знаний в хакинге у меня нет. Знаю только о переполнении буфера. Остальное приходится доходить своим умом и на это уходит бывает несколько дней. Но я также знаю как работает программа, и для программиста вроде меня полезно знать уязвимости, чтобы их не совершать. В каждой такой задачке дан какой-то способ решения задач, и скорее всего один верный. Итак, я взял очередную задачу. Поизучав код, я обнаружил переполнение буфера в функции 0x004014ea.
Screenshot from 2024-04-18 20-35-42.png


Как видно, для стека выделяется всего 16 байт памяти. А функция read принимает 256 байт памяти. Здесь возможно переполнение стека. Но что делать дальше? Можно прыгнуть на адрес
0x00401600
И завершить работу с выигрышным сообщением, но вряд ли этого хотели создатели задачки. Здесь нужно было сделать что-то другое. В задаче сказано, что информация находится в файле
flag.txt
, значит нужен
shellcode
. Получается, я думаю, что есть возможность отправить в стек shellcode и выполнить его. Но, это старая уязвимость, о которой я вспомнил недавно, да и точно не помню, можно было ли в стеке выполнять код или нет. Тем более, что адреса стека меняются постоянно из-за защиты.
Так как я себя чувствовал неважно, я решил распечатать нашу задачку на бумагу и читать в любом месте где только захочется.

maze-document.jpg


Я посмотрел и убедился, что адреса стека меняются, значит shellcode будет проблематично записать в стек, потому что я не знаю адрес возврата. Или тут есть какая-то хитрость, типа как мы можем сделать прыжок на адрес 0x4015be. Нет, здесь наверное какой-то другой способ должен быть и нужно понять лазейку, если есть переполнение буфера.
Читая лежа распечатанный стек, я заметил канарейку.
canary.png


Регистры были таковыми.
Код:
rax = 0x00000000
rbx = 0x7ffe3152a588
rcx = 0x7fc90e1069c4
rdx = 0x00000100
r8 = 0x00000410
r9 = 0x00000001
r10 = 0x7fc90e018ac8
r11 = 0x00000202
r12 = 0x00000000
r13 = 0x7ffe3152a598
r14 = 0x7fc90e29a000
r15 = 0x00403e00
rsi = 0x7ffe3152a440
rdi = 0x00000000
rsp = 0x7ffe3152a440
rbp = 0x7ffe3152a450
rip = 0x0040153b
rflags = 0x00000206
orax = 0xffffffffffffffff

Если прибавить к rsp 256 байт, то мы получим 0x7FFE3152A540, значит нам нельзя передавать полные 256 байт в память. Почему я думаю, что это канарейка? Во-первых, в этом адресе каждый раз меняется значение, а во вторых вот правило для канареек, это код из ядра Linux.

C:
/*
 * On 64-bit architectures, protect against non-terminated C string overflows
 * by zeroing out the first byte of the canary; this leaves 56 bits of entropy.
 */
#ifdef CONFIG_64BIT
# ifdef __LITTLE_ENDIAN
#  define CANARY_MASK 0xffffffffffffff00UL
# else /* big endian, 64 bits: */
#  define CANARY_MASK 0x00ffffffffffffffUL
# endif
#else /* 32 bits: */
# define CANARY_MASK 0xffffffffUL
#endif

Для 64-битной будет в начале первый байт всегда ноль. Анализатор radare не давал мне заглянуть, что ссылается на канарейку, так что пришлось отложить этот вариант, хотя для проверки можно отправить 256 байт и убедиться, что будет stack-smashing. Отправив данные 256 байт, я словил bus error. Передавая данные поменьше я тоже получал bus-error. Нужно было точно понимать что нужно делать, и я стал думать.
Чуть позже до меня дошло, что стековая память в функции main берёт всего 16 байт. И в функции чтения имени 16 байт. Всего 32 байта. Задачка называется цепи. Может нужно правильно оформить стек не знаю. ))
Тогда я решил посмотреть, правильно ли я понимаю, что стек от (Там где вводить логин) будет также доступен и другим функциям в неизменном виде. Может в имя надо пару байт вписать. Потому что если мы переполним функцию ret, то в rbp занесется наше значение (буфер). Получается, если мы впишем много 'A', то после функции leave мы с помощью pop rbp занесём в rbp '0x414141414141414141'. Получается опасный случай, значит нам нужно делать что-то другое.
Помню я как то раньше мог вызывать функции, которые на указаны в программе. Но этот метод тоже не подходит, так как стоит какой-нибудь библиотеке, которую программа использует, измениться (из-за обновлений), то сместятся все позиции функций.
Чтобы было понятно о чем я пишу, я приведу пример. В x86-64 архитектуре, в Linux, если компилировать с помощью gcc, мы можем увидеть следующее: в регистре r8 или r9 (не помню уже), хранится адрес от которого мы можем вычитать или прибавлять смещения и запускать скрытые функции. Сначала, с помощью dlsym мы получаем адреса функций, вычитаем их вместе в r9 регистром, и запоминаем смещения. Потом убираем линковку с -ldl, и можем вызывать эти же функции просто прибавив или вычтя смещения. Этот хитрый способ тоже вроде не подходил. У нас слишком мало возможностей что-либо делать. Поэтому, я решил посмотреть какой будет стек в этих функциях. И вот что вышло.
Итак, главная функция имеет всего 16 байт стека.
В функции 0x40114ea ввода имени мы вычитаем ещё 16 байт.
В функции 0x401223 стек не вычитается.
В функции 0x40128c вычитается 16 байт.
Первый этап. Смотрим адреса rsp и rbp - 0x40114ea
Код:
rsp = 0x7ffc04b45440
rbp = 0x7ffc04b45450

[0x7ffc04b45440]> px 32
- offset -      4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0x7ffc04b45440  0a00 0000 0000 0000 0000 0000 0000 0000  ................
0x7ffc04b45450  7054 b404 fc7f 0000 be15 4000 0000 0000  pT........@.....
[0x7ffc04b45440]>
Второй этап. Смотрим также в 0x401223.
Код:
rsp = 0x7ffc04b45450
rbp = 0x7ffc04b45450

[0x7ffc04b45440]> px 32
- offset -      4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0x7ffc04b45440  0000 0000 0000 0000 8855 b404 fc7f 0000  .........U......
0x7ffc04b45450  7054 b404 fc7f 0000 e815 4000 0000 0000  pT........@.....
[0x7ffc04b45440]>
В этой функции мы хоть и не выделяем для стека память, но записываем в 0x44c данные.

Screenshot from 2024-04-19 16-07-42.png


Смотрим как изменился стек.

Код:
[0x7ffc04b45440]> px 32
- offset -      4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0x7ffc04b45440  0000 0000 0000 0000 8855 b404 0000 0000  .........U......
0x7ffc04b45450  7054 b404 fc7f 0000 e815 4000 0000 0000  pT........@.....
[0x7ffc04b45440]>

Ага, 0xfc7f стирается, значит имя пользователя здесь вообще не имеет значения )), хотя это и так понятно. Идём дальше.
Функция 0x40128c. Здесь регистры опять указывают на то место, где было имя пользователя.
Код:
rsp = 0x7ffc04b45440
rbp = 0x7ffc04b45450

[0x7ffc04b45440]> px 32
- offset -      4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0x7ffc04b45440  0000 0000 0000 0000 8855 b404 9e06 0000  .........U......
0x7ffc04b45450  7054 b404 fc7f 0000 f215 4000 0000 0000  pT........@.....
[0x7ffc04b45440]>

После ввода символа мы получаем такой стек.
Код:
- offset -      4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0x7ffc04b45440  0000 0000 0000 0000 8855 b404 9e06 0044  .........U.....D
0x7ffc04b45450  7054 b404 fc7f 0000 f215 4000 0000 0000  pT........@.....
Именно символ 0x44 был записан как первый, но там ещё записываются данные из других областей памяти и весь логин вообще стирается.

Код:
[0x004012be]> px @rsp
- offset -      4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0x7ffc04b45440  0000 0000 1400 0000 0200 0000 9e06 0044  ...............D
0x7ffc04b45450  7054 b404 fc7f 0000 f215 4000 0000 0000  pT........@.....

Ещё одна функция 0x00401223, и вот какой стек.
Код:
- offset -      4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0x7ffc04b45440  0000 0000 0000 0000 8855 b404 fc7f 0000  .........U......
0x7ffc04b45450  7054 b404 fc7f 0000 e815 4000 0000 0000  pT........@.....

В итоге мы имеем то, что в цикле программа считывает нажатие клавиш и стирает всё то, что было в имени игрока. Здесь получается ничего нельзя сделать, так как в каждой функции мы не читаем из стека в начале, первым делом заносим новое значение в стек, значит старые значения стека не канают и записанные данные никак не влияют на работу стека. Единственное к чему я опять возвращаюсь, это переполнить стек и попытаться что-то сделать. Переписать адрес возврата, но куда? Да, я могу просто прыгнуть в то место, чтобы мне написали, что вы решили лабиринт, но это слишком просто для задачи среднего уровня сложности. Что именно делать, я не понимал, и мне пришлось пойти на прогулку и обдумать что делать.
Решил получше исследовать стек. Понятно было, что шансов узнать адрес стека не было. Я залез в ядро Linux и начал смотреть производимые операции со стеком. Я обнаружил, что для стека даже может быть магическое число установлено, чтобы определить, было ли переполнение или нет.
C:
include/uapi/linux/magic.h:#define STACK_END_MAGIC        0x57AC6E9D

void set_task_stack_end_magic(struct task_struct *tsk)
{
        unsigned long *stackend;

        stackend = end_of_stack(tsk);
        *stackend = STACK_END_MAGIC;    /* for overflow detection */
}

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

Первым делом я выписал две функции srand и time, и оставил в записях только те регистры, которые менялись после выполнения функции.
Код:
time:

rax    = 0x00401561     0x662295a7
rdx    = 0x7ffc092bf5c8 0x7ffc092e5080
r10    = 0x7ffc092bf1d0 0x7ffc092e9258
r11    = 0x00000206     0x7ffc092e9a40
rflags = 0x00000206     0x00000246
-----------------------------------------------------
srand:

rax = 0x662295a7     0x00000001
rcx = 0x00403e00     0x7f758d5ef6a0
rdx = 0x7ffc092e5080 0x7f758d5ef024
r8  = 0x00000000     0x7f758d5ef024
r9  = 0x7f758d6af00e 0x285d1b39
r10 = 0x7ffc092e9258 0x6785bed5
r11 = 0x7ffc092e9a40 0x7f758d445e0e
rsi = 0x7ffc092bf5b8 0xffffffff
rdi = 0x662295a7     0x7f758d5ef0a0
--------------------------------------------------

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

rax    = 0x00401561     0x662295a7
rdx    = 0x7ffc092bf5c8 0x7ffc092e5080
r10    = 0x7ffc092bf1d0 0x7ffc092e9258
r11    = 0x00000206     0x7ffc092e9a40
rflags = 0x00000206     0x00000246
-----------------------------------------------------
srand:

rax = 0x662295a7     0x00000001
rcx = 0x00403e00     0x7f758d5ef6a0
rdx = 0x7ffc092e5080 0x7f758d5ef024
r8  = 0x00000000     0x7f758d5ef024
r9  = 0x7f758d6af00e 0x285d1b39
r10 = 0x7ffc092e9258 0x6785bed5
r11 = 0x7ffc092e9a40 0x7f758d445e0e
rsi = 0x7ffc092bf5b8 0xffffffff
rdi = 0x662295a7     0x7f758d5ef0a0
--------------------------------------------------
srand -->
    rdi = 0x7f758d5ef0a0
    rdi = 0x7fb6f05ef0a0
    
--------------------------------------------
read:

rax = 0x00000000     0x00000001
rcx = 0x7fb6f05069c4 0x7fb6f0505d91
rdx = 0x00000100
r10 = 0x7fb6f0418ac8 0x7fb6f04109f8
r11 = 0x00000202 0x00000246
rflags = 0x00000206 0x00000203
---------------------------------------------
[Symbols]
nth paddr      vaddr      bind   type   size lib name                  demangled
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
13  ---------- 0x00404900 GLOBAL OBJ    8        stdout
14  ---------- 0x00404910 GLOBAL OBJ    8        stdin
1   ---------- ---------- GLOBAL FUNC   16       imp.__libc_start_main
2   0x00001030 0x00401030 GLOBAL FUNC   16       imp.puts
3   0x00001040 0x00401040 GLOBAL FUNC   16       imp.write
4   0x00001050 0x00401050 GLOBAL FUNC   16       imp.strlen
5   0x00001060 0x00401060 GLOBAL FUNC   16       imp.printf
6   0x00001070 0x00401070 GLOBAL FUNC   16       imp.fgetc
7   0x00001080 0x00401080 GLOBAL FUNC   16       imp.read
8   0x00001090 0x00401090 GLOBAL FUNC   16       imp.srand
9   0x000010a0 0x004010a0 GLOBAL FUNC   16       imp.putc
10  ---------- ---------- WEAK   NOTYPE 16       imp.__gmon_start__
11  0x000010b0 0x004010b0 GLOBAL FUNC   16       imp.time
12  0x000010c0 0x004010c0 GLOBAL FUNC   16       imp.fflush

Получалась какая-то фигня. После srand в rdi попадал 0x08 число, но в rsi попадало 0xffffffff. Такое нельзя было подавать в read. Но я попробую. ))
Нет, ничего не вышло толкового. Значит либо алгоритм неправильный, либо я не в том направлении двигался. Придется забросить задачку и порешать другие. Что ж, бывает.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!