bitmap xml大小 安卓_安卓恶意软件分析: 剖析 Hydra Dropper

22ce2309dd9af034c3418690bf88f467.gif

Hydra 是另一个针对银行的安卓木马变种。它使用“覆盖”手段来窃取信息,这种手法与阿努比斯(Anubis)很像。它的名字来源于命令和控制面板。从2018年7月到2019年3月,谷歌官方应用商店上至少有8到10个这种样本。恶意软件的分布类似于阿努比斯,Dropper 恶意应用程序也会上传到谷歌应用商店。但是与 阿努比斯 不同的是,Dropper 的应用程序通过 kinda 速记从 png 文件中提取 dex 文件,并通过这些 dex 文件从命令和控制服务器中下载恶意应用程序。你可以在这个链接里找到我将要介绍的例子: Dropper

本次分析的目标是:

· 在 Java 端绕过检查

· GDB 调试

· Ghidra 的诡计

· 理解 dex 文件的创建过程

· 额外的奖励

首先,如果Dropper应用程序运行在合适的环境中,那么它会加载 dex 文件并连接到命令和控制服务器。它在 java 端和 native 端做了多种检查。我们将使用 gdb 调试 native 端,并使用 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

现在时间检查的控制条件总是返回 true,之后会加载本地库并调用 fyndmmn 本地函数。即使我们这样做了,应用程序仍然不会加载 dex 文件。

fdc0040d6666190243cc2ab9ffdb8fb6.pngGDB 调试

这有一篇很棒的文章,解释了如何设置 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 的连接。我的第一个想法是添加 sleep,然后连接到 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

7c528174f6ac481cda184a6a0927ccd8.png

debug session 调试会话

加载所有的库需要一些时间。将断点设置在本地函数 fymdmmn 上。

3203d138a03d76723dcb3d7c9b5299b8.png

设置断点

如果希望同步 gdb 和 ghidra 地址,请在 gdb 中输入 vmmap 并查找 libhoter.so 的第一个条目。

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

所以 0xe73be000 是我的基址。

转到窗口->内存映射并点击右上角的主页图标。把你的基地址输进去然后查询构建二进制陈谷。

看看 ghdira 中显示本地函数:

4a5dcf74a3caceee4c69e0c6384efed5.png

fyndmmn 函数

为什么要调用 time 函数?难道又是时间检查?重命名 time 函数的返回值(curr_time) ,然后按 ctrl + shift + f 从汇编视图转到上下文为 READ 的位置。

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

所以我们的猜想是对的,这里还是在做时间检查。将当前函数重命名为check_time。计算时间:

>>> 0xffffffff-0xa3651a74+0xd2f00

>>> 1554519179

>>> (1554519179+ 0xa3651a74) & 0xffffffff < 0xd2f00

>>> True

转换为时间后是: Saturday, April 6, 2019 2:52:59 AM,这是应用程序上传到应用商店的时间。检查如何使用这个布尔值。查找函数 check_time 的 xrefs。

2d311363b29d50125015178c79a9a745.png

正如我们之前所想的那样,如果时间不够,程序就会退出。第一个断点或二进制补丁点就在这里。或者我们可以将模拟器或手机的时间更改为2019年4月5日。

b *(base + 0x8ba8)

但是只绕过时间检查是不够的。

fdc0040d6666190243cc2ab9ffdb8fb6.pngGhidra 的诡计

现在进入二进制文件分析阶段,你会发现类似于下面这样的多个函数:

cba3aa12bca9e8ce12def9c513e4cc0f.png

解密过程的代码块

仔细分析 while 循环:

4461d91441451f47ff382cf46bc4a5b9.png

异或操作循环

有两个数据块被执行了异或(XOR)操作。(长度是 0x18)我们可以把断点放在 do while 语句之后,但这不是有效的解决方案。让我们考虑一种编程方式来查找已解密的字符串。这些被异或的数据块彼此相邻。如果我们可以得到数据块的长度,我们可以很容易地得到解密字符串。然后找到使用这些异或数据块的函数并将函数重命名。然后,我们可以跳到 2*length,得到下一个被执行异或操作的数据块。重复这个过程。开始执行异或操作的数据块是0x34035。获取该数据块的 xrefs:

eb80d22c911e10218619459c8a1eb73c.png

异或操作数据块的过程

进入函数里面

33aad557de363342d7b4d46bc9a463b2.png

获取 cmp 的值

从 CMP 指令中获取大小,因为我们知道第一个 异或数据块的地址,所以将大小添加到第一个地址并获得第二个异或数据块的地址。对数据块执行异或操作并重命名调用函数。

Ghidra: 转到窗口->脚本管理器->创建新的脚本->Python。为脚本设置名称,现在让我们编写 ghidra 脚本。

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()

要运行我们编写的脚本,请在脚本管理器中选择已创建的脚本,然后点击“运行”。现在让我们看看脚本的输出。

e0a5bff46f8114207f86da64ee441112.png

ghidra 脚本的输出

你可以看到这些函数: getSimCountryISO,getNetworkCountryIso,getCountry 和一个可疑的字符串: tr。如果不运行脚本,我们可以假设代码将检查这些函数的返回值是否等于 tr。因为我已经知道这个应用程序的攻击目标是土耳其人,所以这个结果是合理的,目的是用来避免沙盒,甚至是手动分析。如果你跟随这些函数的 xrefs 跳到函数 FUN_00018A90()(在时间检查函数之后) ,你可以看到如下代码:

0bc62e698f27b7abf3d1580774c8e23f.png

对国家进行检查

因此,下一个补丁或断点是这样的检查:

b *(base + 0x8c80)

在这些检查之后,代码将删除 dex 并加载它。如果不使用补丁或断点运行,则只显示 edevlet 页面,不会发生任何事情。获取你的基址并尝试绕过检查:

b *(base + 0x8ba8)

b *(base + 0x8c80)

copy eip : .... a8 -> set $eip = .... aa

c

copy eip : .... 80 -> set $eip = .... 82

c

在这些断点之后,应用程序将创建 dex 文件并加载这些文件。如果你操作正确的话,你会看到弹出了无障碍助手页面。

640?wx_fmt=png

绕过检查

或者我们可以将 je 指令补丁到本地库中的 jne,然后再次构建 apk。

fdc0040d6666190243cc2ab9ffdb8fb6.png理解dex文件的创建过程

如果在文件系统中查找该恶意程序创建的文件,你不会看到任何内容。因为文件已经被删除。我们可以很容易地通过 frida 调试分析并得到创建的文件。但是请暂时忘记这件事器,现在我们需要找出这个恶意程序是如何使用 png 文件创建了 dex 文件。

查看 ghidra 脚本输出内容的最后那一部分。

291c2e2a7af4b0d043e1a2da398b154a.png

ghidra 脚本的输出结果

使用 AndroidBitmap 处理 prcnbzqn.png,然后创建了名为 xwchfc.dex 的 dex 文件。然后使用 ClassLoader API 加载 dex 文件,之后调用了类 moonlight.loader.sdk.SdkBuilder。

检查函数: 0xee0

fe5a63c14a79e73cd3c5fca2d25cd8a1.png

从 asset(资产) 文件夹中获取 png 文件

资产文件夹并查找 png 文件。将此函数重命名为asset_caller。访问这个函数的 xref,找到0xe2c0。我重命名了一些函数的名称。dex_header 在内存中创建 dex 文件。dex_dropper 把 dex 文件放到系统中,然后加载。

b54303b161e6bc6af818c71218cd79ed.png

 函数调用层次

dex_header是如何创建 dex 文件的呢? 我们转到函数定义看看。

2b9af0e5a7b0ec7c4e1c8476a06e4181.png

dex 创建函数

bitmap_related函数从 png 文件创建位图。位图对象传递给到 dex_related函数。这里为什么是位图呢?让我们继续往下看。

如果你读取了 png 文件字节,你不能直接得到像素的颜色代码。你需要将其转换为位图。所以应用程序首先传输 png 文件到位图,读取像素的十六进制值。启动 gimp或者paint程序,查看图像第一个像素的十六进制代码,并与下面的图片进行比较:

3ca8ed9654c424fa3d4845deb642dd6b.png

像素的 rgb 值

现在到了有趣的部分。如何使用这些值。在 0xfbf0 处你可以找到dex_related函数。

位图对象被传递给这个函数,现在这里有两个重要的函数:

f5c52c02e1e76c23998a7d4400c451e2.png

两个重要的函数

byte_chooser将返回一个字节, dex_extractor将使用该字节获得最后的 dex 字节。4_cmp 变量在开始时设置为0,在 else 代码块结束时设置为0。所以程序执行流将命中 byte_chooser 2次之前进入 dex_extractor函数。下面是byte_chooser函数的代码:

91f34c97c2d2529acc001f0a994114ea.png

字节选择函数

param_3是像素的十六进制代码。param_2就像一个种子变量。如果它第一次调用byte_chooser时被设置为0,在字节选择器的第二次调用中, param_2 会返回第一次调用的值并左移4位。然后在 else 代码块的末尾将其设置为0。

通过两次调用字节选择器计算字节后,返回值传递给 dex_extractor 函数。

3544ac8de79f4273df7434d9fc8f3943.png

dex字节计算器函数

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 的输出文件:

9c48345e5def7258a28503cc3efd248d.png

删除 dex 文件的代码

还记得 ghidra 脚本输出中的内容吗? 通过对比后可以发现输出是正确的。

fdc0040d6666190243cc2ab9ffdb8fb6.pngFrida

好吧,我写这篇文章就不能不提到 frida。

· 在 Java 端和本地端都有时间检查

· 国家检查

· 文件在本地端被删除

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 

    }

 })

079d9cfb2da8bd1dc9bfaffd1f6eea00.png

Frida 会话的输出内容

使用下面的命令将 dex 文件拖到本地:

adb pull path/xwcnhfc.dex

7a386da301094b9069bb331b554bc26a.png家庭作业

这部分是我为读者布置的家庭作业,这个恶意软件的下一个版本只使用本地 ARM 版的二进制文件。因此,如果没有基于 ARM 的设备,我们很难进行调试。但是我们可以使用我们的 dex dropper python 脚本。恶意软件样本可以在这里找到。把 ARM 二进制文件加载到 ghidra。查找 dex 数据块的正确偏移量和块的大小。dex_extractor 函数可能看起来不太一样,但它的作用是一样的。因此,你只需要更改 python 脚本中的文件名、偏移量和大小变量即可。7ff02fb46009fc96c139c48c28fb61904cc3de60482663631272396c6c6c32ec

fdc0040d6666190243cc2ab9ffdb8fb6.png总结

我们附加 gdb 来调试本地端代码并发现某些检查。然后我们编写了一个 ghidra 脚本来自动解密字符串并使用 frida 脚本来绕过检查。通过分析,我们还发现,png 文件需要与 Bitmap 一起转换,以获得像素值。因此,下次你看到 png 文件和可疑的应用程序时,可以尝试寻找关于位图操作的调用。

fdc0040d6666190243cc2ab9ffdb8fb6.png参考资料

GDB 调试 : https://packmad.github.io/gdb-android/
图片来源 : https://www.deviantart.com/velinov/art/Hydra-monster-144496963

7250edfd58a440f495cfa6ed3aa5a2ed.png

79199a3889fb2af0a449f61830d4c3f6.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值