Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #103, ИЮЛЬ 2007 г.

Бескомпромиссный разбор дампов памяти

Крис Касперски

Хакер, номер #103, стр. 108

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

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

При возникновении необрабатываемого исключения внутри прикладной программы, центральный процессор возбуждает исключение и операционная система завершает работу приложения в аварийном режиме, затем выдает segmentation fault и (при правильно выставленных лимитах) сбрасывает дамп памяти в специальный core-файл, в просторечии именуемый «корой». Кора содержит все сегменты ELF-файла (код, данные), содержимое стековой и динамической памяти.

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

Большинство пользователей удаляет кору не задумываясь; некоторые отсылают ее разработчикам в надежде, что это поможет им выловить баг. Однако надеяться на девелоперов — все равно, что ждать у моря погоды (особенно в open source проектах, которые держатся на голом энтузиазме при полной нехватке времени и финансов).

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

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

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

Кроме того, существуют и специальные анализаторы коры: как коммерческие, так и бесплатные (например, Introspector, созданный James'ом Michael'ем из корпорации DuPont, — introspector.sf.net).

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

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

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

#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;

p = malloc(M);

for(a = 0; a < N; a++)

strcpy(p + a * strlen(S), S);

printf("tell me your name, plz!\n");

fgets(buf, N - 1, stdin);

f = fopen("kpnc.dat", "rw");

// crash! f == 0

fread(buf, N, 1, f);

return 0;

}

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

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

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

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

# 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

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

От обилия информации можно и растеряться. Но мы же хакеры, а не пионеры какие-нибудь, так что не будем паниковать и обратим свой взор к строке 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.

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

Ты спрашиваешь, что тебе делать, если у тебя нет IDA Pro и навряд ли появится? Что ж, попробуй использовать штатную утилиту objdump, запущенную с ключом '-d' и именем анализируемого файла. Только учти, что в листинге не окажется ни символьных имен функций, ни содержимого ASCIIZ-строк, ни прочих прелестей прогресса, делающих жизнь удобнее в мелочах. Поэтому всю недостающую информацию придется добывать вручную, теряя на это огромное количество времени (более подробно о дизассемблировании программ под 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

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

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

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

# 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 processes (-u) 1024

virtual memory (kbytes, -v) unlimited

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

# ulimit -c unlimited

# ulimit -a

core file size (blocks, -c) unlimited

data seg size (kbytes, -d) unlimited

file size (blocks, -f) unlimited

...

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

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

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

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

Как мы помним, наша стендовая программа выделяла блок динамической памяти порядочных размеров и забивала его логотипами «nezumi-souriz-elraton-» (это все мыщъх’и — на японском, французском и испанском языках). Вводим искомую строку и ищем следы ее присутствия в дампе памяти. Hex-редактор долго ждать не заставляет и немедленно отображает интересующий нас результат! Получается, что если передать программисту кору, то вместе с дампом программы он получит всю нашу информацию, с которой, возможно, еще не снят гриф секретности!

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

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

$ gdb -c ./core > error_log

where

info registers

q

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

Заключение

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

Содержание