Разбор коры в Linux и xBSD

Автор: (c)Крис Касперски ака мыщъх

Как умру - похороните, на могильной плите напишите: "segmentation fault (core dumped)"... Когда никсовая программа умирает, ее кора отделяется от эльфа и попадает в специальный файл, хранящий последний вздох усопшей. Это словно "черный ящик", устанавливаемый на борту самолета и позволяющий реконструировать причины катастрофы при падении. Записанная на древнем языке машинных кодов, кора подвластна только гуру, магам и чародеям. Однако, с помощью магического свитка этой статьи даже простые смертные юзеры смогут приобщиться к тайне, выучив пару-тройку волшебных заклинаний

Введение

При возникновении необрабатываемого исключения внутри прикладной программы, центральный процессор возбуждает исключение и операционная система завершает работу приложения в аварийном режиме, сопровождая это знаменитой надписью "segmentation fault" и (при правильно выставленных лимитах) сбрасывает дамп памяти в специальный core-файл, в просторечии называемый "корой". Кора содержит все сегменты ELF-файла (код, данные), содержимое стековой и динамической памяти. (примечание: некоторые источники утверждают - см. напр. en.wikipedia.org/wiki/Core_dump, что кора содержит все пользовательское пространство процесса, но это не совсем верно, точнее - совсем неверно. Кора обладает собственным форматом и состоит из секций, в которые попадают лишь значимые данные, в частности - выделенные блоки динамической памяти, в результате чего размер коры приблизительно равен объему памяти, занятым процессом - от сотен килобайт до нескольких мегабайт, в то время как адресное пространство занимает от 2-х до 3-х гигабайт на x86-машинах).

Большинство пользователей удаляют кору не задумываясь, некоторые отсылают ее разработчикам, в надежде, что это поможет им выловить баг и как следует его прищемить, чтобы он не беспредельничал, однако надеяться на разработчиков - все равно, что ждать у моря погоды (особенно в Open-Source проектах, поддерживаемых на голом энтузиазме при полной нехватке времени и финансов).

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

Где взять IDA Pro? Странный вопрос, не правда ли? Напоминает ситуацию при загнивающем социализме. Все жалуется, что мяса нет, но на содержимом холодильника это обстоятельство никак не сказывается. Короче, тот кто ищет, тот всегда найдет... ну, а если не найдет, так позаимствует. Теоретически, для анализа коры можно воспользоваться утилитой objdump, входящей практически в каждый дистрибутив, однако она не отображает символьные имена библиотечных функций и форматирует неудобочитаемый листинг. Словом, мыщъх категорически не рекомендует ее начинающим.

Отладчик gdb представляет собой компромиссный вариант. Это уже не objdump, но и не IDA Pro. Если IDA Pro предоставляет оконный интерфейс, с которым можно разобраться и методом тыка, то с gdb этот номер уже не пройдет, а многочисленные графические "морды" (недостатка в которых не ощущается) в основном ориентированы на отладчку файлов с исходными текстами и для работы с корой неудобны.

Существуют также и специальные анализаторы коры - как коммерческие (фу, сразу на фиг), так и бесплатные (например, Introspector, созданный James'ом Michael'ем из корпорации DuPont - http://introspector.sourceforge.net).

Вот эти инструменты мы и будем использовать! Но довольно лясы точить! А разгребать кору кто будет?

Подготовка к магическому ритуалу - создание алтаря

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

#include <stdio.h>
#include <malloc.h>

#define S "nezumi-souriz-elraton-"
#define N 0x666
#define M (N * sizeof(S) + 1)

main()
{
        int a; char buf[N]; char *p; FILE *f;

        // heap-test (must be found in core)
        p = malloc(M); for (a = 0; a < N; a++) strcpy(p + a * strlen(S), S);

        // stack-test (must be found in core)
        printf("tell me your name, plz!\n"); fgets(buf, N - 1, stdin);

        // non-exsisten file
        f = fopen("kpnc.dat", "rw");

        // crash! f == 0
        fread(buf, N, 1, f);

        return 0;
}

Листинг 1. Стендовая программа test-core-usr.c.

Набираем ее в любом текстовом редакторе (мыщъх рекомендует vi - см. рис. 1, хотя о вкусах, как говорится, не спорят).

Набивка программы в самом хакерском редакторе всех времен и народов

Рисунок 1. Набивка программы в самом хакерском редакторе всех времен и народов - в знаменитом vi.

Компилируем ("$gcc test-core-usr.c -o test-core-usr") и запускаем образовавшийся файл ./test-core.usr на выполнение, который спрашивает наше имя и тут же грохается с сообщением "segmentation fault" - "ошибка сегментации" или "segmentation fault (code dumped)" - "ошибка сегментации (кора сброшена)" - (см. рис. 2).

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

Реакция системы на критическую ошибку

Рисунок 2. Реакция системы на критическую ошибку в прикладной программе: аварийное завершение без сброса (слева) и со сбросом коры (справа).

Охота на исключения с последующим допросом

В штатную поставку большинства дистрибутивов входит утилита "catchsegv" (своеобразный аналог "Доктора Ватсона" в Windows), которой мы сейчас и воспользуемся. Просто запускаем catchsegv с именем подопытной программы в качестве аргумента и, дождавшись ее запуска, вводим свое имя, нажимаем <ENTER> и... все!!! Баста! Процессор генерирует исключение доступа по нулевому указателю (которым в данном случае является переменная f), передавая бразды правления утилите catchsegv, выводящей на экран содержимое стека вызовов, регистров, карту памяти и т.д. (см. листинг 2).

root@9[core-gdb]# catchsegv ./test-core-usr
tell me your name, plz!
KPNC%69
*** Segmentation fault
Register dump:

 EAX:   00000000   EBX:    4015c620   ECX:   08056bc0   EDX:   4015d0a0
 ESI:   00000001   EDI:    00000666   EBP:   bffff3d8   ESP:   bffff3ac
 EIP:   4008d3e3   EFLAGS: 00000206

 CS:    0023       DS:     002b       ES:    002b       FS:    0000
 GS:    0000       SS:     002b

 Trap:  0000000e   Error   00000004   OldMask:   80000000
  ESP/signal:      bffff3ac           CR2:       00000000

  Backtrace:
  /lib/libc.so.6(_IO_fread+0x33)[0x4008d3e3]
  ??:0(main)[0x80485a5]
  /lib/libc.so.6(__libc_start_main+0xc6)[0x40042dc6]
  ../sysdeps/i386/elf/start.S:105(_start)[0x8048431]

  Memory map:

  08048000-08049000 r-xp 00000000 08:01 115530 /home/core-gdb/test-core-usr
  08049000-0804a000 rw-p 00000000 08:01 115530 /home/core-gdb/test-core-usr
  0804a000-0806b000 rwxp 00000000 00:00 0
  40000000-40016000 r-xp 00000000 08:01 222502 /lib/ld-2.3.2.so
  40016000-40017000 rw-p 00015000 08:01 222502 /lib/ld-2.3.2.so
  40017000-40018000 rw-p 00000000 00:00 0
  40018000-4001b000 r-xp 00000000 08:01 222506 /lib/libSegFault.so
  4001b000-4001c000 rw-p 00002000 08:01 222506 /lib/libSegFault.so
  4001c000-4001e000 rw-p 00000000 00:00 0
  4002d000-40155000 r-xp 00000000 08:01 222518 /lib/libc-2.3.2.so
  40155000-4015d000 rw-p 00127000 08:01 222518 /lib/libc-2.3.2.so
  4015d000-40160000 rw-p 00000000 00:00 0
  bfffe000-c0000000 rwxp fffff000 00:00 0

Листинг 2. Информация, отловленная утилитой catchsegv в момент возникновения исключения.

От обилия информации можно и растеряться, но мы же хакеры, а не пионеры там какие-то, так что не будем паниковать и обратим свой взор к строке "backtrace", содержащей стек обратных вызовов. Самая верхняя строчка была выполнена последней. В нашем случае, это "/lib/libc.so.6(_IO_fread+0x33)[0x4008d3e3]" - команда, расположенная по адресу 4008d3E3h, что соответствует смещению 33h байт от начала функции _IO_fread(), реализованной в библиотеке /lib/libc.so.6.

Ковыряться в библиотечных функциях утомительно, да и бесполезно. Во-первых, они уже давно вылизаны, а во-вторых (и это более важно), функция _IO_fread с вероятностью, близкой к единице, тут совсем не причем. Скорее всего, вызывающий ее код, передал ей неправильные аргументы. А где этот код расположен?

Смотрим на вторую строчку стека вызовов: "??:0(main)[0x80485A5]". Ага! Интересующий нас код расположен по адресу 80485A5h, в который легко заглянуть дизассемблером. Берем IDA Pro, загружаем test-core-usr и нажимаем <G> для перехода к инструкции 80485A5h, которой оказывается машинная команда movsx, следующая за "call _fread" (см. рис. 3#1).

Поиск причины сбоя

Рисунок 3. Поиск причины сбоя в IDA Pro.

Мы знаем, что исключение произошло в функции _fread (являющейся оберткой вокруг _IO_fread), поэтому анализировать необходимо код, непосредственно предшествующий ее вызову, при просмотре которого в глаза бросается вызов _fopen (см. рис. 3#2), пытающийся открыть несуществующий файл "kpnc.dat" (см. рис. 3#3), причем результат, возвращенный _fopen, никак не проверяется, что и приводит к падению. Создание файла kpnc.dat в текущем каталоге восстанавливает работоспособность программы (см. рис 4). Пример, конечно, слегка надуманный, но большинство приложений ремонтируются аналогичным образом (с той лишь разницей, что на анализ уходит намного больше времени).

После создания файла

Рисунок 4. После создания файла "kpnc.dat" падения программы прекращаются.

А что делать тем, у кого нету IDA Pro и навряд ли появится? Что ж, попробуйте использовать штатную утилиту objdump, запущенную с ключом -d и именем анализируемого файла, только учтите, что в листинге не окажется ни символьных имен функций, ни содержимого ASCIIZ-строк, ни... прочих прелестей прогресса, делающих жизнь удобней в мелочах, поэтому всю недостающую информацию придется добывать вручную (см. листинг 3), теряя на это огромное количество времени (более подробно о дизассемблировании программ под UNIX рассказывается во втором издании моей книги "Hacker disassembling uncovered", которая сейчас готовится к печати).

$objdump -d ./test-code-usr
test-core-usr:     формат файла elf32-i386

Диассемблирование раздела .init:
0804836c <_init>:
...
Диассемблирование раздела .plt:
08048384 <.plt>:
...
Диассемблирование раздела .text:
08048410 <_start>:
...
080484d4 <main>:
...
 8048555:     8d 85 78 f9 ff ff       lea    0xfffff978(%ebp),%eax
 804855b:     89 04 24                mov    %eax,(%esp)
 804855e:     e8 51 fe ff ff          call   80483b4 <_init+0x48>   ; _fgets
 8048563:     c7 44 24 04 f4 86 04    movl   $0x80486f4,0x4(%esp)   ; "rb"
 804856b:     c7 04 24 f7 86 04 08    movl   $0x80486f7,(%esp)      ; "kpnc.dat"
 8048572:     e8 6d fe ff ff          call   80483e4 <_init+0x78>   ; _fopen
 8048577:     89 85 70 f9 ff ff       mov    %eax,0xfffff970(%ebp)
 804857d:     8b 85 70 f9 ff ff       mov    0xfffff970(%ebp),%eax
 8048583:     89 44 24 0c             mov    %eax,0xc(%esp)
 8048587:     c7 44 24 08 01 00 00    movl   $0x1,0x8(%esp)
 804858f:     c7 44 24 04 66 06 00    movl   $0x666,0x4(%esp)
 8048597:     8d 85 78 f9 ff ff       lea    0xfffff978(%ebp),%eax
 804859d:     89 04 24                mov    %eax,(%esp)
 80485a0:     e8 ff fd ff ff          call   80483a4 <_init+0x38>   ; _fread
 80485a5:     b8 00 00 00 00          mov    $0x0,%eax
 80485aa:     c9                      leave
 80485ab:     c3                      ret

Листинг 3. Дизассемблирование программы с помощью штатной утилиты objdump (все комментарии расставлены автором).

Поиск и добыча коры в заповедном лесу Linux и xBSD

FreeBSD по умолчанию сбрасывает кору в файл, имеющем то же самое имя, что и упавшая программа, с добавлением расширения ".core" и сохраняемый в одной директории с программой. Кора доступна только администратору, что идеологически правильно, поскольку доступ к дампу памяти может повлечь за собой утечку конфиденциальной информации.

Часть дистрибутивов Linux'а сбрасывают кору в файл "core", находящийся в одной директории с упавшей программой (что при падении нескольких программ вызывает путаницу и прочие неудобства). Часть же - не сбрасывает ее вообще! При этом после сообщения "segmentation fault" строка "(core dumped)" отсутствует. Чем вызван такой беспредел?! Очень просто - поскольку рядовые пользователи Linux'а обладают крайне невысокой квалификацией, кора оседает мощными пластами, транжирящими дисковое пространство, которое просто некому подчистить! Но даже те немногие, кто знают, что такое кора, практически никогда не пользуются ею по прямому назначению, а немедленно стирают. Вот составители дистрибутивов и пошли им навстречу, запретив сброс коры установкой лимитов.

Узнать текущее состояние лимитов можно с помощью штатной утилиты "ulimit", запущенной с ключом "-a". Результат ее выполнения на мыщъхином компьютере следующий:

root@6[core-gdb]# ulimit -a
core file size           (blocks, -c)      0
data seg size            (kbytes, -d)      unlimited
file size                (blocks, -f)      unlimited
max locked memory        (kbytes, -l)      unlimited
max memory size          (kbytes, -m)      unlimited
open files               (-n)              1024
pipe size                (512 bytes, -p)   8
stack size               (kbytes, -s)      unlimited
cpu time                 (seconds, -t)     unlimited
max user processe        s        (-u)     1024
virtual memory           (kbytes, -v)      unlimited

Листинг 4. Просмотр текущих лимитов.

Как видно, предельный размер файла коры выставлен в ноль, а потому кора и не создается. Изменить статус-кво можно (и нужно!) с помощью все той же утилиты "ulimit", запущенной следующим образом:

root@6[core-gdb]# ulimit -c unlimited
root@6[core-gdb]# ulimit -a
core file size           (blocks, -c)      unlimited
data seg size            (kbytes, -d)      unlimited
file size                (blocks, -f)      unlimited
max locked memory        (kbytes, -l)      unlimited
max memory size          (kbytes, -m)      unlimited
open files               (-n)              1024
pipe size                (512 bytes, -p)   8
stack size               (kbytes, -s)      unlimited
cpu time                 (seconds, -t)     unlimited
max user processe        s        (-u)     1024
virtual memory           (kbytes, -v)      unlimited

Листинг 5. Снятие лимитов с коры.

Вот теперь другое дело!!! Теперь кора будет создаваться всегда! (Если, конечно, дискового места хватит). Маленькое замечание мимоходом: установка лимитов носит локальный характер (только для данного пользователя) и после перезапуска системы лимиты будут восстановлены в значения по умолчанию, так что смело экспериментируйте без риска угробить систему.

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

Набираем в командной строке: "$kill -3 <pid>", где <pid> - идентификатор процесса, с которого необходимо содрать кору (сам процесс при этом будет завершен). И (если только процесс активно не сопротивляется дампу) файл коры тут же образуется в текущем каталоге процесса.

Анализ коры различными средствами

И вот, после стольких мучений, файл коры лежит перед нами. Что же с ним можно сделать? Отослать разработчику? Не торопитесь! Сначала проведем небольшой эксперимент: откроем файл коры, сброшенный приложением test-core-usr, в любом hex-редакторе (например, том, что встроен в Midnight Commander) и попробуем найти строку "KPNC%69" (ту самую, которую мы ввели в качестве нашего имени).

Вот так номер! Строка присутствует в коре прямым текстом (см. рис. 5).

Кора содержит в себе все стековые переменные

Рисунок 5. Кора содержит в себе все стековые переменные.

Хорошо (то есть, как раз ничего хорошего), а как насчет кучи? Как мы помним, наша стендовая программа выделяла блок динамической памяти порядочных размеров и забивала его логотипами "nezumi-souriz-elraton-" (это все мыщъхи - на японском, французском и испанском языках). Вводим искомую строку и ищем следы ее присутствия в дампе памяти.

Hex-редактор долго ждать не заставляет и немедленно отображает результат, высаживающий нас на полную измену (см. рис. 6).

Кора содержит в себе данные динамической памяти

Рисунок 6. Кора содержит в себе данные динамической памяти.

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

Кстати, сохранение стековых и динамических данных в коре позволяет написать программу, вытягивающую из дампа памяти всю несохраненную информацию. В случае текстовых редакторов (почтовых клиентов) это можно сделать и лапами - непосредственно в hex-редакторе. Более сложные приложения, работающие с графикой, электронными таблицами, так просто не сдаются и над ними приходится попыхтеть, проанализировав внутреннее представление данных, которые могут быть организованы и в виде списков, и виде двоичных деревьев или еще как (однако при наличии исходных текстов это не такая уж и большая проблема).

Но все же вернемся к нашим баранам. В смысле, к разработчикам. Как им сообщить об ошибке без ущерба для своей конфиденциальности. Да очень просто! И в этом нам поможет могущий отладчик gdb, входящий в штатный комплект поставки большинства дистрибутивов:

root@6[core-gdb]$# запускаем gdb, "-c" указывает на кору, "./core" - имя файла с корой
root@6[core-gdb]$gdb -c ./core > error_log
where
info registers
q
root@6[core-gdb]$

Листинг 6. Выуживание из дампа памяти значений регистров и стека вызовов.

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

Заключение или жизнь после смерти

Сейчас мыщъх вплотную работает над секретным стратегическим проектом, конечной целью которого является оживление упавших программ с возможностью продолжения их нормальной работы (правда без всяких гарантий стабильности). Работа достаточно сложная, сопряженная с необходимостью написания ядерных модулей и потому продвигается не так быстро, как этого хотелось бы, так что посильная помощь (в виде тестирования программы на дампах различных программ во всевозможных конфигурациях) только приветствуется. Оставляйте свои координаты для связи на http://slut96.blogspot.com.