Так как у меня получилось решить "задачку - Канарейка", то я решил взять что-то более сложное и оценить свои силы, но в pwn категории сложных задач не было, только легкие и средние. Потыкав на несколько задач, я обнаружил, что в задачке "Цепи" решили всего 9 человек, когда как в других задач преобладает 10 решений, а то и больше. Меня заинтересовала эта задачка именно этим, и я хотел посмотреть насколько она сложная. Как таковых знаний в хакинге у меня нет. Знаю только о переполнении буфера. Остальное приходится доходить своим умом и на это уходит бывает несколько дней. Но я также знаю как работает программа, и для программиста вроде меня полезно знать уязвимости, чтобы их не совершать. В каждой такой задачке дан какой-то способ решения задач, и скорее всего один верный. Итак, я взял очередную задачу. Поизучав код, я обнаружил переполнение буфера в функции 0x004014ea.
Как видно, для стека выделяется всего 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]>
Второй этап. Смотрим также в 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 данные.
Смотрим как изменился стек.
Код:
Код:
[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. Но я попробую. ))
Нет, ничего не вышло толкового. Значит либо алгоритм неправильный, либо я не в том направлении двигался. Придется забросить задачку и порешать другие. Что ж, бывает.