Bincrypter — известный в узких кругах инструмент для обфускации бинарных файлов. Периодически в атаках нам попадаются семплы, обфусцированные с помощью данного инструмента. В этой статье разберем принцип обфускации, рассмотрим сами обфусцирующие скрипты, а также приведем Yara-правило для обнаружения обфусцированных бинарных файлов. Мы также написали инструмент bindecrypter для автоматизации процесса деобфускации.

Общая архитектура bincrypter

Утилита написана на bash. На странице проекта нам предлагается скачать релизный скрипт, который и осуществляет обфускацию. Он состоит из hook_stub (скрипт, осуществляющий расшифровку основной нагрузки) и bin_stub (скрипт, осуществляющий обфускацию).

В процессе обфускации формируется bash-скрипт, который условно можно разделить на три сегмента-строки:

  1. Shebang \n — (#!/bin/sh)
  2. init \n — содержит директивы декодирования и исполнения hook_stub
  3. # payload — непосредственно шифротекст.

Также bincrypter позволяет контролировать процесс обфускации, однако для его запуска достаточно лишь передать путь до бинарного/shell-файла, который необходимо обфусцировать. Так, среди настроек довольно интересен режим lock (активируется при передаче флага -l в строке аргументов). Это позволяет «привязать» результирующий скрипт к тем или иным параметрам системы, на которой он должен быть запущен. Также он позволяет и задавать переменные окружения:

Переменная

Назначение

PASSWORD

Пароль для шифрования/расшифровки

BC_PASSWORD

То же, что и PASSWORD, однако используется в случае многослойного шифрования

BC_LOCK

Используется при неудачной проверке параметров системы. Может содержать:

  1. Ничего. В случае неудачной ошибки скрипт вернет результат 0
  2. Код ошибки. В этом случае вернется заданное число
  3. Строка. При неудачной проверке содержимое данной строки выполняется как bash-команда
  4. Base64-строка. При неудачной проверке содержимое данной строки выполняется как bash-команда

BC_QUIET

Если установлена, bincrypter работает без вывода

BC_PADDING

Доля «мусорных» байтов внутри нагрузки (в процентах). По умолчанию — 25%. На основе данного значения рассчитывается переменная R — смещение до зашифрованной нагрузки.

 

Минимальный размер — 31337 (ELEET)

[ "$BC_PADDING" != "0" ] && {
   local sz="${#DATA}"
   [ "$sz" -lt 31337 ] && sz=31337
   R="$(( (RANDOM * 32768 + RANDOM) % ((sz / 100) * ${BC_PADDING:-25})))"
  }
  _C+="R=${R:-0}"$'\n'

Деобфускация

Зная вводные, попробуем обфусцировать и деобфусцировать файл /usr/bin/id:

    
[test@test bincrypter]$ ./bincrypter ./id
Compressed: 47408 --> 34836 [73%]
Done
    

В секции shebang всегда находится строка #!/bin/sh, которая говорит о том, что наш обфусцированный файл — это bash-скрипт.

Секция init содержит переменную _, заполненную «мусором», а также декодирует и запускает скрипт расшифровки основной нагрузки, которая находится в сегменте payload после переноса строки и начинается с символа #.

Обфусцированный скрипт
Обфусцированный скрипт

Обратите внимание, что содержимое этой переменной обрамляется символом ', в исходном файле bin_stub данная переменная инициализируется следующим образом:

    
# Начинаем формировать значение переменной
 echo -n "_='"
 # Прочитать 66 случайных байт из /dev/urandom
 # А затем убрать из них все печатные символы, а также нулевой байт, 
 # перенос строки, одинарную кавычку
 _bc_xdd 66 </dev/urandom | _bc_xtr '' "[:print:]\0\n'" 
 # Прочитать из /dev/urandom от 1024 до 5119 случайных байт
 _bc_xdd "$((1024 + RANDOM % 4096))" </dev/urandom| _bc_xtr '' "[:print:]\0{2,}\n'"
 # Конец переменной _
 _bc_xprintf "';" # works on BASH + ZSH
    

Здесь также важно и то, что все содержимое переменной «_» — непечатные символы. После данной переменной следует символ-разделитель «;». За ним следуют команды декодирования основного скрипта расшифровки. Таким образом команды декодирования прячутся среди случайных данных.

Вспомогательная функция _bc_xdd используется для вывода байтовых последовательностей, а _bc_xtr — для замены, с помощью регулярных выражений.

Затем следующий фрагмент кода после «мусорного» сегмента добавляет сегмент с командой eval:

    
echo "$(_bc_obbell 'eval "')\$$(_bc_obbell '(echo ')$HOOK|LANG=C $(_bc_obbell "perl -pe \"s/[^[:print:]]//g\"")$(_bc_obbell "|openssl base64 -A -d)")\""
    

Данный фрагмент формирует код, который деобфусцирует содержимое переменной HOOK, содержащей base64 от HOOK-скрипта, который и осуществляет расшифровку основной нагрузки.

Переменная закодирована и обфусцирована следующим образом:

    
HOOK="$(echo "$str" | openssl base64 -A)"
HOOK="$(_bc_ob64 "$HOOK")"
    

Функция _bc_ob64 осуществляет обфускацию base64-строки. Строки такого формата всегда содержат только печатные ASCII-символы. Соответственно, обфускация происходит путем разбиения строки на короткие отрезки случайной длины (1–4 символа) и вставки между ними длинных последовательностей непечатных байтов. Чтобы деобфусцировать такую строку, достаточно лишь удалить в ней все непечатные символы.

Функция _bc_obbell делит строку на отрезки и добавляет между ними заполняющие последовательности байтов. В разных версиях bincrypter могли использоваться следующие заполняющие последовательности (hex):

  1. 603A7C7C0760
  2. 60230860
  3. 6021203A2626082360

Но каждая последовательность начинается и заканчивается символом «`». Bash выполняет код, заключенный между «`», и вставляет результат выполнения на место этого выражения, а уже после этого выполняет остальные команды. Это позволяет разбавить код конструкциями, не имеющими результата выполнения и, таким образом, не меняющими логику основного кода.

Команда декодирования скрипта расшифровки
Команда декодирования скрипта расшифровки

Прежде чем идти дальше, достанем этот скрипт. Нам нужно пропустить переменную «_» с «мусорными» байтами и отыскать в коде начало команды. Как можно видеть на скриншоте, эта команда начинается перед символами «‘;».

    
def deobf_command(data: bytes):
    garbage_bytes = [
        bytes.fromhex("603A7C7C0760"),
        bytes.fromhex("60230860"),
        bytes.fromhex("6021203A2626082360"),
    ]

    for garbage in garbage_bytes:
        data = data.replace(garbage, b"")

    print(data)

with open(file, "rb") as f:
        data = f.read()

    # find first section with decryption command
    script_begin = data.find(b"';") + 2 # '; is a marker of beginning
    script_end = data.find(b"\n#")
    deobf_nonascii(data[script_begin:script_end])
    

Убрав из строки «мусорные байты», получаем следующую картину:

    
b'eval "$(echo \xd8d\xd8\xd2\xb0W\x13….\xae\xe3\xd5\xceH\x1a\xaaR\xa7yd\xec\x99\xbfW\xda\x90\x87\xd5\x1bU\xee\xdd\x87\xf7\xab\xb2\x87KZ\xf6\x0cmkK\x9b\xc5\x1c|LANG=C perl -pe "s/[^[:print:]]//g"|openssl base64 -A -d)"'
    

Как можно видеть, в команду eval передается декодированный из base64 результат выполнения perl-скрипта, куда с помощью команды echo направляется последовательность байтов, в которую вставлены непечатные символы. В данной последовательности прослеживаются элементы base64-строки. Для удаления непечатных символов bincrypter использует perl с регулярным выражением "s/[^[:print:]]//g", которое удаляет из строки non-ascii-символы.

В python подобное можно осуществить с помощью механизма генераторных выражений:

clean = bytes(c for c in data if 0x20 <= c <= 0x7E)

Во время обфускации bincrypter генерирует переменную R, которая представляет собой смещение до нагрузки. Это смещение будет актуально после расшифровки всего шифротекста. Данная переменная зашифровывается, и ее содержимое хранится внутри переменной C. Также присутствуют переменные P (закодированный в base64 пароль) и S, которые являются составными частями ключа для расшифровки нагрузки. Все эти переменные помещаются в начало скрипта расшифровки.

    
unset BCV BCL
P=NmhodEFkOWFnbk9lamNkago=
S='9Fp2h2qrO9pTO1oF'
C=SbeoVK+6bBQEzlRGlhnQYg==
    

В самом скрипте в функции расшифровки происходит сначала проверка пароля:

    
_P="${PASSWORD:-$BC_PASSWORD}"
    unset _ PASSWORD 
    if [ -n "$P" ]; then
        if [ -n "$BCV" ] && [ -n "$BCL" ]; then
            _bcl_gen_p "$P" || return
        else
            _P="$(echo "$P"|openssl base64 -A -d)"
        fi
    else
        [ -z "$_P" ] && {
            echo >&2 -n "Enter password: "
            read -r _P
        }
    fi
    

Здесь может быть три сценария:

  • При обфускации использовался BC_LOCK. В данном случае в скрипт встраивается процедура _bcl_gen_p, которая генерирует пароль, проверяет параметры системы на соответствие эталонным (проверки реализованы не до конца, на данный момент происходит только генерация ключа).
  • BC_LOCK не использовался, ключ декодируется штатно.
  • Если ключ не задан, скрипт просит пользователя ввести его вручную.

Далее идет блок расшифровки переменной C, в которой в зашифрованном виде хранится оффсет до зашифрованной нагрузки:

    
[ -n "$C" ] && {
        local str
        str="$(echo "$C" | openssl enc -d -aes-256-cbc -md sha256 -nosalt -k "C-${S}-${_P}" -a -A 2>/dev/null)"
        unset C
        [ -z "$str" ] && {
            [ -n "$BCL" ] && echo >&2 "ERROR: Decryption failed."
            return 255
        }
        eval "$str"
        unset str
    }
    

Как можно заметить, для расшифровки используется AES-256-CBC с KDF (Key Derivation Key — функция формирования ключа) SHA256. В качестве ключа используется строка “C-${S}-${_P}”.

Извлечь переменные из скрипта можно с помощью регулярного выражения:

    
def extract_psc(data: bytes) -> Dict[str, bytes]:
    VAR_RE = re.compile(
        rb'^(P|S|C|BCL|BCV)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s#]+))',
        re.MULTILINE
    )

    result: Dict[str, bytes] = {}

    for m in VAR_RE.finditer(data):
        name = m.group(1).decode()
        value = m.group(2) or m.group(3) or m.group(4)
        result[name] = value

    LOG.debug("Extracted vars: %s", result.keys())
    return result
    

После расшифровки получаем оффсет R. Зная этот оффсет, можно найти наш исполняемый файл в расшифрованном плейнтексте, который расшифровывается составным ключом “${S}-${_P}”. Но важно понимать, что сначала необходимо расшифровать всю третью секцию — а перед этим ее нужно подготовить. Вот так выглядит одна из строк расшифровки нагрузки:

    
exec bash -c "$(printf %s "$XS" |LANG=C perl -e '<>;<>;read(STDIN,$_,1);while(<>){s/B3/\n/g;s/B1/\x00/g;s/B2/B/g;print}'|openssl enc -d -aes-256-cbc -md sha256 -nosalt -k "${S}-${_P}" 2>/dev/null|LANG=C perl -e "read(STDIN,\$_, ${R:-0});print(<>)"|gunzip)"
    

Сначала bincrypter пропускает два переноса строки (<>;<>; — это пропуск двух секций, переход сразу к payload). Далее в цикле происходит замена байтов. «B3» меняется на символ переноса строки, «B1» на нулевой байт 0x00, «B2» на символ B. Вероятно, изначально такая замена управляющих символов была сделана в целях совместимости или для удобства поиска секций в обфусцированном файле.

    
ciphertext = data.split(b"\n")[2][1:] # with garbage
ciphertext = ciphertext.replace(b"B3", b"\n").replace(b"B1", b"\x00").replace(b"B2", b"B")
print(len(ciphertext))
payload_passphrase = psc["S"] + b"-" + P_raw
plaintext = decrypt(ciphertext, payload_passphrase)
    

Помимо шифрования нагрузки bincrypter также сжимает ее с помощью gzip. Смещение R, которое вычислялось в скрипте для расшифровки, указывает на начало gzip-потока с оригинальным исполняемым файлом. Распаковав данный поток, получаем исходный elf.

Распакованный id
Распакованный id

Утилиту, которая автоматизирует данный процесс, можно найти у нас на GitHub.

Заключение

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

Утилита использует набор практичных, но действенных приемов: обфускацию строк «мусорными» данными, нестандартное кодирование полезной нагрузки, динамическую сборку и выполнение скрипта, а также упаковку обфусцируемого бинарного файла (в том числе с использованием gzip). В результате статический анализ затрудняется, а вероятность детекта снижается.

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

IOCs

Yara

    
rule nix_bincrypter_a_obf {
  meta: 
    description = "Binary packed with bincrypter"
    author = "4rays"
  strings:
  $beginning = { 23 21 2F 62 69 6E 2F 73 68 0A 5F 3D 27 }

  $dummy1 = { 60 3A 7C 7C 07 60 }
  $dummy2 = { 60 23 08 60 }
  $dummy3 = { 60 21 20 3A 26 26 08 23 60 } // from new versions

  $perl = "LANG=C perl" ascii
  $dd = "dd bs=" ascii

  condition:
    $beginning at 0
  and ( $perl or $dd )
  and 2 of ($dummy*)
}