Пожалуйста, обратите внимание, что пользователь заблокирован
Hydra - это еще один вариант android bankbot. Она использует оверлей для кражи информации, такой как Anubis . Его название происходит от команды и панели управления . В период с июля 2018 года по март 2019 года в Google Play Store оставалось как минимум 8-10 образцов. Распространение вредоносного ПО аналогично случаям Anubis. Приложения Dropper добавляются в Play Store. Но, в отличие от Anubis, приложения Dropper извлекают dex- файл из png- файла с помощью своего рода стенографии и загружают вредоносное приложение с управляющего сервера dex . Вы можете найти образец, который я рассмотрю в этом посте здесь: клик
ToC :
Проверка времени
Когда мы открываем первое приложение с помощью jadx, мы видим проверку времени в классе
Эта функция вызывается в другом классе:
Во-первых, он проверяет время и, если условие выполняется, приложение загрузит собственную библиотеку и вызовет
Я использовал
измените apk с помощью
Теперь контроль времени всегда возвращает истину после загрузки библиотеки и функции
GDB Debug
Вот отличный пост, объясняющий, как настроить GDB для отладки собственных библиотек. Реализация:
Здесь есть небольшая проблема. Приложение загрузит библиотеку, вызовет встроенную функцию и завершит работу. Приложение должно ждать подключения к GDB. Моей первой мыслью было поспать, а затем соединиться с GDB .
Добавить:
Так как переменная locals равна 1, и мы используем дополнительную переменную v1, мы увеличиваем её до 2
Снова подпишем и установим приложение. Если все пойдет хорошо, то приложение будет ждать 60 секунд. Сейчас мы можем соединиться с GDB .
Я использую pwndbg для лучшего соединения GDB, вы можете попробовать peda или что-то другое.
Загрузка всех библиотек занимает некоторое время. Установите breakpoint на нативную функцию
Если вы хотите синхронизировать адреса gdb и ghidra , введите vmmap в gdb и найдите первую запись в
Так
Перейдите в
Посмотрите на запись нативной функции в ghdira :
Зачем вызывать функцию времени ? Опять время проверять? Нажмите
Таким образом, мы были правы, снова проверка времени. Переименуйте текущую функцию в check_time . Рассчитаем время “эпохи”:
Результат преобразования эпохи во время: суббота, 6 апреля 2019 г. 2:52:59
Да, это время, когда приложение было в магазине. Проверьте, как используется этот логический тип . Посмотрите на функцию
Мы думаем, что это возвратится, если время не держится.
Первый breakpoint здесь. Мы можем изменить эмулятор/телефон до 5 апреля 2019 года.
Но обойти проверку времени недостаточно.
Ghidra Shenanigans
Теперь, погрузившись в двоичный файл, вы найдете несколько таких функций :
Если вы посмотрите на цикл while.
2 блока данных XORed . (Длина 0x18) Мы можем поставить breakpoint после do while, но это не будет эффективным решением. Давайте подумаем, программный способ найти расшифрованные строки.
Эти блоки xor находятся рядом друг с другом. Если мы можем получить длину блоков, мы можем легко получить расшифрованную строку. Затем найдите функцию, которая использует эти блоки xor, и переименуйте ее. После этого мы можем перейти на
Начальный блок xor в
Получить внешние ссылки блока:
переходим к функции,
Получим размер CMP, так как мы знаем адрес первого блока XOR, добавим размер к первому адресу и получим адрес второго исключающего блока. Выполните XOR блоки и переименуйте вызывающую функцию.
Ghidra : go to
Задаём имя для скрипта, и пишем наш собственный скрипт гидра .
import ghidra.app.script.GhidraScript
Чтобы запустить скрипт, выберите созданный скрипт в диспетчере скриптов и нажмите «Выполнить».
Теперь посмотрим на вывод.
Как видите, есть функции: getSimCountryISO , getNetworkCountryIso , getCountry и одна подозрительная строка: tr . Без выполнения мы можем предположить, что код проверит , равны ли возвращаемые значения этой функции tr . Я знаю, что это приложение предназначено для турецких людей, поэтому разумно избегать песочницы и даже ручного анализа.
Если вы выполните из внешних ссылок этих функций функцию FUN_ 00018A90 ( ) ( вызывается после проверки времени), вы можете увидеть этот блок:
Итак, следующий патч / breakpoint - это проверка:
После этих проверок код сбросит dex и загрузит его. Если вы работаете без патча / breakpoints, отображается только страница edevlet, и ничего не происходит. Этот код получит ваш базовый адрес а также попытается проверить:
После этих breakpoints приложение создаст файл dex и загрузит его. Вы увидите всплывающую страницу, если вы все сделаете правильно.
Или мы можем исправить JE инструкции JNE в родной библиотеке и снова собрать APK.
Понимание создания dex файла
Если вы ищете пропущенный файл в файловой системе, вы ничего не найдете. Файл удален с помощью remove. Мы можем прикрепить frida и поймать легко удаленный файл. Но пока забудьте об этом и узнайте, как png- файл используется для создания dex-файла.
Посмотрите на вывод Гидры:
prcnbzqn.png обрабатывается с помощью AndroidBitmap, а файл dex создается с именем xwchfc.dex. Тогда с помощью ClassLoader API Загружается dex-файл и вызывается класс moonlight.loader.sd k.SdkBuilder .
Проверьте функцию: 0xeec0
Он перебирает ресурсы и находит png файл. Хорошо. Переименуйте эту функцию asset_caller . Перейдите к ссылкам на эту функцию и найдите 0xe2c0 . Я переименовал некоторые функции. dex_header создает файл dex в памяти. dex_dropper, сбрасывает файл dex в систему и загружает его.
Как dex_header создает dex файл ? Перейдём к определению функции.
bitmap_related создает растровое изображение из файла png . Растровый объект передается в функцию dex_related.Битовая карта?
Если вы просматриваете байты png файла, и не получаете цветовые коды пикселей напрямую. Вам необходимо преобразовать его в растровое изображение. Поэтому приложение сначала преобразует png- файл в растровое изображение и читает шестнадцатеричные значения пикселей.
Теперь самое интересное. Как эти значения используются. В 0xfbf0 вы можете найти функцию dex_related .
Растровый объект передается этой функции. Теперь здесь есть 2 важные функции:
byte_chooser возвращает один байт, а dex_extractor будет использовать этот байт для получения окончательных байтов dex. Переменная 4_cmp установлена на 0 сначала и будет установлена на 0 в конце блока else. Таким образом, поток войдет в byte_chooser 2 раза, прежде чем войти в dex_extractor . Вот byte_chooser
param_3 - это шестнадцатеричные коды пикселей.
param_2 будет возвращать значение первого вызова и будет сдвинут влево на 4 . Затем он устанавливается на 0 в конце блока else.
После вычисления байта путем двойного вызова byte_chooser, возвращаемое значение передается в dex_extractor .
param_2 вычисляется байтом, а param_1 является индексом.
Теперь мы знаем, как создается файл dex . Давайте сделаем это с помощью Python
Давайте посмотрим на выходной файл с JADX
Frida <3
Ну, я не могу написать статью, не упоминая Frida . Bypass проверки с Frida:
Извлеките файл dex с помощью adb pull path/xwcnhfc.dex.
Заключение
Мы подключили gdb для отладки нативного кода и нашли определенные проверки. Написали скрипт ghidra для автоматизации дешифрования строк и скрипт frida для обхода проверок. Также стало известно, что PNG- файлы необходимо конвертировать с помощью Bitmap, чтобы получить значения пикселей. Поэтому в следующий раз, когда вы увидите png- файл и подозрительное приложение, ищите растровые вызовы.
ToC :
- Bypass checks that on the java side
- GDB Debug
- Ghidra shenanigans
- Understanding creation of the dex file
- Bonus
Проверка времени
Когда мы открываем первое приложение с помощью 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
Код:
.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
Загрузка всех библиотек занимает некоторое время. Установите breakpoint на нативную функцию
fymdmmn
Если вы хотите синхронизировать адреса 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 :
Зачем вызывать функцию времени ? Опять время проверять? Нажмите
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.
Мы думаем, что это возвратится, если время не держится.
Первый breakpoint здесь. Мы можем изменить эмулятор/телефон до 5 апреля 2019 года.
b *(base + 0x8ba8)Но обойти проверку времени недостаточно.
Ghidra Shenanigans
Теперь, погрузившись в двоичный файл, вы найдете несколько таких функций :
Если вы посмотрите на цикл while.
2 блока данных XORed . (Длина 0x18) Мы можем поставить breakpoint после do while, но это не будет эффективным решением. Давайте подумаем, программный способ найти расшифрованные строки.
Эти блоки xor находятся рядом друг с другом. Если мы можем получить длину блоков, мы можем легко получить расшифрованную строку. Затем найдите функцию, которая использует эти блоки xor, и переименуйте ее. После этого мы можем перейти на
2 *lenth и получить следующие xor блоки.Начальный блок xor в
0x34035 .Получить внешние ссылки блока:
переходим к функции,
Получим размер 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()
Чтобы запустить скрипт, выберите созданный скрипт в диспетчере скриптов и нажмите «Выполнить».
Теперь посмотрим на вывод.
Как видите, есть функции: getSimCountryISO , getNetworkCountryIso , getCountry и одна подозрительная строка: tr . Без выполнения мы можем предположить, что код проверит , равны ли возвращаемые значения этой функции tr . Я знаю, что это приложение предназначено для турецких людей, поэтому разумно избегать песочницы и даже ручного анализа.
Если вы выполните из внешних ссылок этих функций функцию FUN_ 00018A90 ( ) ( вызывается после проверки времени), вы можете увидеть этот блок:
Итак, следующий патч / 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 и загрузит его. Вы увидите всплывающую страницу, если вы все сделаете правильно.
Или мы можем исправить JE инструкции JNE в родной библиотеке и снова собрать APK.
Понимание создания dex файла
Если вы ищете пропущенный файл в файловой системе, вы ничего не найдете. Файл удален с помощью remove. Мы можем прикрепить frida и поймать легко удаленный файл. Но пока забудьте об этом и узнайте, как png- файл используется для создания dex-файла.
Посмотрите на вывод Гидры:
prcnbzqn.png обрабатывается с помощью AndroidBitmap, а файл dex создается с именем xwchfc.dex. Тогда с помощью ClassLoader API Загружается dex-файл и вызывается класс moonlight.loader.sd k.SdkBuilder .
Проверьте функцию: 0xeec0
Он перебирает ресурсы и находит png файл. Хорошо. Переименуйте эту функцию asset_caller . Перейдите к ссылкам на эту функцию и найдите 0xe2c0 . Я переименовал некоторые функции. dex_header создает файл dex в памяти. dex_dropper, сбрасывает файл dex в систему и загружает его.
Как dex_header создает dex файл ? Перейдём к определению функции.
bitmap_related создает растровое изображение из файла png . Растровый объект передается в функцию dex_related.Битовая карта?
Если вы просматриваете байты png файла, и не получаете цветовые коды пикселей напрямую. Вам необходимо преобразовать его в растровое изображение. Поэтому приложение сначала преобразует png- файл в растровое изображение и читает шестнадцатеричные значения пикселей.
Теперь самое интересное. Как эти значения используются. В 0xfbf0 вы можете найти функцию dex_related .
Растровый объект передается этой функции. Теперь здесь есть 2 важные функции:
byte_chooser возвращает один байт, а dex_extractor будет использовать этот байт для получения окончательных байтов dex. Переменная 4_cmp установлена на 0 сначала и будет установлена на 0 в конце блока else. Таким образом, поток войдет в byte_chooser 2 раза, прежде чем войти в dex_extractor . Вот byte_chooser
param_3 - это шестнадцатеричные коды пикселей.
param_2 будет возвращать значение первого вызова и будет сдвинут влево на 4 . Затем он устанавливается на 0 в конце блока else.
После вычисления байта путем двойного вызова byte_chooser, возвращаемое значение передается в dex_extractor .
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
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
}
})
Извлеките файл dex с помощью adb pull path/xwcnhfc.dex.
Заключение
Мы подключили gdb для отладки нативного кода и нашли определенные проверки. Написали скрипт ghidra для автоматизации дешифрования строк и скрипт frida для обхода проверок. Также стало известно, что PNG- файлы необходимо конвертировать с помощью Bitmap, чтобы получить значения пикселей. Поэтому в следующий раз, когда вы увидите png- файл и подозрительное приложение, ищите растровые вызовы.