Анализ вредоносной программы под Linux: плохое самодельное шифрование


Источник: https://medium.com/@jacob16682/linux-malware-analysis-why-homebrew-encryption-is-bad-48e349b252f9

Изложение от первого лица, но автором является Jacob Pimental.

Linux является одной из моих самых любимых операционных систем, но вредоносную программу для неё встретишь не часто, поэтому меня очень заинтересовал вирус под Linux, пойманный моей приманкой (honeypot). Эта статья будет моим анализом образца, особенно используемой в нём функции дешифрования. Это хороший пример, почему использование своего собственного алгоритма шифрования не очень безопасно.

Как с любым анализом, сначала я забросил файл в VirusTotal, чтобы посмотреть, что он про него знает.

Анализ VirusTotal нашего вируса:

Мы можем увидеть здесь, что только 34 из 59 вендеров идентифицировали его как вирус, ничего удивительного, это ведь исполнимый файл Linux. Исполнимый файл не упакован, и в анализе VirusTotal нет ничего супер интересного. Следующее что я делаю, запускаю его через rabin2 для получения о нём базовой информации.

arch     x86
binsz    646674
bintype  elf
bits     32
canary   false
class    ELF32
crypto   false
endian   little
havecode true
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
rpath    NONE
static   true
stripped false
subsys   linux
va       true

Опять ничего особого интересного. Это исполнимый файл Linux, который был написан на языке программирования C. Интересное начинается когда мы проводим анализ экспортов, используя в rabin2 флаг -E:

[Exports]
956 0x00020070 0x08068070 GLOBAL   FUNC 1365 __vsyslog_chk
957 0x00073c00 0x080bbc00 GLOBAL OBJECT   36 _nl_C_LC_CTYPE
959 0x00021790 0x08069790 GLOBAL   FUNC    8 __stack_chk_fail_local
965 0x0008b9e8 0x080d49e8 GLOBAL OBJECT    4 __morecore
966 0x0001fc80 0x08067c80 GLOBAL   FUNC   41 __getdtablesize
967 0x00014f80 0x0805cf80 GLOBAL   FUNC   40 _IO_remove_marker
969 0x00009090 0x08051090 GLOBAL   FUNC  291 __libc_sigaction
970 0x00051ef0 0x08099ef0 GLOBAL   FUNC   69 __isnanl
971 0x00042ae0 0x0808aae0 GLOBAL   FUNC  170 __libc_pread
974 0x0001db60 0x08065b60 GLOBAL   FUNC   34 strcpy
975 0x0003c460 0x08084460 GLOBAL   FUNC  200 _IO_wdefault_xsgetn
976 0x00012080 0x0805a080 GLOBAL   FUNC    9 __fcloseall
977 0x00020630 0x08068630 GLOBAL   FUNC   44 __syslog
978 0x00000ba3 0x08048ba3 GLOBAL   FUNC   74 V8ULRand
979 0x0000e600 0x08056600 GLOBAL   FUNC  234 __setstate_r
980 0x0006ce50 0x080b4e50 GLOBAL   FUNC  213 _dl_vsym

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

Первое, на что я обратил внимания, этот кусок кода:

Даже не только из того, что написано в комментариях, сколько из-за строки с названием strHost, также показано, что имя функции, которой передаётся строка strHost, называется DecryptData (расшифровка данных). На данный момент мы можем предположить, что вероятно strHost содержит какого-то рода имя хоста или адрес, к которому вирус может подключаться, а DecryptData расшифрует эту строку, чтобы её стало возможно использовать, что невозможно с её текущей формой ‘yy123-e4213-mfs’. Далее мы можем перепрыгнуть в DecryptData, чтобы увидеть, на что она похожа.

Граф Radare2 с видом функции DecryptString:

В начале мы можем видеть, что значение 0 перемещается в переменную counter (счётчик) (имейте ввиду, что я переименовал все переменные в этой функции, используя команду afvn, это намного упрощает для меня анализ). Затем начинается петлевой цикл (loop) в котором проверяется, является ли переменная counter меньшей, чем длина строки, переданной в качестве аргумента. Если это так, то мы переходим к главной логике петлевого цикла.

Новичкам в анализе это может показаться совсем непонятным, но я проведу вас через весь происходящий процесс. Программа передаёт текущий индекс строки, на которой мы находимся, в другую переменную счётчика (не спрашивайте меня почему, я тоже понимаю, что такого же результата можно было бы добиться не делая этого). Затем декларируется (объявляется) новая переменная с шестнадцатеричным значением 0x55555556. Это значение, которое зовётся “magic number” (магическое числов) и используется в улучшении производительности при делении. Это конкретное магическое число сводится к 1431655766 (значение в десятеричном виде). Если мы умножим число на него, а затем сделаем логическое смещение вправо на 32 бита, то это такой же результат, как деление на 3! Я сделаю всё возможное в попытке объяснить это, поскольку сам я впервые узнал об этом, но это действительно очень интересно.


К примеру возьмём 6 делённое на 3, как мы все знаем, это равно 2. Таким образом, в этом случае мы должны бы были сделать 6 * 1431655766, что даёт результат 8589934596, или в двоичном виде:

1000000000000000000000000000000100

Теперь если мы сдвинем вправо все биты на 32 позиции, мы получим:

0000000000000000000000000000000010

Что даёт нам 2! Это очень интересный трюк и многому учит нас о том, как работает архитектура компьютера. Это деление без выполнения деления! За дополнительной информации я предлагаю прочитать эту статью.


Таким образом, то, что приложение делает по сути, — это прогон счётчика и присвоение переменной значения 0, 1 или 2 на основе вычислений, выполненных с использованием магического числа. Если переменная равна 1, то выполняется переход на одну часть кода, если переменная — не равна единице, то делается переход на другую часть кода. Далее мы рассмотрим эти две ветви.

Итак обе ветви делают примерно одинаковые вещи с небольшим изменением. В начале они обе делают проверку, имеет ли символ в этом индексе шестнадцатеричное значение менее или равное 0x20, что является просто символом пробела. Если он имеет, то мы не связываемся с этим символом, увеличиваем наш счётчик на один и делаем следующее итерацию (следующий проход). Если он не имеет, то делается проверка, равно ли значение 0x7f, это символ DELETE в ASCII. Если так, то мы вновь ничего с ним не делаем и продолжаем в следующей итерации. Если нет, то мы продолжаем с расшифровкой. По сути, показанный процесс — это установка границ, в которые попадают печатные ASCII символы, 0x20–0x7f, то есть печатные символы передаются далее для расшифровки.

Итак, если результат наших вычислений с магической переменной равны 1, то тогда мы выполняем левую ветвь, которая в самом конце проверки, входит ли символ в обозначенные границы ASCII, для расшифровки вычитает из кода буквы единицу. Это означает, что B становится A, D становится C и так далее. Если наши вычисления с магической переменной не равны 1, тогда мы добавляем к коду нашей буквы единицу, A становится B, C становится D, и так далее. Это всё, что делает алгоритм, который на самом деле оказался не таким страшным, каким могу видиться в первый раз. Его понимание делает простой для нас расшифровку строки. Давайте взглянем на строку strHost (‘yy123-e4213-mfs’), во что она превращается после прохода через эту функцию.

Вначале я хочу назначить каждому символу в строке одно из значений 0, 1 либо 2. Это имитирует вычисления, выполненные с использованием этого магического числа.

y y 1 2 3 - e 4 2 1 3 - m f s
0 1 2 0 1 2 0 1 2 0 1 2 0 1 2

Теперь для каждого символа, чьё значение не 1, я добавлю к значению ASCII один, и для всех символов, чьё значение 1, я вычту из ASCII кода символов величину 1.

zx232.f3322.net

Итак, вирус почти наверняка подключается к хосту zx232.f3322.net, которые, вероятно, является командным и контрольным центром.

Я быстренько сделал скрипт на Python для расшифровки других строк, с которыми я могу столкнуться.

def decrypt_string(string, length):
    counter = 0 #local_4h
    local_18 = 0
    local_1c = 0
    new_string = ''
    while counter < length:
        if not local_18 == 1:
            letter = string[counter]
            if not letter == ' ':
                new_string += chr(ord(letter)+1)
        else:
            letter = string[counter]
            if not letter == ' ':
                new_string += chr(ord(letter)-1)
        counter += 1
        local_18 += 1
        if local_18 > 2:
            local_18 = 0

А здесь список других зашифрованных строк и их расшифрованные аналоги:

Остальная часть образца довольно проста. Он копирует себя в файлы /etc/.zl, /tmp/.lz и /etc/init.d/.zl. Затем делает копию себя в rc2.d, rc3.d, rc4.d и rc5.d и связывает их символическими ссылками с файлом .zl в /etc/init.d. Создание этих клонов гарантирует закрепление в системе, поскльку файлы в этих папках вызываются при запуске компьютера. Если вы заметили эти файлы в системе, это может стать идентификатором вредоносной программы на основе хоста (host-based identifier). Затем он пингует сервер zx232.f3322.net на порту 54188 и ожидает ответ любого рода. Это может быть идентификатором вируса на основе сетевой активности (network-based identifier). Мне не довелось увидеть, чтобы вирус на самом деле получил ответ, это может быть обусловлено несколькими причинами. Например, этот сервер больше не используется, или владелец не отправлял команды в это время и так далее. Тем не менее я предполагаю, что эта программа получает команды с сервера и исполняет их.

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

Спасибо за чтение и счастливого ревёрсинга!


Рекомендуется Вам:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *