Переполнение буффера?
От: kisatomsk  
Дата: 18.01.06 09:01
Оценка:
Короче говоря вопрос дурацкий, но меня как начинающего интересует. Вобщем пытался эмулировать переполнение буффера (в реалии стека) но ничего не вышло, значение возвращаемое функцией не изменилось. Что в этом коде не так? Опции защиты буфера в компиляторе выключены.


#include <iostream>
using namespace std;

int func(void){
    char b[2]={'A','B'};
    int n=10;
    b[2]='C'; //Переполнение
    return n;
    }

void main(void){

    cout << func(); //Возвращает 10, почему ни мусор или код символа 'C'
    
    }
Re: Переполнение буффера?
От: Аноним  
Дата: 18.01.06 09:06
Оценка:
см. asm листинг и все поймешь.
Re: Переполнение буффера?
От: Bell Россия  
Дата: 18.01.06 09:09
Оценка:
Здравствуйте, kisatomsk, Вы писали:

Для начала выведи адреса b и n.
Любите книгу — источник знаний (с) М.Горький
Re: Переполнение буффера?
От: igna Россия  
Дата: 18.01.06 09:11
Оценка:
Здравствуйте, kisatomsk, Вы писали:

K>Короче говоря вопрос дурацкий, но меня как начинающего интересует. Вобщем пытался эмулировать переполнение буффера (в реалии стека) но ничего не вышло, значение возвращаемое функцией не изменилось. Что в этом коде не так? Опции защиты буфера в компиляторе выключены.


Возможно из-за выравнивания (alignment) n не располагается непосредственно за b. Можно попробовать так:

#include <iostream>
using namespace std;

int func(void){
    char b[4]={'A','B','C','D'};
    int n=10;
    b[4]='E';
    return n;
    }

void main(void){

    cout << func();
    
    }


А еще полезно посмотреть сгенерированный код.
Re: Переполнение буффера?
От: gear nuke  
Дата: 18.01.06 10:49
Оценка: 2 (1) +1
Здравствуйте, kisatomsk, Вы писали:

K>Короче говоря вопрос дурацкий, но меня как начинающего интересует. Вобщем пытался эмулировать переполнение буффера (в реалии стека) но ничего не вышло, значение возвращаемое функцией не изменилось. Что в этом коде не так? Опции защиты буфера в компиляторе выключены.


K>
K>#include <iostream>
K>using namespace std;

K>int func(void){
// массив занимает в стеке 4 байта
K>    char b[2]={'A','B'};
K>    int n=10;
// здесь запись в "свободное место"
K>    b[2]='C'; //Переполнение
K>    return n;
K>    }

K>void main(void){

K>    cout << func(); //Возвращает 10, почему ни мусор или код символа 'C'
    
K>    }

В общем случае нельзя по исходнику сказать, в каких адресах будет переменная n, но скорее всего — ниже, т.е. надо
    b[-2]='C'; //Переполнение
Что бы знать точно, нужно смотреть асм листинг, а лучше бинарник (в отладчике)

Но обычно переполнение делают не для того, что бы функция вернула не то значение, а что бы выполнился некоторый код, из-за перезаписи на стеке адреса возврата. Вот пример где проще наблюдать эффект:
#include<stdio.h>

void oops()
{
    char buf[25];
    scanf("%s", buf);
    buf[sizeof(buf)] = '\0';
 //   puts(buf);
}

int main()
{
    puts("before oops");
    oops();
    puts("after oops");
}
Вводите различное количество символов, after oops не напечатается, если их много.
После этого никогда не будете использовать scanf
People who are more than casually interested in computers should have at least some idea of what the underlying hardware is like. Otherwise the programs they write will be pretty weird (c) D.Knuth
Re[2]: Переполнение буффера?
От: kisatomsk  
Дата: 18.01.06 12:25
Оценка:
Здравствуйте, gear nuke:

Спасибо за наиболее вразумительный ответ, ведь ассемблер я пока не знаю и глядя на лист дизасемблированной программы не могу найти нужного. Темболее там полно др. вещей.

GN>В общем случае нельзя по исходнику сказать, в каких адресах будет переменная n, но скорее всего — ниже, т.е. надо

GN>[c]
GN> b[-2]='C'; //Переполнение

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

Стек памяти под функцию, для переменных класса auto:
Направление заполнения: <---
--------------------------------------|
| Переменная | Массив ------------> |
--------------------------------------|

Из этой модели которую представил себе я наглядно видно, что массивы индексируются и распологаются с лева на право.
Ну а стек памяти под функцию с права на лево, по этому ваш пример который вы привели (откорректировав мой сработал), но можно было не писать b[-2]='C'; достаточно b[-2]='C';.
И отсюда вопрос: b[-2]='C'; — это уже расширение массива до ячейки за переменной n которую требовалось переписать, но видимо это влечет за собой уничтожение значений в переменной n из за того что эта ячейка памяти теперь автоматически вошла в пределы массива, так ли это или я ошибаюсь?
И во вторых почему вернулся мусор, возможно из за догадки которая описана мной чуть выше?


GN>Но обычно переполнение делают не для того, что бы функция вернула не то значение, а что бы выполнился некоторый код, из-за перезаписи на стеке адреса возврата.


Спасибо за разъяснение, это я знал но всеравно пока не понимаю точно как.....

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


Еще раз спасибо gear nuke, за наиболее вразумительную помощь в этом вопросе

--------------------------------------------------------------------------------------------------------------------
P.S: Мне кажется это тот вопрос на который надо найти ответ уже сейчас, а то потом ............
Re[3]: Переполнение буффера?
От: gear nuke  
Дата: 18.01.06 14:28
Оценка: 3 (1) +1
Здравствуйте, kisatomsk, Вы писали:

K>Спасибо за наиболее вразумительный ответ, ведь ассемблер я пока не знаю и глядя на лист дизасемблированной программы не могу найти нужного. Темболее там полно др. вещей.


Попробую прокоментировать листинг после компиляции оригинального варианта:
PUBLIC  ?func@@YAHXZ                    ; func
; Function compile flags: /Odtp
_TEXT   SEGMENT
; "объявления" переменных. цифры - смещения относительно указателя на стековый кадр (см. ebp ниже)
; в стеке всегда выделяется кратное 4м количество байт (особенность x86)
_b$ = -8                        ; size = 2    char b[2]
_n$ = -4                        ; size = 4    int n
?func@@YAHXZ PROC                   ; func
; Line 4
    push    ebp
; регистр ebp служит указателем на стековый кадр (в котором распологаются auto переменные)
    mov ebp, esp
; esp - указатель стека, отнимая 8, резервируется место под переменные
    sub esp, 8
; Line 5
; инициализируется массив char b[2]={'A','B'};
    mov BYTE PTR _b$[ebp], 65           ; 'A'
    mov BYTE PTR _b$[ebp+1], 66         ; 'B'
; Line 6
; инициализируется int n=10;
    mov DWORD PTR _n$[ebp], 10
; Line 7
; b[2]='C'
    mov BYTE PTR _b$[ebp+2], 67 ; реально запись происходит по адресу -8[ebp+2], т.е. в пустое место после массива
; Line 8
    mov eax, DWORD PTR _n$[ebp] ; адрес -4[ebp+2], находится "ниже" чем массив char, запись строкой выше проходит мимо
; Line 9
    mov esp, ebp
    pop ebp
    ret 0 ; важная команда, считывает со стека адрес возврата и переходит по нему (см. далее)
?func@@YAHXZ ENDP                   ; func


GN>> b[-2]='C'; //Переполнение


K>Этот вариант подошол и сработал отлично, тут вы по моему ошиблись с моделями заполнения памяти, но лиш чуть чуть.


K>Стек памяти под функцию, для переменных класса auto:

K>Направление заполнения: <---
K>--------------------------------------|
K> | Переменная | Массив ------------> |
K>--------------------------------------|

K>Из этой модели которую представил себе я наглядно видно, что массивы индексируются и распологаются с лева на право.

K>Ну а стек памяти под функцию с права на лево

Строго говоря, "право-лево" и (моё) "выше-ниже" не являются правильными определениями, из-за них тут даже как-то был долгий спор

Всё же попытался нарисовать часть стека, слева расположены младшие алдреса, справа — старшие алдреса. Сгрупировал по 4 байта, ээто разрядность x86-32.
 int n    char b[2]    сюда указывает    адрес
0 1 2 3    0 1 2 3         ebp          возврата
[ebp-8]    [ebp-4]       [ebp+0]        [ebp+4]

Это картина для нашего случая, но компилятор может поступать как ему заблагорассудится. Здесь алресация к стеку идёт через регистр ebp (но может и через esp). По адресу [ebp+0] находится старое значение этого же регистра (сохраняется самой первой командой — push ebp), могут так же сохраняться и другие регистры, эти значения тоже могут быть переписаны! При выходе из функции ebp восстанавливается командой pop ebp, и если была перезапись, считается другое значение. О адресе возврата будет ниже, главное — он тоже может быть перезаписан.

K>по этому ваш пример который вы привели (откорректировав мой сработал), но можно было не писать b[-2]='C'; достаточно b[-2]='C';.


Гм, а разница между выделенным?

K>И отсюда вопрос: b[-2]='C'; — это уже расширение массива до ячейки за переменной n которую требовалось переписать, но видимо это влечет за собой уничтожение значений в переменной n из за того что эта ячейка памяти теперь автоматически вошла в пределы массива, так ли это или я ошибаюсь?


Да, b[-2]='C' запишет число 67 по адресу ebp-4-2. Это как раз выделенная 2ка в области int n выше.

K>И во вторых почему вернулся мусор, возможно из за догадки которая описана мной чуть выше?


Таким образом n будет:
№байта                      0   1   2    3
значение                    10  0   67   0
шестнадцатеричное значение  0a  00  43   0
С учётом, что байты в int располагаются в памяти задом наперёд (little-endian), n будет рано 0x0043000a или 4390922. Это значение скорее всего и было распечатано программой.

GN>>Но обычно переполнение делают не для того, что бы функция вернула не то значение, а что бы выполнился некоторый код, из-за перезаписи на стеке адреса возврата.


K>Спасибо за разъяснение, это я знал но всеравно пока не понимаю точно как.....


Если выполнить b[8]='C', произойдёт записть в ячейку [ebp-4+8] (или [ebp+4]). Это один из байтов адреса возврата (можно так же переписать и остальные). Потом, когда функция будет делать return (ret на ассемблере), это неправильное значение будет считано со стека и переход будет выполнен непонятно куда, и программа упадёт с ошибкой. Но если записать поверх адреса возврата не просто мусор, а какой-то другой осмысленный адрес, то выполнение будет продолжено с нового адреса!

Если вспомнить мой пример со scanf, и подкорректировать картинки выше соответственно с ним, будет видно, что при определённой длине вводимая строка перезапишет адрес возврата. Если допустить, что известно точное значения ebp в этот момент, можно составить такую строчку, что адрес возврата будет равен ebp (или рядом). Теперь, по ret начнут выполнятся ассемблерные команды из строки введённой с клавиатуры! Если строка достаточно длинная, там может быть много команд. А теперь представим, что строка вводится не с клавиатуры, а приходит по сети.
На самом деле это упрощение, адреса в стеке в общем случае не известны, но это мало спасает от более хитрых способов.

K>Эти знания мне нужны для того что бы сразу привыкать писать устойчивый к таким атакам код, ибо проверка при включенной опции компилятора для защиты от таких вещей показала не состоятельность компилятора — получается то же что и при выключенной опции!


Есть книга: Ховард М., Лебланк Д. "Защищенный код". Кое что о переполнении можно почитать там, заодно и другие вопросы безопасности рассматриваются. Ну и статьи о написании эксплоитов тоже будут полезны, как взгляд с другой стороны

K>P.S: Мне кажется это тот вопрос на который надо найти ответ уже сейчас, а то потом


К сожалению, так кажется далеко не всем. Любая книжка учит ставить assert'ы, но про потенциально опасные ошибки обычно говорят "программа аварийно завершится". А ведь на самом деле, не завершится она, а ещё и другие "программы" заработают.
People who are more than casually interested in computers should have at least some idea of what the underlying hardware is like. Otherwise the programs they write will be pretty weird (c) D.Knuth
Re[4]: Переполнение буффера?
От: kisatomsk  
Дата: 18.01.06 14:46
Оценка:
Здравствуйте, gear nuke:

Большое спасибище за такой подробнейший ответ
Я очень вам признателен!

Было очень интересно проводить изыскания такого рода, буду проводить их дальше..........
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.