herm1t
Октябрь 2007
Оглавление
Разглядывал я как-то ELF-файлы objdump'ом и обратил внимание на секцию .hash, и подумал: а нельзя ли извлечь из нее какую-нибудь пользу? Хорошая ведь секция. Расположена в сегменте кода. Нельзя ли её как-нибудь урезать или убрать? В этой статье я хотел бы поделиться находками.
Хэш таблица используется для ускорения доступа к таблице символов (.dynsym). Устроено это следующим образом (Каждый элемент - это Elf32_Word):
[ nbuckets ]
[ nchains ]
[ buckets[0] ]
.........................
[ buckets[nbuckets-1] ]
[ chains[0] ]
.........................
[ chains[nchains - 1] ]
nbuckets - произвольное число (больше нуля и меньше nchains, желательно простое - для более равномерного распределения значений в таблице), nchains - совпадает с количеством символов).
В glibc для поиска символов по имени используется вот такая функция (elf/do-lookup.h):
То есть:
Попытаюсь проиллюстрировать на примере поиска символа strlen в /bin/ps из моей системы:
(*) elf_hash("strlen") = 45
.hash .dynsym .dynstr
offset +----+
0 nbuckets | 67 |
4 nchains | 74 |
+----+
8 buckets 0 | 0 |
: : :
: : :
188 (*)----> 45 | 60 | ----> dynsym[60].st_name ---> "isatty" (no match)
192 46 | 0 | /
196 47 | 0 | /
: : : /
272 66 | 32 | /
+----+ /
__________/
/
/ +----+
276 chains / 0 | 0 |
: / : :
464 | 47 | 31 |-----> dynsym[31].st_name ---> "strlen" (found!)
: | ^ : :
: | |
: | \_____________
: | \
: | : : \
516 +--> 60 | 47 | ----> dynsym[47].st_name ---> "strdup" (no match)
520 61 | 0 |
: : :
568 73 | 0 |
: +----+
Если по-прежнему не очень понятно, загляните в описание формата ELF [1].
Предполагается, что будущий файл-жертва уже найден, проверен на валидность, открыт на чтение/запись (handle - h, length - l), отображен в память (map - m, ELF header - ehdr). Все примеры на Си. Я так же написал четыре версии (нумерация глав и версий совпадает) демонстрационного вируса (Linux.Hasher) для тех, кто предпочитает ассемблер или просто хотел бы посмотреть, как работают предложенные методы заражения.
Определены макросы:
В начале, я попытался просто удалить соответствующую секцию, но из-за глупой ошибки у меня ничего не вышло (об этом в следующей главе). И я подумал, а что если сконструировать такой хэш минимального размера, чтобы do_lookup всегда обламывался? Будет ли работать искалеченный таким образом файл? Будет. Установим nbuckets в 1 (x % 1 = 0), а buckets[0] в 0 (STN_UNDEF). Все, хэш не работает, совпадений нет. Оставшееся в секции место (размер секции - 12 байт) свободно.
NB! Если nbuckets установить в ноль, то это приведет к Floating point exception в do_lookup_x (/lib/ld-linux.so), что и неудивительно, не так ли?
Когда я пытался удалить хэш, я пытался переименовывать секцию и забивать указатель с типом DT_HASH в .dynamic, а нужно просто поменять тип секции в таблице секций (SHT) с SHT_HASH на SHT_PROGBITS (а что, похоже на правду) или на SHT_NULL (objdump перестанет её показывать). .dynamic тоже почистим (этот шаг можно пропустить):
Linux.Hasher.b dynamic не чистит
Удалять хэш полностью совсем необязательно, попробуем уменьшить его размер так, чтобы в секции поместился и новый хэш, и наш вирус. Этот метод не очень практичен, потому, что в системе не так уж много файлов с большими хэшами, но сам метод построения хэша пригодится нам в дальнейшем. Не долго думая, я взял код из компилятора TCC [2]. Вот немного отредактированная версия:
Не забудьте сделать memset(hash, 0, size_of_hash_section);!
Минимальное, максимальное и среднее значение nbuckets для файлов из /bin:
И так, что нужно сделать:
Кстати, для поиска нужных секций можно воспользоваться полем sh_link:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08047134 000134 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08047148 000148 000020 00 A 0 0 4
[ 3] .dynstr STRTAB 08047168 000168 00030a 00 A 0 0 1 <----+
[ 4] .gnu.liblist GNU_LIBLIST 08047474 000474 00003c 14 A 3 0 4 |
[ 5] .gnu.conflict RELA 080474b0 0004b0 00012c 0c A 7 0 4 |
[ 6] .hash HASH 08048168 001168 00023c 04 A 7 0 4 ---+ |
[ 7] .dynsym DYNSYM 080483a4 0013a4 0004a0 10 A 3 1 4 <--+ |
+-------------+
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Вот так:
Посмотрим, как изменилась хэш-таблица:
ДО Name 'sigfillset', hash 0d06a614 buckets[12]=1 ... 1 (sigfillset) -> 1 Name 'getgrnam', hash 0cae92ad buckets[61]=57 ... 57 (free) ... 48 (lookup_wchan) ... 30 (escape_command) ... 2 (getgrnam) -> 2 пропущено Name '__gmon_start__', hash 0f4d007f buckets[35]=72 ... 72 (__gmon_start__) -> 72 Name 'strcpy', hash 07ab8a79 buckets[5]=73 ... 73 (strcpy) -> 73 ПОСЛЕ 27 hash buckets, 74 chains Name 'sigfillset', hash 0d06a614 buckets[1]=1 ... 1 (sigfillset) -> 1 Name 'getgrnam', hash 0cae92ad buckets[7]=2 ... 2 (getgrnam) -> 2 пропущено Name '__gmon_start__', hash 0f4d007f buckets[6]=27 ... 27 (getpagesize) ... 35 (readtask) ... 67 (fwrite) ... 72 (__gmon_start__) -> 72 Name 'strcpy', hash 07ab8a79 buckets[23]=6 ... 6 (strchr) ... 18 (signal_number_to_name) ... 59 (get_pid_digits) ... 73 (strcpy) -> 73
Надеюсь, всем знаком способ заражения ELF файлов, который по странной причуде называют "добавлением" дополнительного кодового сегмента [3], хотя на самом деле из-за невозможности добавить новый элемент в таблицу сегментов (Program Header Table, PHT), заменяется элемент с типом PT_NOTE, на PT_LOAD. Мы теперь знаем, как найти немного места в сегменте кода (где и расположена PHT), и ничто не мешает нам её увеличить, и действительно добавить сегмент ничего при этом не удаляя.
Что нужно сделать:
sizeof(Elf32_Phdr)) в сегменте кода)ehdr->e_phnum++), и размер сегмента PT_PHDR в файле и в памяти (ph->p_filesz += 32; ph->p_memsz+=32)Вот так:
Как изменился файл (/bin/ps + Linux.Hasher.d):
До заражения
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08047034 0x08047034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08047134 0x08047134 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08047000 0x08047000 0x0ff88 0x0ff88 R E 0x1000
LOAD 0x010000 0x08057000 0x08057000 0x002fc 0x20654 RW 0x1000
DYNAMIC 0x010014 0x08057014 0x08057014 0x000d0 0x000d0 RW 0x4
NOTE 0x000148 0x08047148 0x08047148 0x00020 0x00020 R 0x4
GNU_EH_FRAME 0x00ff38 0x08056f38 0x08056f38 0x00014 0x00014 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section Headers:
...
[ 1] .interp PROGBITS 08047134 000134 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08047148 000148 000020 00 A 0 0 4
[ 3] .dynstr STRTAB 08047168 000168 00030a 00 A 0 0 1
[ 4] .gnu.liblist GNU_LIBLIST 08047474 000474 00003c 14 A 3 0 4
[ 5] .gnu.conflict RELA 080474b0 0004b0 00012c 0c A 7 0 4
[ 6] .hash HASH 08048168 001168 00023c 04 A 7 0 4
...
Dynamic section at offset 0x10014 contains 25 entries:
0x00000004 (HASH) 0x8048168
0x00000005 (STRTAB) 0x8047168
...
0x6ffffef9 (GNU_LIBLIST) 0x8047474
...
0x6ffffef8 (GNU_CONFLICT) 0x80474b0
После
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08047034 0x08047034 0x00120 0x00120 R E 0x4
INTERP 0x000154 0x08047154 0x08047154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08047000 0x08047000 0x0ff88 0x0ff88 R E 0x1000
LOAD 0x010000 0x08057000 0x08057000 0x002fc 0x20654 RW 0x1000
LOAD 0x01107c 0x0804607c 0x0804607c 0x003e8 0x003e8 R E 0x1000
DYNAMIC 0x010014 0x08057014 0x08057014 0x000d0 0x000d0 RW 0x4
NOTE 0x000168 0x08047168 0x08047168 0x00020 0x00020 R 0x4
GNU_EH_FRAME 0x00ff38 0x08056f38 0x08056f38 0x00014 0x00014 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section Headers:
...
[ 1] .interp PROGBITS 08047154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08047168 000168 000020 00 A 0 0 4
[ 3] .dynstr STRTAB 08047188 000188 00030a 00 A 0 0 1
[ 4] .gnu.liblist GNU_LIBLIST 08047494 000494 00003c 14 A 3 0 4
[ 5] .gnu.conflict RELA 080474d0 0004d0 00012c 0c A 7 0 4
[ 6] .hash HASH 08048188 001188 00021c 04 A 7 0 4
...
Dynamic section at offset 0x10014 contains 25 entries:
0x00000004 (HASH) 0x8048188
0x00000005 (STRTAB) 0x8047188
...
0x6ffffef9 (GNU_LIBLIST) 0x8047494
...
0x6ffffef8 (GNU_CONFLICT) 0x80474d0
Чтобы данный способ заражения перестал работать, достаточно поместить .hash не перед, а после секций кода и данных (их двигать нельзя), что, в прочем, не помешает предыдущим вариантам.
Вот собственно и все, любые комментарии приветствуются. herm1t@vx.netlux.org
Исходники Linux.Hasher (a,b,c,d) прилагаются.