安卓恶意软件分析: 剖析 Hydra Dropper

2019-08-21 20:08:569616人阅读

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 文件。


GDB 调试


这有一篇很棒的文章,解释了如何设置 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 

1566004683238715.png

debug session 调试会话

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

1566004691345085.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 中显示本地函数:

1566004704305502.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。

 1566004755827299.png

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

b *(base + 0x8ba8)

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


Ghidra 的诡计


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

1566004783643926.png

 解密过程的代码块

仔细分析 while 循环:

1566004790603489.png

 异或操作循环

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

1566004809341133.png

异或操作数据块的过程

进入函数里面

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

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

1566004866882435.png

 ghidra 脚本的输出

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

 1566004871803900.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 文件并加载这些文件。 如果你操作正确的话,你会看到弹出了无障碍助手页面。 1566004900594224.png

绕过检查

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


理解 dex 文件的创建过程


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

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

1566004915875973.png

 ghidra 脚本的输出结果

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

检查函数: 0xee0

1566004924922314.png

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

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

1566004932646647.png

  函数调用层次

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

1566004938499589.png

 dex 创建函数

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

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

1566004947360908.png

 像素的 rgb 值

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

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

1566004952568465.png

 两个重要的函数

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

1566004959422488 (1).png

 字节选择函数

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

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

1566004959422488 (1).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 的输出文件:

1566005007754667.png

 删除 dex 文件的代码

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


Frida


好吧,我写这篇文章就不能不提到 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 
    }
 })

1566005045125993.png

 Frida 会话的输出内容

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

adb pull path/xwcnhfc.dex


总结

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

参考资料

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


本文翻译自:https://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/

原文链接:https://www.4hou.com/mobile/19755.html

翻译作者:李白


0
现金券
0
兑换券
立即领取
领取成功