2019-08-21 20:08:569616人阅读
本次分析的目标是:
· 在 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
debug session 调试会话
加载所有的库需要一些时间。 将断点设置在本地函数 fymdmmn 上。
设置断点
如果希望同步 gdb 和 ghidra 地址,请在 gdb 中输入 vmmap 并查找 libhoter.so 的第一个条目。
0xe73be000 0xe73fc000 r-xp 3e000 0 /data/app/com.taxationtex.giristexation-1/lib/x86/libhoter.so
所以 0xe73be000 是我的基址。
转到窗口->内存映射并点击右上角的主页图标。 把你的基地址输进去然后查询构建二进制陈谷。
看看 ghdira 中显示本地函数:
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。
正如我们之前所想的那样,如果时间不够,程序就会退出。 第一个断点或二进制补丁点就在这里。 或者我们可以将模拟器或手机的时间更改为2019年4月5日。
b *(base + 0x8ba8)
但是只绕过时间检查是不够的。
Ghidra 的诡计
现在进入二进制文件分析阶段,你会发现类似于下面这样的多个函数:
解密过程的代码块
仔细分析 while 循环:
异或操作循环
有两个数据块被执行了异或(XOR)操作。 (长度是 0x18)我们可以把断点放在 do while 语句之后,但这不是有效的解决方案。 让我们考虑一种编程方式来查找已解密的字符串。 这些被异或的数据块彼此相邻。 如果我们可以得到数据块的长度,我们可以很容易地得到解密字符串。 然后找到使用这些异或数据块的函数并将函数重命名。 然后,我们可以跳到 2*length,得到下一个被执行异或操作的数据块。 重复这个过程。 开始执行异或操作的数据块是0x34035。 获取该数据块的 xrefs:
异或操作数据块的过程
进入函数里面
获取 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()
要运行我们编写的脚本,请在脚本管理器中选择已创建的脚本,然后点击“运行”。 现在让我们看看脚本的输出。
ghidra 脚本的输出
你可以看到这些函数: getSimCountryISO,getNetworkCountryIso,getCountry 和一个可疑的字符串: tr。 如果不运行脚本,我们可以假设代码将检查这些函数的返回值是否等于 tr。因为我已经知道这个应用程序的攻击目标是土耳其人,所以这个结果是合理的,目的是用来避免沙盒,甚至是手动分析。如果你跟随这些函数的 xrefs 跳到函数 FUN_00018A90()(在时间检查函数之后) ,你可以看到如下代码:
对国家进行检查
因此,下一个补丁或断点是这样的检查:
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 文件并加载这些文件。 如果你操作正确的话,你会看到弹出了无障碍助手页面。
绕过检查
或者我们可以将 je 指令补丁到本地库中的 jne,然后再次构建 apk。
理解 dex 文件的创建过程
如果在文件系统中查找该恶意程序创建的文件,你不会看到任何内容。 因为文件已经被删除。我们可以很容易地通过 frida 调试分析并得到创建的文件。但是请暂时忘记这件事器,现在我们需要找出这个恶意程序是如何使用 png 文件创建了 dex 文件。
查看 ghidra 脚本输出内容的最后那一部分。
ghidra 脚本的输出结果
使用 AndroidBitmap 处理 prcnbzqn.png,然后创建了名为 xwchfc.dex 的 dex 文件。 然后使用 ClassLoader API 加载 dex 文件,之后调用了类 moonlight.loader.sdk.SdkBuilder。
检查函数: 0xee0
从 asset(资产) 文件夹中获取 png 文件
资产文件夹并查找 png 文件。 将此函数重命名为asset_caller。 访问这个函数的 xref,找到0xe2c0。 我重命名了一些函数的名称。 dex_header 在内存中创建 dex 文件。 dex_dropper 把 dex 文件放到系统中,然后加载。
函数调用层次
dex_header是如何创建 dex 文件的呢? 我们转到函数定义看看。
dex 创建函数
bitmap_related函数从 png 文件创建位图。 位图对象传递给到 dex_related函数。这里为什么是位图呢?让我们继续往下看。
如果你读取了 png 文件字节,你不能直接得到像素的颜色代码。 你需要将其转换为位图。 所以应用程序首先传输 png 文件到位图,读取像素的十六进制值。 启动 gimp或者paint程序,查看图像第一个像素的十六进制代码,并与下面的图片进行比较:
像素的 rgb 值
现在到了有趣的部分。 如何使用这些值。 在 0xfbf0 处你可以找到dex_related函数。
位图对象被传递给这个函数,现在这里有两个重要的函数:
两个重要的函数
byte_chooser将返回一个字节, dex_extractor将使用该字节获得最后的 dex 字节。 4_cmp 变量在开始时设置为0,在 else 代码块结束时设置为0。 所以程序执行流将命中 byte_chooser 2次之前进入 dex_extractor函数。下面是byte_chooser函数的代码:
字节选择函数
param_3是像素的十六进制代码。 param_2就像一个种子变量。 如果它第一次调用byte_chooser时被设置为0,在字节选择器的第二次调用中, param_2 会返回第一次调用的值并左移4位。 然后在 else 代码块的末尾将其设置为0。
通过两次调用字节选择器计算字节后,返回值传递给 dex_extractor 函数。
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 的输出文件:
删除 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 } })
Frida 会话的输出内容
使用下面的命令将 dex 文件拖到本地:
adb pull path/xwcnhfc.dex
本文翻译自:https://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/
原文链接:https://www.4hou.com/mobile/19755.html
翻译作者:李白