{
	"id": "fc5f31dc-e47e-4b27-9e66-c127fe299685",
	"created_at": "2026-04-06T00:10:12.910061Z",
	"updated_at": "2026-04-10T13:11:55.34036Z",
	"deleted_at": null,
	"sha1_hash": "fc794898f41cf9e5b9d1ec8d7b72c5d796f588c8",
	"title": "Dissecting Hydra Dropper – Pentest Blog",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1206938,
	"plain_text": "Dissecting Hydra Dropper – Pentest Blog\r\nPublished: 2019-07-18 · Archived: 2026-04-05 21:29:03 UTC\r\nHydra is another android bankbot variant. It uses overlay to steal information like Anubis . Its name comes from\r\ncommand and control panel. Through July 2018 to March 2019 there was atleast 8-10 sample on Google Play\r\nStore. Distribution of malware is similar to Anubis cases. Dropper apps are uploaded to Play Store. But unlike\r\nAnubis, Dropper apps extract dex file from png file with kinda stenography and downloads malicious app from\r\ncommand and control server with dropped dex. You can find the sample that I will go through in this post here :\r\nDropper\r\nToC:\r\nBypass checks that on the java side\r\nGDB Debug\r\nGhidra shenanigans\r\nUnderstanding creation of the dex file\r\nBonus\r\nFirst of all, if the dropper app likes the environment it runs, it will load the dex file and connect to the command\r\nand control server. There are multiple checks on java and native side. We will debug the native side with gdb and\r\nuse ghidra to help us to find checks and important functions.\r\nTime Check\r\nWhen we open the first app with jadx we can see time check in class\r\ncom.taxationtex.giristexation.qes.Hdvhepuwy.\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\npublic static boolean j() {\r\nreturn new Date().getTime() \u003e= 1553655180000L \u0026\u0026 new Date().getTime() \u003c= 1554519180000L;\r\n}\r\npublic static boolean j() { return new Date().getTime() \u003e= 1553655180000L \u0026\u0026 new Date().getTime() \u003c=\r\n1554519180000L; }\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 1 of 26\n\npublic static boolean j() {\r\n return new Date().getTime() \u003e= 1553655180000L \u0026\u0026 new Date().getTime() \u003c= 1554519180000L;\r\n}\r\nThis function called in another class : com.taxationtex.giristexation.qes.Sctdsqres\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nclass Sctdsqres {\r\nprivate static boolean L = false;\r\nprivate static native void fyndmmn(Object obj);\r\nSctdsqres() {\r\n}\r\nstatic void j() {\r\nif (Hdvhepuwy.j()) {\r\nH();\r\n}\r\n}\r\nstatic void H() {\r\nif (!L) {\r\nSystem.loadLibrary(\"hoter\");\r\nL = true;\r\n}\r\nfyndmmn(Hdvhepuwy.j());\r\n}\r\n}\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 2 of 26\n\nclass Sctdsqres { private static boolean L = false; private static native void fyndmmn(Object obj); Sctdsqres() { }\r\nstatic void j() { if (Hdvhepuwy.j()) { H(); } } static void H() { if (!L) { System.loadLibrary(\"hoter\"); L = true; }\r\nfyndmmn(Hdvhepuwy.j()); } }\r\nclass Sctdsqres {\r\n private static boolean L = false;\r\n private static native void fyndmmn(Object obj);\r\n Sctdsqres() {\r\n }\r\n static void j() {\r\n if (Hdvhepuwy.j()) {\r\n H();\r\n }\r\n }\r\n static void H() {\r\n if (!L) {\r\n System.loadLibrary(\"hoter\");\r\n L = true;\r\n }\r\n fyndmmn(Hdvhepuwy.j());\r\n }\r\n}\r\nFirst, it checks the time and if the condition holds, the app will load the native library and call\r\nfyndmmn(Hdvhepuwy.j()); which is native function. We need to bypass this check so app will always load the\r\nlibrary.\r\nI used apktool to disassemble apk to smali and changed j() to always return true.\r\napktool d com.taxationtex.giristexation.apk\r\ncd com.taxationtex.giristexation/smali/com/taxationtext/giristexation/qes\r\nedit j()Z in Hdvhepeuwy.smali\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\n.method public static j()Z\r\n.locals 1\r\nconst/4 v0, 0x1\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 3 of 26\n\nreturn v0\r\n.end method\r\n.method public static j()Z .locals 1 const/4 v0, 0x1 return v0 .end method\r\n.method public static j()Z\r\n .locals 1\r\n const/4 v0, 0x1\r\n return v0\r\n.end method\r\nrebuild apk with apktool b com.taxationtex.giristexation -o hydra_time.apk and sign it.\r\nNow time control will always return true and after loading native library and fyndmmn native function is called.\r\nEven with this still app doesn’t load dex file.\r\nGDB Debug\r\nHere is a great post explaining how to setup gdb to debug native libraries. Steps:\r\nDownload android sdk with ndk\r\nadb push ~android-ndk-r20/prebuilt/android-TARGET-ARCH/gdbserver/gdbserver /data/local/tmp\r\nadb shell “chmod 777 /data/local/tmp/gdbserver”\r\nadb shell “ls -l /data/local/tmp/gdbserver”\r\nget process id, ps -A | grep com.tax\r\n/data/local/tmp/gdbserver :1337 –attach $pid\r\nadb forward tcp:1337 tcp:1337\r\ngdb\r\ntarget remote :1337\r\nb Java_com_tax\\TAB\r\nThere is a small problem here. App will load the library and call the native function and exit. The app needs to\r\nwait for gdb connection. My first thought was putting sleep and then connect with gdb.\r\napktool d hydra_time.apk\r\nvim hydra_time/com.taxationtex.giristexation/smali/com/taxationtex/giristexation/qes/Sctdsqres.smali\r\nafter following block:\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 4 of 26\n\n.line 43\r\n:cond_0\r\n.line 43 :cond_0\r\n.line 43\r\n:cond_0\r\nAdd\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nconst-wide/32 v0, 0xea60\r\ninvoke-static {v0, v1}, Landroid/os/SystemClock;-\u003esleep(J)V\r\nconst-wide/32 v0, 0xea60 invoke-static {v0, v1}, Landroid/os/SystemClock;-\u003esleep(J)V\r\nconst-wide/32 v0, 0xea60\r\ninvoke-static {v0, v1}, Landroid/os/SystemClock;-\u003esleep(J)V\r\nand since locals variable is 1 and we use an extra v1 variable, increment it to 2\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\n.method static H()V\r\n.locals 2\r\n.method static H()V .locals 2\r\n.method static H()V\r\n .locals 2\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 5 of 26\n\nAgain sign and install the app. If all goes well the app will wait 60 seconds in a white screen. Now we can connect\r\nwith gdb.\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nps | grep com.tax\r\n/data/local/tmp/gdbserver :1337 --attach $pid\r\nps | grep com.tax /data/local/tmp/gdbserver :1337 --attach $pid\r\nps | grep com.tax\r\n/data/local/tmp/gdbserver :1337 --attach $pid\r\nI use pwndbg for better gdb experience, you can try peda or whatever you want.\r\nadb forward tcp:1337 tcp:1337\r\ngdb\r\ntarget remote :1337\r\ndebug session\r\nIt takes some time to load all libraries. Put breakpoint to native function fymdmmn\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 6 of 26\n\nset breakpoint\r\nIf you want to sync gdb and ghidra addresses, type vmmap at gdb and look for first entry of libhoter.so .\r\n0xe73be000 0xe73fc000 r-xp 3e000 0 /data/app/com.taxationtex.giristexation-1/lib/x86/libhoter.so\r\nSo 0xe73be000 is my base address.\r\nGo to Window -\u003e Memory Map and press Home icon on the upper right. Put your base address and rebase the\r\nbinary.\r\nLook at the entry of native function in ghdira:\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 7 of 26\n\nfyndmmn function\r\nWhy call the time function ? Again time check ? Rename return value of time function (curr_time) and press\r\nctrl+shift+f from assembly view and go to location that context is READ\r\nreturn (uint)(curr_time + 0xa3651a74U \u003c 0xd2f00)\r\nSo we were right, again time check. Rename the current function to check_time . Calculate the epoch time:\r\n\u003e\u003e\u003e 0xffffffff-0xa3651a74+0xd2f00\r\n\u003e\u003e\u003e 1554519179\r\n\u003e\u003e\u003e (1554519179+ 0xa3651a74) \u0026 0xffffffff \u003c 0xd2f00\r\n\u003e\u003e\u003e True\r\nconvert epoch to time : Saturday, April 6, 2019 2:52:59 AM\r\nYep this was the time that app was on play store. Check how this boolean is used. Look for xrefs of check_time\r\nfunction.\r\nYep, as we think it will exit if time doesn’t hold.\r\nFirst breakpoint/binary patch point is here. Or we can change emulator/phone’s time to April 5 2019.\r\nb *(base + 0x8ba8)\r\nBut bypassing time check is not enough.\r\nGhidra Shenanigans\r\nNow diving into binary file you will find multiple functions like this :\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 8 of 26\n\ndecryption blocks\r\nIf you look at while loop.\r\nxor while loop\r\n2 blocks of data are XORed. ( Length 0x18) We can put breakpoint after do while but it will not be efficient\r\nsolution. Let’s think a programmatic way to find decrypted strings.\r\nThese xor blocks are next to each other. If we can get length of blocks we can easily get decrypted string. Then\r\nfind the function that use these xor blocks and rename it. Afterwards we can jump 2*length and get next xor\r\nblocks. Repeat.\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 9 of 26\n\nStarting xor block is at 0x34035 .\r\nGet xrefs of block:\r\nxor block\r\ngo to function,\r\nget cmp value\r\nget size from CMP instruction, since we know the address of first xor block, add size to first address and get the\r\naddress of second xor block. XOR the blocks and rename the calling function.\r\nGhidra : go to Window -\u003e Script Manager -\u003e Create New Script -\u003e Python .\r\nSet name for script and let’s write our ghidra script.\r\nPlain text\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 10 of 26\n\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nimport ghidra.app.script.GhidraScript\r\nimport exceptions\r\nfrom ghidra.program.model.address import AddressOutOfBoundsException\r\nfrom ghidra.program.model.symbol import SourceType\r\ndef xor_block(addr,size):\r\n## get byte list\r\nfirst_block = getBytes(toAddr(addr),size).tolist()\r\nsecond_block = getBytes(toAddr(addr+size),size).tolist()\r\na = \"\"\r\n## decrypt the block\r\nfor i in range(len(first_block)):\r\na += chr(first_block[i]^second_block[i])\r\n## each string have trash value at the end, delete it\r\ntrash = len(\"someval\")\r\nreturn a[:-trash]\r\ndef block(addr):\r\n## block that related to creation of dex file. pass itt\r\nif addr == 0x34755:\r\nreturn 0x0003494f\r\n## get xrefs\r\nxrefs = getReferencesTo(toAddr(addr))\r\nif len(xrefs) ==0:\r\n## no xrefs go to next byte\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 11 of 26\n\nreturn addr+1\r\nfor xref in xrefs:\r\nref_addr = xref.getFromAddress()\r\ntry:\r\ninst = getInstructionAt(ref_addr.add(32))\r\nexcept AddressOutOfBoundsException as e:\r\nprint(\"Found last xor block exiting..\")\r\nexit()\r\n## Get size of block with inst.getByte(2)\r\nblock_size = inst.getByte(2)\r\n## decrypt blocks\r\ndec_str = xor_block(addr,block_size)\r\n## get function\r\nfunc = getFunctionBefore(ref_addr)\r\nnew_name = \"dec_\"+dec_str[:-1]\r\n## rename the function\r\nfunc.setName(new_name,SourceType.USER_DEFINED)\r\n## log\r\nprint(\"Block : {} , func : {}, dec string : {}\".format(hex(addr),func.getEntryPoint(),dec_str))\r\nreturn addr+2*block_size\r\ndef extract_encrypted_str():\r\n## starting block\r\ncurr_block_location = 0x34035\r\nfor i in range(200):\r\ncurr_block_location = block(curr_block_location)\r\ndef run():\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 12 of 26\n\nextract_encrypted_str()\r\nrun()\r\nimport ghidra.app.script.GhidraScript import exceptions from ghidra.program.model.address import\r\nAddressOutOfBoundsException from ghidra.program.model.symbol import SourceType def xor_block(addr,size):\r\n## get byte list first_block = getBytes(toAddr(addr),size).tolist() second_block =\r\ngetBytes(toAddr(addr+size),size).tolist() a = \"\" ## decrypt the block for i in range(len(first_block)): a +=\r\nchr(first_block[i]^second_block[i]) ## each string have trash value at the end, delete it trash = len(\"someval\")\r\nreturn a[:-trash] def block(addr): ## block that related to creation of dex file. pass itt if addr == 0x34755: return\r\n0x0003494f ## get xrefs xrefs = getReferencesTo(toAddr(addr)) if len(xrefs) ==0: ## no xrefs go to next byte\r\nreturn addr+1 for xref in xrefs: ref_addr = xref.getFromAddress() try: inst = getInstructionAt(ref_addr.add(32))\r\nexcept AddressOutOfBoundsException as e: print(\"Found last xor block exiting..\") exit() ## Get size of block\r\nwith inst.getByte(2) block_size = inst.getByte(2) ## decrypt blocks dec_str = xor_block(addr,block_size) ## get\r\nfunction func = getFunctionBefore(ref_addr) new_name = \"dec_\"+dec_str[:-1] ## rename the function\r\nfunc.setName(new_name,SourceType.USER_DEFINED) ## log print(\"Block : {} , func : {}, dec string :\r\n{}\".format(hex(addr),func.getEntryPoint(),dec_str)) return addr+2*block_size def extract_encrypted_str(): ##\r\nstarting block curr_block_location = 0x34035 for i in range(200): curr_block_location =\r\nblock(curr_block_location) def run(): extract_encrypted_str() run()\r\nimport ghidra.app.script.GhidraScript\r\nimport exceptions\r\nfrom ghidra.program.model.address import AddressOutOfBoundsException\r\nfrom ghidra.program.model.symbol import SourceType\r\ndef xor_block(addr,size):\r\n## get byte list\r\nfirst_block = getBytes(toAddr(addr),size).tolist()\r\nsecond_block = getBytes(toAddr(addr+size),size).tolist()\r\na = \"\"\r\n## decrypt the block\r\nfor i in range(len(first_block)):\r\na += chr(first_block[i]^second_block[i])\r\n ## each string have trash value at the end, delete it\r\ntrash = len(\"someval\")\r\nreturn a[:-trash]\r\n \r\ndef block(addr):\r\n ## block that related to creation of dex file. pass itt\r\nif addr == 0x34755:\r\nreturn 0x0003494f\r\n## get xrefs\r\nxrefs = getReferencesTo(toAddr(addr))\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 13 of 26\n\nif len(xrefs) ==0:\r\n## no xrefs go to next byte\r\nreturn addr+1\r\nfor xref in xrefs:\r\nref_addr = xref.getFromAddress()\r\ntry:\r\ninst = getInstructionAt(ref_addr.add(32))\r\nexcept AddressOutOfBoundsException as e:\r\nprint(\"Found last xor block exiting..\")\r\nexit()\r\n \r\n ## Get size of block with inst.getByte(2)\r\nblock_size = inst.getByte(2)\r\n ## decrypt blocks\r\ndec_str = xor_block(addr,block_size)\r\n ## get function\r\nfunc = getFunctionBefore(ref_addr)\r\nnew_name = \"dec_\"+dec_str[:-1]\r\n ## rename the function\r\nfunc.setName(new_name,SourceType.USER_DEFINED)\r\n ## log\r\nprint(\"Block : {} , func : {}, dec string : {}\".format(hex(addr),func.getEntryPoint(\r\nreturn addr+2*block_size\r\ndef extract_encrypted_str():\r\n## starting block\r\ncurr_block_location = 0x34035\r\nfor i in range(200):\r\ncurr_block_location = block(curr_block_location)\r\ndef run():\r\nextract_encrypted_str()\r\nrun()\r\nTo run the script, select created script in Script Manager and press Run.\r\nNow look at the output.\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 14 of 26\n\nghidra script output\r\nAs you can see there are functions : getSimCountryISO , getNetworkCountryIso , getCountry and one\r\nsuspicious string : tr . Without running we can assume code will check if these function’s return values are\r\nequals to tr . I know this app targets Turkish people so this is reasonable to avoid sandbox and even manual\r\nanalyze.\r\nIf you follow from these functions’ xrefs to function FUN_00018A90() (called after time check) you can see this\r\nblock :\r\ncountry check\r\nSo next patch/breakpoint is this check :\r\nb *(base + 0x8c80)\r\nAfter these checks code will drop dex and load it. If you run without patch/breakpoints only edevlet page is\r\nshown and nothing happens. Get your base address and try bypassing checks :\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 15 of 26\n\nb *(base + 0x8ba8)\r\nb *(base + 0x8c80)\r\ncopy eip : .... a8 -\u003e set $eip = .... aa\r\nc\r\ncopy eip : .... 80 -\u003e set $eip = .... 82\r\nc\r\nb *(base + 0x8ba8) b *(base + 0x8c80) copy eip : .... a8 -\u003e set $eip = .... aa c copy eip : .... 80 -\u003e set $eip = .... 82 c\r\nb *(base + 0x8ba8)\r\nb *(base + 0x8c80)\r\ncopy eip : .... a8 -\u003e set $eip = .... aa\r\nc\r\ncopy eip : .... 80 -\u003e set $eip = .... 82\r\nc\r\nAfter these breakpoints, app will create dex file and load it. You will see Accessibility page pop-pup if you do it\r\ncorrectly.\r\nchecks bypassed\r\nOr we can patch je instructions to jne in native library and build apk again.\r\nUnderstanding creation of the dex file\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 16 of 26\n\nIf you look for dropped file in filesystem, you won’t see anything. File is removed with remove . We can attach\r\nfrida and catch dropped file easily. But forget about it for now and find how png file is used to create dex file.\r\nLook at the last parts of the ghidra script’s output.\r\nghidra script output\r\nSomehow prcnbzqn.png is processed with AndroidBitmap and dex file is created with the name xwchfc.dex .\r\nThen with ClassLoader API dex file is loaded and moonlight.loader.sdk.SdkBuilder class is called.\r\nCheck function : 0xeec0\r\nget png file from asset folder\r\nIterates over assets and finds png file. Good. Rename this function asset_caller . Go to xrefs of this func and\r\nfind 0xe2c0 . I renamed some of functions. dex_header creates dex file on memory. dex_dropper drops dex\r\nfile to system and loads.\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 17 of 26\n\nhierarchy of functions\r\nHow dex_header creates dex file ? Go to function definition.\r\ndex creator function\r\nbitmap_related creates bitmap from png file. Bitmap object is passed to dex_related function. Bitmap ?\r\nIf you read png file byte byte you don’t get color codes of pixels directly. You need to convert it to bitmap. So app\r\nfirst transfer png file to bitmap and read hex values of pixels. Fire up gimp/paint and look at the hex codes of first\r\npixel of the image and compare with below picture 🙂\r\nrgb values of pixels\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 18 of 26\n\nNow comes fun part. How these values are used. At 0xfbf0 you can find dex_related function.\r\nBitmap object is passed to this function. Now there are 2 important functions here:\r\ntwo important function\r\nbyte_chooser will return one byte and dex_extractor will use that byte to get final dex bytes. 4_cmp variable\r\nis 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\r\nentering dex_extractor . Here is byte_chooser\r\nbyte chooser function\r\nparam_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\r\nbyte_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\r\nblock.\r\nAfter calculating the byte by calling byte_chooser twice, return value is passed to dex_extractor .\r\ndex byte calculator function\r\nparam_2 is calculated byte param_1 is index.\r\nNow we know how the dex file is created. Let’s do it with python\r\nPlain text\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 19 of 26\n\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nfrom PIL import Image\r\nimport struct\r\nimage_file = \"prcnbzqn.png\"\r\nso_file = \"libhoter.so\"\r\noffset = 0x34755\r\nsize = 0x1fa\r\noutput_file = \"drop.dex\"\r\nim = Image.open(image_file)\r\nrgb_im = im.convert('RGB')\r\nim_y = im.size[1]\r\nim_x = im.size[0]\r\ndex_size = im_y*im_x/2-255\r\nf = open(so_file)\r\nd = f.read()\r\nd = d[offset:offset+size]\r\ndef create_magic(p1,p2,p3):\r\nreturn (p1\u003c\u003c2 \u00264 | p2 \u0026 2 | p2 \u0026 1 | p1 \u003c\u003c 2 \u0026 8 | p3)\r\ndef dex_extractor(p1,p2):\r\nreturn (p1/size)*size\u00260xffffff00| ord(d[p1%size]) ^ p2\r\ncount = 0\r\ndex_file = open(output_file,\"wb\")\r\nsecond = False\r\nmagic_byte = 0\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 20 of 26\n\nfor y in range(0,im.size[1]):\r\nfor x in range(0,im.size[0]):\r\nr, g, b = rgb_im.getpixel((x, y))\r\nmagic_byte = create_magic(r,b,magic_byte)\r\nif second:\r\nmagic_byte = magic_byte \u0026 0xff\r\ndex_byte = dex_extractor(count,magic_byte)\r\ndex_byte = dex_byte \u00260xff\r\nif count \u003e 7 and count-8 \u003c dex_size:\r\ndex_file.write(struct.pack(\"B\",dex_byte))\r\nmagic_byte = 0\r\nsecond = False\r\ncount+=1\r\nelse:\r\nmagic_byte = magic_byte \u003c\u003c 4\r\nsecond = True\r\ndex_file.close()\r\nfrom PIL import Image import struct image_file = \"prcnbzqn.png\" so_file = \"libhoter.so\" offset = 0x34755 size =\r\n0x1fa output_file = \"drop.dex\" im = Image.open(image_file) rgb_im = im.convert('RGB') im_y = im.size[1] im_x\r\n= im.size[0] dex_size = im_y*im_x/2-255 f = open(so_file) d = f.read() d = d[offset:offset+size] def\r\ncreate_magic(p1,p2,p3): return (p1\u003c\u003c2 \u00264 | p2 \u0026 2 | p2 \u0026 1 | p1 \u003c\u003c 2 \u0026 8 | p3) def dex_extractor(p1,p2): return\r\n(p1/size)*size\u00260xffffff00| ord(d[p1%size]) ^ p2 count = 0 dex_file = open(output_file,\"wb\") second = False\r\nmagic_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))\r\nmagic_byte = create_magic(r,b,magic_byte) if second: magic_byte = magic_byte \u0026 0xff dex_byte =\r\ndex_extractor(count,magic_byte) dex_byte = dex_byte \u00260xff if count \u003e 7 and count-8 \u003c dex_size:\r\ndex_file.write(struct.pack(\"B\",dex_byte)) magic_byte = 0 second = False count+=1 else: magic_byte =\r\nmagic_byte \u003c\u003c 4 second = True dex_file.close()\r\nfrom PIL import Image\r\nimport struct\r\nimage_file = \"prcnbzqn.png\"\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 21 of 26\n\nso_file = \"libhoter.so\"\r\noffset = 0x34755\r\nsize = 0x1fa\r\noutput_file = \"drop.dex\"\r\nim = Image.open(image_file)\r\nrgb_im = im.convert('RGB')\r\nim_y = im.size[1]\r\nim_x = im.size[0]\r\ndex_size = im_y*im_x/2-255\r\nf = open(so_file)\r\nd = f.read()\r\nd = d[offset:offset+size]\r\ndef create_magic(p1,p2,p3):\r\nreturn (p1\u003c\u003c2 \u00264 | p2 \u0026 2 | p2 \u0026 1 | p1 \u003c\u003c 2 \u0026 8 | p3)\r\ndef dex_extractor(p1,p2):\r\nreturn (p1/size)*size\u00260xffffff00| ord(d[p1%size]) ^ p2\r\ncount = 0\r\ndex_file = open(output_file,\"wb\")\r\nsecond = False\r\nmagic_byte = 0\r\nfor y in range(0,im.size[1]):\r\nfor x in range(0,im.size[0]):\r\nr, g, b = rgb_im.getpixel((x, y))\r\nmagic_byte = create_magic(r,b,magic_byte)\r\nif second:\r\nmagic_byte = magic_byte \u0026 0xff\r\ndex_byte = dex_extractor(count,magic_byte)\r\ndex_byte = dex_byte \u00260xff\r\nif count \u003e 7 and count-8 \u003c dex_size:\r\ndex_file.write(struct.pack(\"B\",dex_byte))\r\nmagic_byte = 0\r\nsecond = False\r\ncount+=1\r\nelse:\r\nmagic_byte = magic_byte \u003c\u003c 4\r\nsecond = True\r\ndex_file.close()\r\nLet’s look at the output file with jadx\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 22 of 26\n\ndropped dex file\r\nRemember moonlight from output of ghidra script ? Yep this looks correct.\r\nFrida \u003c3\r\nWell I cant write an article without mentioning frida. Bypass checks with frida.\r\nThere are time checks on java and native side.\r\nCountry check\r\nFile is removed at native side.\r\nPlain text\r\nCopy to clipboard\r\nOpen code in new window\r\nEnlighterJS 3 Syntax Highlighter\r\nvar unlinkPtr = Module.findExportByName(null, 'unlink');\r\n// remove bypass\r\nInterceptor.replace(unlinkPtr, new NativeCallback( function (a){\r\nconsole.log(\"[+] Unlink : \" + Memory.readUtf8String(ptr(a)))\r\n}, 'int', ['pointer']));\r\nvar timePtr = Module.findExportByName(null, 'time');\r\n// time bypass\r\nInterceptor.replace(timePtr, new NativeCallback( function (){\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 23 of 26\n\nconsole.log(\"[+] native time bypass : \")\r\nreturn 1554519179\r\n},'long', ['long']));\r\nJava.perform(function() {\r\nvar f = Java.use(\"android.telephony.TelephonyManager\")\r\nvar t = Java.use('java.util.Date')\r\n//country bypass\r\nf.getSimCountryIso.overload().implementation = function(){\r\nconsole.log(\"Changing country from \" + this.getSimCountryIso() + \" to tr \")\r\nreturn \"tr\"\r\n}\r\nt.getTime.implementation = function(){\r\nconsole.log(\"[+] Java date bypass \")\r\nreturn 1554519179000\r\n}\r\n})\r\nvar unlinkPtr = Module.findExportByName(null, 'unlink'); // remove bypass Interceptor.replace(unlinkPtr, new\r\nNativeCallback( function (a){ console.log(\"[+] Unlink : \" + Memory.readUtf8String(ptr(a))) }, 'int', ['pointer']));\r\nvar timePtr = Module.findExportByName(null, 'time'); // time bypass Interceptor.replace(timePtr, new\r\nNativeCallback( function (){ console.log(\"[+] native time bypass : \") return 1554519179 },'long', ['long']));\r\nJava.perform(function() { var f = Java.use(\"android.telephony.TelephonyManager\") var t =\r\nJava.use('java.util.Date') //country bypass f.getSimCountryIso.overload().implementation = function(){\r\nconsole.log(\"Changing country from \" + this.getSimCountryIso() + \" to tr \") return \"tr\" }\r\nt.getTime.implementation = function(){ console.log(\"[+] Java date bypass \") return 1554519179000 } })\r\nvar unlinkPtr = Module.findExportByName(null, 'unlink');\r\n// remove bypass\r\nInterceptor.replace(unlinkPtr, new NativeCallback( function (a){\r\n console.log(\"[+] Unlink : \" + Memory.readUtf8String(ptr(a)))\r\n}, 'int', ['pointer']));\r\nvar timePtr = Module.findExportByName(null, 'time');\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 24 of 26\n\n// time bypass\r\nInterceptor.replace(timePtr, new NativeCallback( function (){\r\n console.log(\"[+] native time bypass : \")\r\n return 1554519179\r\n},'long', ['long']));\r\nJava.perform(function() {\r\n var f = Java.use(\"android.telephony.TelephonyManager\")\r\n var t = Java.use('java.util.Date')\r\n //country bypass\r\n f.getSimCountryIso.overload().implementation = function(){\r\n console.log(\"Changing country from \" + this.getSimCountryIso() + \" to tr \")\r\n return \"tr\"\r\n }\r\n t.getTime.implementation = function(){\r\n console.log(\"[+] Java date bypass \")\r\n return 1554519179000\r\n }\r\n })\r\noutput of frida session\r\nPull the dex file with adb pull path/xwcnhfc.dex .\r\nHomework\r\nThis part is homework for reader 🙂 Next version of this malware only use native arm binaries. So we can’t easily\r\ndebug without having arm based device. But we can use our dex dropper python script. Malware sample. Load the\r\narm binary to ghidra. Find the correct offset of the dex data block and the size of the block. dex_extractor function\r\nmight look different but it does the same thing. So you need to only change the name of the files, offset and size\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 25 of 26\n\nvariables at the python script. Hash of dropped dex file :\r\n7ff02fb46009fc96c139c48c28fb61904cc3de60482663631272396c6c6c32ec\r\nConclusion\r\nWe attached gdb to debug native code and found certain checks. Wrote a ghidra script to automate decryption of\r\nstrings and frida script to bypass checks. Also learned that png files needs to be converted with Bitmap to get pixel\r\nvalues. So next time you see png file and suspicious app, look for bitmap calls 😉\r\nReferences\r\nGDB Debug : https://packmad.github.io/gdb-android/\r\nFeatured image : https://www.deviantart.com/velinov/art/Hydra-monster-144496963\r\nSource: https://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nhttps://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/\r\nPage 26 of 26",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://pentest.blog/android-malware-analysis-dissecting-hydra-dropper/"
	],
	"report_names": [
		"android-malware-analysis-dissecting-hydra-dropper"
	],
	"threat_actors": [
		{
			"id": "75108fc1-7f6a-450e-b024-10284f3f62bb",
			"created_at": "2024-11-01T02:00:52.756877Z",
			"updated_at": "2026-04-10T02:00:05.273746Z",
			"deleted_at": null,
			"main_name": "Play",
			"aliases": null,
			"source_name": "MITRE:Play",
			"tools": [
				"Nltest",
				"AdFind",
				"PsExec",
				"Wevtutil",
				"Cobalt Strike",
				"Playcrypt",
				"Mimikatz"
			],
			"source_id": "MITRE",
			"reports": null
		}
	],
	"ts_created_at": 1775434212,
	"ts_updated_at": 1775826715,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/fc794898f41cf9e5b9d1ec8d7b72c5d796f588c8.pdf",
		"text": "https://archive.orkl.eu/fc794898f41cf9e5b9d1ec8d7b72c5d796f588c8.txt",
		"img": "https://archive.orkl.eu/fc794898f41cf9e5b9d1ec8d7b72c5d796f588c8.jpg"
	}
}