Hydra is another android bankbot variant. It uses overlay to steal information like Anubis . Its name comes from command and control panel. Through July 2018 to March 2019 there was atleast 8-10 sample on Google Play Store. Distribution of malware is similar to Anubis cases. Dropper apps are uploaded to Play Store. But unlike Anubis, Dropper apps extract dex file from png file with kinda stenography and downloads malicious app from command and control server with dropped dex. You can find the sample that I will go through in this post here : Dropper
ToC:
- Bypass checks that on the java side
- GDB Debug
- Ghidra shenanigans
- Understanding creation of the dex file
- Bonus
First of all, if the dropper app likes the environment it runs, it will load the dex file and connect to the command and control server. There are multiple checks on java and native side. We will debug the native side with gdb and use ghidra to help us to find checks and important functions.
Time Check
When we open the first app with jadx we can see time check in class com.taxationtex.giristexation.qes.Hdvhepuwy.
public static boolean j() { return new Date().getTime() >= 1553655180000L && new Date().getTime() <= 1554519180000L; }
This function called in another class : 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()); } }
First, it checks the time and if the condition holds, the app will load the native library and call fyndmmn(Hdvhepuwy.j());
which is native function. We need to bypass this check so app will always load the library.
I used apktool
to disassemble apk to smali and changed j()
to always return 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
rebuild apk with apktool b com.taxationtex.giristexation -o hydra_time.apk
and sign it.
Now time control will always return true and after loading native library and fyndmmn
native function is called. Even with this still app doesn’t load dex file.
GDB Debug
Here is a great post explaining how to setup gdb to debug native libraries. Steps:
- 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
There is a small problem here. App will load the library and call the native function and exit. The app needs to wait for gdb connection. My first thought was putting sleep and then connect with gdb.
- apktool d hydra_time.apk
- vim hydra_time/com.taxationtex.giristexation/smali/com/taxationtex/giristexation/qes/Sctdsqres.smali
after following block:
.line 43 :cond_0
Add
const-wide/32 v0, 0xea60 invoke-static {v0, v1}, Landroid/os/SystemClock;->sleep(J)V
and since locals
variable is 1 and we use an extra v1 variable, increment it to 2
.method static H()V .locals 2
Again sign and install the app. If all goes well the app will wait 60 seconds in a white screen. Now we can connect with gdb.
ps | grep com.tax /data/local/tmp/gdbserver :1337 --attach $pid
I use pwndbg for better gdb experience, you can try peda or whatever you want.
- adb forward tcp:1337 tcp:1337
- gdb
- target remote :1337
It takes some time to load all libraries. Put breakpoint to native function fymdmmn
If you want to sync gdb and ghidra addresses, type vmmap at gdb and look for first entry of libhoter.so
. 0xe73be000 0xe73fc000 r-xp 3e000 0 /data/app/com.taxationtex.giristexation-1/lib/x86/libhoter.so
So 0xe73be000
is my base address.
Go to Window
-> Memory Map
and press Home
icon on the upper right. Put your base address and rebase the binary.
Look at the entry of native function in ghdira:
Why call the time function ? Again time check ? Rename return value of time function (curr_time) and press ctrl+shift+f
from assembly view and go to location that context is READ
return (uint)(curr_time + 0xa3651a74U < 0xd2f00)
So we were right, again time check. Rename the current function to check_time
. Calculate the epoch time:
>>> 0xffffffff-0xa3651a74+0xd2f00
>>> 1554519179
>>> (1554519179+ 0xa3651a74) & 0xffffffff < 0xd2f00
>>> True
convert epoch to time : Saturday, April 6, 2019 2:52:59 AM
Yep this was the time that app was on play store. Check how this boolean is used. Look for xrefs of check_time
function.
Yep, as we think it will exit if time doesn’t hold.
First breakpoint/binary patch point is here. Or we can change emulator/phone’s time to April 5 2019.b *(base + 0x8ba8)
But bypassing time check is not enough.
Ghidra Shenanigans
Now diving into binary file you will find multiple functions like this :
If you look at while loop.
2 blocks of data are XORed. ( Length 0x18) We can put breakpoint after do while but it will not be efficient solution. Let’s think a programmatic way to find decrypted strings.
These xor blocks are next to each other. If we can get length of blocks we can easily get decrypted string. Then find the function that use these xor blocks and rename it. Afterwards we can jump 2*length
and get next xor blocks. Repeat.
Starting xor block is at 0x34035
.
Get xrefs of block:
go to function,
get size from CMP instruction, since we know the address of first xor block, add size to first address and get the address of second xor block. XOR the blocks and rename the calling function.
Ghidra : go to Window
-> Script Manager
-> Create New Script
-> Python
.
Set name for script and let’s write our ghidra script.
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()
To run the script, select created script in Script Manager
and press Run.
Now look at the output.
As you can see there are functions : getSimCountryISO
, getNetworkCountryIso
, getCountry
and one suspicious string : tr
. Without running we can assume code will check if these function’s return values are equals to tr
. I know this app targets Turkish people so this is reasonable to avoid sandbox and even manual analyze.
If you follow from these functions’ xrefs to function FUN_00018A90()
(called after time check) you can see this block :
So next patch/breakpoint is this check :b *(base + 0x8c80)
After these checks code will drop dex and load it. If you run without patch/breakpoints only edevlet
page is shown and nothing happens. Get your base address and try bypassing checks :
b *(base + 0x8ba8) b *(base + 0x8c80) copy eip : .... a8 -> set $eip = .... aa c copy eip : .... 80 -> set $eip = .... 82 c
After these breakpoints, app will create dex file and load it. You will see Accessibility page pop-pup if you do it correctly.
Or we can patch je
instructions to jne
in native library and build apk again.
Understanding creation of the dex file
If you look for dropped file in filesystem, you won’t see anything. File is removed with remove
. We can attach frida and catch dropped file easily. But forget about it for now and find how png file is used to create dex file.
Look at the last parts of the ghidra script’s output.
Somehow prcnbzqn.png
is processed with AndroidBitmap
and dex file is created with the name xwchfc.dex
. Then with ClassLoader
API dex file is loaded and moonlight.loader.sdk.SdkBuilder
class is called.
Check function : 0xeec0
Iterates over assets and finds png file. Good. Rename this function asset_caller
. Go to xrefs of this func and find 0xe2c0
. I renamed some of functions. dex_header
creates dex file on memory. dex_dropper
drops dex file to system and loads.
How dex_header creates dex file ? Go to function definition.
bitmap_related
creates bitmap from png file. Bitmap object is passed to dex_related
function. Bitmap ?
If you read png file byte byte you don’t get color codes of pixels directly. You need to convert it to bitmap. So app first transfer png file to bitmap and read hex values of pixels. Fire up gimp/paint and look at the hex codes of first pixel of the image and compare with below picture 🙂
Now comes fun part. How these values are used. At 0xfbf0
you can find dex_related
function.
Bitmap object is passed to this function. Now there are 2 important functions here:
byte_chooser
will return one byte and dex_extractor
will use that byte to get final dex bytes. 4_cmp
variable is set to 0 at the beginning and will set to 0 at the end of else block. So flow will hit byte_chooser
2 times before entering dex_extractor
. Here is byte_chooser
param_3 is hex codes of pixels. param_2 is like seed. If its first call of byte_chooser it is set to 0. In second call of byte_chooser, param_2 will be return value of first call and left shifted by 4. Then its set to 0 at the end of else block.
After calculating the byte by calling byte_chooser twice, return value is passed to dex_extractor
.
param_2 is calculated byte param_1 is index.
Now we know how the dex file is created. Let’s do it with 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()
Let’s look at the output file with jadx
Remember moonlight from output of ghidra script ? Yep this looks correct.
Frida <3
Well I cant write an article without mentioning frida. Bypass checks with 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 } })
Pull the dex file with adb pull path/xwcnhfc.dex
.
Homework
This part is homework for reader 🙂 Next version of this malware only use native arm binaries. So we can’t easily debug without having arm based device. But we can use our dex dropper python script. Malware sample. Load the arm binary to ghidra. Find the correct offset of the dex data block and the size of the block. dex_extractor function might look different but it does the same thing. So you need to only change the name of the files, offset and size variables at the python script. Hash of dropped dex file : 7ff02fb46009fc96c139c48c28fb61904cc3de60482663631272396c6c6c32ec
Conclusion
We attached gdb to debug native code and found certain checks. Wrote a ghidra script to automate decryption of strings and frida script to bypass checks. Also learned that png files needs to be converted with Bitmap to get pixel values. So next time you see png file and suspicious app, look for bitmap calls 😉
References
GDB Debug : https://packmad.github.io/gdb-android/
Featured image : https://www.deviantart.com/velinov/art/Hydra-monster-144496963