• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Анализ вредоносных программ для Android: Hydra Dropper

NokZKH

Переводчик
Забанен
Регистрация
09.02.2019
Сообщения
99
Реакции
121
Пожалуйста, обратите внимание, что пользователь заблокирован
Hydra - это еще один вариант android bankbot. Она использует оверлей для кражи информации, такой как Anubis . Его название происходит от команды и панели управления . В период с июля 2018 года по март 2019 года в Google Play Store оставалось как минимум 8-10 образцов. Распространение вредоносного ПО аналогично случаям Anubis. Приложения Dropper добавляются в Play Store. Но, в отличие от Anubis, приложения Dropper извлекают dex- файл из png- файла с помощью своего рода стенографии и загружают вредоносное приложение с управляющего сервера dex . Вы можете найти образец, который я рассмотрю в этом посте здесь: клик

ToC :
  • Bypass checks that on the java side
  • GDB Debug
  • Ghidra shenanigans
  • Understanding creation of the dex file
  • Bonus
Прежде всего , если приложению dropper понравится среда, в которой оно работает, оно загрузит файл dex и подключится к серверу управления и контроля. Есть несколько проверок на Java и нативной стороне. Мы будем отлаживать нативную сторону с помощью gdb и будем использовать ghidra, чтобы помочь найти проверки и важные функции.

Проверка времени
Когда мы открываем первое приложение с помощью jadx, мы видим проверку времени в классе com.taxationtex.giristexation.qes.Hdvhepuwy.

Код:
public static boolean j() {
        return new Date().getTime() >= 1553655180000L && new Date().getTime() <= 1554519180000L;
}

Эта функция вызывается в другом классе: com.taxationtex.giristexation.qes.Sctdsqres

Код:
class Sctdsqres {
    private static boolean L = false;
    private static native  void fyndmmn(Object obj);
    Sctdsqres() {
    }
    static void j() {
        if (Hdvhepuwy.j()) {
            H();
        }
    }
    static void H() {
        if (!L) {
            System.loadLibrary("hoter");
            L = true;
        }
        fyndmmn(Hdvhepuwy.j());
    }
}

Во-первых, он проверяет время и, если условие выполняется, приложение загрузит собственную библиотеку и вызовет fyndmmn ( Hdvhepuwy.j ()); которая является родной функцией. Нам нужно обойти эту проверку, чтобы приложение всегда загружало библиотеку.

Я использовал apktool для разборки apk к smali и изменил j( ), чтобы всегда возвращать true.

  • apktool d com.taxationtex.giristexation.apk
  • cd com.taxationtex.giristexation/smali/com/taxationtext/giristexation/qes
  • edit j()Z in Hdvhepeuwy.smali
Код:
.method public static j()Z
    .locals 1
    const/4 v0, 0x1
    return v0
.end method

измените apk с помощью apktool b com.taxationtex.giristexation -o hydra_time.apk и подпишите его.

Теперь контроль времени всегда возвращает истину после загрузки библиотеки и функции fyndmmn. Даже с этим приложение не загрузит файл DEX.

GDB Debug
Вот отличный пост, объясняющий, как настроить GDB для отладки собственных библиотек. Реализация:

  • Download android sdk with ndk
  • adb push ~android-ndk-r20/prebuilt/android-TARGET-ARCH/gdbserver/gdbserver /data/local/tmp
  • adb shell “chmod 777 /data/local/tmp/gdbserver”
  • adb shell “ls -l /data/local/tmp/gdbserver”
  • get process id, ps -A | grep com.tax
  • /data/local/tmp/gdbserver :1337 –attach $pid
  • adb forward tcp:1337 tcp:1337
  • gdb
  • target remote :1337
  • b Java_com_tax\TAB

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

  • apktool d hydra_time.apk
  • vim hydra_time/com.taxationtex.giristexation/smali/com/taxationtex/giristexation/qes/Sctdsqres.smali
Cледующий блок:

Код:
.line 43
:cond_0

Добавить:

Код:
const-wide/32 v0, 0xea60
invoke-static {v0, v1}, Landroid/os/SystemClock;->sleep(J)V

Так как переменная locals равна 1, и мы используем дополнительную переменную v1, мы увеличиваем её до 2

Код:
.method static H()V
    .locals 2

Снова подпишем и установим приложение. Если все пойдет хорошо, то приложение будет ждать 60 секунд. Сейчас мы можем соединиться с GDB .

Код:
ps | grep com.tax
/data/local/tmp/gdbserver :1337 --attach $pid

Я использую pwndbg для лучшего соединения GDB, вы можете попробовать peda или что-то другое.

  • adb forward tcp:1337 tcp:1337
  • gdb
  • target remote :1337
lxGDBGcEydHxhc0jsvYmLP3-5rCbXBHrBAgoaC5iHp3MlkGjQp0d0_qBdiHHbkwdQtXLeJjwp5-4vQ1eX7ia_vJARA82wPsk0O76Esu7Jp2mBCirCEB6cDakJE55L9D4sm59IHJj


Загрузка всех библиотек занимает некоторое время. Установите breakpoint на нативную функцию fymdmmn

ppHAcqvn6Lkg0qqKz-HwjcpZIlQgZrEUuAAUKScj34j09vrVyY-d6XjvN3MBz6GJmYxp09Ef10dg_5MKjcpApVwHYivpYZqZ3gWMr9hcUeyV5zeGBpCqEjeki1zDIWxW9HYLR1cT


Если вы хотите синхронизировать адреса gdb и ghidra , введите vmmap в gdb и найдите первую запись в libhoter.so .

0xe73be000 0xe73fc000 r-xp 3e000 0 /data/app/com.taxationtex.giristexation-1/lib/x86/libhoter.so

Так 0xe73be000 - мой базовый адрес.

Перейдите в Window-> Memory Map и нажмите значок Home в правом верхнем углу. Поставьте свой базовый адрес и перебазируйте двоичный файл.

Посмотрите на запись нативной функции в ghdira :



-isoqPo8zXwAIqEso8NjldS2zAG9GINP0ziQ6wdb58vB1YW2iqCuyHgUOO37oi7Ad_cWdPBv9ycGtbOuDGMJiTwGDFiMvKXp_dAz2EAZ5k_XeYslg5yHFG1hdmWeFD4TQzNoL4HE


Зачем вызывать функцию времени ? Опять время проверять? Нажмите Ctrl + Shift + F.

return (uint)(curr_time + 0xa3651a74U < 0xd2f00)

Таким образом, мы были правы, снова проверка времени. Переименуйте текущую функцию в check_time . Рассчитаем время “эпохи”:

Код:
>>> 0xffffffff-0xa3651a74+0xd2f00
>>> 1554519179
>>> (1554519179+ 0xa3651a74) & 0xffffffff < 0xd2f00
>>> True

Результат преобразования эпохи во время: суббота, 6 апреля 2019 г. 2:52:59

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

nkLPdUrZ1dhE-xHPB480lFPlYZswywm4QgaW5AumyG40zRXpH-TJ2iuMVWqqq99TqRpXNOwxR0swNF_ii_X0YDmvQbWSZJ_LHVeMKQI5YmCZ7_4HdyttAnZEWjdoOYVbMl6S7Y4o


Мы думаем, что это возвратится, если время не держится.

Первый breakpoint здесь. Мы можем изменить эмулятор/телефон до 5 апреля 2019 года.

b *(base + 0x8ba8)

Но обойти проверку времени недостаточно.



Ghidra Shenanigans
Теперь, погрузившись в двоичный файл, вы найдете несколько таких функций :

lnozV1yoPG9n4Z-5UUsbmkVbmt-f4vncziNdjpiGDvzR3zJsDjtqnvVsnCXdqmKtWK-4J0tBk0goeH3sH8sahdh1sFI76z_uTfMTXMtLvsOZFXPVUDrUgiODrq5icunpYvLq85MI


Если вы посмотрите на цикл while.

yizMbvA3VCIfPvoGrhkJkQTF8qquesBG3dAkZAcdjUKg1WFfr3oOMoB3aM5LQQ4Se4oINw_FM5N8GluFAhBTqDVDlNsQp5s0j0y45iVW_fZMMDsWLkSTMNYbs4lfRI_C6daDileE


2 блока данных XORed . (Длина 0x18) Мы можем поставить breakpoint после do while, но это не будет эффективным решением. Давайте подумаем, программный способ найти расшифрованные строки.

Эти блоки xor находятся рядом друг с другом. Если мы можем получить длину блоков, мы можем легко получить расшифрованную строку. Затем найдите функцию, которая использует эти блоки xor, и переименуйте ее. После этого мы можем перейти на 2 *lenth и получить следующие xor блоки.

Начальный блок xor в 0x34035 .

Получить внешние ссылки блока:



UBf9j2-8idQm8kAhc17HSKWgTU9AZ54GZv4qEkHHObzsc6vy6XzLOT0K6WeCd48QKX8p5T0CiG-3RKaBbK1wW4to2EhEtSnrj2Bh3tLIW8NvIicNqkcZo81Poqf54zYaNrg1HSvt


переходим к функции,

7sQL7YwqLIuApzvBNC2YuoOEK6mhwu6i3zDExZEVyA1RjLYiK9WVfLIQTXMRq9o_c8yBcKils8Pg3lclKd1JF8QbZayrAOAlwuH7y84N-VOBBlsAgiWV5eUU_CCH6aVaqec1VQbR


Получим размер CMP, так как мы знаем адрес первого блока XOR, добавим размер к первому адресу и получим адрес второго исключающего блока. Выполните XOR блоки и переименуйте вызывающую функцию.

Ghidra : go to Window -> Script Manager -> Create New Script -> Python.

Задаём имя для скрипта, и пишем наш собственный скрипт гидра .

import ghidra.app.script.GhidraScript

Код:
import ghidra.app.script.GhidraScript
import exceptions
from ghidra.program.model.address import AddressOutOfBoundsException
from ghidra.program.model.symbol import SourceType

def xor_block(addr,size):

    ## get byte list
    first_block = getBytes(toAddr(addr),size).tolist()
    second_block = getBytes(toAddr(addr+size),size).tolist()

    a = ""
    ## decrypt the block
    for i in range(len(first_block)):
        a += chr(first_block[i]^second_block[i])
        ## each string have trash value at the end, delete it
    trash = len("someval")
    return a[:-trash]
    
def block(addr):
   ## block that related to creation of dex file. pass itt
    if addr == 0x34755:
        return 0x0003494f
    ## get xrefs
    xrefs = getReferencesTo(toAddr(addr))
    if len(xrefs) ==0:
        ## no xrefs go to next byte
        return addr+1
        
    for xref in xrefs:
        ref_addr = xref.getFromAddress()
        try:
            inst = getInstructionAt(ref_addr.add(32))
        except AddressOutOfBoundsException as e:
            print("Found last xor block exiting..")
            exit()
            
    ## Get size of block with inst.getByte(2)
        block_size = inst.getByte(2)
    ## decrypt blocks
        dec_str = xor_block(addr,block_size)
    ## get function
        func = getFunctionBefore(ref_addr)
        new_name = "dec_"+dec_str[:-1]
    ## rename the function
        func.setName(new_name,SourceType.USER_DEFINED)
    ## log
        print("Block : {} , func : {}, dec string : {}".format(hex(addr),func.getEntryPoint(),dec_str))

    return addr+2*block_size

def extract_encrypted_str():

    ## starting block
    curr_block_location = 0x34035
    for i in range(200):
        curr_block_location = block(curr_block_location)

def run():
    extract_encrypted_str()

run()

Чтобы запустить скрипт, выберите созданный скрипт в диспетчере скриптов и нажмите «Выполнить».

Теперь посмотрим на вывод.

m0Obd8q_0WDqB_ZRRnS9gENg_ZCZGr0pMREkdYII5-4q4DquUDGohAnDn_T9ZgM4OEZk3CdURDeNyC2tNe_xzMJMJ2jeW0yAgq1CbJwhVTwkZcly1pCyRE11-dYAcu1prMF8_kjj


Как видите, есть функции: getSimCountryISO , getNetworkCountryIso , getCountry и одна подозрительная строка: tr . Без выполнения мы можем предположить, что код проверит , равны ли возвращаемые значения этой функции tr . Я знаю, что это приложение предназначено для турецких людей, поэтому разумно избегать песочницы и даже ручного анализа.

Если вы выполните из внешних ссылок этих функций функцию FUN_ 00018A90 ( ) ( вызывается после проверки времени), вы можете увидеть этот блок:

MrKh1RtFz7O71FplSVw4Ftzwq_CSY7Iy1KlU7HAgTMP7RHZAQAJfbAJ5xaJEBG1Rw0VjSkqUSuRa_u5t-K5v1UBPVkXSYxPcqIEAMSY8W3XXlua41vTdv_kX174jDduSMGMjUJAL


Итак, следующий патч / breakpoint - это проверка:

b *(base + 0x8c80)

После этих проверок код сбросит dex и загрузит его. Если вы работаете без патча / breakpoints, отображается только страница edevlet, и ничего не происходит. Этот код получит ваш базовый адрес а также попытается проверить:

Код:
b *(base + 0x8ba8)
b *(base + 0x8c80)
copy eip : .... a8 -> set $eip = .... aa
c
copy eip : .... 80 -> set $eip = .... 82
c

После этих breakpoints приложение создаст файл dex и загрузит его. Вы увидите всплывающую страницу, если вы все сделаете правильно.

BrM5pCDfisF9cj1GSjJvX1YcUX5QomXj_rrtXnu-KMqAPcB58rfi-S2bMTc-Z69G-HfgvGi1bUiZhEqIsWzIUQd1bpB9j6gA9nBTrRnuDnWcYCmtbSEv-h7VBQz1dNDrsa9k0WT9


Или мы можем исправить JE инструкции JNE в родной библиотеке и снова собрать APK.

Понимание создания dex файла
Если вы ищете пропущенный файл в файловой системе, вы ничего не найдете. Файл удален с помощью remove. Мы можем прикрепить frida и поймать легко удаленный файл. Но пока забудьте об этом и узнайте, как png- файл используется для создания dex-файла.

Посмотрите на вывод Гидры:

wGV2-D-MVWzbQ58lZgwaKGBR70gfiY8-rs5L8fmQ2hHW6J3QHjO28O9ngbmOQHMUu_i-osTl7XvAb3EJ-ymmNzvOtUoTyU6CChrY3NJbGfXgpjnOFjhDoGrycqrjqKt_CL9IPhhV


prcnbzqn.png обрабатывается с помощью AndroidBitmap, а файл dex создается с именем xwchfc.dex. Тогда с помощью ClassLoader API Загружается dex-файл и вызывается класс moonlight.loader.sd k.SdkBuilder .

Проверьте функцию: 0xeec0



M5wL5Wlxx0SlIBt7CdK01kcyjK7t3lrONDIgq3e2zqfkXtgg6PoSKfZcCwNkyX0s2MBjh6EoP9VF5l02f54yeFYKeWqQSN_3gL9M3ELni2i_L5zDJtngvgPAKsVmP6pcxtY3ZDYx

Он перебирает ресурсы и находит png файл. Хорошо. Переименуйте эту функцию asset_caller . Перейдите к ссылкам на эту функцию и найдите 0xe2c0 . Я переименовал некоторые функции. dex_header создает файл dex в памяти. dex_dropper, сбрасывает файл dex в систему и загружает его.

FdH4NpL1eH5N_3esIVE6jZArW_S6O9r-oSYL0DR7PjUnrjg8Jt_pJkQotcks8KdyOsaKVclNPedhYyTT6MHMkv1wAhvqEPQFC_lluiuv3ilI3nTZbiGKlaXeNVNoy-4tFSY0ee0s


Как dex_header создает dex файл ? Перейдём к определению функции.

ZiNswuT32Tk3S0-YOBoI2p8xqiicHGuSdCx43UdjgVDAUeDix8WvVaps25wAI0HnoXbBu2JkYbGN3HiRSCcQVHoGetTDsxl11m2tL0HAdI-UX9eVDmC3b8B4Zxh8ehgoylIrqbnN

bitmap_related создает растровое изображение из файла png . Растровый объект передается в функцию dex_related.Битовая карта?

Если вы просматриваете байты png файла, и не получаете цветовые коды пикселей напрямую. Вам необходимо преобразовать его в растровое изображение. Поэтому приложение сначала преобразует png- файл в растровое изображение и читает шестнадцатеричные значения пикселей.
XWsvi-aJ96yT0uzeBImk2m0bUwSJHmZ5_grVMJk_E7_-JnlZv7494ieCca_-lFuyw9Rjb3A7qwkWaFFouzKe-40o1qjqQsU76MsX7IBJbTO1Z-FM1WFwz2qYCjh0D-V1VX6NmpDq


Теперь самое интересное. Как эти значения используются. В 0xfbf0 вы можете найти функцию dex_related .

Растровый объект передается этой функции. Теперь здесь есть 2 важные функции:



qvCp4vFSkiyMIJk8vQXWFVouwwsFeyVLrOwfjk_wnpSUmH8vTyB5P7bj5ewbwdy0Mimx0s61_y2hhjfQ8xEuc9pEg57oUSZStElbTfJZq7j5dIPA-2uOEgKv5h2PD4iXlEdgdJ3h


byte_chooser возвращает один байт, а dex_extractor будет использовать этот байт для получения окончательных байтов dex. Переменная 4_cmp установлена на 0 сначала и будет установлена на 0 в конце блока else. Таким образом, поток войдет в byte_chooser 2 раза, прежде чем войти в dex_extractor . Вот byte_chooser

vaqSqByu_DDou2LATMlWzPbtaq-RKiwsgbs5PjUZXZlItLXzoWQ4z_qHMBxfG8VizEm1XCbrhdGsrh_CxRQk6UMGotN1cm6bXnlh2fJq8JmxQEp01WkWXX67uK5LmmLPJl_x_MOe

param_3 - это шестнадцатеричные коды пикселей.

param_2 будет возвращать значение первого вызова и будет сдвинут влево на 4 . Затем он устанавливается на 0 в конце блока else.

После вычисления байта путем двойного вызова byte_chooser, возвращаемое значение передается в dex_extractor .

twzMnxnDhpaa7TcZusTGrg8h9auGSE1a_2MDyTlBTpVbziRAthdQX7oHnQCA6Nxg3bboIFh2hQVKdWHmY3LBzearlpe-AX0IcVeiujbd6ho8PDGThrFm2OEdIIStJ1C42ZqEsek4


param_2 вычисляется байтом, а param_1 является индексом.

Теперь мы знаем, как создается файл dex . Давайте сделаем это с помощью Python

Код:
from PIL import Image
import struct

image_file = "prcnbzqn.png"
so_file = "libhoter.so"
offset = 0x34755
size = 0x1fa
output_file = "drop.dex"


im = Image.open(image_file)
rgb_im = im.convert('RGB')
im_y = im.size[1]
im_x = im.size[0]

dex_size = im_y*im_x/2-255

f = open(so_file)
d = f.read()
d = d[offset:offset+size]

def create_magic(p1,p2,p3):
    return (p1<<2 &4 | p2 & 2 | p2 & 1 | p1 << 2 & 8 | p3)

def dex_extractor(p1,p2):
    return (p1/size)*size&0xffffff00| ord(d[p1%size]) ^ p2

count = 0
dex_file = open(output_file,"wb")
second = False
magic_byte = 0
for y in range(0,im.size[1]):
    for x in range(0,im.size[0]):
        r, g, b = rgb_im.getpixel((x, y))
        magic_byte = create_magic(r,b,magic_byte)
        if second:
            magic_byte = magic_byte & 0xff
            dex_byte = dex_extractor(count,magic_byte)
            dex_byte = dex_byte &0xff
            if count > 7 and count-8 < dex_size:
                dex_file.write(struct.pack("B",dex_byte))
            magic_byte = 0
            second = False
            count+=1
        else:
            magic_byte = magic_byte << 4
            second = True

dex_file.close()


Давайте посмотрим на выходной файл с JADX

EXFEfbcQlhPOCKDtZsM7tReYWG9dmsr0GFBVrQYx2dacQmg7l9X3523lwp7pjqgvEonsUfe1m2QeUR4LZW-5h-6-8kkVAvfwQiC2Qk0AmIVxK5472scy57jXHtuOYe1bAdb8jOCg


Frida <3
Ну, я не могу написать статью, не упоминая Frida . Bypass проверки с Frida:

  • There are time checks on java and native side.
  • Country check
  • File is removed at native side.
Код:
var unlinkPtr = Module.findExportByName(null, 'unlink');
// remove bypass
Interceptor.replace(unlinkPtr, new NativeCallback( function (a){
     console.log("[+] Unlink : " +  Memory.readUtf8String(ptr(a)))

}, 'int', ['pointer']));

var timePtr = Module.findExportByName(null, 'time');
// time bypass
Interceptor.replace(timePtr, new NativeCallback( function (){
    console.log("[+] native time bypass : ")
    return 1554519179
},'long', ['long']));

Java.perform(function() {
    var f = Java.use("android.telephony.TelephonyManager")
    var t = Java.use('java.util.Date')
    //country bypass
    f.getSimCountryIso.overload().implementation = function(){
        console.log("Changing country from " + this.getSimCountryIso() + " to tr ")
        return "tr"
    }
    t.getTime.implementation = function(){
    console.log("[+] Java date bypass ")
    return 1554519179000
    }
 })

vrDcjteYsxmvTr-VxakckC_-ySCNR68ONQ4aea3VX1RPrtkNVW0u8TUHZigrXqzmGiwBh8xo_EuwjpcUcIuBbY8YJlIvXz1vEwh5_MPSXaoaXyo2mNBiUj019_Arupvw61dkXujQ


Извлеките файл dex с помощью adb pull path/xwcnhfc.dex.

Заключение
Мы подключили gdb для отладки нативного кода и нашли определенные проверки. Написали скрипт ghidra для автоматизации дешифрования строк и скрипт frida для обхода проверок. Также стало известно, что PNG- файлы необходимо конвертировать с помощью Bitmap, чтобы получить значения пикселей. Поэтому в следующий раз, когда вы увидите png- файл и подозрительное приложение, ищите растровые вызовы.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх