Анализ вредоносной программы под 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). Мне не довелось увидеть, чтобы вирус на самом деле получил ответ, это может быть обусловлено несколькими причинами. Например, этот сервер больше не используется, или владелец не отправлял команды в это время и так далее. Тем не менее я предполагаю, что эта программа получает команды с сервера и исполняет их.
Самая интересная часть этого вируса была преимущественно в функции расшифровки, поэтому эта статья была небольшим уроком, почему самопальное шифрование это плохая идея. В этом случае оно позволило нам расшифровать строки, чтобы узнать больше о действиях вредоносной программы. Надеюсь, это была интересная статья для всех, лично мне было занимательно анализировать этот образец.
Спасибо за чтение и счастливого ревёрсинга!
Связанные статьи:
- Анализ трояна Snojan (97%)
- Обратный инжиниринг с использованием Radare2 (Reverse Engineering) (91.3%)
- Обратный инжиниринг с использованием Radare2 (Reverse Engineering) (часть 2) (91.3%)
- Отладка приложения в Windows с Radare2 (87.9%)
- Деобфускация JavaScript кода (56.5%)
- Поиск сетки вредоносных сайтов (кейс) (RANDOM - 5.7%)