Так как у меня получилось решить "задачку - Канарейка", то я решил взять что-то более сложное и оценить свои силы, но в pwn категории сложных задач не было, только легкие и средние. Потыкав на несколько задач, я обнаружил, что в задачке "Цепи" решили всего 9 человек, когда как в других задач преобладает 10 решений, а то и больше. Меня заинтересовала эта задачка именно этим, и я хотел посмотреть насколько она сложная. Как таковых знаний в хакинге у меня нет. Знаю только о переполнении буфера. Остальное приходится доходить своим умом и на это уходит бывает несколько дней. Но я также знаю как работает программа, и для программиста вроде меня полезно знать уязвимости, чтобы их не совершать. В каждой такой задачке дан какой-то способ решения задач, и скорее всего один верный. Итак, я взял очередную задачу. Поизучав код, я обнаружил переполнение буфера в функции 0x004014ea.
Как видно, для стека выделяется всего 16 байт памяти. А функция read принимает 256 байт памяти. Здесь возможно переполнение стека. Но что делать дальше? Можно прыгнуть на адрес
0x00401600
И завершить работу с выигрышным сообщением, но вряд ли этого хотели создатели задачки. Здесь нужно было сделать что-то другое. В задаче сказано, что информация находится в файле
flag.txt
, значит нужен
shellcode
. Получается, я думаю, что есть возможность отправить в стек shellcode и выполнить его. Но, это старая уязвимость, о которой я вспомнил недавно, да и точно не помню, можно было ли в стеке выполнять код или нет. Тем более, что адреса стека меняются постоянно из-за защиты.
Так как я себя чувствовал неважно, я решил распечатать нашу задачку на бумагу и читать в любом месте где только захочется.
Я посмотрел и убедился, что адреса стека меняются, значит shellcode будет проблематично записать в стек, потому что я не знаю адрес возврата. Или тут есть какая-то хитрость, типа как мы можем сделать прыжок на адрес 0x4015be. Нет, здесь наверное какой-то другой способ должен быть и нужно понять лазейку, если есть переполнение буфера.
Читая лежа распечатанный стек, я заметил канарейку.
Регистры были таковыми.
Если прибавить к rsp 256 байт, то мы получим 0x7FFE3152A540, значит нам нельзя передавать полные 256 байт в память. Почему я думаю, что это канарейка? Во-первых, в этом адресе каждый раз меняется значение, а во вторых вот правило для канареек, это код из ядра Linux.
Для 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
Второй этап. Смотрим также в 0x401223.
В этой функции мы хоть и не выделяем для стека память, но записываем в 0x44c данные.
Смотрим как изменился стек.
Ага, 0xfc7f стирается, значит имя пользователя здесь вообще не имеет значения )), хотя это и так понятно. Идём дальше.
Функция 0x40128c. Здесь регистры опять указывают на то место, где было имя пользователя.
После ввода символа мы получаем такой стек.
Именно символ 0x44 был записан как первый, но там ещё записываются данные из других областей памяти и весь логин вообще стирается.
Ещё одна функция 0x00401223, и вот какой стек.
В итоге мы имеем то, что в цикле программа считывает нажатие клавиш и стирает всё то, что было в имени игрока. Здесь получается ничего нельзя сделать, так как в каждой функции мы не читаем из стека в начале, первым делом заносим новое значение в стек, значит старые значения стека не канают и записанные данные никак не влияют на работу стека. Единственное к чему я опять возвращаюсь, это переполнить стек и попытаться что-то сделать. Переписать адрес возврата, но куда? Да, я могу просто прыгнуть в то место, чтобы мне написали, что вы решили лабиринт, но это слишком просто для задачи среднего уровня сложности. Что именно делать, я не понимал, и мне пришлось пойти на прогулку и обдумать что делать.
Решил получше исследовать стек. Понятно было, что шансов узнать адрес стека не было. Я залез в ядро Linux и начал смотреть производимые операции со стеком. Я обнаружил, что для стека даже может быть магическое число установлено, чтобы определить, было ли переполнение или нет.
Но пока это мало что давало, да и это магическое число мне не удалось найти с помощью radare.
Думая, думая, я стал просматривать как завершается программа и тут меня осенило, а вдруг нам нужно манипулировать именно теми функциями, которые нам доступны в данный момент и адреса которых нам точно известно. Я не знал, что из этого выйдет, так как не было таких функций как execve и так далее. Но нужно было проверить эту гипотезу и я стал записывать какие регистры меняются и на что меняются, когда используются.
Первым делом я выписал две функции srand и time, и оставил в записях только те регистры, которые менялись после выполнения функции.
Функция time указывала регистрами на стековую память как я понял, я посмотрел каждый указатель на память, везде были какие-то данные, и что самое главное, это мне не хотелось вдаваться в подробности, почему такие адреса становятся после выполнения этих функций. В итоге я решил пока попробовать с тремя функциями и вот что вышло в отчете.
Получалась какая-то фигня. После srand в rdi попадал 0x08 число, но в rsi попадало 0xffffffff. Такое нельзя было подавать в read. Но я попробую. ))
Нет, ничего не вышло толкового. Значит либо алгоритм неправильный, либо я не в том направлении двигался. Придется забросить задачку и порешать другие. Что ж, бывает.
Как видно, для стека выделяется всего 16 байт памяти. А функция read принимает 256 байт памяти. Здесь возможно переполнение стека. Но что делать дальше? Можно прыгнуть на адрес
0x00401600
И завершить работу с выигрышным сообщением, но вряд ли этого хотели создатели задачки. Здесь нужно было сделать что-то другое. В задаче сказано, что информация находится в файле
flag.txt
, значит нужен
shellcode
. Получается, я думаю, что есть возможность отправить в стек shellcode и выполнить его. Но, это старая уязвимость, о которой я вспомнил недавно, да и точно не помню, можно было ли в стеке выполнять код или нет. Тем более, что адреса стека меняются постоянно из-за защиты.
Так как я себя чувствовал неважно, я решил распечатать нашу задачку на бумагу и читать в любом месте где только захочется.
Я посмотрел и убедился, что адреса стека меняются, значит shellcode будет проблематично записать в стек, потому что я не знаю адрес возврата. Или тут есть какая-то хитрость, типа как мы можем сделать прыжок на адрес 0x4015be. Нет, здесь наверное какой-то другой способ должен быть и нужно понять лазейку, если есть переполнение буфера.
Читая лежа распечатанный стек, я заметил канарейку.
Регистры были таковыми.
Код:
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]>
Код:
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]>
Смотрим как изменился стек.
Код:
[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........@.....
Код:
[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. Но я попробую. ))
Нет, ничего не вышло толкового. Значит либо алгоритм неправильный, либо я не в том направлении двигался. Придется забросить задачку и порешать другие. Что ж, бывает.